动态规划——dp思想总结
DP思想:常用于解决在一个有限集当中,时间复杂度特别大,求其中的最大值、最小值、某个数的个数等,可以用DP方法来解决。
解题一般分两个步骤:1.状态表示f(v,j);2.状态计算。
两大准则:不重复,不遗漏。
划分依据:寻找最后一个不同点。
题型一:01背包
题目链接
题型是有限物品有限容量求最大值问题。
思路分析:
则有一个物品要么选要么不选,只有两种情况,所以总共的方案数有2的N次方个;也就是一个有限集合求最值问题。
第一步:状态表示f(v,j)
f(v,j):第一维(v)考虑前i个物品;其他维(第二第三维):对选择过程中加上限制(例如体积的限制,重量的限制)。
第二步:状态计算
计算分两个方向:就对与一个物品来说,它只有选与不选,则每当到一个物品面前时,就会分成两个方案:1.所有选择该物品(i)的方案;2.所有不选择该物品(i)的方案。
通过以上两个步骤可得:f(i,j)=max(f(i-1,j),f(i-1,j-vi)+wi)
for循环:for(j=v;j>=vi,j–)
代码一:二维数组
//01背包
#include <iostream>
using namespace std;
const int N=1010;
int n,m;//n表示物品数量,m表示背包的最大体积
int v[N],w[N];//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;
}
代码二:优化后变成一维数组
//01背包
#include <iostream>
using namespace std;
const int N=1010;
int n,m;//n表示物品数量,m表示背包的最大体积
int v[N],w[N];//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>=0;j--)
{
//f[j]=f[j];
//f[i][j]=f[i-1][j];//左半边的子集
if(j>=v[i])//当前背包容量应大于当前选择物品的容量
f[j]=max(f[j],f[j-v[i]]+w[i]);
//f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
cout<<f[m]<<endl;
return 0;
}
/*
优化过后可看出来j循环变成了从大到小循环,因为数组变成了一维数组
,删除了i的部分,f[j]=f[j]等价于f[i][j]=f[i-1][j];
f[j]=max(f[j],f[j-v[i]]+w[i])等价于f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
*/
到这里停一下,来总结一下优化的依据是什么:
就是一定要保证优化前后的核心代码如f[j]=max(f[j],f[j-v[i]]+w[i])一定要是等价的!一定要是等价的!!!
题型二:完全背包
题目链接
先说一下结论:01背包优化后的代码和完全背包是相似的,但是其中的推导过程大不相同。
把01背包中j的从大到小循环再修改为从小到大循环就可以解决完全背包问题。
如代码一:
//01背包
#include <iostream>
using namespace std;
const int N=1010;
int n,m;//n表示物品数量,m表示背包的最大体积
int v[N],w[N];//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=v[i];j<=m;j++)
{
//f[j]=f[j];
//f[i][j]=f[i-1][j];//左半边的子集
//当前背包容量应大于当前选择物品的容量
f[j]=max(f[j],f[j-v[i]]+w[i]);
//f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
cout<<f[m]<<endl;
return 0;
}
/*
优化过后可看出来j循环变成了从大到小循环,因为数组变成了一维数组
,删除了i的部分,f[j]=f[j]等价于f[i][j]=f[i-1][j];
f[j]=max(f[j],f[j-v[i]]+w[i])等价于f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
*/
完全背包中一个物品可以重复选,所以对于一个物品来说,有两种情况,不选该物品,或者该物品选k个。
状态转移方程为:f(i,j)=max(f(i-1,j),f(i,j-v)+w);
先在这里总结一下01背包和完全背包中状态转移方程的区别:
01背包:f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
完全背包:f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);
由此可以清楚的看出来两方程的区别,由于这个区别而造成01背包中j是从大到小,而完全背包中j是从小到大。
代码二:(普通方法)
//01背包
#include <iostream>
using namespace std;
const int N=1010;
int n,m;//n表示物品数量,m表示背包的最大体积
int v[N],w[N];//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=1;j<=m;j++)
{
//f[j]=f[j];
//f[i][j]=f[i-1][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]<<endl;
return 0;
}
/*
优化过后可看出来j循环变成了从大到小循环,因为数组变成了一维数组
,删除了i的部分,f[j]=f[j]等价于f[i][j]=f[i-1][j];
f[j]=max(f[j],f[j-v[i]]+w[i])等价于f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
*/
此时,我们再回顾一下完全背包的代码一,可知为什么j要从小到大了。
在这里带大家具体总结一下j循环方向的原因吧:
先说一下省略一个维度的思想,省略的那一维度是i,而i跑到哪了呢?i是通过第一个for循环来控制的,所以当用一维数组时,for循环中i是几就是对应中二维数组中的i。
在01背包中:
变成一维数组时:f[j]=max(f[j],f[j-v[i]]+w[i]);该方程省略了i-1这个部分,所以应让j从大到小循环,这样就有:先假设上一层设定为i,那该层中就代表的是i-1。
在完全背包中:
变成一维数组时,f[j]=max(f[j],f[j-v[i]]+w[i]);该方程省略了i这个部分,所以应让j从小到大循环,这样就有:先假设上一层设定为i-1,那么该层中就代表的是i。
哈哈哈,终于弄懂了j从大到小或者从小到大的原因了。
在这里说一个通过上述总结出来的顺口溜吧:
j的循环条件,从m开始(01),在m结束(完全)。
————————————————————————————
接下来,我们该接触另一种dp题型
石子合并:
题目链接
首先,进行集合表示:
将(i,j)区间合并成一堆的方案的集合。
求最小值。
代码一:
//石子合并
#include <iostream>
using namespace std;
const int N=310;
int n;
int s[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;
}
最长公共子序列
题目链接
首先,进行集合表示:
所有A(1—i)与B(1—j)的公共子序列的集合。
求最大值。
代码一:
//石子合并
#include <iostream>
using namespace std;
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;
}