彻底理解0-1背包问题

生来就可爱ヽ(ⅴ<●) 提交于 2020-01-18 05:40:07

0-1背包问题概念

背包问题本质是个求最优解的问题:有个背包有V大小的空间可以存放物品,现在有n个物品,每个物品体积分别为v1、v2...、vn,价值分别为w1、w2...、wn。现在求解:如何使物品尽可能多的放在背包中,使得背包中的物品总价值最大?

0-1背包问题指的是每个物品只能使用一次

解决这类问题,大致有2类方法:递归算法、动态规划算法,下面详细讲解3种算法(借鉴了博主的论文)。

一、递归算法

递归算法解决这类问题大致的核心思想:

我们用B[k][C]表示前k个物品放进容量为C的背包里,得到的最大的价值。

我们用自顶向下的角度来看,假如我们已经进行到了最后一步(即求解将k个物品放到背包里获得的最大价值),此时我们便有两种选择:

    1、不放第k个物品,此时总价值为B[k−1][C]
    2、放置第k个物品,此时总价值为wk+B[k−1][C−vk]


两种选择中总价值最大的方案就是我们的最终方案,递推式(有时也称之为状态转移方程)如下
    B[k][C]=max(B[k-1][C],wk+B[k−1][C−wk])

编程如下:

 

public class test {
	//物品总量,最大承重
	static int N = 6;
	static int W = 21;
	static int[][] B = new int[N][W];
	static String[][] B_ = new String[N][W];
	static int w []=new int[]{0,2,3,4,5,9};
	static int v []=new int[]{0,3,4,5,8,10};
	static Set<Integer> set = new HashSet<Integer>();//记录最优解(这个解可能包含其他没用的解,需要借助list_delete来删除)
    static List<Integer> list_delete = new ArrayList<Integer>();//记录不装入背包的物品
	
    /**
     * 时间代价是n
     * @param k
     * @param C
     * @return
     */
	public static  int knapsack1(int k,int C) {
		if(k==0||C==0) {
			return B[k][C];
		}
		if(w[k]>C) {
			B[k][C]=knapsack1(k-1,C);
			list_delete.add(k);
			set.remove(k);
		}else {
			int value1=knapsack1(k-1,C-w[k])+v[k];//装上了:k-1时的最优解+最后装上的价值=最优价值
			int value2=knapsack1(k-1,C);//没装上
			if(value1>value2) {
				set.add(k);
				B[k][C]=value1;
			}else {
				list_delete.add(k);				
				set.remove(k);
				B[k][C]=value2;
			}
		}
		System.out.println("B["+k+"]["+C+"]的价值是"+B[k][C]);
		return B[k][C];
		
		
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
//		knapsack1(5,20);
		System.out.println("B["+5+"]["+20+"]的价值是"+knapsack1(5,20));
		for (Integer integer :list_delete) {
			set.remove(integer);	
		}
		System.out.println(set.toString());
		
		knapsack2();

	}
}

该算法的时间复杂度为N。

 

但是上述的算法还是存在一个问题,自顶向下的思想可能导致计算重复,为此借鉴了博主的论文后,我新加入记忆化搜索(将已经计算好的结果存起来,用到的时候,直接拿来,避免了重复计算)。代码结果如下:

public class test {
	//物品总量,最大承重
	static int N = 6;
	static int W = 21;
	static int[][] B = new int[N][W];
	static String[][] B_ = new String[N][W];
	static int w []=new int[]{0,2,3,4,5,9};
	static int v []=new int[]{0,3,4,5,8,10};
	static Set<Integer> set = new HashSet<Integer>();//记录最优解(这个解可能包含其他没用的解,需要借助list_delete来删除)
    static List<Integer> list_delete = new ArrayList<Integer>();//记录不装入背包的物品
    static int[][] memo = new int[N][W+1];//记忆化搜索
	
    /**
     * 时间代价是n
     * @param k
     * @param C
     * @return
     */
	public static  int knapsack1(int k,int C) {
		if(k==0||C==0) {
			return B[k][C];
		}
		//如果此子问题已经求解过,则直接返回上次求解的结果
        if (memo[k][C] != 0) {
            return memo[k][C];
        }
		if(w[k]>C) {
			B[k][C]=knapsack1(k-1,C);
			list_delete.add(k);
			set.remove(k);
		}else {
			int value1=knapsack1(k-1,C-w[k])+v[k];//装上了:k-1时的最优解+最后装上的价值=最优价值
			int value2=knapsack1(k-1,C);//没装上
			if(value1>value2) {
				set.add(k);
				B[k][C]=value1;
			}else {
				list_delete.add(k);				
				set.remove(k);
				B[k][C]=value2;
			}
		}
		//添加子问题的解,便于下次直接使用
        memo[k][C] = B[k][C];
		System.out.println("B["+k+"]["+C+"]的价值是"+B[k][C]);
		return B[k][C];
		
		
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
//		knapsack1(5,20);
		System.out.println("B["+5+"]["+20+"]的价值是"+knapsack1(5,20));
		for (Integer integer :list_delete) {
			set.remove(integer);	
		}
		System.out.println(set.toString());
		
//		knapsack2();

	}
}

二、动态规划算法

在这里插入图片描述

在这里插入图片描述

/**
	 * 时间代价是N*W
	 */
	public static void knapsack() {
		//物品总量,最大承重
		int N = 6;
		int W = 21;
		int[][] B = new int[N][W];
		String[][] B_ = new String[N][W];
		int w []=new int[]{0,2,3,4,5,9};
		int v []=new int[] {0,3,4,5,8,10};
		
		//第k件物品、当前背包容量
		int k,C;
		for(k=1;k<N;k++) {
			for(C=1;C<W;C++) {
				if(w[k]>C) {
					B_[k][C] = B_[k-1][C]==null?"":B_[k-1][C];
					B[k][C]=B[k-1][C];
				}else {
					int value1=B[k-1][C-w[k]]+v[k];//装上了:k-1时的最优解+最后装上的价值=最优价值
					int value2=B[k-1][C];//没装上
					if(value1>value2) {
						B_[k][C] = (B_[k-1][C-w[k]]==null?"":B_[k-1][C-w[k]])+""+k;
						B[k][C]=value1;
					}else {
						B_[k][C] = B_[k-1][C]==null?"":B_[k-1][C];
						B[k][C]=value2;
					}
				}
				System.out.println("B["+k+"]["+C+"]的价值是"+B[k][C]+";选中的物品是:"+B_[k][C]);
			}	
		}
		
	}
	

 

空间复杂度的优化

上面的动态规划算法使用了O(n*C)的空间复杂度(因为我们使用了二维数组来记录子问题的解),其实我们完全可以只使用一维数组来存放结果,但同时我们需要注意的是,为了防止计算结果被覆盖,我们必须从后向前分别进行计算。

在这里插入图片描述

我们仍然假设背包空间为5,根据
F(i,C)=max(F(i−1,C),v(i)+F(i−1,C−w(i)))
我们可以知道,当我们利用一维数组进行记忆化的时候,我们只需要使用到当前位置的值和该位置之前的值,举个例子
假设我们要计算F(i,4),我们需要用到的值为F(i−1,4)和F(i−1,4−w(i)),因此为了防止结果被覆盖,我们需要从后向前依次计算结果
最终的动态规划代码如下

/**
	 * 一维
	 */
	public static void knapsack2() {
		//物品总量,最大承重
		int N = 6;
		int W = 21;
		int[] B = new int[22];
		String[][] B_ = new String[N][W];
		int w []=new int[]{0,2,3,4,5,9};
		int v []=new int[] {0,3,4,5,8,10};
//		int w []=new int[]{2,3,4,5,9};
//		int v []=new int[] {3,4,5,8,10};
		
		//第k件物品、当前背包容量
		int k,C;
		for(k=1;k<N;k++) {
			for(C=W;C>=w[k];C--) {
				
					int value1=B[C-w[k]]+v[k];//装上了:k-1时的最优解+最后装上的价值=最优价值
					int value2=B[C];//没装上
					if(value1>value2) {
//						B_[k][C] = (B_[k-1][C-w[k]]==null?"":B_[k-1][C-w[k]])+""+k;
						B[C]=value1;
					}else {
//						B_[k][C] = B_[k-1][C]==null?"":B_[k-1][C];
						B[C]=value2;
					}
			}
//				System.out.println("B["+k+"]["+C+"]的价值是"+B[C]);
		}
		System.out.println("B["+W+"]的价值是"+B[21]);
		
	}


 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!