题目 :N件物品放入容量大小为W的背包里,N件物品的重量分别为w1,w2...wn, 各自的价值分别为v1,v2...vn。每件物品有且仅有一件,要么放入背包,要么不放入。求在背包容量范围内,使放入的物品价值总和最大的解法。
动态规划的应用场景
适用动态规划的问题必须满足最优化原理、无后效性和重叠性。
a.最优化原理(最优子结构性质) 最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
b.无后效性 将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
c.子问题的重叠性 动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的算法,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
问题解决:
在解决问题之前,为描述方便,首先定义一些变量:Vi表示第 i 个物品的价值,Wi表示第 i 个物品的体积,定义V(i,j):当前背包容量 j,前 i 个物品最佳组合对应的价值,同时背包问题抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 个物品选或不选)。
递推关系式:
背包问题最优解回溯(求解 这个最优解由哪些商品组成?)
通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些商品组成,故要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式:
- V(i,j)=V(i-1,j)时,说明没有选择第i 个商品,则回到V(i-1,j);
- V(i,j)=V(i-1,j-w(i))+v(i)时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(i-1,j-w(i));
- 一直遍历到i=0结束为止,所有解的组成都会找到。
代码实现:
import java.util.Scanner;
public class Main {
public static int[] solution(int[] weight, int[] val, int n, int w) {
int[][] dp = new int[n+1][w+1];
for(int i=0;i<=n;i++)
for(int j=0;j<=w;j++){
if(i==0 || j==0) dp[i][j]=0;
else{
if(j<weight[i-1]) //直观理解应该是i 但是从0号开始
dp[i][j]=dp[i-1][j];
else {
dp[i][j]=Math.max(dp[i-1][j], dp[i-1][j-weight[i-1]]+val[i-1]);
}
}
}
/*
* 辅助打印dp数组
* for (int a = 0; a <= n; a++) {
for (int b = 0; b <= w; b++) {
System.out.print(dp[a][b] + "\t");
}
System.out.println();
}*/
//便利结果
int[] res =new int[n];
for(;n>0;n--){
if(dp[n][w]==dp[n-1][w]) {
res[n-1]=0;
}
else {
res[n-1] =1;
w -= weight[n-1] ;
}
}
return res;
}
public static void main(String[] args) {
Scanner sc =new Scanner(System.in);
while (sc.hasNext()) {
int n = sc.nextInt();
int w = sc.nextInt();
int[] weight =new int[n];
int[] val =new int[w];
for(int i=0;i<n;i++){
weight[i] = sc.nextInt();
}
for(int i=0;i<n;i++){
val[i] = sc.nextInt();
}
for (int i : solution(weight,val,n,w)) {
System.out.print(i+" ");
}
}
}
}
优化:
dp[i-1][j-w[i]] | dp[i-1][j] | ||
dp[i][j] | |||
如上图所示:dp[i][...]的状态只与的dp[i-1][...]相关
因此可以将空间复杂度进一步降低 降低到一维数组
状态方程就变成了:
dp[v]=max(dp[v],dp[v-c[i]]+w[i])
需要注意的是: 如果数组继续从前往后遍历 计算dp[v]时 dp[v-c[i]]已经被覆盖了(相当于 dp[]i[v-c[i]])而不是 dp[i-1][v-c[i]])
因此遍历的顺序要从后往前走(注意,此优化方法将不能回溯得到装的方案)
for(int i = 0; i <= v; i++)
dp[i] = 0;
// 更新dp值
for(int i = 1; i <= n; i++)
for(int j = v; j >= weight[i-1]; j--) //如果背包容量大于第i件物品 装的下的情况下
dp[j] = Math.max(dp[j], dp[j-weight[i-1]]+value[i-1]); // (2)
return dp[v];
扩展:
有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
恰好装满是指背包容量用完(或者是钱用完),而不是所有物品都装进去,用inf的原因就是只有上一个背包有具体数值(即装满)时用f(i-cost)+weight得到的是实数,才有意义,这样只有上一个背包f(i-cost)装满,再加上这一个背包weight时才也能恰好装满(其实这里还是状态转移,只有上一个满了,这个才满,根据上一个来的),若上一个背包是inf那么再加weight仍是inf
最后f[n]不为inf时证明可以装满,并求其最大值,若f[n]是inf,则没有能装满的情况
若不要求装满则直接求f[]数组中的最大值,而不是f[n]。
装满是指f[n]有一个具体的值!!!!!
题目:
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
public class CanPartition_416 {
/*求出sum 看是否能找出数字装入和为sum/2的背包,另一半和就是sum/2
* 状态方程:
* dp[i][0] = true; //理解为前i种物品装入和为0的背包 不装肯定能满足,初始化为true
* dp[i][j] =dp[i-1][j] || dp[i-1][j-nums[i]]; //前i个num凑出何为j:要么前i-1个num凑出和为j,要么前i-1个num凑出和为j-nums[i]
* 注意 nums从0开始 因此上述的状态方程要改为i-1
*
*/
public boolean canPartition(int[] nums) {
if (nums.length<2) return false;
int sum =0 ;
for (int i : nums) {
sum += i;
}
if ((sum & 1) ==1) return false ;//奇数
else sum /= 2;
boolean[][] dp=new boolean[nums.length+1][sum+1];
for (int i=0 ;i<nums.length+1;i++) {
dp[i][0] = true;
}
for(int i=1;i<nums.length+1;i++)
for (int j=1;j<=sum;j++){
if (j>=nums[i-1]){
dp[i][j] =dp[i-1][j] || dp[i-1][j-nums[i-1]];
}
/*
else{
dp[i][j] =dp[i-1][j];
}*/
}
return dp[nums.length][sum];
}