数组 堆排序 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]) { tmp[k++] = nums[i++]; } else { tmp[k++] = nums[j++]; } } while (i <= m) { 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 个数相同的子数组 :找到含有相同数量的 0 和 1 的最长连续子数组长度
题解:将数组中的 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 ) { 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 ); 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 ); 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能把两岛连起来
题解:
找到任意一个岛
通过dfs将整个岛1标记为2,并把边缘点0入队列
从边缘点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 ; } };
被围绕的区域 :矩阵包含 X 和 O ,找到所有被 X 围绕(四个方向)的区域,并将这些区域里所有的 O 用 X 填充。
题解: 找到未被包围区域不填充即可,且注意到未包围区域一定挨着边界,故从四个边界开始dfs找到O区域即可
状态转换
关键在于“建图”,过程灵活,只需能完成遍历即可
相关题目:剑指 Offer II 109. 开密码锁 ,剑指 Offer II 111. 计算除法
单词接龙 :求字符串 start 转换至 end 最小步数,每次转换仅改变一个字符,且改变后字符串在所给 dic 中
题解:
字符串之间的转换关系可抽象为无向图。遍历找相邻结点时,依次查找改变字符后的新字符串是否在 dic 内
无向图中顶点间的最短路径的长度通过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 )); 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]; 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 ; 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; vector<int > pos (n) ; 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; 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]; }
字符串问题 交错字符串 :问s1和s2交错拼接能否组成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=0或j=0的情况:i=0时,word1为空,只能删除/插入j次;j=0时同理
优化空间
正序遍历 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) { for (int j = 1 ; j <= n; ++j) { if (p[j - 1 ] != '*' ) { dp[i][j] |= check (i, j) && dp[i - 1 ][j - 1 ]; } else { dp[i][j] |= dp[i][j - 2 ]; 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 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次,求最大利润
状态定义:
状态转移
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] }
初始化
dp[0][0][0]=0:第一天没交易过没股票
dp[0][1][1]=-price[0]:第一天第一次交易买了股票
其他均不合法情况,设置为INI_MIN
执行结果
最后一天、不持有股票、交易0~k次中的最大值,即 max{dp[n-1][i][0]}, 0<=i<=k
代码
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]) 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]) 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]) 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) if ((mask>>d & 1 ) == 0 ) 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 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 ; 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 long get_count (long prefix, long n) { long cnt = 0 ; long a = prefix, b = prefix + 1 ; 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 ; while (p < k) { int cnt = get_count (prefix, n); if (p + cnt > k) { prefix *= 10 ; p++; } else { 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的过程可以看作链表,且一定有环,分为两种情况
a->b->c->b->c...不合法
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); } 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
异或 (找缺失数、出现一次数)
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 ; vector<int > elem (m) ,vote (m) ; for (auto x:nums){ bool flag=false ; for (int i=0 ;i<m;++i) if (vote[i]>0 && elem[i]==x){ vote[i]++; flag = true ; break ; } if (flag) continue ; flag = false ; 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) 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的区间内的最大值
在窗口内,若 nums[j] < nums[j+1] 则 nums[j] 没用了可以扔掉,因为有更大的值后出窗口
因此用一个单调递减队列(deque)维护窗口内有意义的元素,直接 deque.front() 获得最大值
用 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 ; 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]); } bool isSame (int u, int v) { u = find (u); v = find (v); return u == v; } 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-先求再存
求第k个丑数,需要遍历每个prime[i]与以有丑数dp[j]相乘的最小值,遍历prime复杂度O(n),遍历已有丑数同为O(n),对于后者可以考虑优化
直观上,prime[i]*dp[0]为最小,但为了防止重复乘dp[0]、dp[1]...,需要用next[i]记录prime[i]已经乘过了前几个丑数,即下次该从下一个 (next[i])丑数开始乘
具体流程,每求下一个丑数时,遍历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 }; 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
根据差分数组求原数组
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; vector<int > nums; int lowBit (int x) { return x & -x; } void add (int index, int val) { while (index < tree.size ()) { tree[index] += val; index += lowBit (index); } } int preSum (int index) { int sum = 0 ; while (index > 0 ) { sum += tree[index]; index -= lowBit (index); } return sum; } int main () { 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 ) { 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;