动态规划
目录
背包问题
背包问题
核心套路
优化一般就是优化状态转移方程
01背包
特点:每个物品仅能使用一次
重要变量&公式解释
f[i][j]:表示所有选法集合中,只从前i个物品中选,并且总体积≤≤j的选法的集合,它的值是这个集合中每一个选法的最大值.
状态转移方程
f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i])
f[i-1][j]:不选第i个物品的集合中的最大值
f[i-1][j-v[i]]+w[i]:选第i个物品的集合,但是直接求不容易求所在集合的属性,这里迂回打击一下,先将第i个物品的体积减去,求剩下集合中选法的最大值.
问题
集合如何划分
一般原则:不重不漏,不重不一定都要满足(一般求个数时要满足)
如何将现有的集合划分为更小的子集,使得所有子集都可以计算出来.
//无优化版
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
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] << endl;
return 0;
}
//有优化版
/*
1. f[i] 仅用到了f[i-1]层,
2. j与j-v[i] 均小于j
3.若用到上一层的状态时,从大到小枚举, 反之从小到大哦
*/
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
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 >= v[i]; j--)
f[j] = max(f[j], f[j-v[i]]+w[i]);
cout << f[m] << endl;
return 0;
}
完全背包问题
#include<iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N],w[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++)
{
for(int k = 0 ; k*v[i]<=j ; k++)
f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
cout<<f[n][m]<<endl;
}
优化思路
我们列举一下更新次序的内部关系:
f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-2*v]+2*w , .....)
由上两式,可得出如下递推关系:
f[i][j]=max(f[i,j-v]+w , f[i-1][j])
有了上面的关系,那么其实k循环可以不要了,核心代码优化成这样:
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]>=0)
f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
这个代码和01背包的非优化写法很像啊!!!我们对比一下,下面是01背包的核心代码
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]>=0)
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
两个代码其实只有一句不同(注意下标)
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包
f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题
因为和01背包代码很相像,我们很容易想到进一步优化。核心代码可以改成下面这样
for(int i = 1 ; i<=n ;i++)
for(int j = v[i] ; j<=m ;j++)//注意了,这里的j是从小到大枚举,和01背包不一样
{
f[j] = max(f[j],f[j-v[i]]+w[i]);
}
综上所述,完全背包的最终写法如下:
#include<iostream>
using namespace std;
const int N = 1010;
int f[N];
int v[N],w[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 = v[i] ; j<=m ;j++)
{
f[j] = max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[m]<<endl;
}
多重背包问题I
- 基本解题思路:
一个基本思路是,将此问题转换为01背包求解!
比如物品1有3件,每件价值为2,我们不妨创建3个物品1,存在数组v和数组w中
最终更新一下总物品数n即可,然后套用01背包问题进行求解。
- 优化时间复杂度到O(mlogn) (多重背包问题II的解法)
前面提到的多重背包问题的解法,是把多件物品,转换成多个单件物品,添加到v和w数组中。
如10件物品A, 则插入十条记录到v和w数组中,实际上这一过程可以进行优化!
我们可以把十件物品A分成若干份,这若干份必须可以组合成0~10以内的任何一个数字。
做法是:1,2,4,…,2(k-1),10-2k+1
即:10可以分为 1,2,4,3
显然这四个数字,可以组合成0~10以内的任何一个数字,如 8 = 1 + 4 + 3
每一份对应的体积和价值,用系数乘以1件物品的体积和价值。
这么做的好处,可以把时间复杂度从O(nm)降为O(m log n),剩下的继续用01背包问题的解法求解。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e5+100;
ll v[N],w[N];
ll f[N];
int main()
{
ll n,m;
ll cnt=1;
cin>>n>>m;
ll a,b,c;
for(ll i=1;i<=n;i++)
{
cin>>a>>b>>c;
for(ll j=1;j<=c;j++)
{
v[cnt]=a;
w[cnt]=b;
cnt++;
}//将多重背包一个一个拆出来
}
for(ll i=1;i<=cnt;i++)
{
for(ll j=m;j>=v[i];j--)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}//01背包
cout<<f[m];
return 0;
}
多重背包问题II
思路和多重背包问题I一样,但这题的数据范围变成1000了,非优化写法时间复杂度O(n^3) 接近 1e9
必超时。
优化多重背包的优化
首先,我们不能用完全背包的优化思路来优化这个问题,因为每组的物品的个数都不一样,是不能像之前一样推导不优化递推关系的。(详情看下面引用的博文)
引用我之前写的博客:动态规划-完全背包问题
我们列举一下更新次序的内部关系:
f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2v]+2w , f[i-1,j-3v]+3w , …)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2v] + w , f[i-1,j-2v]+2*w , …)
由上两式,可得出如下递推关系:
f[i][j]=max(f[i,j-v]+w , f[i-1][j])
接下来,我介绍一个二进制优化的方法,假设有一组商品,一共有11个。我们知道,十进制数字 11 可以这样表示
11=1011(B)=0111(B)+(11−0111(B))=0111(B)+0100(B)
11=1011(B)=0111(B)+(11−0111(B))=0111(B)+0100(B)
正常背包的思路下,我们要求出含这组商品的最优解,我们要枚举12次(枚举装0,1,2…12个)。
现在,如果我们把这11个商品分别打包成含商品个数为1个,2个,4个,4个(分别对应0001,0010,0100,0100)的四个”新的商品 “, 将问题转化为01背包问题,对于每个商品,我们都只枚举一次,那么我们只需要枚举四次 ,就可以找出这含组商品的最优解。 这样就大大减少了枚举次数。
这种优化对于大数尤其明显,例如有1024个商品,在正常情况下要枚举1025次 , 二进制思想下转化成01背包只需要枚举10次。
优化的合理性的证明
先讲结论:上面的1,2,4,4是可以通过组合来表示出0~11中任何一个数的,还是拿11证明一下(举例一下):
首先,11可以这样分成两个二进制数的组合:
11=0111(B)+(11−0111(B))=0111(B)+0100(B)
11=0111(B)+(11−0111(B))=0111(B)+0100(B)
其中0111通过枚举这三个1的取或不取(也就是对0001(B),0010(B),0100(B)的组合),可以表示十进制数0~7( 刚好对应了 1,2,4 可以组合出 0~7 ) , 0~7的枚举再组合上0100(B)( 即 十进制的 4 ) ,可以表示十进制数 0~11。其它情况也可以这样证明。这也是为什么,这个完全背包问题可以等效转化为01背包问题,有木有觉得很奇妙
怎么合理划分一个十进制数?
上面我把11划分为
11=0111(B)+(11−0111(B))=0111(B)+0100(B)
11=0111(B)+(11−0111(B))=0111(B)+0100(B)
是因为 0111(B)刚好是小于11的最大的尾部全为1的二进制 ( 按照上面的证明,这样的划分没毛病 ) , 然后那个尾部全为1的数又可以 分解为 0000…1 , 0000…10 , 0000…100 等等。
对应c++代码:
//设有s个商品,也就是将s划分
for(int k = 1 ; k <= s ;k*=2)
{
s-=k;
goods.push_back({vk,wk});
}
if(s>0)
goods.push_back({vs,ws});
终究AC代码:01优化+二进制优化
ac代码
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 2010;
int f[N],n,m;
struct good
{
int w,v;
};
int main()
{
cin>>n>>m;
vector<good> Good;
good tmp;
//二进制处理
for(int i = 1 ; i <= n ; i++ )
{
int v,w,s;
cin>>v>>w>>s;
//坑,k <= s
for(int k = 1 ; k <= s ; k*=2 )
{
s-=k;
Good.push_back({k*w,k*v});
}
if(s>0) Good.push_back({s*w,s*v});
}
//01背包优化+二进制
for(auto t : Good)
for(int j = m ; j >= t.v ; j--)
f[j] = max(f[j] , f[j-t.v]+t.w ); //这里就是f[j]
cout<<f[m]<<endl;
return 0;
}
分组背包问题
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int f[N][N]; //只从前i组物品中选,当前体积小于等于j的最大值
int v[N][N],w[N][N],s[N]; //v为体积,w为价值,s代表第i组物品的个数
int n,m,k;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s[i];
for(int j=0;j<s[i];j++){
cin>>v[i][j]>>w[i][j]; //读入
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j]; //不选
for(int k=0;k<s[i];k++){
if(j>=v[i][k]) f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
}
}
}
cout<<f[n][m]<<endl;
}
因为只用到了第i-1列,所以可以仿照01背包的套路逆向枚举体积
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int f[N];
int v[N][N],w[N][N],s[N];
int n,m,k;
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
cin>>s[i];
for(int j=0;j<s[i];j++){
cin>>v[i][j]>>w[i][j];
}
}
for(int i=0;i<n;i++){
for(int j=m;j>=0;j--){
for(int k=0;k<s[i];k++){ //for(int k=s[i];k>=1;k--)也可以
if(j>=v[i][k]) f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
}
}
}
cout<<f[m]<<endl;
}
线性DP
数字三角形
最长上升子序列I
最长上升子序列II
最长公共子序列
最短编辑距离
编辑距离
#include<bits/stdc++.h>
using namespace std;
const int N = 15;
const int M = 1001;
int n,m,f[N][N];
char s[M][N];
int edit_dis(char a[],char b[]) {
int lena = strlen(a+1);
int lenb = strlen(b+1);
for(register int i=1; i<=lena; i++) f[i][0] = i;
for(register int i=1; i<=lenb; i++) f[0][i] = i;
for(register int i=1; i<=lena; i++)
for(register int j=1; j<=lenb; j++) {
f[i][j] = min(f[i-1][j]+1 , f[i][j-1]+1);
if(a[i]==b[j]) f[i][j] = min(f[i][j] , f[i-1][j-1]);
else f[i][j] = min(f[i][j] , f[i-1][j-1]+1);
}
return f[lena][lenb];
}
int main(){
scanf("%d%d",&n,&m);
for(register int i=0; i<n; i++) scanf("%s",s[i]+1);
while(m--) {
char q[N];
int limit;
scanf("%s%d",q+1,&limit);
int ans = 0;
for(register int i=0; i<n; i++)
if(edit_dis(s[i],q)<=limit) ans++;
printf("%d\n",ans);
}
return 0;
}