基于回溯法和分支限界法求解01背包问题

本文介绍了如何使用回溯法和分支限界法来解决经典的01背包问题。通过详细描述问题描述、算法设计、正确性证明以及复杂性分析,阐述了两种方法的思路和区别。回溯法采用深度优先搜索,分支限界法则利用广度优先或最小耗费优先策略寻找最优解。

基于回溯法和分支限界法求解01背包问题

问题描述

现有n个物品,1个背包。对物品i,其价值为viv_ivi ,重量为WiW_iWi,背包的容量为WWW,如何选取物品使得背包里转入物品的总价值最大?

在约束条件为:选取物品的重量小于等于背包重量的情况下,尽可能让背包中物品的总价值最大。

算法设计

01背包问题是自己选取问题,该问题的解空间可用子集树表明。设currentTotalValue是当前已经装入背包的物品的总价值、residueValue是剩余的未知是否装入物品的总价值,也就是未被搜索到物品的总价值、pbest是当前已经找到的最优解的价值。

使用两个等长的一维数组存储物品的重量weights[n]weights[n]weights[n]和价值values[n]values[n]values[n],这里第iii个物品的重量就是weights[i]weights[i]weights[i],价值就是values[i]values[i]values[i]

回溯法

回溯法的基本思想,在包含问题所有解的解空间树中,按照深度优先的策略,从根节点出发搜索解空间树。搜索解空间的任意一个节点时候,只要其左儿子节点是一个可行节点,搜索就进入其左子树。当右子树有可能包含最优解时才进入右子树搜索,否则将右子树剪去。

分支限界法

分支限界法解空间的定义、解空间的组织结构、剪枝函数都与回溯法相同。不同的是解空间的搜索方式,这里采用广度优先搜索。

  • 针对当前扩展节点,一次性生成其所有孩子节点,如果孩子节点满足约束条件和限界条件,则将该孩子节点插入到活结点队列末尾;反之则舍弃。
  • 从活结点对了中选取待扩展节点。
  • 搜索过程直到活结点队列为空时停止。

算法描述

回溯法

backtrack(i, weights[n], values[n], W){
	if i > n
		pbest = currentTotalValue
		return
	if weights[i] + currentTotalWeight <= W
		x[i] = 1
		currentTotalWeight += weights[i]
		currentTotalValue += values[i]
		backtrack(i+1, weights[n], values[n], W-currentTotalWeight)
		currentTotalWeight -= weights[i]
		currentTotalValue -= values[i]
	if bound(i+1, W-currentTotalWeight, currentTotalValue, weights[n], values[n]) > pbest
		x[i] = 0
		backtrack(i+1, weights[n], values[n], W-currentTotalWeight)
}


分支限界法

branch(weights[n],values[n],W){
	i = 0, currentTotalWeight = 0, currentTotalValue = 0, queue = null
	up = 0
	while true{
		if currentTotalWeight+w[i] < W
			if currentTotalValue + values[i] > pbest
				pbest = currentTotalValue + values[i]
		up = bound(i+1)
		if up > pbest
			queue.push(new Node(currentTotalWeight, currentTotalValue,
            				up , false , i+1))
        node = queue.back()
        if(node = null)
         	breaks
        currentTotalWeight = node.currentTotalWeight
        currentTotalValue = node.currentTotalValue
        up = node.up
        i = node.level
        if i == n+1
        	stNode = node.ptr
     }
        if(stNode != null)
        	for j=n to 1 by -1
        		x[j] = stNode.isLeft?1:0
        		stNode = stNode.parent
	}
}

正确性证明

复杂性分析

回溯法

  • 判断约束函数需要O(1)O(1)O(1),在最坏的情况下有2n−12^n-12n1个左孩子,约束函数耗时最坏为O(2n)O(2^n)O(2n)

  • 计算上界界限函数需要O(n)O(n)O(n)时间,最坏情况下有2n−12^n-12n1个右孩子需要计算上界,界限函数耗时最坏为O(n2n)O(n2^n)O(n2n)

时间复杂度:O(2n)+O(n2n)=O(n2n)O(2^n)+O(n2^n)=O(n2^n)O(2n)+O(n2n)=O(n2n)

空间复杂度:O(n)O(n)O(n),即递归深度。

分支限界法

计算上界限界函数需要O(n)O(n)O(n)时间, 而最坏情况有2(n+1)–22^(n +1) –22(n+1)2个节点; 若对每个结点用限界函数判断,则其时间复杂度为O(n∗2n)O(n*2^n)O(n2n)​,算法中时间复杂度主要依赖限界函数。最坏情况下需搜索2(n+1)−22^(n +1)-22(n+1)2个节点,需O(2n)O(2^n )O(2n)个空间存储节点。

  • 时间复杂度:O(n∗2n)O(n*2^n)O(n2n)

  • 空间复杂度:O(2n)O(2^n )O(2n)

实现与测试

import java.util.LinkedList;
import java.util.Queue;

public class Solution {
    
    Package01 package01;
    double currentTotalValue;
    double currentTotalWeight;
    double pbest;
    int [] X;

    private void init(){
        double currentTotalValue = 0;
        double currentTotalWeight = 0;
        double pbest = 0;
    }
    
    Solution(Package01 package01){
        this.package01 = package01;
        X = new int[package01.n];
    }

    class Node{
        
        double currentTotalValue;
        double currentTotalWeight;
        double up;
        int level;
        Node parent;
        boolean isLeft;

        public Node(double currentTotalValue, double currentTotalWeight, double up, int level, Node parent, boolean isLeft) {
            this.currentTotalValue = currentTotalValue;
            this.currentTotalWeight = currentTotalWeight;
            this.up = up;
            this.level = level;
            this.parent = parent;
            this.isLeft = isLeft;
        }
    }
    void branch(){
        init();
        int i = 0;
        double up = bound(0, package01.W, 0);
        Queue<Node> queue = new LinkedList<>();
        Node current = null;
        Node stNode = null;
        while (true){
            if (i < package01.n && currentTotalWeight + package01.weights[i] <= package01.W ){
                if (currentTotalValue + package01.values[i] > pbest){
                    pbest = currentTotalValue + package01.values[i];
                    queue.offer(new Node(pbest,currentTotalWeight+package01.weights[i],up,i+1,current,true));
                }
            }
            up = bound(i+1, package01.W-currentTotalWeight,currentTotalValue);
            if (up > pbest)
                queue.offer(new Node(currentTotalValue,currentTotalWeight,up,i+1,current,false));
            current = queue.poll();
            if(current == null){
                break;
            }
            currentTotalWeight = current.currentTotalWeight;
            currentTotalValue = current.currentTotalValue;
            up = current.up;
            i = current.level;
            if(i == package01.n){
                stNode = current;
            }
        }
        for (int j = package01.n-1; j >= 0 ; j--) {
            if(stNode != null) {
                X[j] = stNode.isLeft ? 1 : 0;
                stNode = stNode.parent;
            }
        }
    }
    void backtrack(){
        backtrack(0);
    }
    void backtrack(int i){
        init();
        if(i >= package01.n){
            pbest = currentTotalValue;
            return;
        }
        if (package01.weights[i] + currentTotalWeight <= package01.W){
            X[i] = 1;
            currentTotalWeight += package01.weights[i];
            currentTotalValue += package01.values[i];
            backtrack(i+1);
            currentTotalWeight -= package01.weights[i];
            currentTotalValue -= package01.values[i];
        }

        if (bound(i+1, package01.W-currentTotalWeight,currentTotalValue) > pbest){
            X[i] = 0;
            backtrack(i+1);
        }
    }

    double bound(int i, double cleft, double currentTotalValue){
        if(i >= package01.n)
            return -1;
        double up = currentTotalValue;
        while(i < package01.n && package01.weights[i] <= cleft){
            cleft -= package01.weights[i];
            up += package01.values[i];
            i++;
        }
        if(i < package01.n){
            up += package01.values[i]/package01.weights[i] * cleft;
        }
        return up;
    }
}

class Package01 {
    // 背包容量
    double W;
    // 物品数量
    int n;
    double [] weights;
    double [] values;
    Package01(double [] weights, double [] values, double W){
        this.weights = weights;
        this.values = values;
        this.W = W;
        n = weights.length;
    }
}

测试

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        double W = 7;
        double []weights = {3, 5, 2, 1};
        double []values = {9,10,7,4};
        Package01 package01 = new Package01(weights,values,W);
        Solution solution = new Solution(package01);
        solution.backtrack();
        System.out.println("回溯法结果向量:"+Arrays.toString(solution.X));
        solution.branch();
        System.out.println("分支限界法结果向量"+Arrays.toString(solution.X));
    }
}
回溯法结果向量:[1, 0, 1, 1]
分支限界法结果向量[1, 0, 1, 1]

心得体会

分支限界法和回溯法非常类似,都是在问题的解空间树上搜索问题解的算法。。但在一般情况下,分支限界法与回溯法的求解目标不同。回溯法的求解目标是找出T中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。

由于求解的目标不同,两种方法的搜索方式也不同:

  • 回溯法以深度优先的方式搜索解空间树;
  • 分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值