动态规划 递归关系
状态表示f[i,j]
集合:从前i个物品选且体积小于j
属性:最大值,最小代价,数量
01 背包
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 #include <algorithm> #include <iostream> using namespace std ; const int N = 1010 ;int n, m;int v[N], w[N]; int f[N][N]; int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v[i] >> w[i]; for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) { f[i][j] = f[i - 1 ][j]; if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1 ][j - v[i]] + w[i]); } cout << f[m][n] << ' ' ; return 0 ; } 4 5 1 2 2 4 3 4 4 5 v[i] = 1 2 3 4 w[i] = 2 4 4 5 1 2 3 4 5 j 0 0 0 0 0 0 1 2 2 2 2 2 2 2 4 6 6 6 3 2 4 6 6 8 4 2 4 6 6 8 i
滚动数组写法:
为什么要从后向前更新:因为当你计算 f[j]
时,f[j-v[i]]
可能已经被当前物品 i
更新过了。这意味着你可能会在同一轮次中多次使用同一个物品,违背了 0/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 #include <algorithm> #include <iostream> using namespace std;const int N = 1010 ;int n, m;int v[N], w[N]; int f[N]; int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v[i] >> w[i]; for (int i = 1 ; i <= n; i++) for (int j = m; j >= v[i]; j--) { f[j] = max (f[j], f[j - v[i]] + w[i]); } cout << f[m] << ' ' ; 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 #include <algorithm> #include <iostream> using namespace std;const int N = 1010 ;int n, m;int v[N], w[N];int f[N][N];int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v[i] >> w[i]; for (int i = 1 ; i <= n; i++) for (int j = 0 ; j <= m; j++) for (int k = 0 ; k * v[i] <= j; k++) f[i][j] = max (f[i][j], f[i - 1 ][j - k * v[i]] + k * w[i]); cout << f[n][m] << endl; return 0 ; }
优化,由于物品的数量是无限的,因此可以证明:
f[i,j] = Max(f[i][j], f[i-1,j-v]+w,f[i-1,j-2v]+2w,f[i-1,j-3v]+3w,...)
等于f[i,j-v]
/f[i,j-v] = Max( , f[i-1,j-v] ,f[i-1,j-2v]+w,f[i-1,j-3v]+2w,…)
f[i, j] = Max(f[i - 1, j], f[i, j - v] + w)
正序更新的目的是让物品能够被多次使用。
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 #include <iostream> #include <algorithm> using namespace std;const int N = 1010 ;int n, m;int v[N], w[N];int f[N];int main () { cin >> n >> m; for (int i = 1 ; i <= n; i ++ ) cin >> v[i] >> w[i]; for (int i = 1 ;i <= n; i ++) for (int j = v[i]; j <= m; j ++ ) f[j] = max (f[j], f[j - v[i]] + w[i]); cout << f[m] << endl; return 0 ; }
完全背包与01背包的区别
01背包:f[i, j] = max(f[i - 1, j], f[i - 1, j- v]
+ w)
完全背包:f[i,j] = max(f[i - 1, j], f[i, j - v]
+ w )
循环开始位置不同:01背包是避免覆盖,完全背包是必须覆盖(滑动窗口)
多重背包
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 #include <algorithm> #include <iostream> using namespace std;const int N = 1010 ;int n, m;int v[N], w[N], s[N];int f[N][N];int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v[i] >> w[i] >> s[i]; for (int i = 1 ; i <= n; i++) for (int j = 0 ; j <= m; j++) for (int k = 0 ; k * v[i] <= j && k <= s[i]; k++) f[i][j] = max (f[i][j],f[i - 1 ][j - k * v[i]] + k * w[i]); cout << f[n][m] << endl; return 0 ; }
f[i,j] = f[i - 1,j - v[i] k] + w[i] k
不能装满,因此不能采用完全背包的优化
二进制优化:由于二进制$2^0……2^n$可以表示任意小于$2^n$的数,因此我们可以将一个物品拆成n
个二进制的物品
nlog2000
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 #include <algorithm> #include <iostream> using namespace std;const int N = 25000 ; int n, m;int v[N], w[N];int f[N];int main () { cin >> n >> m; int cnt = 0 ; for (int i = 1 ; i <= n; i++) { int a, b, s; cin >> a >> b >> s; int k = 1 ; while (k <= s) { cnt++; v[cnt] = a * k; w[cnt] = b * k; s -= k; k *= 2 ; } if (s > 0 ) { cnt++; v[cnt] = a * s; w[cnt] = b * s; } } n = cnt; for (int i = 1 ; i <= n; i++) for (int j = m; j >= v[i]; j--) f[j] = max (f[j], f[j - v[i]] + w[i]); cout << f[m] << endl; return 0 ; }
分组背包
类似于01背包问题,我们只需添加组内遍历即可。
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 #include <iostream> #include <algorithm> using namespace std;const int N = 110 ; int n, m;int v[N][N], w[N][N], s[N];int f[N];int main () { cin >> n >>m; for (int i = 1 ; i <= n; i ++) { cin >> s[i]; for (int j = 1 ; j <= s[i]; j ++) cin >> v[i][j] >> w[i][j]; } for (int i = 1 ; i <= n; i ++ ) for (int j = m; j >= 0 ; j --) for (int k = 1 ; k <= s[i]; k ++ ) if (v[i][k] <= j) f[j] = max (f[j], f[j-v[i][k]] + w[i][k]); cout << f[m] <<endl; return 0 ;}
线性dp
AcWing 898. 数字三角形
这题有两种解法:
一种是像y总一样从上向下dp,然后对第n行的数据取最大值
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 #include <iostream> #include <algorithm> using namespace std;const int N = 510 , INF = 1e9 ;int n;int a[N][N];int f[N][N];int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i ++ ) for (int j = 1 ; j <= i; j ++ ) scanf ("%d" , &a[i][j]); for (int i = 0 ; i <= n; i ++ ) for (int j = 0 ; j <= i + 1 ; j ++ ) f[i][j] = -INF; f[1 ][1 ] = a[1 ][1 ]; for (int i = 2 ; i <= n; i ++ ) for (int j = 1 ; j <= i; j ++ ) f[i][j] = max (f[i - 1 ][j - 1 ] + a[i][j], f[i - 1 ][j] + a[i][j]); int res = -INF; for (int i = 1 ; i <= n; i ++ ) res = max (res, f[n][i]); printf ("%d\n" , res); return 0 ; }
另一种是从下往上dp,最后直接输出dp[1][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 28 29 #include <iostream> #include <cstdio> #include <cmath> #include <map> #include <algorithm> using namespace std;int dp[510 ][510 ];int n;int main () { cin >> n; for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= i; j++) { cin >> dp[i][j]; } } for (int i = n - 1 ; i >= 1 ; i--) { for (int j = 1 ; j <= i; j++) { dp[i][j] = max (dp[i][j] + dp[i + 1 ][j], dp[i][j] + dp[i + 1 ][j + 1 ]); } } cout << dp[1 ][1 ] << endl; return 0 ; }
AcWing 895. 最长上升子序列
我们维持到达每个位置的最长上升子序列,在枚举到下一元素时,对前面进行遍历寻求最长序列。
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 #include <iostream> using namespace std;const int N = 1e5 ;int a[N], dp[N];int main () { int n; cin >> n; for (int i = 0 ; i < n; i++) { cin >> a[i]; } for (int i = 0 ; i < n; i++) { dp[i] = 1 ; for (int j = 0 ; j < i; j++) { if (a[j] < a[i]) dp[i] = max (dp[i], dp[j] + 1 ); } } int maxnum = -1e9 ; for (int i = 0 ; i < n; i++) { maxnum = max (maxnum, dp[i]); } cout<< maxnum << endl; }
AcWing 896. 最长上升子序列 II
用二分法来查找那个最大的小于a[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 36 37 38 39 #include <iostream> #include <algorithm> using namespace std;const int N = 100010 ;int n;int a[N];int q[N];int main () { cin >> n; for (int i = 0 ; i < n; i ++ ) cin >> a[i]; 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]; } printf ("%d\n" , len); return 0 ; }
AcWing 897. 最长公共子序列
如果字母相同,则可以取f[i][j] = f[i - 1][j - 1] + 1;
,若不同,由于我们前面序列是确定的(即最长公共子序列),可以确保第i,j时,最长子序列的长度不变,即f[i][j] = max(f[i - 1][j], f[i][j - 1]);
,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> using namespace std;const int N = 1010 ;int n, m;char a[N], b[N];int f[N][N];int main () { cin >> n >> m >> a + 1 >> b + 1 ; for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) { if (a[i] == b[j]) f[i][j] = f[i - 1 ][j - 1 ] + 1 ; else f[i][j] = max (f[i - 1 ][j], f[i][j - 1 ]); } cout << f[n][m] << endl; }
AcWing 902. 最短编辑距离
AcWing 902. 最短编辑距离 - AcWing
1 2 3 4 5 6 7 8 9 10 11 1)删除操作:把a[i]删掉之后a[1~i]和b[1~j]匹配 所以之前要先做到a[1~(i-1)]和b[1~j]匹配 f[i-1][j] + 1 2)插入操作:插入之后a[i]与b[j]完全匹配,所以插入的就是b[j] 那填之前a[1~i]和b[1~(j-1)]匹配 f[i][j-1] + 1 3)替换操作:把a[i]改成b[j]之后想要a[1~i]与b[1~j]匹配 那么修改这一位之前,a[1~(i-1)]应该与b[1~(j-1)]匹配 f[i-1][j-1] + 1 但是如果本来a[i]与b[j]这一位上就相等,那么不用改,即 f[i-1][j-1] + 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 #include <algorithm> #include <iostream> using namespace std;const int N = 1010 ;int n, m;char a[N], b[N];int f[N][N];int main () { scanf ("%d%s" , &n, a + 1 ); scanf ("%d%s" , &m, b + 1 ); for (int i = 0 ; i <= n; i++) f[i][0 ] = i; for (int i = 0 ; i <= m; i++) f[0 ][i] = i; for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) { f[i][j] = min (f[i - 1 ][j] + 1 , f[i][j - 1 ] + 1 ); if (a[i] == b[j]) f[i][j] = min (f[i][j], f[i - 1 ][j - 1 ]); else f[i][j] = min (f[i][j], f[i - 1 ][j - 1 ] + 1 ); } cout << f[n][m] << endl; }
Acwing899编辑距离
这里采用编辑最短距离的思路,但是这题要注意strlen()
函数,由于我们输入的位置是arr+1,因此字符串的首地址不再是pos,而是pos+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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <iostream> #include <string.h> using namespace std;const int N = 15 , M = 1010 ;int f[N][N], x;char str[M][N], a[N];int main () { int n; cin >> n; int m; cin >> m; for (int i = 0 ; i < n; i++) cin >> str[i] + 1 ; while (m--) { int x; cin >> a + 1 >> x; int cnt = 0 ; for (int pos = 0 ; pos < n; pos++) { int lenpos = strlen (str[pos] + 1 ); int lena = strlen (a + 1 ); for (int i = 0 ; i <= lenpos; i++) f[i][0 ] = i; for (int i = 1 ; i <= lena; i++) f[0 ][i] = i; for (int i = 1 ; i <= lenpos; i++) for (int j = 1 ; j <= lena; j++) { f[i][j] = min (f[i - 1 ][j] + 1 , f[i][j - 1 ] + 1 ); if (str[pos][i] == a[j]) f[i][j] = min (f[i - 1 ][j - 1 ], f[i][j]); else f[i][j] = min (f[i - 1 ][j - 1 ] + 1 , f[i][j]); } if (x >= f[lenpos][lena]) cnt++; } cout << cnt << endl; } }
区间DP AcWing 282. 石子合并
这里是从每次合并二堆开始枚举,直到最后一次合并
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 #include <iostream> using namespace std;const int N = 310 ;int n;int s[N];int f[N][N];int main () { cin >> n; for (int i = 1 ; i <= n; i ++) cin >> s[i]; for (int i = 1 ; i<= n; i ++) s[i] += s[i - 1 ]; for (int len = 2 ; len<=n; len++) for (int i = 1 ; i+ len - 1 <= n; i++) { int l = i; int r = i + len - 1 ; f[l][r] = 1e8 ; for (int k = l; k <= r; k++) { f[l][r] = min (f[l][r], f[l][k] +f[k+1 ][r] + s[r] - s[l - 1 ]) ;} } cout << f[1 ][n] << endl; return 0 ; }
AcWing 900. 整数划分
这里采用完全背包的方式,从1开始到n,由于我们一开始是用1枚举的,因此可以保证背包是满的。
//TODO
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 #include <iostream> using namespace std;const int N = 1e3 + 7 , mod = 1e9 + 7 ;int f[N][N];int main () { int n; cin >> n; for (int i = 0 ; i <= n; i ++) { f[i][0 ] = 1 ; } for (int i = 1 ; i <= n; i ++) { for (int j = 0 ; j <= n; j ++) { f[i][j] = f[i - 1 ][j] % mod; if (j >= i) f[i][j] = (f[i - 1 ][j] + f[i][j - i]) % mod; } } cout << f[n][n] << endl; }
AcWing 338. 计数问题
这题我感觉像是模拟+前缀和,分析xxxjyyy的各种情况
AcWing 338. 计数问题<超短写法> - AcWing
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 #include <algorithm> #include <cmath> #include <iostream> #include <utility> using namespace std;int get (int x) { int res = 0 ; while (x) res++, x /= 10 ; return res; } int count (int n, int i) { int res = 0 , dgt = get (n); for (int j = 1 ; j <= dgt; ++j) { int p = pow (10 , dgt - j); int l = n/p/10 ; int r = n % p; int dj = n / p % 10 ; if (i) res+= l*p; else res+= (l - 1 ) * p; if (i == dj) res += r + 1 ; if (i < dj) res += p; } return res; } int main () { int a, b; while (cin >> a >> b, a) { if (a > b) swap (a, b); for (int i = 0 ; i <= 9 ; ++i) cout << count (b, i) - count (a - 1 , i) << ' ' ; cout << endl; } return 0 ; }
状态压缩DP AcWing 291. 蒙德里安的梦想 摆放的小方格方案数 等价于 横着摆放的小方格方案数
当我们确定唯一的横放小方格数量时,竖着放的小方格位置就已经确定了
连续空着的列需要是偶数个
该位置未被其他横放小方块占据
//TODO
AcWing 91. 最短Hamilton路径 AcWing 91. 最短Hamilton路径(超详解) - AcWing
//TODO
f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][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 #include <algorithm> #include <cstring> #include <iostream> using namespace std;const int N = 20 , M = 1 << N;int f[M][N], w[N][N]; int main () { int n; cin >> n; for (int i = 0 ; i < n; i++) for (int j = 0 ; j < n; j++) cin >> w[i][j]; 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 - (1 << j) >> k & 1 ) f[i][j] = min (f[i][j], f[i - (1 << j)][k] + w[k][j]); cout << f[(1 << n) - 1 ][n - 1 ] << endl; }
AcWing 285. 没有上司的舞会
我们可以设置一个节点的状态为选中或未选中
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 #include <cstring> #include <iostream> #include <algorithm> using namespace std;const int N = 6010 ;int n;int h[N], e[N], ne[N], idx;int happy[N];int f[N][2 ];bool has_fa[N]; void add (int a, int b) { e[idx] = b; ne[idx] = h[a]; h[a] = idx ++; } void dfs (int u) { f[u][1 ] = happy[u]; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; dfs (j); f[u][1 ] += f[j][0 ]; f[u][0 ] += max (f[j][0 ], f[j][1 ]); } } int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i ++ ) scanf ("%d" , &happy[i]); memset (h, -1 , sizeof h); for (int i = 0 ; i < n - 1 ; i ++ ) { int a, b; scanf ("%d%d" , &a, &b); add (b, a); has_fa[a] = true ; } int root = 1 ; while (has_fa[root]) root ++; dfs (root); printf ("%d\n" , max (f[root][0 ], f[root][1 ])) }
AcWing 901. 滑雪
f[i][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 41 42 43 44 45 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 310 ;int n,m; int f[N][N]; int h[N][N];int dx[4 ] = {-1 ,0 ,1 ,0 };int dy[4 ] = {0 ,1 ,0 ,-1 };int dp (int x, int y) { int &v = f[x][y]; if (v != -1 ) return v; v = 1 ; for (int i = 0 ;i < 4 ;i ++){ int xx = x + dx[i]; int yy = y + dy[i]; if (xx >= 1 && xx <= n && yy >= 1 && yy <= m && h[x][y] > h[xx][yy]){ v = max (v,dp (xx,yy) + 1 ); } } return v; } int main () { cin>>n>>m; for (int i = 1 ;i <= n;i ++){ for (int j = 1 ;j <= m;j ++){ cin>>h[i][j]; } } memset (f,-1 ,sizeof f); int res = 0 ; for (int i = 1 ;i <= n;i ++){ for (int j = 1 ;j <= m;j ++){ res = max (res,dp (i,j)); } } cout<<res<<endl; return 0 ; }