DP 总结

DP,即动态规划(Dynamic Programming),是数学中比较冷门的学科,但在信息学中的用处很大。主要有(基础):

  • 背包问题
  • 区间 DP
  • 状态压缩 DP
  • 树形 DP

更深入的有:

  • 插头 DP
  • 概率 DP

DP 特征:无后效性,最优子结构,子问题重叠。

  • 无后效性:也就是当前决策完后,后面的决策不再受当前的决策的影响。

  • 最优子结构:最优解的子问题也是最优的

  • 子问题重叠:一个决策不会是独立的,可能在后面的决策中用到。

1|0背包问题:

1|10-1 背包

例题:luogu P1048 采药

  • 题意:

给出物品的重量 wi 和价值 vi,每个物品只能选一次,问最大价值。

  • 思路:

设方程 fi,j 为取前 i 个物品,容量为 j 时的最大价值。
fi,j=max{fi1,j,fi1,jwi+vi}。什么意思呢:如果当前物品不选,那么直接从 i1 转移过来(fi1,j)。如果当前选,那么这个除了物品之外的总容量为 jwi,那我们直接调用 fi1,jwi 加上选的价值即可。

点击查看代码
for (int i = 1; i <= m; i++) { for (int j = t; j >= w[i]; j--) { dp[j] = max(dp[j], dp[j - w[i]] + e[i]); } }

1|2完全背包

例题:luogu P1616 疯狂的采药

  • 题意:

给出物品的重量 wi 和价值 vi,每个物品可以选无限次,问最大价值。

  • 思路:

考虑 O(n3) 暴力,枚举 k 为选择物品的数量(kwij),所以转移方程为:

fi,j=max{fi1,j×k+vi×k}

怎么优化呢?

发现对于 fi,jwifi,jwi×k 转移过来,所以我们就不用枚举 k 了,即 fi,jfi,jwi 转移过来。

然后可以滚动数组优化掉第一维:

点击查看代码
for (long long i = 1; i <= m; i++) { for (long long j = e[i]; j <= t; j++) { dp[j] = max(dp[j], dp[j - e[i]] + w[i]); } }

1|3多重背包

其实就朴素的多重背包。

转移方程:fi,j=maxk=0nki{fi1,jwi×k+vi×k}

1|4混合背包

就是上面三种背包的结合。判断一下做出相应匹配背包即可。

1|5分组背包

例题:luogu P1757 通天之分组背包

  • 题意:

n 件物品和一个大小为 m 的背包,第 i 个物品的价值为 wi,体积为 vi每个物品属于一个组,同组内最多只能选择一个物品。求背包能装载物品的最大总价值。

  • 思路:

就是对于每组求一次 0-1 背包即可。

点击查看代码
for (int k = 1; k <= ts; k++) { for (int i = m; i >= 0; i--) { for (int j = 1; j <= cnt[k]; j++) { if (i >= w[t[k][j]]) { dp[i] = max(dp[i], dp[i - w[t[k][j]]] + c[t[k][j]]); } } } }

2|0区间 DP

顾名思义,就是在区间上进行 DP。

DP 顺序为从区间长度小的转移到大的上来。所以我们可以枚举长度 k,然后枚举两个端点 i,jji+k1。然后我们就在这个区间 [i,j] 做操作。

例题:luogu P1063 能量项链

  • 题意:

有一个环,每次可以选择相邻两个合并,如果前一颗能量珠的头标记为 m,尾标记为 r,后一颗能量珠的头标记为 r,尾标记为 n,则聚合后释放的能量为 m×r×n,头标为 m,尾标为 n。求最大代价。

  • 思路:

首先破环为链。

显然 fi,j=max{fi,j,fi,k+fk+1,j+ai×ak+1×aj+1}

就是由区间 [i,k] 和区间 [k+1,j] 的最大值相加得到的。然后还要加上珠子合并所产生的代价。

点击查看代码
#include <bits/stdc++.h> using namespace std; const int MAXN = 1e2 + 100; int n, a[MAXN], dp[MAXN][MAXN], ans; int main() { cin >> n; for (int i = 1; i <= n; i++) { cin >> a[i]; a[i + n] = a[i]; } for (int l = 2; l <= n; l++) { for (int i = 1; i + l - 1 <= n << 1; i++) { int j = i + l - 1; for (int k = i; k < j; k++) { ans = max(ans, dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + a[i] * a[k + 1] * a[j + 1])); } } } cout << ans; return 0; }

3|0状压 DP

也就是状态压缩 DP,具体的,我们把 x 拆分成二进制形式把他存到 dp 数组里进行处理,所以题目给出的 n 必须很小,一般是在 21 以内。

例题:luogu P1433 吃奶酪

显然我们先要预处理出每一个点间的距离,这个可以用 O(n2) 暴力实现,然后考虑怎么 DP。

我们设 dpi,j 表示走到 i,经过路径的二进制为 j 的最短距离,显然初始化是 dpi,2i1=a0,i

然后我们暴力枚举所有的二进制形式 k,然后我们枚举当前可以到达的点 i,也就是满足 k&2i10,即 i 这个点在 k 中做过没有,然后再去枚举所有可以从 i 出发到达的点 jj 要满足 ij 并且 k&2j10。那我们就可以得到转移方程:dpi,k=min{dpi,k,dpj,k2i1+disi,j}

答案:mini=1ndpi,2n1

点击查看代码
#include <bits/stdc++.h> using namespace std; int t, n, m; double x[16], y[16], f[16][16], dp[16][1 << 16]; double Manhattan(double x1, double y_1, double x2, double y2) { return sqrt(1.0 * (x1 - x2) * (x1 - x2) + 1.0 * (y_1 - y2) * (y_1 - y2)); } int main() { cin >> n; memset(dp, 127, sizeof(dp)); for (int i = 1; i <= n; i++) { cin >> x[i] >> y[i]; } x[0] = y[0] = 0; for (int i = 0; i <= n; i++) { for (int j = i + 1; j <= n; j++) { f[i][j] = f[j][i] = Manhattan(x[i], y[i], x[j], y[j]); } } for (int i = 1; i <= n; i++) { dp[i][1 << (i - 1)] = f[0][i]; } for (int S = 1; S < (1 << n); S++) { for (int i = 1; i <= n; i++) { if ((S & (1 << (i - 1))) == 0) { continue; } for (int j = 1; j <= n; j++) { if (i == j) { continue; } if ((S & (1 << (j - 1))) == 0) { continue; } dp[i][S] = min(dp[i][S], dp[j][S - (1 << (i - 1))] + f[i][j]); } } } double maxx = 10000000.0; for (int i = 1; i <= n; i++) { maxx = min(maxx, dp[i][(1 << n) - 1]); } cout << fixed << setprecision(2) << maxx << '\n'; return 0; }

4|0树形 DP

4|1普通的树形 DP

顾名思义,就是在树上进行 DP,一般和 dfs 一起出现。

这类问题我们优先考虑以当前节点 x 为根的贡献是多少再去转移。

例题:luogu P1352 没有上司的舞会

dpx,0/1 如果 x 去或者不去的最优答案,那么如果 x 去,那么 x 的下属都不会去,也就是 dpx,1=dpy,0+ax(其中 yx 的下属)。那么如果 x 不去,那么 x 的下属都会去,即 dpx,0=max{dpy,0,dpy,1}

最后答案就是 max{dpn,0,dpn,1}

点击查看代码
#include <bits/stdc++.h> using namespace std; vector<int> son[1000001]; int n, dp[100001][2], flag[100001], r[100001]; void dfs(int u, int fa) { dp[u][0] = 0; for (int i = 0; i < son[u].size(); i++) { int v = son[u][i]; dfs(v, u); dp[u][0] += max(dp[v][1], dp[v][0]); dp[u][1] += dp[v][0]; } } int main() { cin >> n; for (int i = 1; i <= n; i++) { cin >> r[i]; dp[i][1] = r[i]; } for (int i = 1; i < n; i++) { int l, k; cin >> l >> k; son[k].push_back(l); flag[l] = 1; } for (int i = 1; i <= n; i++) { if (!flag[i]) { dfs(i, 0); cout << max(dp[i][0], dp[i][1]); return 0; } } return 0; }

4|2换根 DP

换根 DP 也叫二次扫描法,是特殊的一种树形 DP。

分为几个步骤:

  1. 从节点 x 开始 dfs 得到以 x 为根整个树的答案

  2. 换根,试将根换成与 x 有边相连的点统计最优答案。

例题:luogu P3478 [POI2008] STA-Station

显然我们可以枚举每个 i 去 dfs 出以 i 为根的树的答案,再去统计最大值,但是显然时间复杂度 O(n2)TLE

结论:fx=fy+n2×sizx,其中 xy 的儿子。

为什么呢?当以 y 为根的变为 x 为根,那么 x 所有的儿子的深度就会减 1,也就是整体减去 sizx,那么其他不在 x 儿子的节点的深度就会加 1,也就是整体加 (nsizx),那么 fx=fysizx+(nsizx)=fx=fy+n2×sizx。而 sizx 我们可以一次 dfs 得到,时间复杂度 O(n)

点击查看代码
#include <bits/stdc++.h> #define int long long using namespace std; const int MAXN = 7 * 1e6 + 100; vector<int> G[MAXN]; long long dis[MAXN], sum, a[MAXN], id = 1, size[MAXN], n; void dfs(int u) { size[u] = 1; for (auto i : G[u]) { if (dis[i]) { continue; } dis[i] = dis[u] + 1; dfs(i); size[u] += size[i]; } } void dfs1(int u, int fa) { a[u] = a[fa] + n - 2 * size[u]; for (auto i : G[u]) { if (i == fa) { continue; } dfs1(i, u); } } signed main() { cin >> n; for (int i = 1; i < n; i++) { int x, y; cin >> x >> y; G[x].push_back(y); G[y].push_back(x); } dis[1] = 1; dfs(1); for (int i = 1; i <= n; i++) { sum += dis[i]; } sum -= n; a[1] = sum; for (int i = 0; i < G[1].size(); i++) { int v = G[1][i]; dfs1(v, 1); } for (int i = 1; i <= n; i++) { if (a[i] > sum) { sum = a[i]; id = i; } } cout << id << endl; return 0; }

5|0数位 DP

形如问 [L,R] 中有多少个满足条件的数,考虑前缀和思想:f(R)f(L1)f(x) 表示 [1,x] 满足条件的数。

然后我们采用记忆化来写数位 DP。

  • x 表示当前 dfs 到第 x 位,注意从高位枚举到低位。

  • sum 表示第 x 位的答案。

  • vis 表示上一位是否是满足条件的最大值,具体的,如果范围是 672281,第 1 位如果是 6,那么第 2 位就只能取 07 了,否则第二位就可以取 09 了。

eg:Luogu P4999 烦人的数学作业

点击查看代码
#include <bits/stdc++.h> #define int unsigned long long using namespace std; const int N = 1e5 + 100; const int mod = 1e9 + 7; int L, R, a[N], f[200][200], t, cnt; int dfs(int x, int sum, bool vis) { // cout << x << endl; if (!x) { // 第 0 位 return sum % mod; } if (!vis && f[x][sum]) { // 有答案了 return f[x][sum] % mod; } int r = vis ? a[x] : 9, ans = 0; // 具体见 vis 的定义 for (int i = 0; i <= r; i++) { ans = (ans + dfs(x - 1, sum + i, vis && i == r ? 1 : 0) % mod) % mod; // 从高位开始,数码和 + i,vis 的大小 } if (!vis) { // 记忆化 f[x][sum] = ans % mod; } return ans % mod; } int solve(int n) { // 分解 cnt = 0; memset(a, 0, sizeof(a)); while (n) { a[++cnt] = n % 10; n /= 10; } return dfs(cnt, 0, 1) % mod; } signed main() { cin >> t; while (t--) { cin >> L >> R; cout << (solve(R) - solve(L - 1) + mod) % mod << endl; } return 0; }

6|0好题:

luogu P2224 [HNOI2001] 产品加工

dpi,j 为前 i 个物品 A 机器做了 j 分钟时,B 机器的最少时间。

然后我们分三种情况讨论:

当前物品为 k

  • A 做,B 的时间不变,为 dpi1,jt1i

  • B 做,那么 A 的时间不变,dpi,j=dpi1,j+t2i

  • A,B 同时做,那么时间都要变,dpi,j=dpi1,jt3i+t3i

初始化:dp 数组全部赋成极大值,显然 dp0,0=0

答案:mini=1n{max{i,dpn,i}}

但是题目的空间限制为 125MB,肯定会 MLE,那么怎么优化呢?

观察到 dpi,j 都是从 dpi1,x 推来的,所以我们类似 01 背包的想法,把第一维 i 滚掉,把 j 的循环从大到小就可以了。

点击查看代码
#include <bits/stdc++.h> using namespace std; const int MAXN = 2e5 + 100; int n, f[MAXN], t1, t2, t3, ans = 2e9; // f[i][j] 表示前 i 个物品,A 机器做了 j 分钟,B 机器做的最小值 // f[i][j] = max({f[i - 1][j] + t2[i], f[i - 1][j - t3[i]] + t3[i], f[i - 1][j - t1[i]}); // 空间 125 MB 会 MLE,优化掉一维,j 循坏改从大到小 int main() { ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> n; for (int k = 1; k <= n; ++k) { cin >> t1 >> t2 >> t3; for (int j = 20000; j >= 0; --j) { if (t2) { f[j] += t2; } else { f[j] = 2e9; } if (t1 && j >= t1) { f[j] = min(f[j], f[j - t1]); } if (t3 && j >= t3) { f[j] = min(f[j], f[j - t3] + t3); } } } for (int i = 0; i <= 20000; i++) { ans = min(ans, max(i, f[i])); } cout << ans; return 0; }

luogu P7914 [CSP-S 2021] 括号序列

dpi,j,0 表示 ij 中形如 **...*** (均为 *)的答案。

dpi,j,1 表示 ij 中形如 (...) (括号匹配) 的答案。

dpi,j,2 表示 ij 中形如 (...)***(...)*** (左起括号,右接 *)的答案。

dpi,j,3 表示 ij 中形如 (...)***(...)***(...) (左起括号,右接括号)的答案。

dpi,j,4 表示 ij 中形如 ***(...)***(...)*** (左起 *,右接 *)的答案。

考虑怎么转移:

  • sj*? 时, dpi,j,0=dpi,j1,0

  • dpi,j,1 可以包括其他合法的序列,但是要满足括号要匹配,dpi,j,1=dpi1,j+1,0+dpi1,j+1,2+dpi1,j+1,3+dpi1,j+1,4

  • 根据 dpi,j,2 的定义,可以分成 (...)***(...)***(...) 加上一个 (...),根据乘法原理,也就是 dpi,j,2=t=ij1dpi,j,3×dpi,j,0

  • 左起括号,右边随便,只有 2,3 符合(3 包含 1),所以状态转移方程为 dpi,j,3=t=lr1((dpi,t,2+dpi,t,3×dpt+1,j,1))+dpi,j,1,乘上 dpi,t,3 是因为 dpi,j,3 的定义是括号结尾,所以要加上一个括号。dpi,j,1 的话就是加上 (...) 的情况。

  • 发现 dpi,j,4 的定义其实是 dpi,j,2 的定义的逆序列,所以方程式的位置调换一下即可,也就是 dpi,j,4=t=lr1dpi,t,0×dpt+1,j,3

答案:dp1,n,3

初始化:dpi,i1,0=1

注:代码中 dp 数组的下标比上面的思路增加 1

点击查看代码
#include <bits/stdc++.h> #define int long long using namespace std; const int MAXN = 5e2 + 100; const int mod = 1e9 + 7; int n, k, dp[MAXN][MAXN][6]; string s; // dp[i][j][1] i ~ j **..** // dp[i][j][2] i ~ j (...) // dp[i][j][3] i ~ j (...)***(...)*** // dp[i][j][4] i ~ j (...)***(...)***(...) // dp[i][j][5] i ~ j ***(...)***(...) bool check(int x, int y) { return (s[x] == '?' || s[x] == '(') && (s[y] == '?' || s[y] == ')'); } signed main() { cin >> n >> k >> s; s = ' ' + s; for (int i = 1; i <= n; i++) { dp[i][i - 1][1] = 1; } for (int l = 2; l <= n; l++) { for (int i = 1; i + l - 1 <= n; i++) { int j = i + l - 1; if (l <= k && (s[j] == '*' || s[j] == '?')) { dp[i][j][1] = dp[i][j - 1][1]; } if (l < 2) { continue; } dp[i][j][2] = check(i, j) * (dp[i + 1][j - 1][1] + dp[i + 1][j - 1][3] + dp[i + 1][j - 1][4] + dp[i + 1][j - 1][5]) % mod; for (int t = i; t < j; t++) { dp[i][j][3] = (dp[i][j][3] + (dp[i][t][4] * dp[t + 1][j][1]) % mod) % mod; dp[i][j][4] = (dp[i][j][4] + (dp[i][t][3] + dp[i][t][4]) * dp[t + 1][j][2]) % mod; dp[i][j][5] = (dp[i][j][5] + (dp[i][t][1] * dp[t + 1][j][4] % mod) % mod); } dp[i][j][4] = (dp[i][j][4] + dp[i][j][2]) % mod; } } // for (int i = 1; i <= n; i++) { // for (int j = 1; j <= n; j++) { // for (int k = 1; k <= 4; k++) { // cout << "k=" << k << " i=" << i << " j=" << j << " =" << dp[i][j][k] << endl; // } // } // } cout << dp[1][n][4] % mod << endl; return 0; }

luogu P6280 [USACO20OPEN] Exercise G

明显 k=lcm{ai},其中 ai 为环的数量。

我们设 dpi,j 为前 i 个质数和为 jk 的方案数,显然我们可以枚举每一个质数 p 的指数幂 pki,显然 dpi,j=dpi1,jpki×pki。然后按照套路发现 dpi,xdpi1,y 转移而来,所以我们可以滚动数组优化掉一维,j 改为从大到小。

点击查看代码
#include <bits/stdc++.h> #define int long long using namespace std; const int MAXN = 1e3 + 1; int n, m, f[MAXN], b[MAXN], cnt; bool vis[MAXN]; void init() { for (int i = 2; i <= MAXN; i++) { if (!vis[i]) { b[++cnt] = i; } for (int j = 1; j <= cnt && b[j] * i <= MAXN; j++) { vis[i * b[j]] = 1; if (i % b[j] == 0){ break; } } } } signed main() { cin >> n; init(); f[0] = 1; for (int i = 1; i <= cnt; i++) { for (int j = n; j >= b[i]; j--) { for (int k = b[i]; k <= n; k *= b[i]) { if (j >= k) { f[j] += f[j - k] * k; } } } } int sum = 0; for (int i = 0; i <= n; i++) { sum += f[i]; // cout << f[i] << ' '; } // cout << '\n'; // cout << f[n]; cout << sum << endl; return 0; }

codeforces 710E

难度:*2000

fi 表示长度为 i 的字符串的最小代价:

如果 i 是偶数:fi=min(fi1+x,fi2+y)

因为你可以从 i1 直接加上一个字符,代价为 x,也可以从 i2 直接翻倍,代价为 y

如果 i 是奇数:fi=min(fi1+x,fi2+1+y+x)

i1 部分和上面一样,然而你也可以从 i2+1 处直接翻倍,但是这样会多出一个字符,必须花费 x 才能消除。

点击查看代码
#include <bits/stdc++.h> #define int long long using namespace std; const int N = 1e7 + 100; int n, x, y, dp[N]; // dp[i] 表示长度为 i 的字符串的最小代价: // i 是偶数:min(dp[i - 1] + x, dp[i / 2] + y) // i 是奇数:min(dp[i - 1] + x, dp[i / 2 + 1] + y + x) signed main() { cin >> n >> x >> y; memset(dp, 127, sizeof(dp)); dp[0] = 0; for (int i = 1; i <= n; i++) { if (i & 1) { dp[i] = min(dp[i - 1] + x, dp[i / 2 + 1] + y + x); } else { dp[i] = min(dp[i - 1] + x, dp[i / 2] + y); // dp[1] = 62 // dp[2] = } } cout << dp[n]; return 0; }

参考:oi-wiki dp 专题题解 P1433 【吃奶酪】题解 P7914 【[CSP-S 2021] 括号序列】


__EOF__

本文作者ようこそ!
本文链接https://www.cnblogs.com/ydq1101/p/17643141.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   ydq1101  阅读(163)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示