动态规划总结
〇、说在前面
近日应教练要求,刷了很多各方面的题目,但感觉刷地不精,需要好好总结总结。本篇博客将从各个方面理顺每一道dp题目,以此巩固基础,提升我的dp能力。
一、背包动态规划
1. 0/1背包
- 题目描述
有一个背包和一些有对应价值,重量的物品,每个物品只能选一次,求不超过背包重量的前提下物品价值的最大值。
- 一般的解 :
一般地,需要开两个数组,保存对应物品的价值和重量,最终动态规划求出结果。
转移方程 :
时间复杂度
典型的 0/1 背包模型。
直接上代码
#include<iostream>
using namespace std;
int n, m;
int dp[1005], w[105], c[105];
int main(){
scanf("%d%d", &m, &n);
for(int i = 1; i <= n; i++)
scanf("%d%d", &w[i], &c[i]);
for(int i = 1; i <= n; i++)
for(int j = m; j >= w[i]; j--)
dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
printf("%d\n", dp[m]);
return 0;
}
-
练习题目
T2 开心的金明
HZOJ P237 板子题,不解释。
#include<iostream> #include<cstring> using namespace std; int T; int dp[30005], w[30005], c[30005]; int main(){ int m, n; while(~scanf("%d%d", &m, &n)){ memset(dp, 0, sizeof dp); memset(w, 0, sizeof w); memset(c, 0, sizeof c); for(int i = 1; i <= n; i++){ int x, y; scanf("%d%d", &x, &y); w[i] = x; c[i] = x * y; } for(int i = 1; i <= n; i++) for(int j = m; j >= w[i]; j--) dp[j] = max(dp[j], dp[j - w[i]] + c[i]); printf("%d\n", dp[m]); } return 0; }
2.完全背包
- 题目描述
有一个背包和一些有对应价值,重量的物品,每个物品可选无数次,求不超过背包重量的前提下物品价值的最大值。
- 一般的解 :
一般地,需要开两个数组,保存对应物品的价值和重量,最终动态规划求出结果。
转移方程 :
时间复杂度
-
难点 :
· 正序枚举
· 搞清楚dp数组存储的是每一个重量
4.模板题目
T3 模板题,不解释
#include<iostream>
using namespace std;
int m, n;
int dp[205], w[35], c[35];
int main(){
scanf("%d%d", &m, &n);
for(int i = 1; i <= n; i++)
scanf("%d%d", &w[i], &c[i]);
for(int i = 1; i <= n; i++)
for(int j = w[i]; j <= m; j++)
dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
printf("max=%d\n", dp[m]);
return 0;
}
T4 模板题,不解释
#include<iostream>
using namespace std;
int m, n;
int dp[10005], w[10005], c[10005];
int main(){
scanf("%d%d", &m, &n);
for(int i = 1; i <= n; i++)
scanf("%d%d", &c[i], &w[i]);
for(int i = 1; i <= n; i++)
for(int j = w[i]; j <= m; j++)
dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
printf("%d\n", dp[m]);
return 0;
}
3. 可行性类背包
- 问题描述
背包问题的第一个难点。
有数个具有不同重量的物品,每个物品各有若干个,问能有多少种重量被表示。常搭配完全背包出题。
- 一般的解
一般地,先根据题面的描述,确定背包类型,一般分成三层循环分别枚举每个物品物品、取物品的个数、每个背包的重量。
时间复杂度
- 难点 :
· 没有固定转移方程
· 注意枚举时分好状态,结合题意再套模板枚举
- 练习题
T5 砝码称重问题
题意分析 : 一个砝码可以使用无限多次,是一个完全背包问题, 但给定了砝码的个数,因此直接在第二层正序枚举即可;背包大小是给定1000的,则解法显然了。
#include<iostream>
using namespace std;
int a[10], dp[1005];
int dx[8] = {0, 1, 2, 3, 5, 10, 20};
int main(){
for(int i = 1; i <= 6; i++)
scanf("%d", &a[i]);
dp[0] = 1;
for(int i = 1; i <= 6; i++)//枚举每一种
for(int j = 1; j <= a[i]; j++)//枚举每种砝码取几个
for(int k = 1000; k >= 0; k--)//在已有基础上枚举还可以增加的称重数量
if(dp[k])
dp[k + dx[i]] = 1;
int ans = 0;
for(int i = 1; i <= 1000; i++)
ans += (dp[i] != 0);
printf("Total=%d\n", ans);
return 0;
}
T6 最小乘车费用 HZOJ_P241
题意分析: 显然就是有10个物品,由题意也是给定物品个数的完全背包问题,那么与上一题就没有什么区别了,需要注意的是此处的背包容量拟化为了路程。
#include<iostream>
#include<cstring>
#define int long long
using namespace std;
int a[15], l;
int dp[10000005];
signed main(){
for(int i = 1; i <= 10000005; i++)
dp[i] = 0x7ffffffffff;
for(int i = 1; i <= 10; i++)
scanf("%lld", &a[i]), dp[i] = a[i];
scanf("%lld", &l);
for(int i = 1; i <= 10; i++)//枚举每一种
for(int j = 1; j <= l; j++)//枚举走多少公里
for(int k = l; k >= 1; k--)//枚举每一个起点
if(dp[k])//如果可行
dp[k] = min(dp[k], dp[k - i] + a[i]);//更新
cout<<dp[l];
return 0;
}
T11 HZOJ_P247
本的求解过程很简单,与前面几道题没有什么区别,但输出很恶心,需要pre前驱数组记录输出,较为麻烦。
#include <iostream>
#include <algorithm>
using namespace std;
int n, m, sum, cnt;
int f[100005], w[100005], ans[200005], vis[200005];
int main() {
scanf("%d%d", &m, &n);
for (int i = 1; i <= n; i++)
scanf("%d", &w[i]), sum += w[i];
f[0] = 1;
m = sum - m;
for (int i = 1; i <= n; i++)
for (int j = m; j >= w[i]; j--)
if (f[j - w[i]]) {
if (!f[j])
vis[j] = i;
f[j] += f[j - w[i]];
}
if (!f[m]) puts("0");
else if (f[m] >= 2) puts("-1");
else {
for (int i = m; i >= 1; i -= w[vis[i]])
ans[++cnt] = vis[i];
sort(ans + 1, ans + 1 + cnt);
for(int i = 1; i <= cnt; i++)
printf("%d ", ans[i]);
}
return 0;
}
T12 HZOJ_P248
看上去很难,但没有什么思维难度的背包题。
简化题意之后发现就是一个多次上文的砝码问题再判断共解。
#include<iostream>
#include<cstring>
using namespace std;
int n;
int ans;
bool dp[105][10005];
int w[105];
int cnt[105];
int main(){
scanf("%d", &n);
for(int k = 1; k <= n; k++){
int x;
memset(w, 0, sizeof(w));
while(scanf("%d", &x) && x != -1)
w[++cnt[k]] = x;
dp[k][0] = 1;
for(int i = 1; i <= cnt[k]; i++)
for(int j = 10000; j >= w[i]; j--)
if(dp[k][j - w[i]])
dp[k][j] = 1;
}
for(int k = 10000; k >= 1; k--){
for(int i = 1; i <= n; i++)
if(!dp[i][k])
goto P;
printf("%d\n", k);
return 0;
P:;
}
putchar('0');
return 0;
}
4. 一般的多重背包
-
问题描述 : 在 0/1 背包问题上增加了每个物品的个数
-
一般的解 : 增加第三层循坏来枚举选择物品的个数,其它没有什么区别
时间复杂度
-
难点 : 基本没有
-
例题 : T7庆功会 HZOJ_P243 模板题,不解释
#include<iostream> using namespace std; int m, n; int dp[60005], w[605], c[605], s[605]; int main(){ scanf("%d%d", &n, &m); for(int i = 1; i <= n; i++) scanf("%d%d%d", &w[i], &c[i], &s[i]); for(int i = 1; i <= n; i++) for(int j = m; j >= w[i]; j--) for(int k = 1; k <= s[i]; k++){ if(k * w[i] > j)//这个判断一定要加 ! 当前物品的重量必须 <= 总重量,不然就失败了~~ break; dp[j] = max(dp[j], dp[j - w[i] * k] + c[i] * k); } printf("%d\n", dp[m]); return 0; }
5. 二进制优化多重背包
- 概述 :
背包问题的第二个难点。
多重背包的时间复杂度是
考虑到二进制的性质, 每一个整数都可以由2的每一个正数幂次累加再加上一个常数得到 ,我们可以利用这个性质优化多重背包,使它变成 0/1 背包。
例如 23 -> (1 + 2 + 4 + 8) + 8 ,原本的23个物品被拆解成了5个物品,效率大大提升了,原有的
2.例题 T8 HZOJ_P242 算是二进制优化多重背包的模板题了
#include <iostream>
using namespace std;
int m, n, cnt;
int dp[100005], w[100005], c[100005], s[100005];
int W[100005], C[100005];
void solve() {
//注意该函数的写法。一定要严格按照这个函数来写,要不然细节会挂
for(int i = 1; i <= n; i++){
int k = 1;
while(s[i]){
W[++cnt] = w[i] * k;
C[cnt] = c[i] * k;
s[i] -= k;
k *= 2;
if(s[i] < k){
W[++cnt] = w[i] * s[i];
C[cnt] = c[i] * s[i];
break;
}
}
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &w[i], &c[i], &s[i]);
if (!s[i])
s[i] = 114514;
}
solve();
for (int i = 1; i <= cnt; i++)
for (int j = m; j >= W[i]; j--)
dp[j] = max(dp[j], dp[j - W[i]] + C[i]);
printf("%d\n", dp[m]);
return 0;
}
6. 混合背包
-
题目描述 : 前文所述多种背包的缝合怪
-
一般的解: 分类讨论即可
-
难点 : 无
-
例题 T9 HZOJ_P244 模板题,不解释
#include<iostream> using namespace std; int m, n; int dp[100005], w[100005], c[100005], s[100005]; int main(){ scanf("%d%d", &m, &n); for(int i = 1; i <= n; i++) scanf("%d%d%d", &w[i], &c[i], &s[i]); for(int i = 1; i <= n; i++) if(s[i]){ for(int j = m; j >= w[i]; j--) for(int k = 1; k <= s[i]; k++){ if(k * w[i] > j) break; dp[j] = max(dp[j], dp[j - w[i] * k] + c[i] * k); } } else for(int j = w[i]; j <= m; j++) dp[j] = max(dp[j], dp[j - w[i]] + c[i]); printf("%d\n", dp[m]); return 0; }
7. 多维背包
-
题目描述 : 就是在原有的 0/1 背包上增加了一个表示物品重量的维度而已。
-
一般的解 : 那就再多开一层循环就好
-
难点 : 无
-
例题 T10 HZOJ_P245 模板题,不解释
#include<iostream>
using namespace std;
int m1, m2;
int n;
int dp[505][505];
int T[505], V[505], w[505];
int main(){
scanf("%d%d%d", &m1, &m2, &n);
for(int i = 1; i <= n; i++)
scanf("%d%d%d", &T[i], &V[i], &w[i]);
for(int i = 1; i <= n; i++)
for(int j = m1; j >= T[i]; j--)//两个维度
for(int k = m2; k >= V[i]; k--)
dp[j][k] = max(dp[j][k], dp[j - T[i]][k - V[i]] + w[i]);
printf("%d\n", dp[m1][m2]);
return 0;
}
二、线性动态规划
1. 最长下降子序列 一类的问题
- 问题描述 : 略。
- 一般的解 :
地枚举区间的两个左右断端点,易得转移方程为 - 例题
HZOJ_P216 模板题,不解释,就是输出比较恶心。
#include<iostream>
#include<stack>
using namespace std;
int n;
int a[1005];
int dp[1005], list[1005];
stack<int>q;
void print(int x){
q.push(a[x]);
if(list[x])
print(list[x]);
}
int main() {
int x;
while(scanf("%d", &x) == 1)
a[++n] = x;
dp[1] = 1;
for (int i = 1; i <= n; i++) {
dp[i] = max(dp[i], 1);
for (int j = i + 1; j <= n; j++) {
if (a[j] > a[i] && dp[i] + 1 > dp[j]) {
dp[j] = dp[i] + 1;
list[j] = i;
}
}
}
int ans = 0, id;
for (int i = 1; i <= n; i++)
if (ans < dp[i])
ans = dp[i], id = i;
printf("max=%d\n", ans);
print(id);
while(!q.empty()){
printf("%d ", q.top());
q.pop();
}
return 0;
}
看似有一定的难度,实则不难。本题的本质就是做两次的线性dp, 找到第一个点到每个点的最长上升子序列长度和最长下降子序列长度,最后加和减一即可。
#include<iostream>
using namespace std;
int dp1[105], dp2[105];
int n;
int a[105];
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; i++)
scanf("%d", &a[i]), dp1[i] = dp2[i] = 1;
for(int i = n - 1; i >= 1; i--)
for(int j = i + 1; j <= n; j++)
if(a[i] > a[j])
dp1[i] = max(dp1[i], dp1[j] + 1);
for(int i = 2; i <= n; i++)
for(int j = 1; j < i; j++)
if(a[j] < a[i])
dp2[i] = max(dp2[i], dp2[j] + 1);
int ans = 0;
for(int i = 1; i <= n; i++)
ans = max(ans, dp1[i] + dp2[i] - 1);
printf("%d\n", n - ans);
return 0;
}
HZOJ_P220 本题在手玩几个样例之后不难发现本质就是将区间的一端排序之后计算另一边的最长不下降子序列。
#include <iostream>
#include <algorithm>
using namespace std;
int n;
struct Node {
int x, y;
} e[5005];
bool operator < (const Node &a, const Node &b) {
return a.x < b.x;
}
int dp[5005];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d%d", &e[i].x, &e[i].y);
sort(e + 1, e + 1 + n);
for (int i = 1; i <= n; i++) {
dp[i] = max(dp[i], 1);
for (int j = i + 1; j <= n; j++)
if (e[j].y >= e[i].y)
dp[j] = max(dp[j], dp[i] + 1);
}
int ans = 0;
for(int i = 1; i <= n; i++)
ans = max(ans, dp[i]);
printf("%d\n", ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】