- 动态规划算法核心思想:
1.刻画问题的最优解的结构特征
2.递归定义最优解的值
3.自底向上的方式计算最优解的值
4.构造问题的最优解
-
背包问题
给定n个重量为w1,w2,...,wn,价值为v1,v2,...,vn的物品和一个承重量为W的背包,
求这些物品中最有价值的一个子集并且要能够放进背包中。
在这里我们先假设重量为整数
解法推导:
- 刻画最优解的结构特征(最复杂的过程):
先考虑由前 i 个物品(1 <= i <= n) 定义的一个实例,物品的重量为w1,...wi,价值为v1,...,vi,而背包的承重量为j(1 <= j <= W)。那么求前i个物品中最有价值且能够放进背包中的一个子集这个问题就构成了前面所述的问题的一个子问题。
我们先设F(i,j)为此时这个子问题的最优解,其返回值即为最大价值(那么我们要求的就是F(n,W)),通过求解F(i,j)来寻找F(i,j)与更小的子问题之间的递推关系。
此时我们可以用到分治法的思想,将求解F(i,j)这个问题按照第i个物品是否在F(i,j)中划分为两个子问题:1)包括第i个物品时的最优解 2)不包括第i个物品时的最优解
然后比较这两个谁更 “优秀” 一些,在这个问题中,即为比较谁的价值总和更大一些。
- 情况一:包括第i个物品时,F(i,j) = F(i-1,j - wi) + vi
- 情况二:不包括第i个物品时,F(i,j) = F(i-1,j)
上述两种情况的结论可以用反证法进行证明。
- 得到最优解值的递归定义:
- 2.1: j - wi >= 0: F(i,j) = max(F(i-1,j) , F(i-1,j-wi)+vi)
- 2.2:j - wi < 0: F(i,j) = F(i-1,j)
- 自底向上求解:
- 我们很容易得到如下的初始条件:
表中最上面一行:3.1:F(0,j) = 0(0 <= j <= W)
表中最靠左边一列:3.2:F(i,0) = 0(0 <=i <=W)
然后采用填表的方式得到后续每一项F(i,j)的值
- 构造问题的最优解
求得F(n,W)
实例:
对于承重为5的背包有:
物品 | 重量 | 价值/美元 |
---|---|---|
1 | 2 | 12 |
2 | 1 | 10 |
3 | 3 | 20 |
4 | 2 | 15 |
根据以上信息和公式2.1,2.2,3.1,3.2,我们得到如下的动态规划表:
i-前i个物品中
j-背包的承重量
i\j | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 12 | 12 | 12 | 12 |
2 | 0 | 10 | 12 | 22 | 22 | 22 |
3 | 0 | 10 | 12 | 22 | 30 | 32 |
4 | 0 | 10 | 15 | 25 | 30 | 37 |
因此最优解F(4,5)=37.读者们可以自行应用前文所述算法来体验这个表的填表过程,这样就可以明白动态规划自底向上解决背包问题的优势所在:即许多表格单元的数据无需重复计算,当数据量比较大的时候,如果没有这张表,直接使用自上向下递归的方法那么求解一个问题将会有许多重复计算的数据。
下面给出利用动态规划自底向上求解背包问题的过程回溯,以此来得到我们最优解当中到底放了那些物品进背包:(i,j)
(4,5) -> (3,3)->(2,3)-> (1,2)->(0,0)
由上述过程知道我们放入的物品以此为1,2,4.
下面给出自底向上的动态规划求解背包问题的源代码:
/**
* 背包问题
* 给定n个重量为w1,w2,...,wn,价值为v1,v2,...,vn的物品和一个承重量为W的背包,
* 求这些物品中最有价值的一个子集并且要能够放进背包中。
* @author Conor Xu
*/
public class NapsackQuestion {
private int[] weight;
private double[] value;
private int[] stuff;
private int n = 0;
private int w = 0;
//麻袋可以装下的总重量
private int capacityOfSack;
//用来存F(i,j)的结果
//i:0~n,j:0~capacityOfSack
private double[][] func;
public NapsackQuestion(int[] weight,double[] value,int capacityOfWeight) {
this.weight = weight;
this.value = value;
this.capacityOfSack = capacityOfWeight;
//表格的横向长度为承重量+1,纵向长度为物品总数+1(最上一行和最左边一列都是0)
func = new double[weight.length + 1][capacityOfWeight + 1];
stuff = new int[weight.length];
}
/**
*
* @param n 对前n个物品求解
* @param w 背包最大承受重量为w
* @return i=n,j=w时的最优解
*/
public double resolve1(int n,int w) {
int k = 0;
for(int i = 1;i <= n;i++) {
for(int j = 1;j <= w;j++) {
if(j - weight[i-1] < 0)
func[i][j] = func[i-1][j];
else if(func[i-1][j-weight[i-1]] + value[i-1] <= func[i-1][j]) {
func[i][j] = func[i-1][j];
}
else {
func[i][j] = func[i-1][j-weight[i-1]] + value[i-1];
}
}
}
//n,w用于打印当前解决方法的“路径”,即装入的物品的序列。
this.n = n;
this.w = w;
return func[n][w];
}
public void printStuff() {
int i = n;
int j = w;
int k = 0;
while(i > 0 && j > 0) {
if(func[i][j] == func[i-1][j]) {
i--;
}
else {
stuff[k++] = i;
i--;
j -= weight[i];
}
}
while(k > 1) {
System.out.print(stuff[--k] + "->");
}
if(stuff[0] != 0)
System.out.println(stuff[0]);
else {
System.out.println("there is nothing in your sack");
}
}
public static void main(String[] args) {
int[] weight = {2,1,3,2};
double[] value = {12,10,20,15};
NapsackQuestion sackQues = new NapsackQuestion(weight,value,5);
System.out.println(sackQues.resolve1(4,4));
sackQues.printStuff();
}
其中printstuff用于打印装入背包的所有物品。