最近打算认真学习一下数据结构与算法,第一步就是动态规划。
应用:背包(01背包,完全背包,多重背包),最长公共子序列,最长上升子序列,矩阵乘法链,最短路径,无交叉子集,图像压缩,元件折叠。寻路问题,资源分配,线性模型,博弈类问题,树形DP
https://github.com/tianyicui/pack/tree/c1c368e3f084a11aef65c015981f71eac64b5e1f
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。用子问题定义状态:即f[i][v]表 示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
【注意】我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。
为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
#include
#define INR 0x7fffffff
int max(int a,int b){return a>b? a:b;}
struct E{
int w;//物品的体积
int v;//物品的价值
}list[101];
int dp[101][1001] ;//记录状态数组,dp[i][j] 表示前i个物品组成的总体积不大于j的最大价值和
int main(){
int s,n;
while(scanf("%d%d",&s,&n) != EOF){
for(int i=1;i<=n;i++){
scanf("%d%d",&list[i].w,&list[i].v);
}
for( i=0;i<=s;i++){
dp[0][i]=0;
}
for( i=1;i<= n ;i++){
for(int j=s;j >= list[i].w ;j--){
dp[i][j] = max(dp[i-1][j],dp[i-1][j-list[i].w]+list[i].v);
}
for( j=list[i].w-1;j>=0;j--){
dp[i][j]=dp[i-1][j];
}
}
printf("%d\n",dp[n][s]);//输出答案
}
return 0;
}
//空间优化
http://ac.jobdu.com/problem.php?pid=1123
#include
#define INR 0x7fffffff
int max(int a,int b){return a>b? a:b;}
struct E{
int w;//物品的体积
int v;//物品的价值
}list[101];
int dp[1001] ;
int main(){
int s,n;
while(scanf("%d%d",&s,&n) != EOF){
for(int i=1;i<=n;i++){
scanf("%d%d",&list[i].w,&list[i].v);
}
for( i=0;i<=s;i++){
dp[i]=0;
}
for( i=1;i<= n ;i++){
for(int j=s;j >= list[i].w ;j--){
dp[j] = max(dp[j],dp[j-list[i].w]+list[i].v);
}
}
printf("%d\n",dp[s]);//输出答案
}
return 0;
}
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
for i=1..N
for v=0..V
f[v]=max{f[v],f[v-cost]+weight}
你会发现,这个伪代码与P01的伪代码只有v的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么P01中要按照v=V..0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v=0..V的顺序循环。这就是这个简单的程序为何成立的道理。
for(int i=1;i<= n ;i++){
for(int j= list[i].w;j<= s;j++){
dp[j]=max(dp[j],dp[j-list[i].w]+list[i].v);
}
}
//完全背包例题:http://ac.jobdu.com/problem.php?pid=1454
//空间优化
#include
#define INF 0x7fffffff
int min(int a,int b){return a//取最小值
struct E{
int w;//物品的体积
int v;//物品的价值
}list[501];
int dp[10001] ;
int main(){
int T;
scanf("%d",&T);
while(T--){
int s,tmp;
scanf("%d%d",&tmp,&s);
s-=tmp;
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d",&list[i].v,&list[i].w);
}
for( i=0;i<=s;i++){
dp[i]=INF;//要求刚好装满,并且求的是最小值,所以用INF来初始化
}
dp[0]=0;//要求刚好装满:除dp[0]以外,其余dp[j]均为无穷(或着不存在)
for( i=1;i<= n ;i++){
for(int j=list[i].w;j <= s ;j++){//完全背包,顺序遍历所有可能的转移的状态
if(dp[j-list[i].w] != INF)//若dp[j-list[i].w]不为无穷,就可以由此状态转移而来
dp[j] = min(dp[j],dp[j-list[i].w]+list[i].v);//取转移值和原值的较小值
}
}
if(dp[s] != INF) //若存在一种方案使背包敲好装满,输出其最小值
printf("The minimum amount of money in the piggy-bak is %d.\n",dp[s]);
else
puts("This is impossible.");
}
return 0;
}
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
解法:将每种物品拆分为:1,2,4,……k-2^c+1 (其中c为使k-2^c+1大于0的最大整数。)这种类似于二进制的拆分,不仅将物品数量大大降低,同时通过对这些若干个原物品组合得到新物品的不同组合,可以得到0到k之间的任意件物品的价值重量和,所以对所有这些新物品做0-1背包,即可得到多重背包的解。由于转换后的0-1背包物品数量大大降低,其时间复杂度也得到较大优化。
//http://ac.jobdu.com/problem.php?pid=1455
#include
int max(int a,int b){ return a>b? a:b;}
struct E{
int w;
int v;
}list[2001];
int dp[101];
int main(){
int T;
scanf("%d",&T);
while(T--){
int s,n;
scanf("%d%d",&s,&n);
int cnt=0;
for(int i=1;i<=n;i++){
int w,v,k;
scanf("%d%d%d",&w,&v,&k);
int c=1;
while(k-c >0){
k-=c;
list[++cnt].w=c*w;
list[cnt].v=c*v;
c*=2;
}
list[++cnt].w=k*w;
list[cnt].v=k*v;
}
for(int i=1;i<=s;i++) dp[i]=0;
for(int i=1;i<=cnt;i++){
for(int j=s;j>=list[i].w;j--)
dp[j]=max(dp[j],dp[j-list[i].w]+list[i].v);
}
printf("%d\n",dp[s]);
}
return 0;
}
【01背包与完全背包的混合】
考虑到在P01和P02中给出的伪代码只有一处不同,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是O(VN)。伪代码如下:
for i=1..N
if 第i件物品属于01背包
for v=V..0
f[v]=max{f[v],f[v-c[i]]+w[i]};
else if 第i件物品属于完全背包
for v=0..V
f[v]=max{f[v],f[v-c[i]]+w[i]};
【再加上多重背包】这个没有看懂,或者说不知道怎么实现
如果再加上有的物品最多可以取有限次,那么原则上也可以给出O(VN)的解法:遇到多重背包类型的物品用单调队列解即可。但如果不考虑超过NOIP范围的算法的话,用P03中将每个这类物品分成O(log n[i])个01背包的物品的方法也已经很优了。
当然,更清晰的写法是调用我们前面给出的三个相关过程。
for i=1..N
if 第i件物品属于01背包
ZeroOnePack(c[i],w[i])
else if 第i件物品属于完全背包
CompletePack(c[i],w[i])
else if 第i件物品属于多重背包
MultiplePack(c[i],w[i],n[i])
在最初写出这三个过程的时候,可能完全没有想到它们会在这里混合应用。我想这体现了编程中抽象的威力。如果你一直就是以这种“抽象出过程”的方式写每一类背包问题的,也非常清楚它们的实现中细微的不同,那么在遇到混合三种背包问题的题目时,一定能很快想到上面简洁的解法,对吗?
问题
二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。
算法
费用加了一维,只需状态也加一维即可。设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。状态转移方程就是:
f[i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}
如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量v和u采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时拆分物品。这里就不再给出伪代码了,相信有了前面的基础,你能够自己实现出这个问题的程序。
问题
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
算法
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有:
f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于组k}
使用一维数组的伪代码如下:
for 所有的组k
for v=V..0
for 所有的i属于组k
f[v]=max{f[v],f[v-c[i]]+w[i]}
注意这里的三层循环的顺序,甚至在本文的第一个beta版中我自己都写错了。“for v=V..0”这一层循环必须在“for 所有的i属于组k”之外。这样才能保证每一组内的物品最多只有一个会被添加到背包中。
另外,显然可以对每组内的物品应用P02中“一个简单有效的优化”。
简化的问题
这种背包问题的物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。
算法
这个问题由NOIP2006金明的预算方案一题扩展而来。遵从该题的提法,将不依赖于别的物品的物品称为“主件”,依赖于某主件的物品称为“附件”。由这个问题的简化条件可知所有的物品由若干主件和依赖于每个主件的一个附件集合组成。
按照背包问题的一般思路,仅考虑一个主件和它的附件集合。可是,可用的策略非常多,包括:一个也不选,仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……无法用状态转移方程来表示如此多的策略。(事实上,设有n个附件,则策略有2^n+1个,为指数级。)
考虑到所有这些策略都是互斥的(也就是说,你只能选择一种策略),所以一个主件和它的附件集合实际上对应于P06中的一个物品组,每个选择了主件又选择了若干个附件的策略对应于这个物品组中的一个物品,其费用和价值都是这个策略中的物品的值的和。但仅仅是这一步转化并不能给出一个好的算法,因为物品组中的物品还是像原问题的策略一样多。
再考虑P06中的一句话: 可以对每组中的物品应用P02中“一个简单有效的优化”。 这提示我们,对于一个物品组中的物品,所有费用相同的物品只留一个价值最大的,不影响结果。所以,我们可以对主件i的“附件集合”先进行一次01背包,得到费用依次为0..V-c[i]所有这些值时相应的最大价值f’[0..V-c[i]]。那么这个主件及它的附件集合相当于V-c[i]+1个物品的物品组,其中费用为c[i]+k的物品的价值为f’[k]+w[i]。也就是说原来指数级的策略中有很多策略都是冗余的,通过一次01背包后,将主件i转化为V-c[i]+1个物品的物品组,就可以直接应用P06的算法解决问题了。
较一般的问题
更一般的问题是:依赖关系以图论中“森林”的形式给出(森林即多叉树的集合),也就是说,主件的附件仍然可以具有自己的附件集合,限制只是每个物品最多只依赖于一个物品(只有一个主件)且不出现循环依赖。
解决这个问题仍然可以用将每个主件及其附件集合转化为物品组的方式。唯一不同的是,由于附件可能还有附件,就不能将每个附件都看作一个一般的01背包中的物品了。若这个附件也有附件集合,则它必定要被先转化为物品组,然后用分组的背包问题解出主件及其附件集合所对应的附件组中各个费用的附件所对应的价值。
事实上,这是一种树形DP,其特点是每个父节点都需要对它的各个儿子的属性进行一次DP以求得自己的相关属性。这已经触及到了“泛化物品”的思想。看完P08后,你会发现这个“依赖关系树”每一个子树都等价于一件泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有儿子的对应的泛化物品之和。
没太懂后续补充
没太懂后续补充