CSP-J/S 动态规划模版讲解
https://algo.codefather.cn/
https://algo.codefather.cn/
part 1:动态规划起源与理解
起源
在一次信奥赛中一位选手使用一种新算法秒掉了其他选手,该算法时间复杂度比那些O(N2)快多了,定名为动态规划
理解
实际上就是做出一个表格,每一个代表一个状态,所以做这个最需要的就是状态转移方程
其实动态规划(Dynamic Programming)是一种通过将复杂问题分解为更简单的子问题来解决问题的方法。它适用于有重叠子问题和最优子结构性质的问题。
part 2:为什么讲动态规划而不是其他算法
请看2019~2023年CSP第一轮考试知识点
| 年份 | 阅读程序1 | 阅读程序2 | 阅读程序3 | 完善程序1 | 完善程序2 |
|---|---|---|---|---|---|
| 2019 | 枚举 | 递推 | 分治 | 递归 | 计数排序 |
| 2020 | 枚举 | 枚举 | 质因数分解 | 约瑟夫环 | 二分 |
| 2021 | 递归 | 动规 | 二分 | 质因数分解 | 贪心 |
| 2022 | 位运算 | 动规 | 二分 | 递推 | 广搜 |
| 2023 | 枚举 | 字符串 | 质因数分解 | 二分 | 动规 |
可以看到,在2019~2023年间二分考了4次,枚举考了3次,而动规也考了3次。
但是为什么不讲二分或枚举呢?
很简单,因为枚举在编程题不经常考,
而二分只是一种思想,并没有固定代码模版 (换句话说就是不好讲)。
可以看到,动规在一轮考的次数不算少,所以二轮占的比重一定很大(基本3,4题里面肯定有一道)。再看看历年真题,大部分题目不想超时就得用特殊的方法,动规就是其中的一种
part 3:代码模版
第一类:背包问题
听到他一定很耳熟,大部分背包问题都是用动规求解
代码都很相似,不要搞错了
1.1 01背包
就是这种东西要么不装要么装
二维状态转移方程:
for(int i=1;i<=n;i++){
for(int j=1;j<=V;j++){
f[i][j]=f[i-1][j];
if(v[i]<=j){
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
}
}
}
cout<<f[n][V];
一维状态转移方程:
for(int i=1;i<=n;i++){
for(int j=V;j>=v[i];j--){
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[V];
1.2 完全背包
二维状态转移方程:
for(int i=1;i<=n;i++){
for(int j=1;j<=V;j++){
f[i][j]=f[i-1][j];
if(j>=v[i]){
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
}
}
}
cout<<f[n][V];
一维状态转移方程:
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]);
}
}
cout<<f[V];
1.3 多重背包
二维状态转移方程:
for(int i=1;i<=n;i++){
for(int j=V;j>=v[i];j--){
for(int k=0;k<=s[i]&&k*v[i]<=j;i++)
f[j]=max(f[j-k*v[i]]+k*w[i]);
}
}
上述代码时间复杂度约为:
O(n⋅m⋅s),很容易超时
多重背包二进制优化:转为二进制,用01背包求解
for(int i=1;i<=n;i++){
int vi,wi,si,t=1;
cin>>vi>>wi>>si;
while(si>=t){
v[++cnt]=vi*t;
w[cnt]=wi*t;
si-=t;
t*=2;
}
if(s>0){
v[++cnt]=vi*si;
w[cnt]=wi*si;
}
}
for(int i=1;i<=cnt;i++){
for(int j=V;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[V];
这样就可以减少一层循环,比原来的快约s倍
1.4 分组背包
for(int i=1;i<=n;i++){
for(int j=V;j>=1;j--){
for(int k=1;k<=s[i];k++){
if(j>=v[i][k])
f[j]=max(f[j],f[j-v[i][k]]+v[i][k]);
}
}
}
cout<<f[V];
第二类:线性DP
2.1:数字三角形
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]);
}
}
2.2:最长上升子序列LIS
for(int i=1;i<=n;i++){
f[i]=1;
for(int j=1;j<=i;j++){
if(a[j]<a[i])
f[i]=max(f[i],f[j]+1);
}
}
int ans=0;
for(int i=0;i<=n;i++)
ans=max(ans,f[i]);
cout<<ans;
第三类:区间DP
3.1:石子合并
描述
在一个操场上一排地摆放着N堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。
设计一个程序,计算出将N堆石子合并成一堆的最小得分。
输入描述
第一行为一个正整数N (2≤N≤100);
以下N行,每行一个正整数,小于10000,分别表示第i堆石子的个数(1≤i≤N)。
输出描述
为一个正整数,即最小得分。
用例输入 1
7
13
7
8
16
21
4
18
用例输出 1
239
来源
动态规划 区间动归
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,r=i+len+1;
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];

1623

被折叠的 条评论
为什么被折叠?



