动态规划原理
动态规划这玩意儿,就好比是在拓扑图上玩跳格子游戏。在图论中,咱们是从特定的节点跳到其他节点;而在动态规划里呢,我们是从一个状态 “嗖” 地转移到另一个状态。状态一般用数组来表示,就像 f [i][j],它就是一种超重要的状态,能从类似 f [i - 1][j] 这样的状态 “跑” 过来哦。
1-线性动态规划用法
确定状态:得根据问题的特点,像寻宝一样找出合适的状态表示,通常就是用数组来存这些状态啦。
找出状态转移方程:这可是关键中的关键,要搞清楚状态之间是怎么 “串门” 的,也就是当前状态是咋从之前的状态推导出来的。
处理边界条件:得先确定好初始状态的值,这就像游戏开始时要站好初始位置,这样状态转移才能顺顺利利地开始。
计算结果:按照状态转移方程这个 “魔法公式”,一步步稳稳地算出最终结果。
算法模板
cpp
// 定义状态数组const int N = 10010;int f[N][N];
// 输入数据// ...
// 初始化边界条件// ...
// 状态转移for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + ...; // 状态转移方程
}}
// 计算结果int ans = 0;for (int i = 1; i <= n; i++) {
ans = max(ans, f[n][i]);}
cout << ans;
例题分析
数字三角形问题
这题简直就像是在一个特殊的数字迷宫里找最长的宝藏路线。状态 f [i][j] 表示从最高点一路 “探险” 到第 i 行第 j 列的最大路径和。状态转移方程 f [i][j] = max (f [i - 1][j - 1], f [i - 1][j]) + g [i][j],这里的 g [i][j] 就是第 i 行第 j 列那个闪闪发光的数字啦。宝子们,冲呀,解开这个谜题不在话下 (๑・̀ㅂ・́)و✧!
代码:
#include <bits/stdc++.h>using namespace std;const int N = 10010;int f[N][N], g[N][N];int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
scanf("%d", &g[i][j]);
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + g[i][j];
}
}
int ans = 0;
for (int i = 1; i <= n; i++) ans = max(ans, f[n][i]);
cout << ans;}
最长上升子序列相关问题
合唱队形问题
这题就像是在给一群同学排一个超酷炫的合唱队形。要找出最少出列同学数量,让剩下同学能排出那个特别的合唱队形。咱们可以先从左到右求最长上升子序列 f1,再从右到左求最长上升子序列 f2,最后找出 f1 [i] + f2 [i] 的最大值,用总人数减去这个最大值再减 1 就是答案啦。加油,宝子们,这种题目对你们来说,肯定是小菜一碟 (๑・.・๑)!
代码
#include<bits/stdc++.h>using namespace std;
const int N = 110;int a[N];int f1[N], f2[N];int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j < i; j++) {
if (a[i] > a[j])
f1[i] = max(f1[i], f1[j] + 1);
}
}
for (int i = n; i; i--) {
for (int j = n; j > i; j--) {
if (a[i] > a[j])
f2[i] = max(f2[i], f2[j] + 1);
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, f1[i] + f2[i]);
}
cout << n - ans - 1;}
奶牛吃牧草问题
有一群可爱的奶牛要去吃牧草啦,这里有 N 个区间,每个区间 x, y 代表 y - x + 1 堆优质牧草,咱要帮奶牛选不重复区间,让它们吃到最多的牧草。可以先按区间右端点排序,再用二分查找找出能和当前区间不重叠的最大区间编号,最后用动态规划求解。宝子们,开动脑筋,帮奶牛们实现牧草自由 (๑・̀ㅂ・́)و✧!
cpp
#include <bits/stdc++.h>using namespace std;const int N = 150010;
pair<int, int> a[N];int f[N];int n;
int find(int l, int r, int val) {
int res = 0;
while (l <= r) {
int mid = (l + r) >> 1;
if (a[mid].first < val) {
res = mid, l = mid + 1;
} else r = mid - 1;
}
return res;}int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
int s, ss;
scanf("%d%d", &s, &ss);
a[i] = {ss, s};
}
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i++) {
int h = a[i].second, t = a[i].first;
f[i] = max(f[i - 1], f[find(1, i - 1, h)] + t - h + 1);
}
cout << f[n];}
2-背包问题
0 - 1 背包问题
原理:对于每个物品,只有选和不选两种情况,目标是在背包容量限制下获得最大利益。
状态定义:f[i][j] 表示考虑前 i 个物品,背包剩余体积为 j 时能获得的最大价值。
状态转移方程:f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]),其中 v[i] 是第 i 个物品的体积,w[i] 是其价值。
滚动数组优化原理:由于 f[i] 层状态只依赖于 f[i - 1] 层,可使用一维数组 f[j] 替代二维数组。但要倒序枚举体积 j,防止一个物品被重复选择。优化后的状态转移方程为 f[j] = max(f[j], f[j - v[i]] + w[i])。
例题:采药问题
#include <bits/stdc++.h>using namespace std;int n,v;const int N=110,M=1010;int w[N],c[N],f[M];int main(){
cin>>v>>n;
int pre=0;
for(int i=0;i<n;i++){
cin>>c[i]>>w[i];
}
for(int i=0;i<n;i++){
pre+=c[i];
for(int j=min(pre,v);j>=c[i];j--)
f[j]=max(f[j],f[j-c[i]]+w[i]);
}
int ans=0;
for(int i=0;i<=v;i++)ans=max(ans,f[i]);
cout<<ans;}
多维 0 - 1 背包问题(以二维为例)
原理:有两个消耗维度(如金币和精力),在考虑物品选择时要同时满足两个维度的限制。
状态定义:f[i][j][k] 表示到第 i 个物品时,花费了 j 个金币,花费了 k 个精力所获得的收益。
状态转移:与一维 0 - 1 背包类似,只是要同时考虑两个维度的限制。
kkksc03 实现愿望问题
kkksc03 的时间和金钱是有限的,所以他很难满足所有同学的愿望。所以他想知道在自己的能力范围内,最多可以完成多少同学的愿望?
#include<bits/stdc++.h>using namespace std;const int N=210;int f[N][N];int n,v1,v2;int main(){
cin>>n>>v1>>v2;
for(int i=1;i<=n;i++){
int c1,c2;
cin>>c1>>c2;
for(int j=v1;j>=c1;j--)
for(int k=v2;k>=c2;k--)
f[j][k]=max(f[j][k],f[j-c1][k-c2]+1);
}
cout<<f[v1][v2];}
多重背包问题
原理:每个物品有 s[i] 个,可选择 0 到 s[i] 个该物品。
方法一:朴素枚举
for (int i = 1; i <= n; i++)
for(int j = 0; j <= s[i]; j++)
for(int k = m; k >= j * v[i]; k--)
f[k] = max(f[k], f[k - j * v[i]] + j * w[i]);
方法二:二进制枚举
将数量为 s[i] 的物品拆分成若干个不同的 “新物品”,数量分别为 1, 2, 4, ..., 2^k, r(其中 r = s[i] - (1 + 2 + 4 + ... + 2^k) 且 1 + 2 + 4 + ... + 2^k <= s[i]),通过这些 “新物品” 的不同组合可表示 0 到 s[i] 之间的任意数量,然后对这些 “新物品” 使用 0 - 1 背包方法求解。
代码
#include <bits/stdc++.h>using namespace std;
const int N = 2010;int f[N];
int main() {
int n, V;
cin >> n >> V;
for (int i = 0; i < n; i++) {
int v, w, s;
cin >> v >> w >> s;
for (int k = 1; k <= s; k *= 2) {
for (int j = V; j >= k * v; j--) {
f[j] = max(f[j], f[j - k * v] + k * w);
}
s -= k;
}
if (s > 0) {
for (int j = V; j >= s * v; j--) {
f[j] = max(f[j], f[j - s * v] + s * w);
}
}
}
cout << f[V] << endl;
return 0;}
方法三:单调队列优化:
通过单调队列对状态转移进行优化,减少不必要的计算,但实现相对复杂。
完全背包问题
原理:每个物品可以选无限个。
状态转移方程:f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]),优化到一维数组后,正序枚举体积 j,因为每个物品可以选多次,f[j - v[i]] 可能已经是选了若干个第 i 个物品后的状态。
代码:
for(int i = 1; i <= n; i++)
for(int j = v[i]; j <= V; j++)
f[j] = max(f[j], f[j - v[i]] + w[i]);
分组背包问题
原理:物品被分成不同的组,每组只能选 1 个物品。
状态转移:按组、体积、组内物品的顺序进行枚举。
伪代码
for(组)
for(体积)
for(组中第几个)
// 状态转移
拓展:每组选 s[i] 个物品的解决方法
可以将每个组内的物品进行组合,生成新的 “组合物品”,每个组合看作一个新物品,其体积和价值是组合内物品的体积和价值之和,然后对这些新的 “组合物品” 使用分组背包的方法求解。也可以结合 DFS 进行搜索。
有依赖的背包问题
原理:选择某一个物品时需要先选择其前驱物品。
解决方法:
转换为 0 - 1 背包:把物品的不同选择情况看作不同的物品,例如买了 a 才能买 b 和 c,买了 b 才能买 d,则子树的所有情况 a, ab, ac, abc, abd, abcd 相当于转换成了 6 个不同的物品。
树形 DP:把物品之间的依赖关系看作一棵树,根节点是主物品,子节点是依赖于主物品的物品,从叶子节点开始向上递推,计算每个节点及其子树的所有可能选择情况。
开心的金明
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 n 元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子: 主件 附件 电脑 打印机,扫描仪 书柜 图书 书桌 台灯,文具 工作椅 无 如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有 0 个、1 个或 2 个附件。每个附件对应一个主件,附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的 n 元。
于是,他把每件物品规定了一个重要度,分为 5 等:用整数 1∼5 表示,第 5 等最重要。他还从因特网上查到了每件物品的价格(都是 10 元的整数倍)。他希望在不超过 n 元的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第 j 件物品的价格为 v j ,重要度为 w j ,共选中了 k 件物品,编号依次为 j 1 ,j 2 ,…,j k ,则所求的总和为: v j 1 ×w j 1 +v j 2 ×w j 2 +⋯+v j k ×w j k 请你帮助金明设计一个满足要求的购物单。
输入格式 第一行有两个整数,分别表示总钱数 n 和希望购买的物品个数 m。 第 2 到第 (m+1) 行,每行三个整数,第 (i+1) 行的整数 v i ,p i ,q i 分别表示第 i 件物品的价格、重要度以及它对应的的主件。如果 q i =0,表示该物品本身是主件。
cpp
#include <iostream>#include <cstring>using namespace std;
const int N = 60; // 物品最大数量const int M = 32010; // 钱数的上限
int n, m; // n 表示总钱数,m 表示物品个数int v[N], w[N]; // v 存储物品价格,w 存储物品价格与重要度的乘积int f[M]; // 记录状态
struct Item {
int v, w; // 主件的价格和价格与重要度的乘积
int v1, w1; // 附件 1 的价格和价格与重要度的乘积
int v2, w2; // 附件 2 的价格和价格与重要度的乘积};
Item items[N];
int main() {
cin >> n >> m;
n /= 10; // 因为价格都是 10 元的整数倍,这里统一处理为 10 元为单位
for (int i = 1; i <= m; i++) {
int p, q;
cin >> v[i] >> p >> q;
v[i] /= 10; // 统一为 10 元为单位
w[i] = v[i] * p;
if (q == 0) { // 是主件
items[i].v = v[i];
items[i].w = w[i];
} else { // 是附件
if (items[q].v1 == 0) { // 第一个附件
items[q].v1 = v[i];
items[q].w1 = w[i];
} else { // 第二个附件
items[q].v2 = v[i];
items[q].w2 = w[i];
}
}
}
for (int i = 1; i <= m; i++) {
if (items[i].v > 0) { // 是主件
for (int j = n; j >= items[i].v; j--) {
// 不选附件的情况
f[j] = max(f[j], f[j - items[i].v] + items[i].w);
// 选附件 1 的情况
if (j >= items[i].v + items[i].v1) {
f[j] = max(f[j], f[j - items[i].v - items[i].v1] + items[i].w + items[i].w1);
}
// 选附件 2 的情况
if (j >= items[i].v + items[i].v2) {
f[j] = max(f[j], f[j - items[i].v - items[i].v2] + items[i].w + items[i].w2);
}
// 选附件 1 和附件 2 的情况
if (j >= items[i].v + items[i].v1 + items[i].v2) {
f[j] = max(f[j], f[j - items[i].v - items[i].v1 - items[i].v2] + items[i].w + items[i].w1 + items[i].w2);
}
}
}
}
cout << f[n] * 10 << endl; // 还原为原来的价格单位
return 0;}
背包拓展问题
背包计数问题
原理:统计满足某个条件的数量。
状态转移方程:通常为 f[j] += f[j - v]。
例题:奶牛组队问题
#include<bits/stdc++.h>using namespace std;const int N=2010;const int mod=1e8;int f[N][N];int a[N];int n,m;int op(int x){
return ((x%m)+m)%m;}int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
a[i]=a[i]%m;
f[i][a[i]]=1;
}
for(int i=1;i<=n;i++){
for(int j=0;j<m;j++)
f[i][j]=((f[i][j]+f[i-1][j])%mod+f[i-1][op(j-a[i])])%mod;
}
cout<<f[n][0];}
背包路径问题
原理:要求输出具体选择了哪些物品,使用一个额外的数组记录最优值的转移来源。
#include <bits/stdc++.h>using namespace std;
const int N = 1010;int v[N], w[N];int f[N][N];bool g[N][N]; // 记录每个状态是从哪里转移来的
int main() {
int n, m;
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++) {
f[i][j] = f[i - 1][j];
if (j >= v[i] && f[i - 1][j - v[i]] + w[i] > f[i][j]) {
f[i][j] = f[i - 1][j - v[i]] + w[i];
g[i][j] = true; // 表示选择了第 i 个物品
}
}
}
// 输出最大价值
cout << f[n][m] << endl;
// 回溯找出选择的物品
vector<int> path;
for (int i = n, j = m; i; i--) {
if (g[i][j]) {
path.push_back(i);
j -= v[i];
}
}
// 输出选择的物品编号
for (int i = path.size() - 1; i >= 0; i--) {
cout << path[i] << " ";
}
cout << endl;
return 0;}
这些背包问题虽然各有特点,但核心都是通过合理定义状态和状态转移方程来解决。多做练习,能熟练掌握哒!(๑・.・๑)