Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

Yveltals Blog

数组

堆排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void adjust(vector<int>& a, int len, int i){
int l = i * 2 + 1, r = l + 1, maxId = i;
if (l < len && a[maxId] < a[l]) maxId = l;
if (r < len && a[maxId] < a[r]) maxId = r;
if (maxId != i) {
swap(a[i], a[maxId]);
adjust(a, len, maxId);
}
}
void hsort(vector<int>& a){
for(int i = a.size() / 2 - 1; i >= 0; --i){
adjust(a, a.size(), i);
}
for(int i = a.size() - 1; i > 0; --i){
swap(a[0], a[i]);
adjust(a, i, 0);
}
}

归并排序

在merge过程中,易知两子数组元素大小的个数关系,如以下两题

数组中的逆序对 计算右侧小于当前元素的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int ans{0};  // 逆序对
vector<int> tmp;
vector<int> sortArray(vector<int>& nums) {
tmp.assign(nums.size(), 0);
mergesort(nums, 0, nums.size() - 1);
return nums;
}
void mergesort(vector<int>& nums, int l, int r){
if (l < r) {
int m = l + r >> 1;
mergesort(nums, l, m);
mergesort(nums, m + 1, r);
merge(nums, l, m, r);
}
}
void merge(vector<int>& nums, int l, int m, int r){
int i = l, j = m + 1, k = l;
while (i <= m && j <= r) {
if (nums[i] <= nums[j]) {
// ans += j - m - 1;
tmp[k++] = nums[i++];
} else {
tmp[k++] = nums[j++];
}
}
while (i <= m) {
// ans += j - m - 1;
tmp[k++] = nums[i++];
}
while (j <= r) tmp[k++] = nums[j++];
for (int i = l; i <= r; ++i) nums[i] = tmp[i];
}

原地哈希

限制不使用额外空间,哈希函数为:f(nums[i]) = nums[i] - 1

原地哈希就相当于,让每个数字 n 都回到下标为 n-1 的家里(或者下标为 n 的家里)

而那些没有回到家里的就成了孤魂野鬼流浪在外,他们要么是根本就没有自己的家(n<=0 || n>=nums.size()),要么是自己的家被别人占领了(出现了重复)。这些流浪汉被临时安置在下标为 i 的空房子里,之所以有空房子是因为房子 i 的主人 i+1 失踪了(数字 i+1 缺失)

因此通过原地构建哈希让各个数字回家,我们就可以找到原始数组中重复的数字还有消失的数字

几道例题

前缀和+哈希

和为K的子数组:求和为K的子数组个数(无法用滑动窗口,因为存在负值不好收缩窗口)

题解:哈希记录每个前缀和出现次数,当 mp[prefix - K] 存在时,ans += mp[prefix - K]

剑指 Offer II 011. 0 和 1 个数相同的子数组:找到含有相同数量的 01 的最长连续子数组长度

题解:将数组中的 0 视作 −1,则原问题转换成「求最长的连续子数组,其元素和为 0」

  • 前缀和:求任意区间元素和,前缀和 s[i]=s[j] 时,区间[i,j]元素和为0
  • 哈希表:保存前缀和每个值首次出现的下标,可直接查找左区间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int findMaxLength(vector<int>& nums) {
int n=nums.size(),ans=0,sum=0;
unordered_map<int, int> mp;
mp[sum] = -1;
for (int i = 0; i < n; i++) {
if (nums[i] == 1) sum++;
else sum--;
if (mp.count(sum)) {
int prevIndex = mp[sum];
ans = max(ans, i-prevIndex);
} else {
mp[sum] = i;
}
}
return ans;
}

链表

K个一组反转链表(不遍历求长度)

Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ListNode *reverseKGroup(ListNode *head, int k) {
ListNode *dummy = new ListNode(), *cur = dummy;
ListNode *p = head, *q = nullptr;
while (p) {
int cnt = k;
ListNode *last_reverse_node = p;
while (cnt && p) {
q = p->next;
p->next = cur->next;
cur->next = p;
p = q;
cnt--;
}
if (cnt > 0) { // tail_k not enough, should reverse again
p = cur->next;
cur->next = nullptr;
while (p) {
q = p->next;
p->next = cur->next;
cur->next = p;
p = q;
}
break;
}
cur = last_reverse_node;
}
return dummy->next;
}

排序链表:找链表中位数 + 合并链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ListNode *midLeft(ListNode* p){
ListNode *f = p, *s = p;
while(f->next && f->next->next){
s = s->next;
f = f->next->next;
}
return s;
}
ListNode *reverse(ListNode* p){
ListNode *dummy=new ListNode(),*q;
while(p){
q=p->next;
p->next=dummy->next;
dummy->next=p;
p=q;
}
return dummy->next;
}

字符串

字符串加法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
string addStr(string num1, string num2) {
string ans;
int add = 0,i = num1.size() - 1, j = num2.size() - 1;
while(add || i >= 0 || j >= 0){
int x= i >= 0 ? num1[i] - '0': 0;
int y= j >= 0 ? num2[j] - '0': 0;
int result = x + y + add;
ans += char('0' + result % 10);
add = result / 10;
i--; j--;
}
reverse(ans.begin(), ans.end());
return ans;
}

字符串乘法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
string ans;
string multiply(string num1, string num2) {
if (num1 == "0" || num2 == "0") return "0";
int m = num1.size(), n = num2.size();
for(int i = n - 1; i >= 0; i--){
string tmp(n - 1 - i, '0');
int add = 0, y = num2[i] - '0';
for(int j = m - 1; j >= 0 || add; --j){
int x = j < 0 ? 0 : num1[j] - '0';
int multi = x * y + add;
tmp += char('0' + multi % 10);
add = multi / 10;
}
reverse(tmp.begin(), tmp.end());
ans = addStr(tmp, ans);
}
return ans;
}

字符串分割

1
2
3
4
5
6
7
8
vector<string> ret;
void split(string s, char del){
stringstream ss(s);
string tmp;
while(getline(ss, tmp, del)){
if(tmp.size()) ret.push_back(tmp);
}
}

计算器

基本计算器 II 参考『宫水三叶』题解

对遍历到的字符做分情况讨论:

  • 空格 : 跳过
  • ( : 直接加入 op 中,等待与之匹配的 )
  • ) : 使用现有的 num 和 op 进行计算,直到遇到左括号为止,将结果存入 num
  • 数字 : 从当前继续往右,取出完整数字加入 num
  • 运算符 : 放入 op 中,并且在放入前应先把栈内可以算的 运算符优先级更高/同 的都算掉,每次取 2 个 num 和 1 个 op 计算并存储,直到 没有操作 或者 遇到左括号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
stack<char> op;
stack<int> num;
unordered_map<char,int> level {
{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}, {'^', 3}
};
void cal(){
long b = num.top(); num.pop();
long a = num.top(); num.pop();
char op_ =op.top(); op.pop();
long long res;
switch(op_){
case '+': res = a + b; break;
case '-': res = a - b; break;
case '*': res = a * b; break;
case '/': res = a / b; break;
case '^': res = pow(a, b); break;
}
num.push(res);
}
int calculate(string s) {
num.push(0); //确保2个操作数
int n = s.size();
for (int i = 0; i < n; ++i) {
if (s[i] == ' ') continue;
else if (s[i] == '(') {
op.push('(');
if(s[i + 1] == '-'){
num.push(0); //确保2个操作数
op.push('-');
i++;
}
} else if(s[i] == ')') {
while (op.top() != '(')
cal();
op.pop();
} else if(isdigit(s[i])) {
int k = i + 1;
while(k < n && isdigit(s[k])) k++;
num.push(stoi(s.substr(i, k - i)));
i = k - 1;
} else {
while(!op.empty() && op.top() != '(' && level[op.top()] >= level[s[i]])
cal();
op.push(s[i]);
}
}
while(!op.empty() && op.top() != '(')
cal();
return num.top();
}

KMP

Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int strStr(string haystack, string needle) {
int k=-1,n=haystack.length(),p=needle.length();
if(!p) return 0;
vector<int> next(p,-1);
calNext(needle,next);
for(int i=0;i<n;++i){
while(k>-1 && needle[k+1]!=haystack[i])
k=next[k];
if(needle[k+1]==haystack[i])
k++;
if(k==p-1)
return i-p+1;
}
return -1;
}
void calNext(string& needle,vector<int>& next){
for(int j=1,p=-1;j<needle.length();++j){
while(p>-1 && needle[p+1]!=needle[j])
p=next[p];
if(needle[p+1]==needle[j])
p++;
next[j]=p;
}
}

二叉树

ACM模式树插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TreeNode *buildTree(vector<int>& vec) {
vector<TreeNode*> tree(vec.size(), nullptr);
for (int i = 0; i < vec.size(); ++i) {
if (vec[i] != -1) tree[i] = new TreeNode(vec[i]);
}
for (int i = 0; i < vec.size() / 2; ++i) {
if (vec[i] != -1) {
tree[i]->left = tree[i * 2 + 1];
tree[i]->right = tree[i * 2 + 2];
}
}
return tree[0];
}
int main(){
vector<int> v = {4,1,6,0,2,5,7,-1,-1,-1,3,-1,-1};
TreeNode* root = Build(v);
}

迭代遍历

push 节点的顺序与遍历序相反:

  • 先序遍历:右 左 中
  • 中序遍历:右 中 左
  • 后序遍历:中 右 左
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 先序遍历
stack<TreeNode*> st;
if (root) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top(); st.pop();
if (node) {
if (node->right) st.push(node->right); // 添加右节点
if (node->left) st.push(node->left); // 添加左节点
st.push(node); st.push(nullptr); // 添加中节点,虽然访问过但没处理,空节点标记
} else { // 弹出空节点
node = st.top(); st.pop(); // 重新取出栈中元素
cout << node->val << endl;
}
}

二叉树的最近公共祖先:找 p、q 的最近公共祖先节点

1
2
3
4
5
6
7
8
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (!root || root == p || root == q) return root;
auto l = lowestCommonAncestor(root->left, p, q);
auto r = lowestCommonAncestor(root->right, p, q);
if (!l) return r;
if (!r) return l;
return root;
}

图论

DFS/BFS

最短的桥:二维数组A中0代表海,1代表岛(四个方向相连),求至少将几个 0 变为 1能把两岛连起来

题解:

  1. 找到任意一个岛
  2. 通过dfs将整个岛1标记为2,并把边缘点0入队列
  3. 从边缘点0作bfs扩散,访问过的记录为2,碰到另一个岛1时的层数为所得
Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution {
public:
int n;
vector<int> d{-1, 0, 1, 0, -1};
queue<pair<int, int>> q;
void dfs(vector<vector<int>>& grid, int i, int j) {
if (i < 0 || j < 0 || i == n || j == n || grid[i][j] == 2) return;
if (grid[i][j] == 0) {
q.emplace(i, j);
return;
}
grid[i][j] = 2;
for (int k = 0; k < 4; ++k) {
dfs(grid, i + d[k], j + d[k + 1]);
}
}
int shortestBridge(vector<vector<int>>& grid) {
n = grid.size();
for (int i = 0, stop = 0; i < n && !stop; ++i) {
for (int j = 0; j < n && !stop; ++j) {
if (grid[i][j] == 1) {
dfs(grid, i, j);
stop = 1;
}
}
}
int ans = 0;
while (!q.empty()) {
ans++;
int size = q.size();
while (size--) {
auto [i, j] = q.front(); q.pop();
for (int k = 0; k < 4; ++k) {
int r = i + d[k], c = j + d[k + 1];
if (r < 0 || c < 0 || r == n || c == n) continue;
if (grid[r][c] == 1) return ans;
if (grid[r][c] == 0) {
q.emplace(r, c);
grid[r][c] = 2;
}
}
}
}
return -1;
}
};

被围绕的区域:矩阵包含 XO ,找到所有被 X 围绕(四个方向)的区域,并将这些区域里所有的 OX 填充。

题解: 找到未被包围区域不填充即可,且注意到未包围区域一定挨着边界,故从四个边界开始dfs找到O区域即可

状态转换

关键在于“建图”,过程灵活,只需能完成遍历即可

相关题目:剑指 Offer II 109. 开密码锁剑指 Offer II 111. 计算除法

单词接龙:求字符串 start 转换至 end 最小步数,每次转换仅改变一个字符,且改变后字符串在所给 dic

题解:

  1. 字符串之间的转换关系可抽象为无向图。遍历找相邻结点时,依次查找改变字符后的新字符串是否在 dic
  2. 无向图中顶点间的最短路径的长度通过BFS得到。为防止回路,将访问过的结点记录下来
Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
queue<string> q;
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> dic(wordList.begin(), wordList.end());
if(!dic.count(endWord)) return 0;
int step = 1;
q.push(beginWord);
dic.erase(beginWord);//访问后直接删除,防止回路
while(!q.empty()){
int k = q.size();
while(k--){
string s = q.front(); q.pop();
//判断更改一个字符后是否满足结果
if(check(s, endWord, dic)){
return step + 1;
}
}
step++;
}
return 0;
}
bool check(string cur, string target, unordered_set<string>& dic){
for(int i = 0; i < cur.size(); ++i){
char tmp = cur[i];
for(char c = 'a'; c <= 'z'; c++){
if(tmp == c) continue;
cur[i] = c;
if(dic.count(cur)){
if(cur == target) return true;
else{
q.push(cur);
dic.erase(cur);//访问后直接删除,防止回路
}
}
}
cur[i] = tmp;
}
return false;
}

水壶问题:两水壶容量x,y,能否经操作使得盛水之和为z:①装满一水壶 ②清空一水壶 ③相互倒水至满或空

题解:记当前盛水量为状态(a,b)

  • 每次操作有6种状态转换:

    ① a倒满至(x,b) ② b倒满至(a,y) ③ a清空至(0,b) ④ b清空至(a,0)

    ⑤ a往b倒水至(a-move,b+move) ⑥ 反之(a+move,b-move)

  • BFS进行搜索,set记录已访问过状态(将pair转换成int64_t存储)

Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
inline int64_t hash(int a, int b) {
return (int64_t)a << 32 | b;
}
pair<int, int> op(int i, int a, int b, int x, int y) {
switch (i) {
case 0: return make_pair(0, b);
case 1: return make_pair(a, 0);
case 2: return make_pair(x, b);
case 3: return make_pair(a, y);
case 4: {
int move = min(a, y - b);
return make_pair(a - move, b + move);
}
case 5: {
int move = min(b, y - a);
return make_pair(a + move, b - move);
}
}
return make_pair(0, 0);
}
bool canMeasureWater(int x, int y, int target) {
queue<pair<int, int>> q;
unordered_set<int64_t> vis;
q.emplace(0, 0);
while (!q.empty()) {
int size = q.size();
while (size--) {
auto [a, b] = q.front(); q.pop();
for (int i = 0; i < 6; ++i) {
auto [new_a, new_b] = op(i, a, b, x, y);
if (new_a + new_b == target) return true;
if (!vis.count(hash(new_a, new_b))) {
vis.insert(hash(new_a, new_b));
q.emplace(new_a, new_b);
}
}
}
}
return false;
}

Dijkstra

Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int n;
vector<int> vis,dis;
vector<vector<int>> g; // 邻接矩阵
void dijkstra(int start){
vis.assign(n, 0);
dis.assign(n, INT_MAX);
dis[start]=0;
while (true) {
int nearest_id = -1, min_dis = INT_MAX;
for (int i = 0; i < n; ++i){
if (!vis[i] && dis[i] < min_dis){
min_dis = dis[i];
nearest_id=i;
}
}
if (nearest_id < 0) break;
vis[nearest_id] = 1;
for (int i = 0; i < n; ++i) {
if(!vis[i] && g[nearest_id][i] < INT_MAX){
dis[i] = min(dis[i], dis[nearest_id] + g[nearest_id][i]);
}
}
}
}

回溯

两个诀窍:一是按值传状态,二是所有的状态修改在递归完成后回改。
两种情况:一种是修改最后一位输出,如排列组合;一种是修改访问标记,如矩阵里搜字符串

一维

全排列 II:序列包含重复数字,按任意顺序返回所有不重复的全排列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
vector<int> vis, tmp;
vector<vector<int>> ans;
vector<vector<int>> permuteUnique(vector<int>& nums) {
vis.assign(nums.size(), 0);
sort(nums.begin(), nums.end());
fun(nums);
return ans;
}
void fun(vector<int>& nums) {
if (tmp.size() == nums.size()) {
ans.push_back(tmp);
return;
}
for (int i = 0; i < nums.size(); ++i) {
if (vis[i] || i && nums[i] == nums[i - 1] && vis[i - 1]) {
continue;
}
tmp.push_back(nums[i]);
vis[i] = 1;
fun(nums);
vis[i] = 0;
tmp.pop_back();
}
}

N 皇后:如何将 n 个皇后放置在 n×n 的棋盘上,使得横竖斜直线上均仅一个皇后

题解:实质从0到n-1行依次选择一个位置(一维搜索),选择前修改矩阵,退出dfs后恢复修改。另需设置set集合记录哪些行列已存在皇后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vector<string> cur;
vector<vector<string>> ans;
set<int> row, bias1, bias2;

void dfs(int i, int n){
if(i >= n){
ans.push_back(cur);
return;
}
for(int j = 0; j < n; ++j){
if(!row.count(j) && !bias1.count(j - i) && !bias2.count(j + i)){
cur[i][j] = 'Q'; row.insert(j); bias1.insert(j - i); bias2.insert(j + i);
dfs(i + 1, n);
cur[i][j] = '.'; row.erase(j); bias1.erase(j - i); bias2.erase(j + i);
}
}
}
vector<vector<string>> solveNQueens(int n) {
cur = vector<string>(n, string(n, '.'));
dfs(0, n);
return ans;
}

二维

解数独:数字 1-9 在每行、每列、每个粗实线分隔的 3x3 宫内只能出现一次,矩阵由数字和空白 '.' 表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bool check(vector<vector<char>>& a, int i, int j, char x){
for(int k = 0; k < 9; ++k){
if(a[i][k] == x) return false;
if(a[k][j] == x) return false;
if(a[i / 3 * 3 + k / 3][j / 3 * 3 + k % 3] == x) {
return false;
}
}
return true;
}
int dfs(vector<vector<char>>& a,int i,int j){
if(j == 9) j = 0, i++;
if(i == 9) return true;
if(a[i][j] != '.') return dfs(a, i, j + 1);

for(char x = '1'; x <= '9'; ++x){
if(check(a, i, j, x)){
a[i][j] = x;
if(dfs(a, i, j + 1)) return true;
a[i][j] = '.';
}
}
return false;
}
void solveSudoku(vector<vector<char>>& a) {
dfs(a, 0, 0);
}

记忆化搜索

最长递增路径:求矩阵中最长递增路径长度(上下左右移动)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int m, n, ans;
vector<int> d{-1, 0, 1, 0, -1};
vector<vector<int>> dp;
int longestIncreasingPath(vector<vector<int>>& matrix) {
m = matrix.size(), n = matrix[0].size();
dp.assign(m, vector<int>(n, -1)); // cache, 仅增加了注释的三行代码
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
ans = max(ans, fun(matrix, i, j, INT_MAX + 1l));
}
}
return ans;
}
int fun(vector<vector<int>>& matrix, int i, int j, long value) {
if (i < 0 || j < 0 || i == m || j == n || (long)matrix[i][j] >= value) {
return 0;
}
if (dp[i][j] != -1) return dp[i][j]; // load
int len = 0;
for (int k = 0; k < 4; ++k) {
len = max(len, fun(matrix, i + d[k], j + d[k + 1], matrix[i][j]));
}
dp[i][j] = len + 1; // save
return len + 1;
}

动态规划

子序列问题

最长递增子序列(贪心+二分):返回最长递增子序列(字典序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 返回字典序最小的序列
vector<int> lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> d; // d[i] 维护当前长度为 len 的 LIS
vector<int> pos(n); // pos[i]: nums[i] 在 d 中的下标
d.push_back(nums[0]);
for (int i = 1; i < n; ++i) {
if (nums[i] > d.back()) {
d.push_back(nums[i]);
pos[i] = d.size();
} else {
auto it = lower_bound(d.begin(), d.end(), nums[i]);
*it = nums[i];
pos[i] = it - d.begin() + 1;
}
}
int len = d.size(); // 长度
vector<int> lis;
for (int i = n - 1; i >= 0 && len; --i) {
if (pos[i] == len) {
lis.push_back(nums[i]);
len--;
}
}
reverse(lis.begin(), lis.end());
return lis;
}

最长公共子序列:返回两个字符串的最长公共序列

方案一:二维DP空间 + 输出全部LCS路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size(), n = text2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
int len = dp[m][n];
vector<string> paths; // 保存LCS序列

function<void(int, int, string)> fun = [&](int i, int j, string lcs) {
while (i > 0 && j > 0) {
if (text1[i - 1] == text2[j - 1]) {
lcs += text1[i - 1];
i--; j--;
} else {
if (dp[i - 1][j] > dp[i][j - 1]) i--;
else if (dp[i - 1][j] < dp[i][j - 1]) j--;
else {
fun(i - 1, j, lcs);
fun(i, j - 1, lcs);
break;
}
}
}
if (lcs.size() == len) {
reverse(lcs.begin(), lcs.end());
paths.push_back(lcs);
}
};

fun(m, n, "");
return len;
}

方案二:一维DP空间,用 leftup 变量保存 dp[i - 1][j - 1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int longestCommonSubsequence1(string text1, string text2) {
int m = text1.size(), n = text2.size();
vector<int> dp(n + 1);
for (int i = 1; i <= m; ++i) {
int leftup = dp[0];
for (int j = 1; j <= n; ++j) {
int tmp = dp[j];
if (text1[i - 1] == text2[j - 1]) dp[j] = leftup + 1;
else dp[j] = max(dp[j], dp[j - 1]);
leftup = tmp;
}
}
return dp[n];
}

字符串问题

交错字符串:问s1s2交错拼接能否组成s3

单词拆分:字符串 s 能否由 wordDict 中的字符串列表拼接出来

  • 记忆化回溯:定义 bool fun(i) 为下标 i 之前的 s 可否被拼接出来,从 fun(n) 缩小问题到 fun(0),同时记录 fun(i) 的结果防止重复子问题
  • DP解法:正向求 dp[i] 表示前 i 能否被拼接出,从 0 ~ n 遍历求解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool wordBreak(string s, vector<string>& wordDict) {
int n = s.size(), max_len = 0;
vector<int> dp(n);
unordered_set<string> st;
for (auto& w : wordDict) {
max_len = max(max_len, (int)w.size());
st.insert(w);
}
for (int i = 0; i < n; ++i) {
for (int j = max(0, i - max_len + 1); j <= i; ++j) {
string cur = s.substr(j, i - j + 1);
if (!st.count(cur)) continue;
if (j == 0 || dp[j - 1] > 0) {
dp[i] = 1;
break;
}
}
}
return dp.back();
}

不同的子序列:返回在 s 的子序列中 t 出现的个数

  • dp[i][j] 为 s 的前 i 个字符与 t 的前 j 个匹配,初始化 dp[i][0] = 1
  • s[i-1] == t[j-1]dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
  • s[i-1] != t[j-1]dp[i][j] = dp[i-1][j]

编辑距离:求将 word1 转换成 word2 所使用的最少操作数,可以任意位置删除、替换、插入

两个字符串的删除操作:编辑距离的简化版,仅删除操作

题解:

  • 定义dp[i][j]为使两单词前i j部分相同的最少操作数
  • 状态转移方程
    • word1[i]=word2[j]dp[i][j]=dp[i-1][j-1]
    • word1[i]≠word2[j]
      • i后插入/j处删除:dp[i][j]=dp[i][j-1]+1
      • j后插入/i处删除:dp[i][j]=dp[i-1][j]+1
      • i/j处修改:dp[i][j]=dp[i-1][j-1]+1
  • 考虑初值i=0j=0的情况:i=0时,word1为空,只能删除/插入j次;j=0时同理
    • dp[0][j]=j, dp[i][0]=i
  • 优化空间
    • 正序遍历 j 会丢失 dp[i-1][j-1],倒序遍历又不知道 dp[i][j-1] 的结果
    • 因此选择正序遍历,并用 leftup 保存 dp[i-1][j-1]
    • 初值 dp[0][j] = j,仅在第一轮遍历中用到 -> dp[j] = j
    • 初值 dp[i][0] = i,每轮遍历中用到 -> 每轮循环中 dp[0] = i
Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int j = 1; j <= n; ++j) dp[0][j] = j;
for (int i = 1; i <= m; ++i) dp[i][0] = i;
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
else dp[i][j] = 1 + min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]});
}
}
return dp[m][n];
}
// 优化空间复杂度
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<int> dp(n + 1, 0);
for (int j = 1; j <= n; ++j) dp[j] = j;
for (int i = 1; i <= m; ++i) {
int leftup = dp[0];
dp[0] = i;
for (int j = 1; j <= n; ++j) {
int tmp = dp[j];
if (word1[i - 1] == word2[j - 1]) dp[j] = leftup;
else dp[j] = min({dp[j], dp[j - 1], leftup}) + 1;
leftup = tmp;
}
}
return dp[n];
}

正则表达式:字符串 s 能否被 p 匹配:. 匹配任意单个字符,* 匹配任意个前一个元素

Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool articleMatch(string s, string p) {
int m = s.size(), n = p.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
auto check = [&] (int i, int j) {
if (!i || !j) return false;
if (p[j - 1] == '.') return true;
return p[j - 1] == s[i - 1];
};
dp[0][0] = 1;
for (int i = 0; i <= m; ++i) { // "" match "A**"
for (int j = 1; j <= n; ++j) {
if (p[j - 1] != '*') {
dp[i][j] |= check(i, j) && dp[i - 1][j - 1];
} else {
// *匹配 0 个
dp[i][j] |= dp[i][j - 2];
// s[i-1] = p[j-2], 各抵消一个字符a后,S对应前i-1,而P的a*是无限个,j不减
dp[i][j] |= check(i, j - 1) && dp[i - 1][j];
}
}
}
return dp[m][n];
}

分割回文串:求s的任意子串是否回文

1
2
3
4
5
6
7
vector<vector<int>> dp(n, vector<int>(n));
for (int i = n - 1; i >= 0; --i) {
for (int j = i; j < n; ++j) {
if (i + 1 >= j - 1) dp[i][j] = s[i] == s[j];
else dp[i][j] = dp[i + 1][j - 1] && s[i] == s[j];
}
}

最长有效括号:从含 '('')' 的字符串中找出最长有效括号子串的长度


背包问题

递推公式

问能否装满背包(或最多装多少):dp[j] |= dp[j - nums[i]]

本质是 dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])

问装满背包有几种方法:dp[j] += dp[j - nums[i]]

  • 目标和:给数组每个数用+-串联计算出结果,求结果为target的方式数
    • 0-1背包,倒着遍历空间。令+的部分和为 A,-的部分和为 B,则 A + B = SUM, A - B = target,即 A = (SUM + target) / 2,求装满 A 的方法数
  • 零钱兑换II:给定不同面额的硬币(无限个),求凑成某总金额的硬币组合数
    • 完全背包,正向遍历空间。注意是求装满空间的组合数,要外层循环物品(硬币)、内层循环空间(金额),这样挑物品不会重复。否则,对于金额 6 会出现 {1, 5}, {5, 1} 这两个重复组合
  • 组合总和 Ⅳ:从 nums 中找出总和为 target 的元素组合的个数(顺序不同的序列视作不同组合)
    • 完全背包,正向遍历空间。注意是求装满空间的排列数,要外层循环空间(target)、内层循环物品(nums),这样对于 target = 3 时 {1, 2} 和 {2, 1} 两种均有效

问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

  • 一和零:strs 是仅含 0/1 的字符串数组,找出 strs 的最大子集的长度,该子集中最多有 m 个 0 和 n 个 1
    • 空间限制变成了二维,多加一重循环即可
    1
    2
    3
    4
    5
    6
    7
    for (auto& str : strs) {
    for (int i = m; i >= zero; --i) {
    for (int j = n; j >= one; --j) {
    dp[i][j] = max(dp[i][j], dp[i - zero][j - one] + 1);
    }
    }
    }

问装满背包所有物品的最小个数:dp[j] = min(dp[j], dp[j - coins[i]] + 1)

  • 零钱兑换:硬币面额 coins,总金额 amount,求凑成总金额的最少的硬币个数
  • 完全平方数:求和为 n 的完全平方数的最少数量

遍历顺序

(一) 01背包

1
2
3
4
5
6
7
8
9
int knapsack(vector<int> weights, vector<int> values, int space) {
vector<int> dp(space + 1, 0);
for (int i = 1; i <= N; ++i) {
int w = weights[i - 1], v = values[i - 1];
for (int j = space; j >= w; --j)
dp[j] = max(dp[j], dp[j - w] + v);
}
return dp[space];
}
二维DP
1
2
3
4
5
6
7
8
9
10
11
12
13
// 二维 dp 先遍历物品或背包都行
int knapsack(vector<int> weights, vector<int> values, int N, int W) {
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; ++i) {
int w = weights[i-1], v = values[i-1];
for (int j = 1; j <= W; ++j)
if (j >= w)
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v);
else
dp[i][j] = dp[i-1][j];
}
return dp[N][W];
}

(二) 完全背包

纯完全背包的一维dp数组实现,先遍历物品或背包都行,且第二层循环从小到大遍历。

但是要注意特殊情况:

1
2
3
4
5
6
7
8
9
int knapsack(vector<int> weights, vector<int> values, int space) {
vector<int> dp(space + 1, 0);
for (int i = 1; i <= N; ++i) {
int w = weights[i - 1], v = values[i - 1];
for (int j = w; j <= space; ++j)
dp[j] = max(dp[j], dp[j - w] + v);
}
return dp[W];
}
二维DP
1
2
3
4
5
6
7
8
9
10
11
12
int knapsack(vector<int> weights, vector<int> values, int N, int W) {
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; ++i) {
int w = weights[i-1], v = values[i-1];
for (int j = 1; j <= W; ++j)
if (j >= w)
dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v);
else
dp[i][j] = dp[i-1][j];
}
return dp[N][W];
}

股票问题

买卖股票的最佳时机 IV:给定price[i]共可交易k次,求最大利润

  1. 状态定义:

    • dp[i][j][0]表示第 i 天共交易 j 次且当前不持有股票,跟在dp[i-1][j][1]之后(买入后才进入新的交易次数)

    • dp[i][j][1]表示第 i 天共交易 j 次且当前持有股票,跟在dp[i-1][j-1][0]之后

  2. 状态转移

    • dp[i][j][0] = max{ dp[i-1][j][0],dp[i-1][j][1]+price[i] }
    • dp[i][j][1] = max{ dp[i-1][j][1],dp[i-1][j-1][0]-price[i] }
  3. 初始化

    • dp[0][0][0]=0:第一天没交易过没股票
    • dp[0][1][1]=-price[0]:第一天第一次交易买了股票
    • 其他均不合法情况,设置为INI_MIN
  4. 执行结果

    • 最后一天、不持有股票、交易0~k次中的最大值,即 max{dp[n-1][i][0]}, 0<=i<=k
  5. 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int maxProfit(int k, vector<int>& prices) {
int n = prices.size(), ans = 0;
if(!k || !n) return 0;
vector<vector<int>> tmp(k + 1, vector<int>(2));
vector<vector<vector<int>>> dp(n, tmp);

for(int i = 0; i <= k; ++i)
dp[0][i][0]=dp[0][i][1] = INT_MIN / 2;
dp[0][0][0] = 0;
dp[0][1][1] = -prices[0];

for(int i = 1; i < n; ++i){
for(int j = 1; j <= k; ++j){
dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
}
}
for(int i = 1; i <= k; ++i)
if(ans < dp[n - 1][i][0])
ans = dp[n - 1][i][0];
return ans;
}

以下几种情况均不强调制定交易次数,可省略k维度

买卖股票的最佳时机:限定交易次数 k=1

1
2
3
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
// k=0时没有交易次数,dp[i-1][0][0]必为0
dp[i][1] = max(dp[i - 1][1], -prices[i])

买卖股票的最佳时机 II:交易次数无限制 k = +infinity

1
2
3
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
// 前一天持有 or 前一天不持有然后买入
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])

最佳买卖股票时机含冷冻期:卖出后至少隔1天才能买

1
2
3
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
//冷却时间1天,所以要从 i-2 天转移状态
dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i])

买卖股票的最佳时机含手续费:每次交易含手续费

1
2
3
//定义在卖出的时候扣手续费
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee)
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])

树状DP

打家劫舍 III:从树中挑选部分节点,要求两两不连续,求最大值

节点 A 可选偷或不偷,截止到 A 的最大值依赖于 A->left 偷/不偷A->right 偷/不偷 这四个情况,即当前状态是由前面多个状态推导出的,只能采用 DFS (记忆化回溯)的方式

1
2
3
4
5
6
7
8
9
10
11
pair<int, int> dfs(TreeNode* t) {
if (!t) return {0, 0};
pair<int, int> l = dfs(t->left), r = dfs(t->right);
int stole = t->val + l.first + r.first;
int unstole = max(l.first, l.second) + max(r.first, r.second);
return {unstole, stole};
}
int rob(TreeNode* root) {
auto pair = dfs(root);
return max(pair.first, pair.second);
}

数位DP

1~n 整数中 1 出现的次数:无须考虑前导零

n转换成字符串 s,定义 f(i,cnt,isLimit) 表示构造从左往右第i位及其之后数位中 1 的个数

  • cnt:表示前面填了多少个 1。
  • isLimit:表示当前是否受到了 n 的约束。若为真,则第 i 位填入的数字至多为 s[i],否则可以是 9。如果在受到约束的情况下填了 s[i],那么后续填入的数字仍会受到 n 的约束。
Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int numberOf1sInRange(int n) {
string s=to_string(n);
int m=s.size();
vector<vector<int>> dp(m,vector<int>(m,-1));
function<int(int,int,bool)> f = [&](int i,int cnt,bool limit)->int{
if(i==m) return cnt;
if(!limit && dp[i][cnt]>=0) return dp[i][cnt];
int res=0;
for(int d=0,up=limit?s[i]-'0':9;d<=up;++d){
res += f(i+1,cnt+(d==1),limit&&d==up);
}
if(!limit) dp[i][cnt]=res;
return res;
};
return f(0,0,true);
}

1~n 中特殊整数的次数(每个数位互不相同):须考虑前导零

f(i,mask,isLimit,isNum) 表示构造从左往右第 i 位及其之后数位的合法方案数:

  • mask:表示前面选过的数字集合,即第 i 位要选的数字不能在 mask 中。
  • isNum:表示 i 前面的数位是否填了数字。若为假,则当前位可以跳过(不填数字),或者要填入的数字至少为 1;若为真,则必须填数字,且要填入的数字可以从 0 开始。
Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int countSpecialNumbers(int n) {
auto s = to_string(n);
int m = s.length(), dp[m][1 << 10];
memset(dp, -1, sizeof(dp));
function<int(int,int,bool,bool)>f=[&](int i,int mask,bool is_limit,bool is_num)->int{
if (i==m) return is_num;
if (!is_limit && is_num && dp[i][mask]>=0) return dp[i][mask];
int res = 0;
if (!is_num) res = f(i+1, mask, false, false); // 可以跳过当前数位
for (int d=1-is_num, up=is_limit?s[i]-'0':9; d<=up; ++d) // 枚举要填入的数字d
if ((mask>>d & 1) == 0) // d不在mask中
res += f(i+1, mask|(1<<d), is_limit && d==up, true);
if (!is_limit && is_num) dp[i][mask] = res;
return res;
};
return f(0, 0, true, false);
}

二分法

参考链接: 二分法题单

Lower_bound

1
2
3
4
5
6
7
8
9
10
// 返回首个≥target的元素下标
int Bsearch(vector<int>& nums,int target){
int l = 0,r = nums.size() - 1;
while(l < r){
int m = l + r >> 1;
if(nums[m] < target) l = m + 1;
else r = m;
}
return nums[l] == target ? l : -1;
}

常见模板

l = mid可能导致相邻两元素时陷入死循环:区间 [l, r] 落入 [mid, r] 后实际没变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 升序 从左向右找
while(l < r) {
int mid = l + r >> 1;
if(flag) l = mid + 1;
else r = m;
}
// 升序 从右向左找
while(l < r) {
int mid = l + r + 1 >> 1;
if(flag) r = mid - 1;
else l = mid;
}

while(l <= r){
int mid = l + r >> 1;
if(flag) ans = mid, l = mid + 1; // 需记录答案
else r = mid - 1;
}

旋转数组

面试题 10.03. 搜索旋转数组:查找值(有重复,返回最小索引)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int search(vector<int>& arr, int target) {
int n = arr.size(), l = 0, r = n - 1;
while (l <= r) {
int m = l + r >> 1;
if (arr[m] == target) { // 重复选首个下标
while (m && arr[m - 1] == arr[m]) m--;
return m;
}
if (arr[l] < arr[m]) { // 左侧有序
if (arr[l] <= target && arr[m] > target)
r = m - 1;
else l = m + 1;
} else if (arr[l] > arr[m]) { // 右侧有序
if (arr[m] < target && arr[r] >= target)
l = m + 1;
else r = m - 1;
} else {
l++;
}
}
return -1;
}

寻找旋转排序数组中的最小值 II:找最小值(有重复)

1
2
3
4
5
6
7
8
9
10
11
int findMin(vector<int>& nums) {
int l = 0, r = nums.size() - 1;
while (l < r) {
if (nums[l] < nums[r]) break; // 特判 case: [1,2,3 ...]
int m = l + r >> 1;
if (nums[m] < nums[l]) r = m;
else if (nums[m] > nums[l]) l = m + 1;
else l++;
}
return nums[l];
}

数学问题

FindK

找第K小的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int FindK(vector<int>& a, int k){
int l = 0, r = a.size() - 1;
while(l <= r){
int pivot = Part(a, l, r);
if (pivot == k) return a[k];
else if (pivot > k) r = pivot - 1;
else l = pivot + 1;
}
return -1;
}
int Part(vector<int>& a, int l, int r){
int pivot = a[l];
while(l < r ) {
while(l < r && a[r] >= pivot) r--;
a[l] = a[r];
while(l < r && a[l] <= pivot) l++;
a[r] = a[l];
}
a[l] = pivot;
return l;
}

找两个有序数组中第K大

Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int findK(vector<int>& a, vector<int>& b, int k){
int m = a.size(), n = b.size();
int i = 0, j = 0;
while(true) {
if(i == m) return b[j + k - 1];
if(j == n) return a[i + k - 1];
if(k == 1) return min(a[i], b[j]);

int ir = min(m - 1, i + k / 2 - 1);
int jr = min(n - 1, j + k / 2 - 1);
if (a[ir] <= b[jr]) {
k -= ir + 1 - i;
i = ir + 1;
} else {
k -= jr + 1 - j;
j = jr + 1;
}
}
}

找[1,n]中字典序第K大

Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 前缀为 prefix 且不超过 n 的数
long get_count(long prefix, long n) {
long cnt = 0;
long a = prefix, b = prefix + 1;
// example : prefix = 1, n = 123:
// cnt += 2 - 1
// cnt += 20 - 10
// cnt += 124 - 100
while (a <= n) {
cnt += min(n + 1, b) - a;
a *= 10;
b *= 10;
}
return cnt;
}
int findKthNumber(int n, int k) {
int prefix = 1, p = 1; // 字典序前缀 prefix, 当前为第 p 个数
while (p < k) {
int cnt = get_count(prefix, n);
if (p + cnt > k) { // step in tree's next level
prefix *= 10;
p++;
} else { // step to other node in same level
prefix++;
p += cnt;
}
}
return prefix;
}

快慢指针

环形链表 II:输入一个链表,输出环的入节点,否则返回空指针

题解:实质为链表找环路问题:Floyed判圈法。从head开始,指针fast和slow每次前进2、1步,若无环则能走到尽头,否则两者会相遇。第一次相遇时,令fast=head,此后fast和slow每次均前进一步,第二次相遇时节点即为入环节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ListNode* detectCycle(ListNode* head) {
ListNode *fast = head, *slow = head;
while (fast->next && fast->next->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) break;
}
if (!fast->next || !fast->next->next) return nullptr;
fast = head;
while (fast != slow) {
slow = slow->next;
fast = fast->next;
}
return fast;
}

快乐数:判断正整数 n 是不是快乐数,其定义为:

  • 每次将该数替换为其各位数字的平方和
  • 无限循环这个过程,若最终变为 1 就是快乐数,否则不是

题解:N1->N2->...->Ni的过程可以看作链表,且一定有环,分为两种情况

  1. a->b->c->b->c...不合法
  2. a->b->c->1->1...合法,即快慢指针的值相等时为1

快速幂

1
2
3
4
5
6
7
8
9
double quickMul(double x, int N){
double ans = 1.0;
while(N) {
if(N & 1) ans *= x;
x *= x;
N >>= 1;
}
return ans;
}

因数/倍数/质数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 最大公因数
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
// 最小公倍数
int lcm(int a, int b) {
return a * b / gcd(a, b);
}
// 质数(小于n)
vector<bool> primes(int n) {
vector<bool> prime(n, true);
prime[0] = prime[1] = false;
for(int i = 2; i < n; ++i){
if(!prime[i]) continue;
for(int j = 2 * i; j < n; j += i) {
prime[j] = false;
}
}
return prime;
}

位运算

与 (处理某位)

  • 取低位第k位:1&(num>>k)
  • 将最低位的1变为0:n&(n-1),如110100->110000
  • 保留最低位1其他清0:n&(-n),如110100->000100

异或 (找缺失数、出现一次数)

  • 第k位取反:num^(1<<k)
x ^ 000 = x x ^ 111 = ~x x ^ x = 0

1. 只出现一次的数字:数组中每个数都出现两次,找出仅有出现一次的

题解:遍历全部异或一遍,x^x=0, x^0=x,最后结果就是仅出现一次的

2. 丢失的数字:找出范围0~n的数组中缺少的一个数

题解:先将0~n异或一遍,再异或该数组,这样缺失值即仅出现一次

摩尔投票法

一次遍历,统计次数超过 n/k 的数,可知这样的数最多 k-1

假设当前遍历到的元素为 x

  • 如果 x 本身是候选者,票数加一
  • 如果 x 不是候选者,检查是否有候选者票数为 0
    • 若有,则让 x 代替其成为候选者,记录票数为 1
    • 若无,则让所有候选者票数减一

最后二次遍历,将票数符合要求的候选者加到答案

Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
vector<int> majorityElement(vector<int>& nums) {
vector<int> ans;
const int m=2; //adjust
vector<int> elem(m),vote(m);
for(auto x:nums){
bool flag=false; // x是候选者,票+1
for(int i=0;i<m;++i)
if(vote[i]>0 && elem[i]==x){
vote[i]++;
flag = true; break;
}
if(flag) continue;

flag = false; // x非候选者,且有空闲候选位
for(int i=0;i<m;++i)
if(vote[i]==0){
elem[i]=x; vote[i]++;
flag = true; break;
}
if(flag) continue;

for(int i=0;i<m;++i) // x非候选者,且无空闲候选位
vote[i]--;
}
vector<int> cnts(m);
for(auto x:nums){
for(int i=0;i<m;++i)
if(x==elem[i] && vote[i]>0)
cnts[i]++;
}
for(int i=0;i<m;++i){
if(vote[i]>0 && cnts[i]>nums.size()/3)
ans.push_back(elem[i]);
}
return ans;
}

约瑟夫环

1
2
3
4
5
6
int lastRemaining(int n, int m) {
int ans=0;
for(int i = 2;i <= n; ++i)
ans = (ans + m) % i;
return ans;
}

随机化

1.随机数生成

1
2
3
4
random_device rd;
mt19937 gen(rd());
uniform_int_distribution<> dis(1, 10);
cout << dis(gen) << endl;

2.已知小范围随机数,得到一个大范围随机数

对于 randN = [1,n],通过(randN-1) * N + randN 得到 [1,n*n] 范围的等概率随机数

3.已知小范围随机数,得到一个确定范围的随机数

先采用 1 得到更大范围的随机数 randK,然后对超过确定范围数进行拒绝

4.已知大范围随机数,得到一个小范围随机数

如果 N 是 K 的整数倍,可以通过 randN 一步得到 randK:randK = (randN % K) + 1;


单调栈

寻找下一个最/更大值

柱状图中最大的矩形: 给定 n 个柱子的高度(宽度为 1),求能勾勒矩形的最大面积

题解:遍历每个高度,以当前高度为基准,寻找最大的宽度组成最大的矩形面积,也就是分别找左边/右边首个小于当前高度的下标left、right。用递增的单调栈来确定这两个边界:right 为当前遍历的下标 i,left 为单调栈中保存的前一个下标(高度更低)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int largestRectangleArea(vector<int>& heights) {
vector<int> h{0, 0};
h.insert(h.begin() + 1, heights.begin(), heights.end());
int ans = 0;
stack<int> st;
for (int i = 0; i < h.size(); ++i) {
while (!st.empty() && h[i] < h[st.top()]) { // <=也可以
int height = h[st.top()]; st.pop();
if (st.empty()) continue;
int width = i - st.top() - 1;
ans = max(ans, height * width);
}
st.push(i);
}
return ans;
}

滑动窗口最大值:求数组中每个长度为K的区间内的最大值

  1. 在窗口内,若 nums[j] < nums[j+1]nums[j] 没用了可以扔掉,因为有更大的值后出窗口
  2. 因此用一个单调递减队列(deque)维护窗口内有意义的元素,直接 deque.front() 获得最大值
  3. l, i 维护窗口左右边界,从队列中 pop 的条件是窗口大小固定为 k,即 i - l == k

并查集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int n = 1005; // n根据题目中节点数量而定
vector<int> father = vector<int>(n);

void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
father[v] = u;
}

字典树

剑指Offer II :62-67

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Trie {
public:
bool isWord;
vector<Trie*> child;
Trie(): isWord(false), child(26, nullptr) {}

void insert(string& s) {
Trie* p = this;
for(auto c: s) {
if(p->child[c - 'a'] == nullptr)
p->child[c - 'a'] = new Trie();
p = p->child[c - 'a'];
}
p->isWord = true;
}
};

添加与搜索单词:将一些 word 添加到 dict 中,用来判断能否与后续字符串匹配(含 ‘.’ 匹配符)

Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class WordDictionary {
public:
bool end{false};
vector<WordDictionary*> childs;
WordDictionary(): childs(26, nullptr) {}

void addWord(string word) {
WordDictionary *w = this;
for (auto c : word) {
if (!w->childs[c - 'a']) {
w->childs[c - 'a'] = new WordDictionary();
}
w = w->childs[c - 'a'];
}
w->end = true;
}
bool search(string word) {
return find(word, 0, this);
}
bool find(string& word, int i, WordDictionary* w) {
if (i == word.size()) return w->end;
if (isalpha(word[i])) {
// 单路径
return w->childs[word[i]-'a'] &&
find(word, i + 1, w->childs[word[i] - 'a']);
} else {
// 通配符,需遍历多个分支,其一满足即可
for (auto c : w->childs) {
if (c && find(word, i + 1, c)) return true;
}
}
// 都走不通
return false;
}
};

多路归并

查找和最小的 K 对数字

超级丑数:给定质数primes数组,丑数由其中任意质数累乘得到,求第k个丑数

DP-先求再存

  1. 求第k个丑数,需要遍历每个prime[i]与以有丑数dp[j]相乘的最小值,遍历prime复杂度O(n),遍历已有丑数同为O(n),对于后者可以考虑优化
  2. 直观上,prime[i]*dp[0]为最小,但为了防止重复乘dp[0]、dp[1]...,需要用next[i]记录prime[i]已经乘过了前几个丑数,即下次该从下一个(next[i])丑数开始乘
  3. 具体流程,每求下一个丑数时,遍历prime[i]*dp[next[i]]找到最小值,对于每个最小令next[i]++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int nthSuperUglyNumber(int n, vector<int>& primes) {
vector<int> next(primes.size(), 0), dp{1};
while (dp.size() < n) {
int pid = -1, ugly = INT_MAX;
for (int i = 0; i < primes.size(); ++i) {
long tmp = (long)primes[i] * dp[next[i]];
if (tmp < ugly) {
ugly = tmp;
pid = i;
}
}
next[pid]++;
if (dp.back() < ugly) { // 去重
dp.push_back(ugly);
}
}
return dp.back();
}

优先队列-先存再求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 时间复杂度高,可能乘法溢出
int nthSuperUglyNumber(int n, vector<int>& primes) {
priority_queue <double, vector<double>, greater<double>> q;
unordered_set<int> s;
s.insert(1); // 去重
int answer = 1;
for (int i = 1; i < n; ++i){
for (int j: primes) {
int multi = answer * j;
if(!s.count(multi)){
q.push(multi);
s.insert(multi);
}
}
answer = q.top();
q.pop();
}
return answer;
}

区间相关

滑动窗口

最小覆盖子串:返回 s 中涵盖 t 所有字符的最小子串

题解:每轮遍历窗口右扩 1,更新有效字符数 valid;valid 等于 t 长度时,保存结果,不断尝试收缩左窗口;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
string minWindow(string s, string t) {
int valid = 0, ans_idx = -1, ans_len = INT_MAX;
unordered_map<char, int> mp;
for (auto c: t) mp[c]++;
for (int i = 0, l = 0; i < s.size(); ++i) {
char c = s[i];
if (mp.count(c)) {
if (--mp[c] >= 0) valid++;
}
while (valid >= t.size()) {
if (i - l + 1 < ans_len) {
ans_len = i - l + 1;
ans_idx = l;
}
char d = s[l];
if (mp.count(d)) {
if (++mp[d] > 0) valid--;
}
l++;
}
}
return ans_idx < 0 ? "" : s.substr(ans_idx, ans_len);
}

字符串的排列:判断 s1 的排列之一是否为 s2 的子串

题解:每轮遍历窗口右扩 1,更新合法字符数 valid ;窗口大于 s1 长度时,收缩左窗口 1 并更新;期间 valid==s1.size() 时返回 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool checkInclusion(string s1, string s2) {
int valid = 0;
unordered_map<char, int> mp;
for (auto c: s1) mp[c]++;
for (int i = 0, l = 0; i < s2.size(); ++i) {
char c = s2[i];
if (mp.count(c)) {
if (--mp[c] >= 0) valid++;
}
if (i - l == s1.size()) {
char d = s2[l];
if (mp.count(d)) {
if (++mp[d] > 0) valid--;
}
l++;
}
if (valid == s1.size()) return true;
}
return false;
}

前缀和

前缀与后缀和的统一写法,但需要特殊处理 0 个元素的前缀/后缀和

1
2
3
4
5
6
7
8
9
10
vector<int> nums{1,0,2,4,2,3,1};
// pre_sum[i] 为 [0, ..., i],suf_num[i] 为 [i, ..., n-1]
vector<int> pre_sum(nums.size()), suf_sum(nums.size());
pre_sum[0] = nums[0];
for (int i = 1; i < nums.size(); i++)
pre_sum[i] = pre_sum[i - 1] + nums[i];

suf_sum.back() = nums.back();
for (int i = nums.size() - 2; i >= 0; --i)
suf_sum[i] = suf_sum[i + 1] + nums[i];

差分

区间修改(同增同减)- 求修改后的数组

原数组为a,差分数组为b

1
2
b[0] = a[0]
b[i] = a[i] - a[i - 1]

对区间 [x1, x2] 所有值加 y

1
2
b[x1] += y
b[x2 + 1] -= y // x2 + 1 < n

根据差分数组求原数组

1
2
a[0] = b[0]
a[i] = a[i - 1] + b[i]

树状数组

单点修改 - 区间查询、单点查询

区域和检索 - 数组可修改:求区间和且会更新数组元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
vector<int> tree;  // tree[i] 为前 i-1 前缀和
vector<int> nums; // 原数组

int lowBit(int x) {
return x & -x;
}
// 把序列第 index 个数增加 val,O(nlgn)
void add(int index, int val) {
while (index < tree.size()) {
tree[index] += val;
index += lowBit(index);
}
}
// 查询前 index 个元素的前缀和,O(nlgn)
int preSum(int index) {
int sum = 0;
while (index > 0) {
sum += tree[index];
index -= lowBit(index);
}
return sum;
}
int main(){
// input nums
tree.assign(nums.size() + 1, 0);
for(int i = 0; i < nums.size(); ++i){
add(i + 1, nums[i]);
}
}

线段树

区间修改 - 区间查询、单点查询


一些笔试题目

对称飞行器

如果没有对称飞行器,就是一道普通广搜题。但是加了飞行器,如果飞行次数没有限制,也是一道普通广搜题只是除了四个direction多了一个分支。可是飞行器有次数限制。那么如果用(x,y)点的状态应该再加一个维度:飞行次数z。因为当z1不等于z2时,P(x,y,z1)和Q(x,y,z2)是两种不同的状态。

假设z1 < z2,即P和Q在相同的横纵坐标,但是P剩余的飞行次数更多。如果从(x,y,z2)走到终点的最优解是n步,则从(x,y,z1)一定可以走n步到达终点。此时P状态走到最优点的步数更少。所以在层次遍历的最优解中不该包含z2.

也就是说在向z增加转移的过程中,我们应该看 matrix[x][y][0]...matrix[x][y][z] 中是否已经有为1的点,如果有就不必再在第z层再走x,y。

Folding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
struct Node {
int x, y;
int fly;
Node() = default;
Node(int x_, int y_, int f_): x(x_), y(y_), fly(f_) {}
};

int n, m;
queue<Node> q;
vector<vector<int>> vis;
vector<vector<char>> board;
vector<int> delta{-1, 0, 1, 0, -1};

bool check(Node& node) {
return node.x >= 0 && node.x < n && node.y >= 0 && node.y < m &&
board[node.x][node.y] != '#' && !vis[node.x][node.y];
}
int bfs(int i, int j) {
q.emplace(i, j, 5);
vis[i][j] = 1;
int level = 0;
while (!q.empty()) {
int size = q.size();
while (size--) {
Node cur = q.front(); q.pop();
if (board[cur.x][cur.y] == 'E') {
return level;
}
for (int i = 0; i <= 4; ++i) {
Node tmp;
if (i == 4) { // fly
if (cur.fly > 0) {
tmp.x = n - 1 - cur.x;
tmp.y = m - 1 - cur.y;
tmp.fly = cur.fly - 1;
}
} else {
tmp.x = cur.x + delta[i];
tmp.y = cur.y + delta[i + 1];
tmp.fly = cur.fly;
}
if (check(tmp)) {
vis[tmp.x][tmp.y] = 1;
q.push(tmp);
}
}
}
level++;
}
return -1;
}
int main() {
cin >> n >> m;
board.assign(n, vector<char>(m, 0));
vis.assign(n, vector<int>(m, 0));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
cin >> board[i][j];
}
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (board[i][j] == 'S') {
cout << bfs(i, j) << endl;
return 0;
}
}
}
}

自定义类型的map/set需要重载hash<=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Node {
int a, b;
bool operator<(const Node& other) const {
return a == other.a && b == other.b;
};
bool operator==(const Node& other) const {
return a == other.a && b == other.b;
};
};
template<>
struct hash<Node> {
size_t operator()(const Node& node) const {
return hash<int>()(node.a) & hash<int>()(node.b);
}
};

unordered_map<Node, int> mp;
unordered_set<Node> st;