动态规划问题与实际问题联系紧密,所以动态规划算法有着很广泛的应用,各类比赛题中也必不可少。最近花了不少功夫弄明白了经典的0-1背包问题,做个总结。
0-1背包问题:有n种物品,每种只有一个。第i种物品的体积为Vi,重量为Wi。选一些物品到一个容量为C的背包中,使得背包内物品在总体积不超过C的前提下重量尽量大。1<=n<=100,1<=Vi<=C<=10000,1<=Wi<=10^6。
在刘汝佳的紫书中使用”阶段”来描述的状态,可以辅助我们思考。
首先要确定规划的方向,有两种:
1、逆向规划,则我们说定义的当前阶段的状态为d(i,j)表示当前在i层,把第i,第i+1,第i+2,…,第n个物品装入容量为j的背包中的最大的重量和。那么规划的终点就是d(1,C)(把第1,2,3….,n个物品装入背包容量为C的背包中的最大重量和),规划的起点就是d(n,0)或者d(n,C),也就是说从第n个物品开始,因为我们定义的状态已经决定了这个规划的方向,因为我们在更新写一个阶段的时候必须要有上一个阶段的值,否则没办法更新。
不难写出状态转移方程为f(i,j)=max{f(i+1,j),f(i+1,j-V[i])+W[i]}。
给出这部分的代码:
for(int i=n;i>=1;i--)
for(int j=0;j<=C;j++)
{
d[i][j]=(i==n?0:d[i+1][j]);
//用上一阶段值刷新到当前阶段也就是d(i,j)=d(i+1,j)
//因为要与上一阶段留出V[i]容量装第i个物品后做一个大小的比较,所以必须要刷新
if(j>=V[i])
d[i][j]=max(d[i][j],d[i+1][j-V[i]]+W[i]);
}
2、正向规划。与逆向规划是对称的方向,那么此时的状态d(i,j)定义为把前i个物品装到容量为j的背包中的最大重量和。规划的起点是d(1,0)或d(1,C),规划的终点为d(n,C)。
状态转移方程为:f(i,j)=max{f(i-1,j),f(i-1,j-V[i])+W[i]}
代码如:
for(int i=1;i<=n;i++){
cin>>V>>W;
for(int j=0;j<=C;j++)
d[i][j]=(i==1?0:d[i-1][j]);//用上一阶段值刷新到当前阶段
if(j>=V[i])d[i][j]=max(d[i][j],d[i-1][j-V]+W);
}
两种规划方向都能得到最优解,但是区别主要有:
1、正向规划可以边录入V、W边进行计算,节约了空间
2、正向规划在需要打印解的时候不方便,一方面打印解的时候需要将V、W都记录下来,另一方面,因为规划方向是正向的,所以打印解的方向必须从终点开始,因为起点的位置不知道,终点的位置是确定的,就算是这样,得到的结果依然是逆序的,还要再反向打印输出才是正确的结果,并且不能保证打印的结果是字典序最小的。
3、反向规划的好处在于打印结果的时候能够保证字典序最小,所以在要求字典序最小的场合,规划方向要选择好。
下面分别说说两种规划方向怎么打印出解:
1、反向规划
输出1代表取这个物品
代码如下,只需要从终点d(1,W)开始按最优的规划顺序遍历即可
int s = W;
for (int i = 1; i <= N; i++)
{
if (d[i][s]>d[i+1][s])
{
s -= Weight[i];
printf("%d", 1);
}
else printf("%d", 0);
if (i != N) printf(" ");
}
2、正向规划
从终点d(n,W)开始按最优的规划顺序遍历即可,但是显然遍历的顺序是逆序的,所以遍历一次得到的结果是逆序的,还要再逆序一遍
//打印路径
int s = W;
for (int i = N; i >= 1; i--)
{
if (d[i][j] > d[i - 1][j])
{
out[i] = 1;
s -=Weight[i];
}
}
for (int i=1; i <= N; i++)
{
int temp = 0;
out[i] == 1 ? temp = 1 : temp=0;
if (i == N)
printf("%d", temp);
else printf("%d ", temp);
}
还可以使用一维数组(又叫滚动数组)解决这个问题,但是不能打印出最终的最佳方案,所以不使用。
j逆序枚举,d[j]相当于保存的是d[i-1][j]值,d[j-v]相当于保存的是d[i-1][j-V]的值,每一个阶段完成之后上一个阶段保存的值就会被覆盖掉。所以不能打印出方案。
memset(d,0,sizeof(d));
for(int i=1;i<=n;i++)
{
scanf("%d%d",&V,&W);
for(int j=C;j>=0;j--)//这里要逆序枚举,防止丢失了d[i-1][j-V]状态的值
if(j>=V)d[j]=max(d[j],d[j-V]+W);
}
最后给出一道NOJ上的题目作为该问题的训练(吐槽下。。这个OJ卡的不行。。。)
http://acm.njupt.edu.cn/acmhome/problemdetail.do?&method=showdetail&id=1308
背包问题
时间限制(普通/Java) : 1000 MS/ 3000 MS 运行内存限制 : 65536 KByte
比赛描述
试设计一个用回溯法搜索子集空间树的函数。该函数的参数包括结点可行性判定函数和上界函数等必要的函数,并将此函数用于解0-1背包问题。
0-1 背包问题描述如下:给定n 种物品和一个背包。物品i 的重量是 wi ,其价值为 v i,背包的容量为C。应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
在选择装入背包的物品时,对每种物品i只有2 种选择,即装入背包或不装入背包。不能将物品i 装入背包多次,也不能只装入部分的物品i。
0-1 背包问题形式化描述:给定C>0, Wi >0, Vi >0,1≤i≤n,要求n 元0-1向量( x1 ,x2 ,…, xn ),xi ∈{0,1},1≤i≤n,使得 达到最大
输入
第一行有2个正整数n和c。n是物品数,c是背包的容量。接下来的1 行中有n个正整数,表示物品的价值。第3 行中有n个正整数,表示物品的重量。
输出
计算出装入背包物品的最大价值和最优装入方案。
样例输入
5 10
6 3 5 4 6
2 2 6 5 4
样例输出
15
1 1 0 0 1
给出两种规划顺序的AC代码:
1、逆向规划
#include <algorithm>
#include <iostream>
#include <string.h>
#include <stdio.h>
using namespace std;
int Value[1001];
int Weight[1001];
int Memory[1001][1001];
int main()
{
int N = 0, W = 0, maxValue = 0;
memset(Memory, 0, sizeof(Memory));
scanf("%d%d", &N, &W);
for (int i = 1; i <= N; i++)
scanf("%d", &Value[i]);
for (int i = 1; i <= N; i++)
scanf("%d", &Weight[i]);
for (int i = N; i >= 1; i--)
for (int j = 0; j <= W; j++)
{
Memory[i][j] = (i == N ? 0 : Memory[i + 1][j]);
if (j >= Weight[i])
{
if (Memory[i +1][j - Weight[i]] + Value[i] > Memory[i][j])
Memory[i][j] = Memory[i + 1][j - Weight[i]] + Value[i];
}
}
printf("%d\n", Memory[1][W]);
int s = W;
for (int i = 1; i <= N; i++)
{
if (Memory[i][s]>Memory[i+1][s])
{
s -= Weight[i];
printf("%d", 1);
}
else printf("%d", 0);
if (i != N) printf(" ");
}
return 0;
}
2、正向规划
#include <algorithm>
#include <iostream>
#include <string.h>
#include <stdio.h>
using namespace std;
int Value[1001];
int Weight[1001];
int Memory[1001][1001];
int out[1001];
int main()
{
int N = 0, W = 0, maxValue = 0;
memset(Memory, 0, sizeof(Memory));
memset(out, 0, sizeof(out));
scanf("%d%d", &N, &W);
for (int i = 1; i <= N; i++)
scanf("%d", &Value[i]);
for (int i = 1; i <= N; i++)
scanf("%d", &Weight[i]);
for (int i = 1; i<= N; i++)
for (int j = 0; j <= W; j++)
{
Memory[i][j] = (i == 1 ? 0 : Memory[i - 1][j]);
if (j >= Weight[i])
{
if (Memory[i -1][j - Weight[i]] + Value[i] > Memory[i][j])
{
Memory[i][j] = Memory[i -1][j - Weight[i]] + Value[i];
}
}
}
printf("%d\n", Memory[N][W]);
//打印路径
int j = W;
for (int i = N; i >= 1; i--)
{
if (Memory[i][j] > Memory[i - 1][j])
{
out[i] = 1;
j = j - Weight[i];
}
}
for (int i=1; i <= N; i++)
{
int temp = 0;
out[i] == 1 ? temp = 1 : temp=0;
if (i == N)
printf("%d", temp);
else printf("%d ", temp);
}
return 0;
}