第3章动态规划(一)【背包问题】
基本思想:
动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,但是经分解得到的子问题往往不是互相独立的。不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而得到多项式时间算法。
基本要素:
1.最优子结构性质
问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。
在分析问题的最优子结构性质时,所用的方法具有普遍性:首先假设由问题的最优解导出的子问题的解不是最优的,然后再设法说明在这个假设下可构造出比原问题最优解更好的解,从而导致矛盾。
利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解。最优子结构是问题能用动态规划算法求解的前提。
2.重叠子问题性质
递归算法求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。这种性质称为子问题的重叠性质。
动态规划算法,对每一个子问题只解一次,而后将其解保存在一个表格中,当再次需要解此子问题时,只是简单地用常数时间查看一下结果。
通常不同的子问题个数随问题的大小呈多项式增长。因此用动态规划算法只需要多项式时间,从而获得较高的解题效率。
3.备忘录方法
备忘录方法的控制结构与直接递归方法的控制结构相同,区别在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看,避免了相同子问题的重复求解。
基本步骤:
1.找出最优解的性质,并刻划其结构特征。
2.递归地定义最优值。
3.以自底向上的方式计算出最优值。
4.根据计算最优值时得到的信息,构造最优解。
3.1 背包问题(问题分析来自HTML Help)
背包问题(Knapsack problem)是一种组合优化的NP完全问题。
1.01背包问题
有N件物品和一个容量为V的背包。第i件物品的重量是wi[i],价值是val[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即DP[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程是:
DP[i][v]=max{ DP[i-1][v], DP[i-1][v-wi[i]]+val[i] }。
(可以压缩空间,DP[v]=max{DP[v],DP[v-wi[i]]+val[i]})
方程解释:“将前i件物品放入容量为v的背包中"这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只涉及前i-1件物品的问题。如果不放第i件物品,问题转化为"前i-1件物品放入容量为v的背包中”,价值为DP[i-1][v];如果放第i件物品,问题转化为"前i-1件物品放入剩下的容量为v-wi[i]的背包中",此时能获得的最大价值就是DP [i-1][v-wi[i]]再加上通过放入第i件物品获得的价值val[i]。
优化空间复杂度
DP[i][v]是由DP[i-1][v]和DP[i-1] [v-wi[i]]两个子问题递推而来,能否保证在推DP[i][v]时(也即在第i次主循环中推DP[v]时)能够得到DP[i-1][v]和DP[i-1] [v-wi[i]]的值呢?事实上,这要求在每次主循环中我们以v=V…0的顺序推DP[v],这样才能保证推DP[v]时DP[v-wi[i]]保存的是状态 DP[i-1][v-wi[i]]的值。伪代码如下:
for i=1…N
for v=V…0
DP[v]=max{DP[v],DP[v-wi[i]]+val[i]};
算法复杂度:时间复杂度:O(VN),压缩空间后空间复杂度O(V)
代码:
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<cstdio>
#include<cmath>
#include<string>
#include<algorithm>
using namespace std;
int DP[1010],wi[1010],val[1010];
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
int i,j,n,v;
//初始化的细节问题
//(1)要求恰好装满背包,初始化时:DP[0]=0,DP[1..v]=-∞,这样就可以保证最终得到的DP[N]是一种恰好装满背包的最优解。
//(2)没有要求必须把背包装满,只希望价格尽量大,初始化时:DP[0..v]=0。
memset(DP,0,sizeof(DP));
memset(wi,0,sizeof(wi));
memset(val,0,sizeof(val));
scanf("%d%d",&n,&v);
for(i=1; i<=n; i++)
{
scanf("%d",&val[i]);
}
for(i=1; i<=n; i++)
{
scanf("%d",&wi[i]);
}
for(i=1; i<=n; i++)
{
for(j=v; j>=wi[i]; j--)
{
DP[j]=max(DP[j],DP[j-wi[i]]+val[i]);
}
}
for(i=1; i<=v; i++)
{
printf("%d ",DP[i]);
}
printf("\n");
}
return 0;
}
2.完全背包
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
使用一维数组,伪代码:
for i=1…N
for v=0…V
DP[v]=max{DP[v],DP[v-cost]+weight}
分析:与01背包的伪代码只有v的循环次序不同。01背包中要按照v=V…0的逆序来循环是因为要保证第i次循环中的状态DP[i][v]是由状态DP[i-1] [v-val[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的 子结果DP[i-1][v-wi[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果DP[i][v-wi[i]],所以就可以并且必须采用v=0…V的顺序循环。
【hdu1114】代码:
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<cstdio>
#include<cmath>
#include<string>
#include<algorithm>
using namespace std;
#define INF 0x3fffffff
int p[10010],w[10010],dp[10010];
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
int e,f,v,i,j,n;
scanf("%d%d",&e,&f);
v=f-e;
scanf("%d",&n);
for(i=1; i<=n; i++)
{
scanf("%d%d",&p[i],&w[i]);
}
for(i=1; i<=v; i++)
{
dp[i]=INF;
}
for(i=1; i<=n; i++)
{
for(j=w[i]; j<=v; j++)
{
dp[j]=min(dp[j],dp[j-w[i]]+p[i]);
}
}
/* for(j=1; j<=v; j++)
{
printf("%d ",dp[j]);
}
printf("\n");*/
if(dp[v]==INF)
{
printf("This is impossible.\n");
}
else
{
printf("The minimum amount of money in the piggy-bank is %d.\n",dp[v]);
}
}
return 0;
}
3.多重背包问题
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
可转化为01背包求解:把第i种物品换成n[i]件01背包中的物品,则得到了物品数为Σn[i]的01背包问题,直接求解,复杂度仍然是O(V*Σn[i])。
方法:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 1,2,4,…,2(k-1),n[i]-2k+1,且k是满足n[i]-2^k+1>0的最大整数。例如,如果n[i]为13,就将这种 物品分成系数分别为1,2,4,6的四件物品。
算法复杂度:将第i种物品分成了O(log n[i])种物品,将原问题转化为了复杂度为O(V*Σlog n[i])的01背包问题。
代码:
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<cstdio>
#include<cmath>
#include<string>
#include<algorithm>
using namespace std;
int dp[200];
void CompletePack(int cost, int weight,int n)
{
int i;
for(i=cost; i<=n; i++)
dp[i]=max(dp[i],dp[i-cost]+weight);
}
void Zero_OnePack(int cost,int weight,int n)
{
int i;
for(i=n; i>=cost; i--)
dp[i]=max(dp[i],dp[i-cost]+weight);
}
int main()
{
int cost[200],weight[200],amount[200];
int i,j,k;
int t,n,m;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
for(i=0; i<m; i++)
scanf("%d%d%d",&cost[i],&weight[i],&amount[i]);
memset(dp,0,sizeof(dp));
for(i=0; i<m; i++)
{
if(cost[i]*amount[i]>n) CompletePack(cost[i],weight[i],n);
else
{
k=1;
while(k<amount[i])
{
Zero_OnePack(k*cost[i],k*weight[i],n);
amount[i]-=k;
k*=2;
}
Zero_OnePack(amount[i]*cost[i],amount[i]*weight[i],n);
}
}
printf("%d\n",dp[n]);
}
return 0;
}