闫氏DP分析法 学习笔记
求解的过程
DP问题 == 有限集上的最值问题
DP的两个阶段:
- 化零为整,寻找共性——状态表示
-
- 集合
所谓 化零为整 是指我们对于零星的情况,不是一个一个去枚举,而是每次根据这些零星的情况的某些特性去枚举一类情况(即一个子集)
- 集合
f(i)需要考虑的问题:
-
- 属性
表示的是一个怎样的集合?
f(i)保存的值是什么意思(max/min/count/…)?与集合有什么关系?
- 属性
- 化整为零,寻找不同——状态计算
如何去求f(i)呢?
我们要把它们划分成若干个不同的子集来求,每一个子集分别去求。需要满足的条件有:
-
-
- 不重复(可以不满足,例如求min,max。但是求count的时候,就不能重复)
-
-
-
- 不遗漏
例如,我们假设f(i)表示的是某个集合的最大值,那么可以每个子集的最大值的最大值。
那么问题来了,进行集合f(i)的划分的依据?
寻找最后一个不同点。
- 不遗漏
-
01背包
问题:
有N件物品和一个容量式V的背包,每件物品只能使用一次
第i件物品的体积式vi,价值是wi
求将这些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大
分析:
是一个有限集合的最值问题
- 状态表示:f(i,j)
-
- 集合:只考虑前i个物品,且总体积不超过j的选择方案集合
-
- 属性:max f(N,V)
- 状态计算:
-
- 集合1,不选第i个物品的方案,maxf(i-1,j)
-
- 集合2,选第i个物品的方案,maxf(i-1,j-vi)+wi
两个集合满足:不重复和不遗漏
- 集合2,选第i个物品的方案,maxf(i-1,j-vi)+wi
-
- 将集合1和集合2取最大值就是所求答案 f(i,j)=max(f(i-1,j),f(i-1,j-vi)+wi)
- 优化
分析状态计算可知,f(i,j)只记录f(i-1)即可。
朴素代码:
#include <bits/stdc++.h> using namespace std; typedef long long ll; #define PII pair<int, int> #define x first #define y second const int maxn = 1e6 + 10; const int INF = 0x3f3f3f3f; const ll M = 1e9 + 7; const int N = 1010; int v[N],w[N]; int f[N][N]; int n,m; int main() { 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][j]=max(f[i][j],f[i-1][j]-v[i]+w[i]); } } cout<<f[n][m]; return 0; }
DP问题的优化是对代码进行等价替换,和题目无关。
优化代码:
#include <bits/stdc++.h> using namespace std; typedef long long ll; #define PII pair<int, int> #define x first #define y second const int maxn = 1e6 + 10; const int INF = 0x3f3f3f3f; const ll M = 1e9 + 7; const int N = 1010; int v[N], w[N]; int f[N]; int n, m; int main() { 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 = m; j >= 0; j--) { if (j >= v[i]) f[j] = max(f[j], f[j] - v[i] + w[i]); } /* 等价于for (int j = m; j >= v[i]; j--) { f[j] = max(f[j], f[j] - v[i] + w[i]); } */ } cout << f[n][m]; return 0; }
完全背包
完全背包的每个物品可以用无限次,其他条件与01背包一致。
- 状态表示:
-
- 集合:只考虑前i个物品,且总体积不超过j的选择方案集合
-
- 属性:max
- 状态计算:
-
- 集合0:选0个第i个物品,f(i-1,j)
-
- 集合1:选1个第i个物品,f(i-1,j-Vi)+Wi
-
- 集合 …
-
- 集合K:选k个第i个物品,f(i-1,j-kVi)+kWi
-
- 集合 …
每个集合具有排他性
- 集合 …
推导f(i-1,j-kVi)+kWi
我们容易得到
但是对于
立即推:
朴素代码:
#include <bits/stdc++.h> using namespace std; typedef long long ll; #define PII pair<int, int> #define x first #define y second const int maxn = 1e6 + 10; const int INF = 0x3f3f3f3f; const ll M = 1e9 + 7; const int N = 1010; int v[N],w[N]; int f[N][N]; int n,m; int main() { 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][j]=max(f[i][j],f[i][j-v[i]]+w[i]); } } cout<<f[n][m]; return 0; }
优化代码
#include <bits/stdc++.h> using namespace std; typedef long long ll; #define PII pair<int, int> #define x first #define y second const int maxn = 1e6 + 10; const int INF = 0x3f3f3f3f; const ll M = 1e9 + 7; const int N = 1010; int v[N], w[N]; int f[N]; int n, m; int main() { 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 = v[i]; j <= m; j++) { f[j] = max(f[j], f[j - v[i]] + w[i]); } } cout << f[m]; return 0; }
区间DP
合并石子问题
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量和,合并后与这两堆石子相邻的石子和新堆相邻,合并时由于选择的顺序不同,合并的代价也不同
比如有4堆 1 3 5 2 ,我们可以先合并1、2堆,代价为4,得到4,5,2,又合并1,2堆代价为9,得到9,2,再合并得到11,总代价为4+9+11
找出一种合适的方法,使总合并代价最小
- 状态表示:
-
- 集合:所有将[i,j]合并成一堆的方案的集合
-
- 属性:min
- 状态计算:
-
- f[i,j] = f[i,k]+f[k+1,j]+s[j]-s[i-1];(s为本步骤代价)
分析:
对于区间[i,j],可以划分为左边和右边,问题的每一步都是左边和右边进行合并
我们把左半边的最后一堆设为分界点 k ,
因此我们问题变成,如何合并[i,k]和[k+1,j](
容易想到两部分是完全独立的,可以分开计算,
对于求f(i,k)和f(k+1,j)就是在各个子集中求出最小值,
然后再对它们取个 min 就是f(i,j)。
- f[i,j] = f[i,k]+f[k+1,j]+s[j]-s[i-1];(s为本步骤代价)
代码:
#include <bits/stdc++.h> using namespace std; typedef long long ll; #define PII pair<int, int> #define x first #define y second const int maxn = 1e6 + 10; const int INF = 0x3f3f3f3f; const ll M = 1e9 + 7; const int N = 1010; int n; int s[N]; //前n项和 int f[N][N]; int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>s[i],s[i]+=s[i-1]; for(int len=2;len<=n;len++){ for(int i=1;i+len-1<=n;i++){ int j=i+len-1; f[i][j]=1e8; for(int k=i;k<j;k++) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]); } } cout<<f[1][n]<<endl; return 0; }
线性dp
求最长公共子序列
给定两个长度分别为N和M的字符串A,B,求即是A的子序列又是B的子序列的字符串长度是多少。
- 状态表示:
-
- 集合:所有A[1,i]与B[1,j]的公共子序列的集合
-
- 属性:max
- 状态计算:
-
- f[i,j] = max(f[i-1,j],f[i,j-1],f[i-1,j-1]+1)
对最后一个不同点可以划分四类:
- f[i,j] = max(f[i-1,j],f[i,j-1],f[i-1,j-1]+1)
公共子序列不包含A [i] 也不包含 B [j]
公共子序列不包含A [i] 包含 B [j]
公共子序列包含A [i] 不包含 B [j]
公共子序列包含A [i] 也包含 B [j]
四种状态不重复不遗漏具有排他性
四种状态分别用00,01,10,11表示。
-
- 当情况为11时
当情况为11时,其必满足A [ i ] = B [ j ](字符相等)
将这一子集根据变与不变划分为两个部分:
一是前面 A 的 i - 1个字符与 B 的 j - 1个字符的情况(变的部分),就是 f(i-1,j-1)
二是A [ i ] = b [ j ](不变的部分),就是1
此时f(i,j) =f(i-1,j-1)+1
- 当情况为11时
-
- 当情况是00时
易得f(i,j) = f(i-1,j-1)
- 当情况是00时
-
- 当情况是01时
由于f(i-1,j) 包含两种情况,一种是B [ j ] 在公共子序列中,一种是B [ j ] 不在公共子序列中 。
但本题重复对于求最大值没有影响,所以可以直接使用
即f(i,j) = f(i-1,j)
- 当情况是01时
-
- 当情况是10时
f(i,j) = f(i,j-1) 。原因同上
- 当情况是10时
由上可以看出,01子集已包含00情况下的式子,
所以我们在代码构造中只需要计算 01、10、11三个部分即可。
代码:
#include <bits/stdc++.h> using namespace std; typedef long long ll; #define PII pair<int, int> #define x first #define y second const int maxn = 1e6 + 10; const int INF = 0x3f3f3f3f; const ll M = 1e9 + 7; const int N = 1010; int n,m; char a[N],b[N]; int f[N][N]; int main(){ cin>>n>>m>>a+1>>b+1; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ f[i][j] = max(f[i-1][j],f[i][j-1]); if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1); } } cout<<f[n][m]<<endl; return 0; }
本文作者:kingwzun
本文链接:https://www.cnblogs.com/kingwz/p/15827756.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步