DP大法,玄妙无比。
动态规划的原理
动态规划(dyanmic programming)与分治方法相似,都是通过组合子问题的解来求解原问题。 分治方法:将问题划分为互不相交的子问题,递归求解子问题,再将姐组合起来,求出原问题的解。 与之相反,动态规划应用于子问题重叠情况,即不同子问题具有公共的子子问题(将子子问题存入表格,只求解一次) 刻画最优解的结构特征。 利用计算信息构造一个最优解。 “动态规划算法通常利用重叠子问题性质:对每一个子问题求解一次,将解存入表中,再次需要直接查表。 ” 适合动态规划方法求解的最优化问题应具备两个要素:最优子结构和问题重叠。 “如果一个问题的最优解包含其子问题的最优解,我们称此问题具有最优子结构性质。
01背包问题
解题思路:
二维数组版
-
状态
f[i][j]
定义:前 i 个物品,背包容量 j 下的最优解(最大价值):当前的状态依赖于之前的状态,可以理解为从初始状态
f[0][0] = 0
开始决策,有 N 件物品,则需要 N 次决 策,每一次对第 i 件物品的决策,状态f[i][j]
不断由之前的状态更新而来。 -
当前背包容量不够( j < v[ i ] ),没得选,因此前 i 个物品最优解即为前 i−1 个物品最优解:
对应代码:f [i][j] = f [i - 1][j]
-
当前背包容量够,可以选,因此需要决策选与不选第 i 个物品:
选:
f [i][j] = f [i - 1][j - v[i]] + w[i]
不选:
f[i][j] = f[i - 1][j]
我们的决策是如何取到最大价值,因此以上两种情况取 max() 。
代码如下
#include<iostream>
using namespace std;
int n,m;
int f[1010][1010];
int v[1010],w[1010];
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>=1;j--) //for(int j=1;j<=m;j++) 也可以
{
//if(j<v[i])
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;
}
优化版–> 一维滚动数组
将状态f[i][j]
优化到一维f[j]
,实际上只需要做一个等价变形。
为什么可以这样变形呢?我们定义的状态f[i][j]可以求得任意合法的i与j最优解,但题目只需要求得最终状态f[n][m],因此我们只需要一维的空间来更新状态。
- 状态
f[j]
定义:N
件物品,背包容量j
下的最优解。 - 注意枚举背包容量
j
必须从m
开始。(逆序) - 为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态
f[i][j]
是由上一轮i - 1
的状态得来的,f[i][j]
与f[i -1][j]
是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]
更新到f[较大体积]
,则有可能本应该用第i-1
轮的状态却用的是第i
轮的状态。
例如,一维状态第i
轮对体积为 3 的物品进行决策,则f[7]
由f[4]
更新而来,这里的f[4]
正确应该是f[i - 1][4]
,但从小到大枚举j这里的f[4]
在第i轮计算却变成了f[i][4]
。当逆序枚举背包容量j时,我们求f[7]同样由f[4]
更新,但由于是逆序,这里的f[4]
还没有在第i
轮计算,所以此时实际计算的f[4]
仍然是f[i - 1][4]
。
简单来说,一维情况正序更新状态f[j]
需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。
状态转移方程为:f[j] = max(f[j], f[j - v[i]] + w[i]
代码如下
#include<iostream>
using namespace std;
int n,m;
int f[1010];
int v[1010],w[1010];
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];
return 0;
}
注意 f[j-v[i]]==f[i-1][j-v[i]]
,更新是用上一轮没更新的状态,不是用当前的状态更新。
完全背包问题
朴素版 O(n*n) 有可能超时
#include<iostream>
using namespace std;
const int N = 3010;
int n, m;
int dp[N][N], v[N], w[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 ++ )
for(int k = 0; k * v[i] <= j; k ++ )
dp[i][j]=max(dp[i][j], dp[i-1][j-k*v[i]]+k*w[i]);
cout << dp[n][m] << endl;
return 0;
}
所以我们要改进
f[i][j - v] =
max(f[i - 1][j - v], f[i - 1][j - 2 * v] + w, f[i - 1][j - 3 * v] + 2 * w, .....)
;
而我们需要的f[i][j]
的状态表示是:
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] = max( f[i - 1][j], f[i][j - v] +w );
二维版
#include<iostream>
using namespace std;
const int N = 1010;
int n, m;
int f[N][N], v[N], w[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ )
{
int v, w;
cin >> v >> w;
for(int j = 0; j <= m; j ++ )
{
f[i][j] = f[i - 1][j];
if(j >= v)
f[i][j] = max(f[i][j], f[i][j - v] + w);
}
}
cout << f[n][m] << endl;
return 0;
}
优化版
#include<iostream>
using namespace std;
int n,m;
int f[1010];
int v[1010],w[1010];
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;
}
注意f[j-v[i]]==f[i][j-v[i]]
完全背包一维的滚动数组代码和 01背包 的代码只有一处区别,即 01背包的j
是从大到小遍历,而 完全背包 的j
是从小到大遍历,两者的含义不同。
多重背包问题 I
思路和 01背包相似
#include<iostream>
using namespace std;
const int N=110;
int s[N],v[N],w[N];
int f[N][N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>> v[i] >> w[i]>> s[i];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for( int k=0; k<= s[i] && k * v[i]<= j; k++ )
f[i][j]=max( f[i][j],f[i-1][j-v[i]*k]+w[i]*k );
cout<<f[n][m];
return 0;
}
或者
#include <iostream>
using namespace std;
int f[1005];
int main()
{
int n, m ;
cin >> n >> m;
for(int l=1;l<=n;l++)
{
int v, w, k;
cin >> v >> w >> k;
for (int i = 1; i <= k; i++)
for (int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v] + w);
}
cout << f[m];
}
多重背包问题 II
由于时间限制,必须要优化,否则就 TLE 。
二进制优化!
以 2013 为例
把2013分组为 1,2,4,8,,,512 ,任意几组相加,可得到1~2013,从原本0 ~ 2013 遍历,变成 0 ~ 9遍历,,遍历次数就减少,时间变成log(2013)
#include<iostream>
#include<algorithm>
using namespace std;
const int N=25000;
int v[N],w[N];
int f[N];
int main()
{
int n,m;
cin>>n>>m;
int cnt=0;
for(int i=1;i<=n;i++)
{
int a,b,s;
cin>>a>>b>>s;
int k=1;
while(k<=s)
{
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
if(s>0)
{
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
n=cnt;
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];
return 0;
}
分组背包问题
一、状态表示:f[i][j]
- 集合:从前i组物品中选,且总体积不超过j的所有方案的集合.
- 属性:最大值
二、状态计算:
- 思想-----集合的划分
- 集合划分依据:根据从第i组物品中选哪个物品进行划分.
f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k])
代码如下
#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int v[N][N],w[N][N];
int f[N],s[N];
int main()
{
int n,m;
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=m;j>=0;j--)
for(int k=0;k<=s[i];k++)
if(v[i][k]<=j)
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
cout<<f[m];
return 0;
}
欢迎点赞与评论~
记得收藏