语法
1 2 3 4 5 6 7 8 9 string line; getline (cin, line); if (line.back () != ')' ) line += " (+0)" ; int h1, m1, s1, h2, m2, s2, day; sscanf (line.c_str (), "%d:%d:%d %d:%d:%d (+%d)" , &h1, &m1, &s1, &h2, &m2, &s2, &day); int S = h1*3600 + m1*60 + s1; int E = h2*3600 + m2*60 + s2; int ans = E - S + day*24 *3600 ; printf ("%02d:%02d:%02d\n" ,ans/3600 ,ans/60 %60 ,ans%60 );
优先unordered_map,要求顺序的时候用map(自动排序)
二分
if的判断条件是让mid落在满足你想要结果的区间内
1.循环必须是l < r
2.if判断条件看是不是不满足条件, 然后修改上下界
3.若if else后是r = mid - 1,则前面mid 语句要加1
4.出循环一定是l == r,所以l和r用哪个都可以
4
二分只有下面两种情况
1:找大于等于给定数的第一个位置 (满足某个条件的第一个数)
2:找小于等于给定数的最后一个数 (满足某个条件的最后一个数)
l r r l 1 lr1lr1
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 int SL (int l, int r) { while (l < r) { int mid = l + r >> 1 ; if (check (mid)) r = mid; else l = mid + 1 ; } return l; } int SR (int l, int r) { while (l < r) { int mid = l + r + 1 >> 1 ; if (check (mid)) l = mid; else r = mid - 1 ; } return r; } double bsearch_3 (double l, double r) { const double eps = 1e-6 ; while (r - l > eps){ double mid = (l + r) / 2 ; if (check (mid)) r = mid; else l = mid; } return l; }
进制转换
十进制转R进制 如果R大于10,可以定义一个转换数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 char h[17 ] = {'0' ,'1' ,'2' ,'3' ,'4' ,'5' ,'6' ,'7' ,'8' ,'9' ,'A' ,'B' ,'C' ,'D' ,'E' ,'F' };do { a[num ++] = n % 16 ; n /= 16 ; }while (n != 0 ); for (int i = num - 1 ; i >= 0 ; i --) cout << h[a[i]];string res; do { int y = x % b; if (y <= 9 ) res += y + '0' ; else res += y - 10 + 'A' ; x /= b; }while (x != 0 );
R进制转十进制 秦九韶算法
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 int res = 0 , sgn = 1 ;if (str[0 ] != '-' ){ for (int i = 2 ; str[i]; i ++ ) if (isdigit (str[i])) res = res * 16 + str[i] - '0' ; else res = res * 16 + tolower (str[i]) - 'a' + 10 ; } else { sgn = -1 ; for (int i = 3 ; str[i]; i ++ ) if (isdigit (str[i])) res = res * 16 + str[i] - '0' ; else res = res * 16 + tolower (str[i]) - 'a' + 10 ; } cout << sgn * res << endl; string s = "2022" ; int res = 0 ;for (int i = 0 ; i < s.size (); i++) res = res * 9 + s[i] - '0' ;
日期模板
求从2001年1月1日到2021年12月31日中 有多少个完全日期
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 #include <bits/stdc++.h> using namespace std;typedef long long ll;const int N = 1e5 + 5 , mod = 1e9 + 7 ;struct Time { int y, m, d; }; int res, day[] = {0 , 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 };void add (Time &a) { a.d++; int x = day[a.m]; if (((a.y % 4 == 0 && a.y % 100 != 0 ) || a.y % 400 == 0 ) && a.m == 2 ) x++; if (a.d > x) a.m++, a.d = 1 ; if (a.m > 12 ) a.y++, a.m = 1 ; } bool check (Time a) { int s[] = {a.y, a.m, a.d}, y = 0 ; for (int i = 0 ; i < 3 ; i++) { int x = s[i]; while (x) { y += x % 10 ; x /= 10 ; } } int z = sqrt (y); return z * z == y; } int main () { Time a = {2001 , 1 , 1 }, b = {2021 , 12 , 31 }; while (a.y != b.y || a.m != b.m || a.d != b.d) { if (check (a)) res++; add (a); } cout << res; return 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 int year (int n) { if ((n%4 ==0 &&n%100 !=0 ) || (n%400 ==0 )){ return 29 ; } else { return 28 ; } } int quzhen (int n) { int sum=0 ; while (n){ int k=n%10 ; sum+=k; n=n/10 ; } return sum; } int check (int n) { int k=sqrt (n); if (k*k==n){ return 1 ; } return 0 ; } int main () { int day[13 ]={0 ,31 ,28 ,31 ,30 ,31 ,30 ,31 ,31 ,30 ,31 ,30 ,31 }; int ans=0 ; for (int i=2001 ;i<=2021 ;i++){ for (int j=1 ;j<=12 ;j++){ day[2 ]=year (i); for (int z=1 ;z<=day[j];z++){ int a=quzhen (i)+quzhen (j)+quzhen (z); cout<<i<<" " <<j<<" " <<z<<" " <<a<<" " <<check (a)<<endl; if (check (a)){ ans++; } } } } cout<<ans<<endl; return 0 ; }
素数筛
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int primes[N], cnt; bool st[N]; void get_primes (int n) { st[0 ] = true ; st[1 ] = true ; for (int i = 2 ; i <= n; i ++ ){ if (!st[i]) primes[cnt++] = i; for (int j = 0 ; primes[j] <= n / i; j++){ st[primes[j] * i] = true ; if (i % primes[j] == 0 ) break ; } } }
前缀和与差分
涉及到区间的求交 求并集
原数组就是差分数组的前缀和。我们可以举个例子:
原始数组:9 3 6 2 6 8
差分数组:9 -6 3 -4 4 2
高精度加减法
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 vector<int > add (vector<int >& a, vector<int > &b) { int t = 0 ; vector <int > c; for (int i = 0 ; i < a.size () || i < b.size (); i++){ if (i < a.size ()) t += a[i]; if (i < b.size ()) t += b[i]; c.push_back (t % 10 ); t /= 10 ; } if (t) c.push_back (1 ); return c; } bool cmp (vector<int > &A , vector<int > &B) { if (A.size () != B.size ()) return A.size () > B.size (); for (int i = A.size () - 1 ; i >= 0 ; i --) if (A[i] != B[i]) return A[i] > B[i]; return true ; } vector<int > sub (vector<int > &A , vector<int > &B) { int t = 0 ; vector<int > C; for (int i = 0 ; i < A.size (); i++){ t = A[i] - t; if (i < B.size ()) t -= B[i]; C.push_back ((t + 10 ) % 10 ); if (t < 0 ) t = 1 ; else t = 0 ; } while (C.size () > 1 && C.back () == 0 ) C.pop_back (); return C; } for (int i = a.length () - 1 ; i >= 0 ; i--) va.push_back (a[i] - '0' );for (int i = b.length () - 1 ; i >= 0 ; i--) vb.push_back (b[i] - '0' );auto vc = add (va,vb);if (cmp (a, b)) vc = sub (a, b); else vc = sub (b, a) , cout << '-' ; for (int i = vc.size () - 1 ; i >= 0 ; i--) printf ("%d" ,vc[i]);
离散化
把离散的数据紧凑化,减少遍历次数
1 2 3 4 5 6 7 8 vector<int > alls; sort (alls.begin (), alls.end ()); alls.erase (unique (alls.begin (), alls.end ()), alls.end ()); int pos = lower_bound (alls.begin (), alls.end (), x) - alls.begin () + 1 ;
裸LCA
求两个节点的最小公共祖先。如果会写倍增版本的LCA,可以快一个数量级。
1 2 3 4 5 6 7 8 9 10 int LCA (int x, int y) { while (x != y){ if (x < y) y >>= 1 ; else x >>= 1 ; } return x; }
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 #include <bits/stdc++.h> using namespace std;const int N = 1010 ;int n, m;int l[N], r[N], p[N], d[N];void dfs (int root, int depth) { if (root == -1 ) return ; d[root] = depth; dfs (l[root], depth+1 ); dfs (r[root], depth+1 ); } int getParent (int x1, int x2) { if (d[x1] < d[x2]) return getParent (x2, x1); while (d[x1] > d[x2]) x1 = p[x1]; while (x1 != x2) x1 = p[x1], x2 = p[x2]; return x1; } int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++){ int left, right; cin >> left >> right; l[i] = left, r[i] = right; if (left != -1 ) p[left] = i; if (right != -1 ) p[right] = i; } dfs (1 , 1 ); int x1, x2; while ( m -- ){ cin >> x1 >> x2; int parent = getParent (x1, x2); cout << d[x1] + d[x2] - 2 * d[parent] << endl; } return 0 ; }
字典树
trie树中 二维数组son存的是节点的下标 第一维就是下标的值 第二维代表着儿子(理解为具体元素)
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 son[N][26 ]; int cnt[N]; int idx; char str[N]; void insert (char str[]) { int p = 0 ; for (int i = 0 ; str[i]; i++) { int u = str[i] - 'a' ; if ( !son[p][u]) son[p][u] = ++idx; p = son[p][u]; } cnt[p] ++; } int query (char str[]) { if ( !son[p][u] ) return 0 ; return cnt[p]; }
线段树
拓扑排序
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 int cnt = 0 ;struct edge { int to; int next; } e[N]; void add (int u, int v) { cnt++; e[cnt].to = v; e[cnt].next = h[u]; h[u] = cnt; } while (m--) { int x, y; cin >> x >> y; add (x,y); d[y]++; } for (int i = 1 ;i <= n; ++i) if (d[i] == 0 ) q.push (i); while (!q.empty ()){ int t = q.front (); q.pop (); top[index++] = t; for (int i = h[t]; i; i = e[i].next){ int j = e[i].to; d[j]--; if (d[j] == 0 ) q.push (j); } } for (int i = 1 ; i <= n; i++) cout << top[i] << ' ' ;
Floyd
1 2 3 4 5 6 7 8 9 10 11 12 13 for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= n; j++) if (i == j) d[i][j] = 0 ; else d[i][j] = INF; for (int k = 1 ; k <= n; k++) for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= n; j++) d[i][j] = min (d[i][j], d[i][k] + d[k][j]);
数论
容斥原理
求矩形交集等
约数
算数基本定理
如果 N = p1^c1 * p2^c2 * … *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * … * (ck + 1)
约数之和: (p1^0 + p1^1 + … + p1^c1) * … * (pk^0 + pk^1 + … + pk^ck
合数一定可以分解成唯一的一组质数的乘积
翡蜀定理
关于ax + by=cax + by=c 有整数解的定理 :设a,b是整数且 gcd(a, b) = d,
如果d不能整除c,那么方程ax+by=c没有整数解
如果d能整除c,那么存在无穷多个整数解
不难得知,a与b互质时,也存在无穷多个整数解
模运算
运算的结合律: (a+b) % p = (a%p + b %p) %p
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 void divide (int x) { for (int i = 2 ; i <= x / i; i ++ ){ if (x % i == 0 ) { int s = 0 ; while (x % i == 0 ) { x /= i; s++; } cout << i << '^' << s << endl; } } if (x > 1 ) cout << x << '^' << 1 << endl; } vector<int > res; void get_divisors ( int x) { for (int i = 1 ; i <= x / i; i ++ ){ if (x % i == 0 ) { res.push_back (i); if (i != x / i) res.push_back (x / i); } } sort (res.begin (), res.end ()); } long long quick_power (int a, int b, int p) { long long ans = 1 % p; while (b) { if (b & 1 ) ans = (long long )ans * a % p; a = (long long )a * a % p; b >>= 1 ; } return ans; }
归并排序
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 void merge_sort (int l,int r) { int mid = (l + r) >> 1 ; merge_sort (l, mid); merge_sort (mid + 1 ,r); int i = l,j = mid + 1 ,k = 0 ; while (i <= mid && j <= r){ if (a[i] <= a[j]) tmp[k++] = a[i++]; else { tmp[k++] = a[j++]; } } while (i <= mid) tmp[k++] = a[i++]; while (j <= r) tmp[k++] = a[j++]; }
滑动窗口
维护一个单调递增的双向队列。窗口前进条件:①窗口尺寸超出k,删除队头 ②队尾小于当前元素,删除队尾并添加当前元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 for (int i = 0 ; i < n; ++i) { while (!q.empty () && i - k >= q.front ()) q.pop_front (); while (!q.empty () && a[q.back ()] >= a[i]) q.pop_back (); q.push_back (i); if (i + 1 >= k) cout << a[q.front ()] << ' ' ; }
区间贪心
求最大不相交的区间个数
按右端点排序,则直接比较。否则用优先队列维护右端点
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 #include <iostream> #include <algorithm> using namespace std;const int N = 100010 ;int n;struct Range { int l, r; bool operator < (const Range &W)const { return r < W.r; } }range[N]; int main () { scanf ("%d" , &n); for (int i = 0 ; i < n; i ++ ) scanf ("%d%d" , &range[i].l, &range[i].r); sort (range, range + n); int res = 0 , ed = -2e9 ; for (int i = 0 ; i < n; i ++ ) if (range[i].l > ed) { res ++ ; ed = range[i].r; } printf ("%d\n" , res); return 0 ; }
Dijkstra
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 typedef pair<int ,int > PII; priority_queue<PII, vector<PII>, greater<PII>> q; int n, m, s;int dist[N];int h[N], cnt;bool st[N];struct edge { int to; int w; int next; } e[N]; void add (int u, int v, int w) { cnt++; e[cnt].to = v; e[cnt].w = w; e[cnt].next = h[u]; h[u] = cnt; } void f () { dist[s] = 0 ; q.push ({0 , s}); while (!q.empty ()) { auto t = q.top (); q.pop (); int dis = t.first; int ver = t.second; if (!st[ver]){ st[ver] = true ; for (int i = h[ver]; i; i = e[i].next) { int j = e[i].to; if (dist[j] > dis + e[i].w) { dist[j] = dis + e[i].w; q.push ({dist[j], j}); } } } } }
SPFA
dijkstra是基于贪心的思想,每次选择最近的点去更新其它点,过后就不再访问。
spfa算法,只要有某个点的距离被更新了,就把它加到队列中,去更新其它点,所有每个点有被重复加入队列的可能。st是一个集合 ,不是检验队列中的点。用到的时候就加入到集合中,否则就弹出集合。
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 queue<int > q; void f () { q.push (s); st[s] = true ; while (!q.empty ()) { int t = q.front (); q.pop (); st[t] = false ; for (int i = h[t]; i; i = e[i].next) { int j = e[i].to; if (dist[j] > dist[t] + e[i].w) { dist[j] = dist[t] + e[i].w; if (!st[j]){ st[j] = true ; q.push (j); } } } } }
引申:判断是否存在负环
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 queue<int > q; bool f () { for (int i = 1 ; i <= n; i++){ q.push (i); st[i] = true ; } while (!q.empty ()) { int t = q.front (); q.pop (); st[t] = false ; for (int i = h[t]; i; i = e[i].next) { int j = e[i].to; if (dist[j] > dist[t] + e[i].w) { dist[j] = dist[t] + e[i].w; cnt[j] = cnt[t] + 1 ; if (cnt[v] >= n) return true ; if (!st[j]){ st[j] = true ; q.push (j); } } } } return false ; }
并查集
(1)朴素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int p[N]; int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } for (int i = 1 ; i <= n; i ++ ) p[i] = i;p[find (a)] = find (b);
(2)维护size
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int p[N], size[N];int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } for (int i = 1 ; i <= n; i ++ ){ p[i] = i; size[i] = 1 ; } size[find (b)] += size[find (a)]; p[find (a)] = find (b);
(3)带权
先记录下原本父节点的编号,因为在路径压缩后父节点就变为根节点了。将当前节点的权值加上原本父节点的权值,父节点的权值为父节点到根节点的权值,因此加上这个权值就会得到当前节点到根节点的权值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int find (int x) { if (x != p[x]){ int t = p[x]; p[x] = find (p[x]); value[x] += value[t]; } return p[x]; } int px = find (x);int py = find (y);if (px != py){ p[px] = py; value[px] = -value[x] + value[y] + s; }
Kruskal
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 int n, m; int p[N]; struct Edge { int a, b, w; bool operator < (const Edge &W)const { return w < W.w; } }edges[M]; int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } int kruskal () { sort (edges, edges + m); for (int i = 1 ; i <= n; i ++ ) p[i] = i; int res = 0 , cnt = 0 ; for (int i = 0 ; i < m; i ++ ){ int a = edges[i].a, b = edges[i].b, w = edges[i].w; a = find (a), b = find (b); if (a != b) { p[a] = b; res += w; cnt ++ ; } } if (cnt < n - 1 ) return INF; return res; }
双指针
对于一个序列,用两个指针维护一段区间
1 2 3 4 5 6 7 8 for (int i = 0 , j = 0 ; i < n; i ++){ while (j < i && check (i, j)) j ++ ; ``` ``` }
扩展欧几里得
1 2 3 4 5 6 7 8 9 10 int exgcd (int a, int b, int &x, int &y) { if (!b){ x = 1 ; y = 0 ; return a; } int d = exgcd (b, a % b, y, x); y -= (a/b) * x; return d; }
组合数
加法原理
a位巨佬中抽出b位巨佬的方案数 可以 分为两类
1.选抽风巨佬 只要从a-1位巨佬中选出b-1位巨佬
2.不选抽风巨佬 从剩下a-1位巨佬中选出b位巨佬
1 2 3 4 5 6 7 for (int i = 0 ; i < N; i ++ ) for (int j = 0 ; j <= i; j ++ ) if (!j) c[i][j] = 1 ; else c[i][j] = (c[i - 1 ][j] + c[i - 1 ][j - 1 ]) % mod;
递归实现组合数枚举
参数:临时数组的下标、起始下标、[已经选的数字个数…](int u , int start, […])
**变式:**1.要求每个数字可以重复选取,改为 dfs( u + 1, i )
2.去重问题:给定的集合中有多个重复元素,要求在满足题意的子集中,不能有完全相同的子集。翻译:不能用同一层使用过的元
素。需要先排序并引入bool数组。更具体的,如下代码所示。
3.未必是在n个数中选m个固定的数, 可以在一定的条件下保存答案并退出
4.排列问题需要用到used数组
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 void f (vector<int >& candidates, int target, int sum, int startIndex, vector<bool >& used) { if (sum == target) { result.push_back (path); return 0 ; } for (int i = startIndex; i < candidates.size () && sum + candidates[i] <= target; i++) { if (i > 0 && candidates[i] == candidates[i - 1 ] && used[i - 1 ] == false ) { continue ; } sum += candidates[i]; path.push_back (candidates[i]); used[i] = true ; backtracking (candidates, target, sum, i + 1 , used); used[i] = false ; sum -= candidates[i]; path.pop_back (); } }
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 #include <iostream> using namespace std;int n, m; const int N = 20 ;int a[20 ];void dfs (int u, int start) { if (u == m + 1 ){ for (int i = 1 ; i <= m; i ++ ) cout << a[i] << ' ' ; cout << endl; return ; } for (int i = start; i <= n; i ++ ){ a[u] = i; dfs (u + 1 , i + 1 ); a[u] = 0 ; } } int main () { cin >> n >> m; dfs (1 , 1 ); return 0 ; }
二进制枚举
在集合中选出所有子集 时间复杂度为2^n
1 2 3 4 5 6 7 8 9 10 for ( int i = 0 ; i < 1 << n; i++){ for (int j = 0 ; j < n; j ++){ if ( i >> j & 1 ) { } } }
DP
要点:1.初始化 2.dp的严格定义 3.不重不漏地转移 4.套经典模型
走方格
一个人位于一个 m x n 网格的左上角,每次只能向下或者向右移动一步。网格中有障碍物。那么从左上角到右下角会有多少条不同的路径?
1 2 3 4 5 6 7 8 9 10 11 12 for (int i = 0 ; i < m; i++) dp[i][0 ] = 1 ;for (int j = 0 ; j < n; j++) dp[0 ][j] = 1 ; for (int i = 1 ; i < m; i++) { for (int j = 1 ; j < n; j++) { if (有障碍) continue ; dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ]; } } cout << dp[m - 1 ][n - 1 ] << endl;
爬楼梯
一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。 计数问题。
1 2 3 4 5 6 7 8 9 10 11 int climbStairs (int n) { vector<int > dp (n + 1 , 0 ) ; dp[0 ] = 1 ; for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { if (i - j >= 0 ) dp[i] += dp[i - j]; } } return dp[n]; }
背包
外层物品正序,内层背包倒序或者正序。
01背包
有一维和二维表示
1 2 3 4 5 6 7 8 vector<int > dp (bagWeight + 1 , 0 ) ;for (int i = 0 ; i < weight.size (); i++) { for (int j = bagWeight; j >= weight[i]; j--) { dp[j] = max (dp[j], dp[j - weight[i]] + value[i]); } } cout << dp[bagWeight] << endl;
完全背包
完全背包的物品是可以添加多次的,所以要从小到大去遍历
1 2 3 4 5 6 for (int i = 0 ; i < weight.size (); i++) { for (int j = weight[i]; j <= bagWeight ; j++) { dp[j] = max (dp[j], dp[j - weight[i]] + value[i]); } }
线性
最长上升子序列
根据数据范围选择不同的处理方式
1 2 3 4 5 6 7 8 9 10 11 12 for (int i = 0 ; i < n; i++) cin >> a[i]; for (int i = 0 ; i < n; i++){ f[i] = 1 ; for (int j = 0 ; j < i; j++) if (a[i] > a[j]) f[i] = max (f[i], f[j] + 1 ); res = max (res, f[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 30 31 32 33 34 35 int len = 0 ;for (int i = 0 ; i < n; i++){ int l = 0 , r = len; while (l < r){ int mid = (l + r + 1 ) >> 1 ; if (q[mid] < a[i]) l = mid; else r = mid - 1 ; } len = max (len, r + 1 ); q[r + 1 ] = a[i]; f[i] = r + 1 ; } cout << len << endl; for (int i = k - 1 , t = len; i > 0 ; i--){ if (f[i] == t){ res[t] = cnt[i]; t--; } if (t == 0 ) break ; } for (int i = 1 ; i <= len; i++) cout << res[i] << ' ' ;
变式:最大子序列和。 f[i] = max(f[i], f[j] + a[i]);
最长公共子序列
1 2 3 4 5 6 7 8 9 10 11 vector<vector<int >> dp (text1.size () + 1 , vector <int >(text2.size () + 1 , 0 )); for (int i = 1 ; i <= text1.size (); i++) { for (int j = 1 ; j <= text2.size (); 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 ]); } } } return dp[text1.size ()][text2.size ()];
变式1:给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
思路:只要求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
变式2:给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 例如,"ace"是"abcde"的一个子序列,而"aec"不是 。
1 2 if (s[i - 1 ] == t[j - 1 ]) dp[i][j] = dp[i - 1 ][j - 1 ] + 1 ;else dp[i][j] = dp[i][j - 1 ];
最大连续子序列和
1 2 3 4 5 6 7 8 dp[0 ] = nums[0 ]; int result = dp[0 ];for (int i = 1 ; i < nums.size (); i++) { dp[i] = max (dp[i - 1 ] + nums[i], nums[i]); if (dp[i] > result) result = dp[i]; } return result;
连续子数组的最大和
1 2 3 4 5 6 7 int res = -0x3f3f3f3f , sum = 0 ;for (int i = 0 ; i < n; i++){ sum = max (sum, 0 ) + nums[i]; res = max (res, sum); } return res;
变式: 二维数组的最大子矩阵
编辑距离
初始化的坑。
dp[i]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显dp[i] = i。
dp[j]的话同理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 vector<vector<int >> dp (word1.size () + 1 , vector <int >(word2.size () + 1 , 0 )); for (int i = 0 ; i <= word1.size (); i++) dp[i][0 ] = i;for (int j = 0 ; j <= word2.size (); j++) dp[0 ][j] = j;for (int i = 1 ; i <= word1.size (); i++) { for (int j = 1 ; j <= word2.size (); j++) { if (word1[i - 1 ] == word2[j - 1 ]) { dp[i][j] = dp[i - 1 ][j - 1 ]; } else { dp[i][j] = min ({dp[i - 1 ][j - 1 ], dp[i - 1 ][j], dp[i][j - 1 ]}) + 1 ; } } } return dp[word1.size ()][word2.size ()];
变式:只能有删除操作,求最小步数。
当word1[i - 1] 与 word2[j - 1]相同的时候 dp[i] = dp[i-1] ;
当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:
情况一:删word1[i - 1],最少操作次数为dp[i-1] + 1
情况二:删word2[j - 1],最少操作次数为dp[i+1]
情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i-1]+2
那最后当然是取最小值
1 dp[i][j] = min ({dp[i - 1 ][j - 1 ] + 2 , dp[i - 1 ][j] + 1 , dp[i][j - 1 ] + 1 });
区间
最长回文子序列
如果s[i] == s[j] 构造出以s[i]为开头和s[j]为结尾的回文子序列
如果s[i] != s[j] 不可能构造以s[i]开头同时以s[j]结尾的最长回文子序列
1 2 3 4 5 6 7 8 9 10 11 12 for (int i = 0 ; i < n; i ++ ) dp[i][i] = 1 ; for (int len = 2 ; len <= n; len ++ ) for (int i = 0 ; i + len - 1 < n; i ++ ){ int l = i, r = i + len - 1 ; if (s[l] == s[r]) dp[l][r] = dp[l + 1 ][r - 1 ] + 2 ; else dp[l][r] = max (dp[l][r - 1 ], dp[l + 1 ][r]); } return dp[0 ][n - 1 ];
合并石子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 for (int i = 1 ; i <= n; i ++) { cin >> a[i]; s[i] += s[i - 1 ] + a[i]; } memset (f, 0x3f , sizeof f);for (int len = 1 ; len <= n; len ++) { for (int i = 1 ; i + len - 1 <= n; i ++) { int j = i + len - 1 ; if (len == 1 ) { f[i][j] = 0 ; continue ; } for (int k = i; k < j; k ++) { f[i][j] = min (f[i][j], f[i][k] + f[k + 1 ][j] + s[j] - s[i - 1 ]); } } } cout << f[1 ][n] << endl;
计数(背包恰好装满)
01背包和完全背包变式,只有求排列数时才要改变循环顺序。如果求组合数就是外层for循环遍历物品,内层for遍历背包 。如果求排列数就是外层for遍历背包,内层for循环遍历物品 。
首先判断是01背包还是完全背包,即确定了遍历背包是正序还是倒序。然后默写
求组合类问题:
1 2 3 dp[0 ] = 1 ; dp[j] += dp[j - nums[i]];
求排列类问题:遍历外背包内物品
1 2 3 4 5 6 7 8 9 dp[0 ] = 1 ; for (int i = 0 ; i <= target; i++){ for (int j = 0 ; j < nums.size (); j++){ if (i >= nums[j]) dp[i] += dp[i - nums[j]]; } } return dp[target];
树形
eg. 求最大的子树。f[u] 在以u为根的子树中 所有联通块的权值的最大值
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 70 71 #include <iostream> #include <cmath> #include <algorithm> #define INF 1e8 using namespace std;const int N = 2e5 + 1e2 ;int cnt, a, b, n;int res = -INF;int h[N], f[N], w[N];struct edge { int to; int next; } e[N]; void add (int u, int v) { cnt++; e[cnt].to = v; e[cnt].next = h[u]; h[u] = cnt; } void dfs (int u, int father) { f[u] = w[u]; for (int i = h[u]; i; i = e[i].next) { int j = e[i].to; if (j == father) continue ; dfs (j, u); if (f[j] > 0 ) { f[u] += f[j]; } } } int main () { cin >> n; for (int i = 1 ; i <= n; i ++) cin >> w[i]; int m = n - 1 ; while (m--){ cin >> a >> b; add (a, b); add (b, a); } dfs (1 , -1 ); for (int i = 1 ; i <= n; i ++) { res = max (res, f[i]); } cout << res << endl; return 0 ; }
状态机
多维dp 不同维度存储不同信息
数位
windy数
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 70 71 72 73 74 75 76 77 78 79 80 81 #include <iostream> #include <vector> #include <cmath> using namespace std;const int N = 20 ;int f[N][N], l, r;void init () { for (int i = 0 ; i <= 9 ; i++) f[1 ][i] = 1 ; for (int i = 2 ; i <= N; i++) for (int j = 0 ; j <= 9 ; j++) for (int k = 0 ; k <= 9 ; k++) if (abs (j - k) >= 2 ) f[i][j] += f[i - 1 ][k]; } int dp (int n) { vector <int > a; int res = 0 , last = -1 ; if (!n) return 0 ; while (n){ a.emplace_back (n % 10 ); n /= 10 ; } int len = a.size () - 1 ; for (int i = len; i >= 0 ; i--){ int x = a[i]; if (i == len){ for (int j = 1 ; j < x; j++) if (abs (j - last) >= 2 ) res += f[i + 1 ][j]; }else { for (int j = 0 ; j < x; j++) if (abs (j - last) >= 2 ) res += f[i + 1 ][j]; } if (abs (x - last) < 2 ) break ; else last = x; if (!i) res++; } for (int i = 1 ; i <= len; i++) for (int j = 1 ; j <= 9 ; j++) res += f[i][j]; return res; } int main () { init (); cin >> l >> r; cout << dp (r)- dp (l - 1 ) << endl; return 0 ; }
状态压缩
最短Hamilton路径。给定一张 n 个点的带权无向图,点从 0~n−1 标号,求起点 0 到终点 n−1 的最短路径。不重不漏地经过每个点恰好一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 memset (f, 0x3f , sizeof f); f[1 ][0 ] = 0 ; for (int i = 0 ; i < 1 << n; i ++ ) for (int j = 0 ; j < n; j ++ ) if (i >> j & 1 ) for (int k = 0 ; k < n; k ++ ) if (i >> k & 1 ) f[i][j] = min (f[i][j], f[i - (1 << j)][k] + w[k][j]);
**分割等和子集:**将数组分割成两个子集,使得两个子集的元素和相等。 01背包
1 2 3 4 5 6 7 8 9 10 11 12 13 int target = sum / 2 ;for (int i = 0 ; i < nums.size (); i++) { for (int j = target; j >= nums[i]; j--) { dp[j] = max (dp[j], dp[j - nums[i]] + nums[i]); } } if (dp[target] == target) return true ;
**粉碎石子:**有一堆石头, 每一回合,从中选出任意两块石头,然后将它们一起粉碎。 最后,最多只会剩下一块石头。返回此石头最小的可能重量。
尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了 。 与上题类似,改return即可。
1 return sum - dp[target] - dp[target];
**目标和:**给定一个数组和一个目标数。对于数组中的任意一个整数,从 + 或 -中选择一个符号添加在前面。返回可以使最终数组和为目标数 S的所有添加符号的方法数。
转化为01背包问题。假设加法的总和为x,那么减法对应的总和就是sum - x。
所以我们要求的是 x - (sum - x) = S 即x = (S + sum) / 2。此时问题就转化为,装满容量为x背包,有几种方法 。组合计数问题。
1 dp[j] += dp[j - nums[i]];
**零钱兑换:**给定不同面额的硬币和一个总金额。计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 同上,计数完全背包问题。装满容量为总金额的背包。
**零钱兑换变式:**求凑成总金额所需的最少的硬币个数。
1 dp[j] = min (dp[j - coins[i]] + 1 , dp[j]);
**组合总和:**给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。 同上,计数完全背包问题。装满容量为目标正整数的背包。
**完全平方数:**找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。需要让组成和的完全平方数的个数最少。给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
1 2 3 4 5 6 7 8 9 vector<int > dp (n + 1 , INT_MAX) ; dp[0 ] = 0 ; for (int i = 1 ; i <= n; i++) { for (int j = i * i; j <= n; j++) { dp[j] = min (dp[j - i * i] + 1 , dp[j]); } } return dp[n];