闫氏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
我们容易得到
\(f(i,j) = max(f(i-1,j),f(i-1,j-V_i)+W_i,f(i-1,j-2V_i)+2W_i,…,f(i-1,j-kV_i)+kW_i)\)
但是对于\(f(i,j-v) = max(f(i-1,j-V_i),f(i-1,j-2V_i)+W_i,…,f(i-1,j-kV_i)+kW_i)\)
立即推:\(f(i,j) = max( f(i-1,j),f(i,j-V_i)+W_i)\)
朴素代码:
#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;
}