dp练习集
动态规划(DP)
// 以下题目来自牛客网
删括号
f[i][j][k] 表示序列s的前i个匹配序列t的前j个,序列s删除部分左括号与右括号数量差为k的情况是否可行
答案为 f[sl][tl][0]
状态转移:
当 f[i][j][k] 可行时
- s[i+1]==t[j+1] 且 k==0 则 f[i+1][j+1][k] = 1
- s[i+1]=='(' 则s串删去当前括号可匹配,即 f[i+1][j][k+1] = 1
- s[i+1]==')' 则 k>0 时s串多删去一个左括号匹配,即 f[i+1][j][k-1] = 1
#include<iostream> #include<cstdio> #include<cstring> using namespace std; bool f[110][110][110]; //s前i个删去括号能否成为t前j个,左右括号差为k char s[110], t[110]; int main() { scanf("%s", s+1); scanf("%s", t+1); int sl = strlen(s+1), tl = strlen(t+1); f[0][0][0] = 1; for(int i=0;i<sl;i++) { for(int j=0;j<tl;j++) { for(int k=0;k<sl;k++) if(f[i][j][k]) { if(k==0 && s[i+1]==t[j+1]) f[i+1][j+1][k] = 1; if(s[i+1]=='(') f[i+1][j][k+1] = 1; else if(k) f[i+1][j][k-1] = 1; } } } printf("%s\n", f[sl][tl][0]?"Possible":"Impossible"); return 0; }
回文子序列计数
错误思路:x[i] = 左右26个小写字母选取0~min(l[i][j], r[i][j]) (0<=j<26)的组合数之积。
正确求法:见代码。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int mod = 1e9+7; typedef long long ll; ll x[3010], dp[3010]; // dp[i]位回文子序列个数 // dp[i+1] char s[3010]; int main() { scanf("%s", s); int n = strlen(s); for(int i=0;i<n;i++) x[i] = 1; for(int i=1;i<n;i++) { ll sum = 0, tmp; for(int j=n-1;j>i;j--) { tmp = dp[j]; if(s[j]==s[i-1]) { dp[j] = (dp[j] + sum + 1) % mod; } sum = (sum + tmp) % mod; x[i] = (x[i] + dp[j]) % mod; } } ll ans = 0; for(int i=0;i<n;i++) { ans = ans^((i+1) * x[i]) % mod; } printf("%lld\n", ans); return 0; }
牛牛的计算机内存
状压dp
直接 dp[22][1<<20] 会MLE,只能用滚动数组记录状态。
int dp[1<<20]; // dp[S]: 前i条指令状态为S的最小代价
int state[1<<20]; // state[i]:j 指令状态i执行完后的内存状态为j
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int INF = 0x3f3f3f3f; int dp[1<<20]; // dp[S]: 前i条指令,访问完状态为S的最小代价 int state[1<<20]; // state[i]:S 前i条指令执行完状态为S int a[22]; int main() { memset(dp, INF, sizeof(dp)); dp[0] = 0; int n, m; char ins[22]; scanf("%d %d", &n, &m); for(int i=0;i<n;i++) { scanf("%s", ins); int k = 0; for(int j=0;j<m;j++) { a[i] = a[i]*2 + (ins[j]-'0'); if(ins[j]=='1') ++k; } state[1<<i] = a[i]; dp[1<<i] = k*k; } for(int S=0;S<(1<<n);S++) { if(dp[S]==INF) continue; for(int i=0;i<n;i++) { if((S>>i)&1) continue; int nexS = S|(1<<i), k = 0; for(int j=0;j<m;j++) { if((a[i]>>j)&1 && ((state[S]>>j)&1)==0) { ++k; } } if(dp[nexS]>dp[S]+k*k) { dp[nexS] = dp[S] + k*k; state[nexS] = state[S]|a[i]; } } } printf("%d\n", dp[(1<<n)-1]); return 0; }
棋盘的必胜策略
可以用 f[i][j][step] 记录到 mp[i][j] 用了step步的胜负状态,dfs即可。
- 如果下一步有必败态,当前则为必胜态
- 否则当前为必败态
- mp[i][j]终点为必败态
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int dx[] = {0, 0, 1, -1}; const int dy[] = {1, -1, 0, 0}; int r, c, k; char mp[55][55]; int f[55][55][110]; bool check(int x, int y) { if(x<0||y<0||x>=r||y>=c) return false; if(mp[x][y]=='#') return false; return true; } int dfs(int x, int y, int k) { if(f[x][y][k]!=-1) return f[x][y][k]; if(mp[x][y]=='E') // 走到终点,无法移动,必败 return f[x][y][k] = 0; if(k==0) return 0; // 走不了,必败 for(int i=0;i<4;i++) { int nx = x + dx[i]; int ny = y + dy[i]; if(check(nx, ny) && dfs(nx, ny, k-1)==0) return f[x][y][k] = 1; } return f[x][y][k] = 0; } int main() { cin>>r>>c>>k; for(int i=0; i<r; i++) scanf("%s",mp[i]); memset(f, -1, sizeof(f)); int sx, sy; for(int i=0;i<r;i++) { for(int j=0;j<c;j++) { if(mp[i][j] == 'T') { sx = i; sy = j; } } } printf("%s\n", dfs(sx, sy, k)?"niuniu":"niumei"); return 0; }
看起来像博弈论,其实分析一下最多走两步就能确定胜负,不用搜索状态也能解决。
分析见代码。
#include<iostream> #include<cstdio> using namespace std; const int dx[] = {0, 0, 1, -1}; const int dy[] = {1, -1, 0, 0}; int r, c, k; char mp[55][55]; bool check(int x, int y) { if(x<0||y<0||x>=r||y>=c) return false; if(mp[x][y]=='#') return false; return true; } bool win(int x, int y) { for(int i=0;i<4;i++) { int nx = x + dx[i]; int ny = y + dy[i]; if(check(nx, ny) && mp[nx][ny]=='E') return true; } return false; } int main() { cin>>r>>c>>k; for(int i=0; i<r; i++) scanf("%s",mp[i]); int sx, sy; for(int i=0;i<r;i++) { for(int j=0;j<c;j++) { if(mp[i][j] == 'T') { sx = i; sy = j; } } } bool f = false; // 第一步能否走 for(int i=0;i<4;i++) { int nx = sx + dx[i]; int ny = sy + dy[i]; if(check(nx, ny)) { f = true; if(mp[nx][ny]=='E') return 0 * printf("niuniu\n"); } } if(!f) { // 动不了 return 0 * printf("niumei\n"); } if(k==1) { // 只走一步 return 0 * printf("niuniu\n"); } if(k%2==0) { // 偶数步,往返走,走后必胜 return 0 * printf("niumei\n"); } // 奇数步,第二步无法胜,第三步开始往返走,先走必胜 for(int i=0;i<4;i++) { int nx = sx + dx[i]; int ny = sy + dy[i]; if(check(nx, ny) && mp[nx][ny]=='.' && !win(nx, ny)) { return 0 * printf("niuniu\n"); } } puts("niumei"); return 0; }
牛牛与数组
状态转移很好写,记录一下前缀和,减去dp[i-1][j] j的整数倍的部分即为dp[i][j]
#include<iostream> #include<cstdio> using namespace std; const int mod = 1e9+7; int dp[12][100010]; int main() { int n, k; scanf("%d %d", &n, &k); for(int i=0;i<=k;i++) dp[0][i] = 1; for(int i=1;i<=n;i++) { int sum = 0; for(int j=1;j<=k;j++) sum = (sum + dp[i-1][j]) % mod; for(int j=1;j<=k;j++) { int sum1 = 0; for(int l=2*j;l<=k;l+=j) { sum1 = (sum1 + dp[i-1][l])% mod; } dp[i][j] = ((sum - sum1)%mod+mod)%mod; } } printf("%d\n", dp[n][k]); return 0; }
牛牛去买球
n个盒子,每个盒子有a[i]个红球,b[i]个篮球,但a[i],b[i]有正负1的偏差,总和不变。买每个盒子的费用为c[i],求买k个相同的球的最小花费。
三种情况
- 买k个红球,每个盒子都当做a[i]-1个红球
- 买k个蓝球,每个盒子都当做b[i]-1个蓝球
- 买2k-1个球,至少保证有k个相同颜色的球
用滚动数组上限为最多的球数,而不是k。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; int dp[20010]; int a[10010], b[10010], c[10010]; int main() { int n, k; cin>>n>>k; for(int i=1;i<=n;i++) scanf("%d", &a[i]); for(int i=1;i<=n;i++) scanf("%d", &b[i]); for(int i=1;i<=n;i++) scanf("%d", &c[i]); int ans = 0x3f3f3f3f, up = 20000; memset(dp, 0x3f, sizeof(dp)); dp[0] = 0; for(int i=1;i<=n;i++) { int v = a[i] - 1; for(int j=up;j>=v;j--) { dp[j] = min(dp[j], dp[j-v]+c[i]); } } for(int i=k;i<=2*k;i++) ans = min(ans, dp[i]); memset(dp, 0x3f, sizeof(dp)); dp[0] = 0; for(int i=1;i<=n;i++) { int v = b[i] - 1; for(int j=up;j>=v;j--) { dp[j] = min(dp[j], dp[j-v]+c[i]); } } for(int i=k;i<=2*k;i++) ans = min(ans, dp[i]); memset(dp, 0x3f, sizeof(dp)); dp[0] = 0; for(int i=1;i<=n;i++) { int v = a[i]+b[i]; for(int j=up;j>=v;j--) { dp[j] = min(dp[j], dp[j-v]+c[i]); } } for(int i=2*k-1;i<=up;i++) ans = min(ans, dp[i]); if(ans==0x3f3f3f3f) ans = -1; printf("%d\n", ans); return 0; }
小明打联盟
有3个小技能一个大招,大招的伤害值随时间线性变化。给定T时间,以及各个技能的释放时间和伤害值,问最大的伤害值是多少。
不考虑大招的话,就是多重背包问题。
把一个大招看成两个L, R时刻释放的大招d, e,中间时刻释放只会用一次。 (假设用两次m时刻的大招可以转化为大招e + (2m-l)时刻的大招,还是相当于用一次)
然后再枚举L,R区间的最大伤害值即可。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; int t; int v[5]; int w[5]; long long dp[100010]; int main() { while(scanf("%d", &t)!=EOF) { for(int i=0;i<3;i++) { scanf("%d %d", &v[i], &w[i]); } int L, R, temp, A; scanf("%d %d %d %d", &L, &R, &temp, &A); v[3] = L; w[3] = temp; v[4] = R; w[4] = temp + A*(R-L); memset(dp, 0, sizeof(dp)); for(int i=0;i<5;i++) { for(int j=v[i];j<=t;j++) { // 多重背包 dp[j] = max(dp[j], dp[j-v[i]]+w[i]); } } for(int j=L;j<=R;j++) { dp[t] = max(dp[t], dp[t-j]+temp+1LL*A*(j-L)); } printf("%lld\n", dp[t]); } return 0; }
树形dp
// 以下题目来自洛谷
P1352 没有上司的舞会
状态转移方程很简单,1A
#include<iostream> #include<cstdio> #include<vector> #include<algorithm> using namespace std; int n, fa[6010]; int w[6010]; vector<int> G[6010]; int dp[6010][2]; // dp[u][0] u没有参加 // dp[u][1] u参加 void dfs(int u, int fa) { dp[u][1] = w[u]; for(int i=0;i<G[u].size();i++) { int v = G[u][i]; if(v==fa) continue; dfs(v, u); dp[u][1] += dp[v][0]; dp[u][0] += max(dp[v][0], dp[v][1]); } } int main() { scanf("%d", &n); for(int i=1;i<=n;i++) scanf("%d", &w[i]); int u, v; for(int i=1;i<n;i++) { scanf("%d %d", &u, &v); G[u].push_back(v); G[v].push_back(u); fa[u] = v; } int rt = -1; for(int i=1;i<=n;i++) if(!fa[i]) { rt = i; break; } dfs(rt, -1); printf("%d\n", max(dp[rt][0], dp[rt][1])); return 0; }
P2016 战略游戏
选出一棵树上最少的节点,能覆盖所有边。
这题结构跟上面类似,每一点放/不放两个状态。
查看题解有大佬指出这是最小点覆盖问题,使用匈牙利算法,对于无向图答案为 ans / 2 。
#include<iostream> #include<cstdio> #include<vector> #include<algorithm> using namespace std; const int maxn = 1510; vector<int> G[maxn]; int n; int f[maxn][2]; void dfs(int u, int fa) { f[u][1] = 1; for(int i=0;i<G[u].size();i++) { int v = G[u][i]; if(v==fa) continue; dfs(v, u); f[u][0] += f[v][1]; f[u][1] += min(f[v][0], f[v][1]); } } int main() { scanf("%d", &n); for(int i=0;i<n;i++) { int u, v, k; scanf("%d %d", &u, &k); while(k--) { scanf("%d", &v); G[u].push_back(v); G[v].push_back(u); } } dfs(1, -1); printf("%d\n", min(f[1][0], f[1][1])); return 0; }
P2015 二叉苹果树
保留K条边苹果树上的最大苹果数量。
注意子树边的数量写法:dfs儿子后 sz[u] += sz[v] + 1;
#include<iostream> #include<cstdio> #include<vector> #include<algorithm> using namespace std; const int maxn = 110; int n, K; struct Edge { int to, w; Edge(int v, int ww):to(v), w(ww){} }; vector<Edge> G[maxn]; int sz[maxn]; int dp[maxn][maxn]; // dp[u][i] : 以u为根的子树保留i条边的最多苹果数量 void dfs(int u, int fa) { for(int i=0;i<G[u].size();i++) { int v = G[u][i].to; if(v==fa) continue; dfs(v, u); sz[u] += sz[v] + 1; // 边的数量 for(int j=min(sz[u], K);j>=1;j--) { // 01背包,逆序 for(int k=0;k<=min(sz[v], j-1);k++) { dp[u][j] = max(dp[u][j], dp[u][j-k-1] + dp[v][k] + G[u][i].w); } } } } int main() { scanf("%d %d", &n, &K); int u, v, w; for(int i=1;i<n;i++) { scanf("%d %d %d", &u, &v, &w); G[u].push_back(Edge(v, w)); G[v].push_back(Edge(u, w)); } dfs(1, -1); printf("%d\n", dp[1][K]); return 0; }
P2014 选课
课程之间有依赖关系,求选M门课程的最大学分。
将没有直接先修课的课程连在根为 0 的树上,从节点 0 dfs 即可。
#include<iostream> #include<cstdio> #include<vector> #include<algorithm> using namespace std; const int maxn = 310; int n, K; vector<int> G[maxn]; int sz[maxn], w[maxn]; int dp[maxn][maxn]; // dp[u][i] : 以u为根的子树选i门课的最大学分 void dfs(int u) { sz[u] = 1; for(int i=0;i<G[u].size();i++) { int v = G[u][i]; dfs(v); sz[u] += sz[v]; for(int j=min(sz[u], K);j>=1;j--) { for(int k=0;k<=min(j-1, sz[v]);k++) { dp[u][j] = max(dp[u][j], dp[u][j-k-1] + dp[v][k]); } } } } int main() { scanf("%d %d", &n, &K); int fa; for(int i=1;i<=n;i++) { scanf("%d %d", &fa, &w[i]); G[fa].push_back(i); } for(int i=1;i<=n;i++) dp[i][0] = w[i]; dfs(0); printf("%d\n", dp[0][K]); return 0; }
P1270 “访问”美术馆
读入采用dfs形式给出美术馆的通过走廊的时间和藏画数量,问T时间内能盗窃多少幅画。
坑点:时间有效时间为 T - 1
记搜 / 树形dp 。由于要返回根节点,时间可以直接乘以 2 读入。
#include<iostream> #include<cstdio> #include<vector> #include<algorithm> using namespace std; const int maxn = 110; int T, tot; struct node { int cost, val; }tree[maxn*4]; int dp[maxn*4][610]; void dfs(int u, int t) { if(dp[u][t] || t==0) return; // 0为0直接返回 if(tree[u].val) { // 根节点 dp[u][t] = min(tree[u].val, (t-tree[u].cost)/5); return; } for(int i=0;i<=t-tree[u].cost;i++) { dfs(u*2, i); dfs(u*2+1, t-i-tree[u].cost); // 右边剩下时间= t - i - 2倍走廊时间 dp[u][t] = max(dp[u][t], dp[u*2][i]+dp[u*2+1][t-i-tree[u].cost]); } } void build(int rt) { scanf("%d %d", &tree[rt].cost, &tree[rt].val); tree[rt].cost *= 2; if(!tree[rt].val) { build(rt*2); build(rt*2+1); } } int main() { scanf("%d", &T); build(1); dfs(1, T-1); printf("%d\n", dp[1][T-1]); return 0; }
数位DP
// 以下来自洛谷
P2657 [SCOI2009]windy数
求A,B区间内满足相邻两位数字之差大于等于2的整数个数。
注意是在 !lim && !zero 条件下记忆化,没加这个条件调了半天。
#include<iostream> #include<cstdio> #include<cmath> #include<cstring> using namespace std; typedef long long ll; ll dp[12][11]; // dp[i][j]:长度为i中最高位是j的windy数的个数 int bit[12]; ll dfs(int pos, int lim, int last, int zero) { if(pos<0) return 1; if(!lim && !zero && dp[pos][last]!=-1) return dp[pos][last]; int res = 0; int up = lim?bit[pos]:9; for(int i=0;i<=up;i++) { if(abs(i-last)<2) continue; res += dfs(pos-1, lim&&(i==up), zero&&(i==0)?100:i, zero&&(i==0)); } if(!lim && !zero) dp[pos][last] = res; return res; } ll cal(ll x) { int cnt = 0; while(x) { bit[cnt++] = x%10; x /= 10; } memset(dp, -1, sizeof(dp)); return dfs(cnt-1, 1, 100, 1); } int main() { ll A, B; while(cin>>A>>B) printf("%lld\n", cal(B)-cal(--A)); return 0; }
洛谷题解翻到别人的代码处理: