动态规划——0-1背包问题

文章出处:极客时间《数据结构和算法之美》-作者:王争。该系列文章是本人的学习笔记。

1 0-1背包问题

背包能够承受的总重量一定w,每个物品的总量不同int[] weight表示。怎么放才能让背包中物品的总重量最大。

每次决定一种物品,要不要放入到背包中。当物品放完了或者总重量等于w,就停止放入,选择最大的总量保存下来。

2 用回溯法实现

 public class Package {
    private int[] weight =  new int[]{2,2,4,6,3};
    private int n = 5;//物品个数
    private int w = 9;//背包承受的最大重量
    private int maxW = Integer.MIN_VALUE;//结果

    /**
     * 处理第i个物品的情况,当前重量是cw
     * 这是回溯法,复杂度是指数级的。有些状态会计算多次。
     * @param i
     * @param cw
     */
    public void f(int i,int cw){
        if(cw==w || i==n){
            maxW =Math.max(cw,maxW);
            return;
        }
        f(i+1,cw);//第i个物品,不装入背包
        if(cw+weight[i]<=w){
            f(i+1,cw+weight[i]);//第i个物品,装入背包
        }
    }
	public int maxWeight(){
		f(0,0)
		return maxW;
	}
}

我们根据上面这个特殊的例子,把回溯求解问题的递归树画出来。
在这里插入图片描述
递归树中的每个节点表示一个状态,用(i,cw)表示。i 表示要将要处理第i个物品,cw表示当前总重量。例如(2,2)表示我们将要处理第2个物品,在处理之前已经放入的物品总重量是2。
从递归树中能看到某些状态被重复计算了,例如f(2, 2) 和 f(3,4)被计算了两次。为了解决这个问题,可以有两种方法解决。

3 第一种:备忘录

我们可以使用备忘录,遇到状态已经计算过的就不再计算了。改进代码如下。

private boolean[][] mem = new boolean[n][w+1];

    /**
     * 记录状态,已经计算过的状态就不再计算了
     * @param i
     * @param cw
     */
    public void fV2(int i,int cw){
        if(cw==w || i==n){
            maxW =Math.max(cw,maxW);
            return;
        }
        if(mem[i][cw]) return;
        mem[i][cw] = true;
        f(i+1,cw);//第i个物品,不装入背包
        if(cw+weight[i]<=w){
            f(i+1,cw+weight[i]);//第i个物品,装入背包
        }

    }

4 第二种:动态规划

我们把整个过程看做n个阶段,每个阶段只决策一种物品是否放入。每个物品决策(放或者不放)完成之后,背包中物品的重量会有多种情况。也就是说会有多种状态,对应递归树中不同的节点。

我们把每一层重复的节点合并,只记录不同的状态。基于上一层的状态集合,推导下一层集合的状态。我们合并每一层的状态,保证每一层节点个数不会超过w个。这样就避免了每一层状态节点个数指数级增长。

我们用states[n][w+1]来记录每一层可以达到的不同状态。例如上面例子中分析有(2,2)这个节点,那么states[2][2]=true。

第0个物品的重量是2,要么装入背包,要么不装入背包,决策之后会对应背包中的两种状态,背包中的总总量是0或者2.我们用state[0][0]=true,state[0][2]=true来表示这两种状态。

第1个物品的重量是2,要么装入背包,要么不装入背包,决策之后对应的背包状态:
  0+0=0
  0+2=2
  2+2=4
  这是基于上一步背包的状态计算得到的
  我们用state[1][0]=true state[1][2]=true state[1][4]=true 来表示。
  
以此类推,一直到第n-1个物品。找到state[n-1] 的 数组中找到最大的state[n-1][j]=true,返回j。

4.1 状态表

这个过程用状态表来表示,就是下图。

在这里插入图片描述在这里插入图片描述
代码如下。代码时间复杂度O(n*w)。

public int knapsnack(int[] weight,int n,int w){
        boolean[][] states = new boolean[n][w+1];
        states[0][0] = true;
        if(weight[0]<w){
            states[0][weight[0]] = true;
        }
        for(int i=1;i<n;i++){
            for(int j=0;j<w;j++){
                if(states[i-1][j]==true){
                    states[i][j] = true;
                }
            }
            for(int j=0;j<=w-weight[i];j++){
                if(states[i-1][j]==true){
                    states[i][j+weight[i]] = true;
                }
            }
        }
        for(int j=w;j>=0;j--){
            if(states[n-1][j]) return j;
        }
        return 0;
    }

上面的代码实现用到二维数组。经过观察,我们发现,每次for循环里面,在计算states[i]的时候,只与states[i-1]有关系。我们应该只用一维数组就能实现。

	public int knapsnackV2(int[] weight,int n,int w){
        boolean[] states = new boolean[w+1];
        states[0] = true;
        if(weight[0]<w){
            states[weight[0]] = true;
        }
        for(int i=1;i<n;i++){
            //使用一维数组需要从后向前计算,否则会有多余的计算
            for(int j=w-weight[i];j>=0;j--){
                if(states[j]==true){
                    states[j+weight[i]] = true;
                }
            }
        }
        for(int j=w;j>=0;j--){
            if(states[j]) return j;
        }
        return 0;
    }

4.2 状态方程

这道题目用状态方程来表示不太好表示。
2021-10-25:再次看这个状态方程是可以表示的。
state[i][j]=true表示当第i个物品决策完之后,背包可能的重量是j。
state[i][j]=false表示当第i个物品决策完之后,背包不可能是j。

state[i][j]=true, if state[i-1][j]=true
state[i][j+weights[i]] = true, if state[i-1][j]=true and j+weights[i]<=w(不超重)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回溯法是解决0-1背包问题的一种常用方法。该问题是指在给定n种物品和一个容量为C的背包的情况下,如何选择装入背包的物品,使得装入背包中物品的总价值最大。回溯法的基本思路是搜索所有可能的解,并在搜索过程中剪枝,以达到减少搜索次数的目的。具体实现可以参考引用中的递归函数rKnap。 在回溯法中,我们首先将物品按照单位重量的价值递减排序,然后从第一个物品开始搜索。对于每个物品,我们有两种选择:将其放入背包或不放入背包。如果将其放入背包,我们需要检查当前背包容量是否足够,如果足够,则将其放入背包,并更新当前背包的重量和价值。然后递归搜索下一个物品。如果不将其放入背包,则直接递归搜索下一个物品。在搜索过程中,我们需要记录当前背包的重量和价值,以及当前最优解的最大价值。如果当前背包的价值已经超过当前最优解的最大价值,则可以剪枝,不再继续搜索。 C++代码实现可以参考以下范例: <<范例: #include <iostream> #include <algorithm> using namespace std; const int MAXN = 100; int n, c; int w[MAXN], v[MAXN]; int bestv = 0, curv = 0, curw = 0; void backtrack(int i) { if (i > n) { bestv = max(bestv, curv); return; } if (curw + w[i] <= c) { curw += w[i]; curv += v[i]; backtrack(i + 1); curw -= w[i]; curv -= v[i]; } if (curv + v[i] * (c - curw) > bestv) { backtrack(i + 1); } } int main() { cin >> n >> c; for (int i = 1; i <= n; i++) { cin >> w[i] >> v[i]; } sort(w + 1, w + n + 1); sort(v + 1, v + n + 1); backtrack(1); cout << bestv << endl; return 0; } >>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值