算法:背包问题解析_i++

0. 前言

学习算法以来,总会遇到背包问题,包括0-1背包问题、完全背包问题等,但还没做过一次正式的总结,所以,这就来啦!

1. 背包问题概述

背包问题通常指的是给出一个有最大承重的背包,和一系列有固定价值和重量的物品,求出背包中装载最大价值物品的方案。根据物品的数量限制与分割限制,有如下四种背包问题:

  • 部分背包问题:每种物品只有一件,物品可分割
  • 0-1背包问题:每种物品只有一件,不可分割
  • 完全背包问题:每种物品有无限件,不可分割
  • 多重背包问题:每种物品有有限件(大于1),不可分割

这里先仅讨论1、2个问题。

2. 部分背包问题(贪心算法)

部分背包问题由于物品可分割,因此可以采用贪心算法求解,优先装入单位价值高的物品,装载完后依次装入单位价格次高的物品。

void knapsack(int n,float M,float y[],float w[],float x[]){//其中n指物品的件数,M指背包最大承重量,y[]存储每种物品的价值,w[]	
														   //存储每种物品的质量,x[]存储背包最佳装载方案中,每种物品的数量
	sort(n,v,w);//将数组按照单位价值由高到低排序
	int i;
	float c=M;
	for(i=0;i<n;i++) x[i]=0;//初始化
	for(i=0;i<n;i++){
		if(w[i]>c)//若背包剩余容量不足以装载整个物品
			break;
		else{
			x[i]=1;
			c-=w[i];
		}
	}
	if(i!=n){
		x[i]=c/w[i];
	}																					    
}

3. 0-1背包问题

对于每件物品,0-1背包问题只有两种选择,装或者不装,这种选或不选的问题,首先可以用动态规划解决,其次,由于可选方案为有限种,也可对所有可能的方案采用回溯法进行遍历搜索。

3.1 动态规划法(C++)

使用动态规划法的关键是找到问题中的递归式,即前一个状态或后一个状态与当前状态的关系。用m(i,j)表示当第[i,n]个物品可选,背包剩余容量j。
因此,有如下递归关系式:
算法:背包问题解析_递归_02
基于上述递归关系式,将m(i,j)保存在数组中(条件是每个物品的质量为整数),代码如下所示:

int dp(int n,int M,int w[],int v[]){
    int m[n][M];
    int i,j;
    for(i=0;i<M;i++){//初始化可选择物品仅为n的情况
        if(i>w[n-1])
            m[n-1][i]=v[n-1];
        else
            m[n-1][i]=0;
    }
    for(i=n-2;i>=0;i--){//从后向前
        for(j=0;j<M;j++){//遍历所有背包容量
            if(j>=w[i])//若背包容量大于等于该物品重量
                m[i][j]=max(m[i+1][j],m[i+1][j-w[i]]+v[i]);
            else//否则无法装入
                m[i][j]=m[i+1][j];
        }
    }
    return max(m[0][M-1],m[0][M-w[0]]+v[0]);//第一个物品可能装入,可能不装入,需要进行判断
}

3.2 回溯法(C++)

回溯法即对解空间进行有限制条件的深度优先搜索,此问题中,在向下搜索时,需满足背包容量大于等于所选物品质量。实现回溯方法,可以采用递归回溯迭代回溯两种。在这里采用递归回溯实现:

int backtrack(int i,int j){//w[]中存放每个物品的重量,v[]中存放每个物品的价值,n为物品数量,
    int l=0,r=0;
    if(i<n){
        if(j>=w[i]){
            r=backtrack(i+1,j-w[i])+v[i];
        }
        l=backtrack(i+1,j);
        return max(l,r);
    }
    else if(i==n){
        return 0;
    }
}