完全背包问题理解(转)

转自: http://blog.csdn.net/insistgogo/article/details/11081025

摘自Tianyi Cui童鞋的《背包问题九讲》,稍作修改,方便理解。

本文包含的内容:

<1> 问题描述

<2> 基本思路(直接扩展01背包的方程)

<3> 转换为01背包问题求解(直接利用01背包)

<4> O(VN)的算法

---------------------------------------------

1、问题描述

已知:有一个容量为V的背包和N件物品,第i件物品的重量是weight[i],收益是cost[i]。

条件:每种物品都有无限件,能放多少就放多少。

问题:在不超过背包容量的情况下,最多能获得多少价值或收益

举例:物品个数N = 3,背包容量为V = 5,则背包可以装下的最大价值为40.

----------------------------------------------

2、基本思路(直接扩展01背包的方程)

由于本问题类似于01背包问题,在01背包问题中,物品要么取,要么不取,而在完全背包中,物品可以取0件、取1件、取2件...直到背包放不下位置。因此,可以直接在01背包的递推式中扩展得到。

f[i][v]:表示前i件物品放入容量为v的容量中时的最大收益
递推式:
f[i][v] = max(f[i - 1][v],f[i - K * weight[i]] + K * Value[i]); 其中  1 <= K * weight[i] <= v,(v指此时背包容量)
//初始条件
f[0][v] = 0;
f[i][0] = 0;
f[i][v]:表示前i件物品放入容量为v的容量中时的最大收益
递推式:
f[i][v] = max(f[i - 1][v],f[i - K * weight[i]] + K * Value[i]); 其中  1 <= K * weight[i] <= v,(v指此时背包容量)
//初始条件
f[0][v] = 0;
f[i][0] = 0;

代码:

#include <iostream>
#include <assert.h>
using namespace std;
/*
f[i][v]:前i件物品放入背包容量为v的背包获得的最大收益

f[i][v] = max(f[i - 1][v],f[i - 1][v - k * Wi] + k * Vi,其中 1<=k<= v/Wi)

边界条件
f[0][v] = 0;
f[i][0] = 0;
*/

const int N = 3;
const int V = 5;
int weight[N + 1] = {0,3,2,2};
int Value[N + 1] = {0,5,10,20};

int f[N + 1][V + 1] = {0};

int Completeknapsack()
{
    //边界条件
    for (int i = 0;i <= N;i++)
    {
        f[i][0] = 0;
    }
    for (int v = 0;v <= V;v++)
    {
        f[0][v] = 0;
    }
    //递推
    for (int i = 1;i <= N;i++)
    {
        for (int v = 1;v <= V;v++)
        {
            f[i][v] = 0;
            int nCount = v / weight[i];
            for (int k = 0;k <= nCount;k++)
            {
                f[i][v] = max(f[i][v],f[i - 1][v - k * weight[i]] + k * Value[i]);
            }
        }
    }
    return f[N][V];
}

int main()
{
    cout<<Completeknapsack()<<endl;
    system("pause");
    return 1;
}

复杂度分析:

程序需要求解N*V个状态,每一个状态需要的时间为O(v/Weight[i]),总的复杂度为O(NV*Σ(V/c[i]))。

代码优化:

完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足c[i]<=c[j]且w[i]>=w[j],则将物品j去掉,不用考虑。

即,如果一个物品A是占的地少且价值高,而物品B是占地多,但是价值不怎么高,那么肯定是优先考虑A物品的。

这里代码略。

----------------------------------------------

3、转换为01背包问题求解(直接利用01背包)

思路 1、完全背包的物品可以取无限件,根据背包的总容量V和第i件物品的总重量Weight[i],可知,背包中最多装入V/Weight[i](向下取整)件该物品。因此可以直接改变第i件物品的总个数,使之达到V/Weight[i](向下取整)件,之后直接利用01背包的思路进行操作即可。

举例:物品个数N = 3,背包容量为V = 5。

拆分之前的物品序列:

拆分之后的物品序列:

根据上述思想:在背包的最大容量(5)中,最多可以装入1件物品一,因此不用扩展物品一。最多可以装入2件物品二,因此可以扩展一件物品二。同理,可以扩展一件物品三。

时间复杂度的分析:O(NNew*V),其中V表示扩展前背包容量,NNew表示扩展后物品的个数,NNew = Σ(V/Weight[i](向下取整))

思路 2、对物品进行拆分时,拆成二进制的形式。

具体思路:把第i种物品拆成费用为weight[i]*2^k、价值为w[i]*2^k的若干件物品,其中k满足weight[i]*2^k<=V。

思路:这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。

这样把每种物品拆成O(log V/weight[i])件物品,是一个很大的改进。

举例:物品个数N = 3,背包总容量为V = 5。 拆分之前的物品序列:

拆分之后的物品序列:

为了和前面的例子保持一致,这里才用之前的例子,但是这个例子没有更好的说明二进制的拆分方法拆分的物品个数会少写。

假设物品A的重量为2,收益为3,背包的总重量为20。

根据第一种拆分,可以拆成10个物品,每一个物品的重量为2,收益为3。

根据第二种拆分方法,可以拆成4个物品,分别是物品一(重量为1*2,收益为3),物品二(重量为2*2,收益为6),物品三(重量为4*2,收益为12),物品四(重量为8*2,收益为24)。

时间复杂度的分析:O(NNEW*V),其中V表示扩展前背包容量,NNew表示扩展后物品的个数,NNew = Σ(log V/weight[i](向下取整))

代码:

#include <iostream>
#include <vector>
#include <assert.h>
using namespace std;
/*
f[v]:表示第i件物品放入容量为v的背包后,获得的最大容量
f[v] = max(f[v],f[v - weight[i]] + value[i]);
初始条件:f[0] = 0;
*/

const int N = 3;
const int V = 20;//5
int weight[N + 1] = {0,3,2,2};
int Value[N + 1] = {0,5,10,20};

int NNew = 0;
vector<int> weightVector;
vector<int> Valuevector;
int f[V + 1] = {0};
/*拆分物品*/
void SplitItem()
{
	//从1开始
	weightVector.push_back(0);
	Valuevector.push_back(0);
	//开始拆分
	int nPower = 1;
	for (int i = 1;i <= N;i++)
	{
		nPower = 1;
		while (nPower * weight[i] <= V)
		{
			weightVector.push_back(nPower * weight[i]);
			Valuevector.push_back(nPower * Value[i]);
			nPower <<= 1;
		}
	}
}

int Completeknapsack()
{
	//拆分物品
	SplitItem();
	//转化为01背包处理
	NNew = weightVector.size() - 1;//多加了一个0,要减去

	for (int i = 1;i <= NNew;i++)//物品个数变化
	{
		for (int v = V;v >= weightVector[i];v--)//背包容量仍是V
		{
			f[v] = max(f[v],f[v - weightVector[i]] + Valuevector[i]);
		}
	}

	return f[NNew];
}
int main()
{
	cout<<Completeknapsack()<<endl;
	system("pause");
	return 1;
}

4、O(VN)的算法

伪代码

[cpp] view plaincopyprint?

  1. for (int i = 1;i <= N;i++)
  2. {
  3. for (int v = weight[i];v <= V;v++)
  4. {
  5. f[v] = max(f[v],f[v - weight[i]] + Value[i]);
  6. }
  7. }
for (int i = 1;i <= N;i++)
{
	for (int v = weight[i];v <= V;v++)
	{
		f[v] = max(f[v],f[v - weight[i]] + Value[i]);
	}
}

分析:这和01背包的伪代码很相似,在01背包的代码中,v变化的区间是逆序循环的,即[V,Weight[i]]。而这里,v变化的区间是顺序循环的,即为[Weight[i],V]。

原因:

再次给出定义:

f[i][v]表示把前i件物品放入容量为v的背包时的最大代价。

f[i-1][v-c[i]]表示把前i  - 1件物品放入容量为v的背包时的最大代价.

在01背包中,v变化的区间是逆序循环的原因:要保证由状态f[i-1][v-c[i]]递推状态f[i][v]时,f[i-1][v-c[i]]没有放入第i件物品。之后,在第i循环时,放入一件第i件物品。

01背包的方程:

[cpp] view plaincopyprint?

  1. f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + Value[i])
f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + Value[i])  

在完全背包中,v变化的区间是顺序循环的原因:完全背包的特点是每种物品可选无限件,在求解加选第i种物品带来的收益f[i][v]时,在状态f[i][v-c[i]]中已经尽可能多的放入物品i了,此时在f[i][v-c[i]]的基础上,我们可以再次放入一件物品i,此时也是在不超过背包容量的基础下,尽可能多的放入物品i。

完全背包的方程:

[cpp] view plaincopyprint?

  1. f[i][v] = max(f[i - 1][v],f[i][v - weight[i]] + Value[i]);
f[i][v] = max(f[i - 1][v],f[i][v - weight[i]] + Value[i]);

举例:

物品个数N = 3,背包总容量为V = 5。

物品信息:

完全背包:

分析:

i = 2,表示正在处理第2件物品。在求解f[2][4]时,如果要计算把第2件物品放入背包后的代价时,我们需要知道f[2][2],此时f[2][2]中已经尽全力放入第2件物品了(已经放入一件)。此时此刻还可以在放入一件第2件物品,在背包容量为4时,最多可以放入两件第二件物品。

总结下,f[i][v]:表示在背包容量为v时,尽全力的放入第i件物品的代价。f[i][v - weight[i]]:表示在背包容量为v  - weight[i]时,尽全力的放入第i件物品的代价。因此由f[i][v - weight[i]]转换为f[i][v]时,也是在f[i][v - weight[i]]的基础上有加入了一件物品i。

为了节省保存状态的空间,可以直接使用一维数组保存状态。

代码:迭代方程:f[i][v] = max(f[i - 1][v],f[i][v - weight[i]] + Value[i]);

[cpp] view plaincopyprint?

#include <iostream>
#include <vector>
#include <assert.h>
using namespace std;
const int N = 3;
const int V = 5;//5
int weight[N + 1] = {0,3,2,2};
int Value[N + 1] = {0,5,10,20};

int f[N + 1][V + 1] = {0};

int Completeknapsack()
{
	//初始化
	for (int i = 0;i <= N;i++)
	{
		f[i][0] = 0;
	}
	for (int v = 0;v <= V;v++)
	{
		f[0][v] = 0;
	}
	for (int i = 1;i <= N;i++)
	{
		for (int v = weight[i];v <= V;v++)
		{
			f[i][v] = max(f[i - 1][v],f[i][v - weight[i]] + Value[i]);
		}
	}
	return f[N][V];
}

int main()
{
	cout<<Completeknapsack()<<endl;
	system("pause");
	return 1;
}

代码:迭代方程:f[v] = max(f[v],f[v - weight[i]] + Value[i]);

[cpp] view plaincopyprint?

#include <iostream>
using namespace std;
const int N = 3;
const int V = 5;//5
int weight[N + 1] = {0,3,2,2};
int Value[N + 1] = {0,5,10,20};

int f[V + 1] = {0};

int Completeknapsack()
{
    f[0] = 0;
    for (int i = 1;i <= N;i++)
    {
        for (int v = weight[i];v <= V;v++)
        {
            f[v] = max(f[v],f[v - weight[i]] + Value[i]);
        }
    }
    return f[V];
}
int main()
{
    cout<<Completeknapsack()<<endl;
    system("pause");
    return 1;
}
时间: 2024-08-05 10:49:24

完全背包问题理解(转)的相关文章

从01背包问题理解动态规划---初体验

01背包问题具体例子:假设现有容量10kg的背包,另外有3个物品,分别为a1,a2,a3.物品a1重量为3kg,价值为4:物品a2重量为4kg,价值为5:物品a3重量为5kg,价值为6.将哪些物品放入背包可使得背包中的总价值最大? 这个问题有两种解法,动态规划和贪婪算法.本文仅涉及动态规划. 先不套用动态规划的具体定义,试着想,碰见这种题目,怎么解决? 首先想到的,一般是穷举法,一个一个地试,对于数目小的例子适用,如果容量增大,物品增多,这种方法就无用武之地了. 其次,可以先把价值最大的物体放入

01背包问题--动态规划解法

从01背包问题理解动态规划 01背包问题具体例子:假设现有容量10kg的背包,另外有3个物品,分别为a1,a2,a3.物品a1重量为3kg,价值为4:物品a2重量为4kg,价值为5:物品a3重量为5kg,价值为6.将哪些物品放入背包可使得背包中的总价值最大? 这个问题有两种解法,动态规划和贪婪算法.本文仅涉及动态规划. 先不套用动态规划的具体定义,试着想,碰见这种题目,怎么解决? 首先想到的,一般是穷举法,一个一个地试,对于数目小的例子适用,如果容量增大,物品增多,这种方法就无用武之地了. 其次

动态规划之01背包问题(最易理解的讲解)

01背包问题,是用来介绍动态规划算法最经典的例子,网上关于01背包问题的讲解也很多,我写这篇文章力争做到用最简单的方式,最少的公式把01背包问题讲解透彻. 01背包的状态转换方程 f[i,j] = Max{ f[i-1,j-Wi]+Pi( j >= Wi ),  f[i-1,j] } f[i,j]表示在前i件物品中选择若干件放在承重为 j 的背包中,可以取得的最大价值. Pi表示第i件物品的价值. 决策:为了背包中物品总价值最大化,第 i件物品应该放入背包中吗 ? 题目描述: 有编号分别为a,b

如何理解背包问题

问题 假定背包的最大容量为W,N件物品,每件物品都有自己的价值和重量,将物品放入背包中使得背包内物品的总价值最大.   背包问题wiki 可以想象这样一个场景--小偷在屋子里偷东西,他带着一只背包.屋子里物品数量有限--每件物品都具有一定的重量和价值--珠宝重量轻但价值高,桌 子重但价值低.最重要的是小偷背包容量有限.很明显,他不能把桌子分成两份或者带走珠宝的3/4.对于一件物品他只能选择带走或者不带走. 示例: Knapsack Max weight : W = 10 (units) Tota

动态规划本质理解:01背包问题

题目描述:01背包问题 w:重量 v:价值 cap:承重 参考代码: package Dp; import org.junit.Test; /** * 01背包问题 w:重量 v:价值 cap:承重 * * @author Tongkey */ public class Backpack { public int[][] result; /** * 递归解法,时间复杂度为O(2^n) * * @param w * 重量 * @param v * 价值 * @param cap * 承重 * @p

1616 疯狂的采药(完全背包问题)

难度:普及- 题目类型:动规 提交次数:1 涉及知识:背包动规 题目背景 此题为NOIP2005普及组第三题的疯狂版. 此题为纪念LiYuxiang而生. 题目描述 LiYuxiang是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师.为此,他想拜附近最有威望的医师为师.医师为了判断他的资质,给他出了一个难题.医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值.我会给你一段时间,在这段时间里,你可以采到一些草药.如

01背包问题:POJ3624

背包问题是动态规划中的经典问题,而01背包问题是最基本的背包问题,也是最需要深刻理解的,否则何谈复杂的背包问题. POJ3624是一道纯粹的01背包问题,在此,加入新的要求:输出放入物品的方案. 我们的数组基于这样一种假设: totalN表示物品的种类,totalW表示背包的容量 w[i]表示第i件物品的重量,d[i]表示第i件物品的价值. F(i,j)表示前i件物品放入容量为j的背包中,背包内物品的最大价值. F(i,j) = max{ F(i-1,j) , F(i-1,j-w[i])+d[i

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

0-1背包问题描述如下: 有一个容量为V的背包,和一些物品.这些物品分别有两个属性,体积w和价值v,每种物品只有一个.要求用这个背包装下价值尽可能多的物品,求该最大价值,背包可以不被装满.因为最优解中,每个物品都有两种可能的情况,即在背包中或者不存在(背 包中有0个该物品或者 1个),所以我们把这个问题称为0-1背包问题. 用dp[i][j]表示前i个物品在总体积不超过j的情况下,放到背包里的最大价值.由此可以推出状态转移方程: dp[0][j] = 0; dp[i][j] = max{dp[i

01背包问题的动态规划算法

01背包问题我最初学会的解法是回溯法,第一反应并不是用动态规划算法去解答.原因是学习动态规划算法的时候,矩阵连乘.最长公共子串等问题很容易将问题离散化成规模不同的子问题,比较好理解,而对于01背包问题则不容易想到将背包容量离散化抽象出子问题,从情感上先入为主也误以为动态规划算法不是解决01背包问题的好方法,实际上并不是这样的.另外,动态规划算法不对子问题进行重复计算,但是要自底向上将所有子问题都计算一遍,直到计算出最终问题的结果也就是我们要的答案,有点像爬山的感觉. 问题描述:给定n种物品和一背