分数背包问题:贪心算法及示例
什么是贪心策略?
贪心算法类似于动态规划算法,通常用于解决最优问题(根据特定标准找到问题的最佳解决方案)。
贪心算法通过实施最优的局部选择,希望这些选择能够为要解决的问题带来最优的全局解决方案。贪心算法通常不难设置,速度快(时间复杂度通常是线性函数或非常接近二次函数)。此外,这些程序也不难调试,内存占用少。但结果不总是最优解。
贪心策略常用于通过构建选项A来解决组合优化问题。选项A通过选择A的每个组件Ai直到完成(足够n个组件)来构建。对于每个Ai,你都会选择最优的Ai。这样,在最后一步可能一无所获,只能接受最后一个剩余的值。
贪心决策有两个关键组成部分
- 贪心选择的方式。你可以选择当前最佳的解决方案,然后解决由最后一次选择所产生的子问题。贪心算法的选择可能取决于之前的选择。但它不能依赖于任何未来的选择,也不能依赖于子问题的解决方案。算法以循环方式进行选择,同时将给定问题缩小为更小的子问题。
- 最优子结构。如果一个问题的最优解包含其子问题的最优解,那么该问题就具有最优子结构。
贪心算法有五个组成部分
- 一组候选者,从中创建解决方案。
- 一个选择函数,用于选择最佳候选者添加到解决方案中。
- 一个可行函数,用于决定是否可以使用候选者来构建解决方案。
- 一个目标函数,用于确定解决方案或不完整解决方案的值。
- 一个评估函数,用于指示何时找到完整解决方案。
贪心一号的思路
有了第一个思路,贪心一号的步骤如下
- 按值非递增顺序排序。
- 依次考虑排序后的包裹,如果背包的剩余容量足够容纳该包裹(即放入背包的包裹总重量加上考虑的包裹重量不超过背包容量),则将考虑的包裹放入背包。
然而,这种贪心算法并非总是能给出最优解。这里有一个反例
- 问题参数:n = 3;M = 19。
- 包裹:{i = 1; W[i] = 14; V[i] = 20}; {i = 2; W[i] = 6; V[i] = 16}; {i = 3; W[i] = 10; V[i] = 8} **->** 价值高但重量也大。
- 算法将选择包裹1,总价值为20,而问题的最优解是(包裹2,包裹3),总价值为24。
贪心二号的思路
有了第二个思路,贪心二号的步骤如下
- 按重量非递减顺序排序。
- 依次考虑排序后的包裹,如果背包的剩余容量足够容纳该包裹(即放入背包的包裹总重量加上考虑的包裹重量不超过背包容量),则将考虑的包裹放入背包。
然而,这种贪心算法并非总是能给出最优解。这里有一个反例
- 问题参数:n = 3;M = 11。
- 包裹:{i = 1; W[i] = 5; V[i] = 10}; {i = 2; W[i] = 6; V[i] = 16}; {i = 3; W[i] = 10; V[i] = 28} **->** 重量轻但价值也很轻。
- 算法将选择(包裹1,包裹2),总价值为26,而问题的最优解是(包裹3),总价值为28。
贪心三号的思路
有了第三个思路,贪心三号的步骤如下。事实上,这是最广泛使用的算法。
- 按照单位成本值非递增的顺序对包裹进行排序
。你有
- 依次考虑排序后的包裹,如果背包的剩余容量足够容纳该包裹(即放入背包的包裹总重量加上考虑的包裹重量不超过背包容量),则将考虑的包裹放入背包。
**思路**:该问题的贪心思路是计算每个 的比例
。然后按比例降序排序。你将选择最高的
包裹,并且背包的容量可以容纳该包裹(剩余 > wi)。每次将一个包裹放入背包时,它也会减少背包的容量。
选择包裹的方式
- 考虑单位成本数组。你根据单位成本的递减顺序选择包裹。
- 假设你找到了一个部分解决方案:(x1, …, xi)。
- 背包的价值已获得
- 对应于已放入背包的包裹的重量
- 因此,背包剩余的重量限制是
算法步骤
你看到这是一个寻找最大值的问题。包裹列表按单位成本的降序排序以进行分支考虑。
- **步骤1**:根节点代表背包的初始状态,即你还没有选择任何包裹。
- 总价值 = 0。
- 根节点的上限 UpperBound = M * 最大单位成本。
- **步骤2**:根节点将具有子节点,对应于选择具有最高单位成本的包裹的能力。对于每个节点,重新计算参数
- 总价值 = 总价值(旧)+ 所选包裹数量 * 每个包裹的价值。
- M = M(旧)– 所选包裹数量 * 每个包裹的重量。
- UpperBound = 总价值 + M(新)* 下一个要考虑的包裹的单位成本。
- **步骤3**:在子节点中,优先处理具有较大上限的节点。此节点的子节点对应于选择下一个具有高单位成本的包裹的能力。对于每个节点,必须根据步骤2中提到的公式重新计算参数总价值、M、上限。
- **步骤4**:用以下说明重复步骤3:对于上限值小于或等于已找到的选项的临时最大成本的节点,无需再为该节点进行分支。
- **步骤5**:如果所有节点都已分支或被剪枝,则寻找最昂贵的选项。
算法的伪代码
Fractional Knapsack (Array W, Array V, int M) 1. for i <- 1 to size (V) 2. calculate cost[i] <- V[i] / W[i] 3. Sort-Descending (cost) 4. i ← 1 5. while (i <= size(V)) 6. if W[i] <= M 7. M ← M – W[i] 8. total ← total + V[i]; 9. if W[i] > M 10. i ← i+1
算法的复杂度
- 如果使用简单的排序算法(选择、冒泡…),则整个问题的复杂度为 O(n2)。
- 如果使用快速排序或归并排序,则整个问题的复杂度为 O(nlogn)。
贪心三号的Java代码
- 首先,定义类 KnapsackPackage。该类具有以下属性:重量、价值以及每个包裹的相应成本。该类的成本属性用于主算法中的排序任务。每个成本的值是
每个包裹的比例。
public class KnapsackPackage {
private double weight;
private double value;
private Double cost;
public KnapsackPackage(double weight, double value) {
super();
this.weight = weight;
this.value = value;
this.cost = new Double(value / weight);
}
public double getWeight() {
return weight;
}
public double getValue() {
return value;
}
public Double getCost() {
return cost;
}
}
- 然后创建一个函数来执行算法贪心三号。
public void knapsackGreProc(int W[], int V[], int M, int n) {
KnapsackPackage[] packs = new KnapsackPackage[n];
for (int i = 0; i < n; i ++) {
packs[i] = new KnapsackPackage(W[i], V[i]);
}
Arrays.sort(packs, new Comparator<KnapsackPackage>() {
@Override
public int compare(KnapsackPackage kPackA, KnapsackPackage kPackB) {
return kPackB.getCost().compareTo(kPackA.getCost());
}
});
int remain = M;
double result = 0d;
int i = 0;
boolean stopProc = false;
while (!stopProc) {
if (packs[i].getWeight() <= remain) {
remain -= packs[i].getWeight();
result += packs[i].getValue();
System.out.println("Pack " + i + " - Weight " + packs[i].getWeight() + " - Value " + packs[i].getValue());
}
if (packs[i].getWeight() > remain) {
i ++;
}
if (i == n) {
stopProc = true;
}
}
System.out.println("Max Value:\t" + result);
}

代码解释
- 为每个背包包裹初始化重量和价值。
- 按成本降序对背包包裹进行排序。
- 如果选择包裹i。
- 如果选择的包裹i数量足够。
- 浏览所有包裹时停止。
在本教程中,您有两个示例。这是使用两个示例运行上述程序的 Java 代码
public void run() {
/*
* Pack and Weight - Value
*/
//int W[] = new int[]{15, 10, 2, 4};
int W[] = new int[]{12, 2, 1, 1, 4};
//int V[] = new int[]{30, 25, 2, 6};
int V[] = new int[]{4, 2, 1, 2, 10};
/*
* Max Weight
*/
//int M = 37;
int M = 15;
int n = V.length;
/*
* Run the algorithm
*/
knapsackGreProc(W, V, M, n);
}
输出结果
- 第一个例子
Pack 0 - Weight 10.0 - Value 25.0 Pack 0 - Weight 10.0 - Value 25.0 Pack 0 - Weight 10.0 - Value 25.0 Pack 2 - Weight 4.0 - Value 6.0 Pack 3 - Weight 2.0 - Value 2.0 Max Value: 83.0
- 第二个示例
Pack 0 - Weight 4.0 - Value 10.0 Pack 0 - Weight 4.0 - Value 10.0 Pack 0 - Weight 4.0 - Value 10.0 Pack 1 - Weight 1.0 - Value 2.0 Pack 1 - Weight 1.0 - Value 2.0 Pack 1 - Weight 1.0 - Value 2.0 Max Value: 36.0
分析第一个示例
- 问题参数:n = 4;M = 37。
- 包裹:{i = 1; W[i] = 15; V[i] = 30; Cost = 2.0}; {i = 2; W[i] = 10; V[i] = 25; Cost = 2.5}; {i = 3; W[i] = 2; V[i] = 4; Cost = 1.0}; {i = 4; W[i] = 4; V[i] = 6; Cost = 1.5}。
- 按照单位成本值非递增的顺序对包裹进行排序。您有:{i = 2} **->** {i = 1} **->** {i = 4} **->** {i = 3}。
应用第一个示例算法的步骤
- 定义x1、x2、x3、x4为每个选定包裹的数量,分别对应包裹{i = 2} **->** {i = 1} **->** {i = 4} **->** {i = 3}。
- 节点根 N 代表未选择任何包裹的状态。然后
- 总价值 = 0。
- M = 37(如建议)。
- UpperBound = 37 * 2.5 = 92.5,其中37是M,2.5是包裹{i = 2}的单位成本。
- 对于包裹{i = 2},您有4种可能性:选择3个包裹{i = 2}(x1 = 3);选择2个包裹{i = 2}(x1 = 2);选择1个包裹{i = 2}(x1 = 1)和不选择包裹{i = 2}(x1 = 0)。根据这4种可能性,将根节点N分支到4个子节点N[1]、N[2]、N[3]和N[4]。
- 对于子节点N1,您有
- 总价值 = 0 + 3 * 25 = 75,其中3是选定的包裹{i = 2}的数量,25是每个包裹{i = 2}的价值。
- M = 37 – 3 * 10 = 7,其中37是背包的初始容量,3是包裹{i = 2}的数量,10是每个包裹{i = 2}的重量。
- UpperBound = 75 + 7 * 2 = 89,其中75是总价值,7是背包的剩余重量,2是包裹{i = 1}的单位成本。
- 类似地,您可以计算节点N[2]、N[3]和N[4]的参数,其中上限分别为84、79和74。
- 在节点N[1]、N[2]、N[3]和N[4]中,节点N[1]具有最大的上限,因此您将首先分支节点N[1],希望从这个方向获得一个好的计划。
- 从节点N[1]开始,您只有一个子节点N[1-1],对应于x2 = 0(因为背包剩余重量为7,而每个包裹{i = 1}的重量为15)。确定N[1-1]按钮的参数后,您发现N[1-1]的上限为85.5。
- 您继续分支节点N[1-1]。节点N[1-1]有两个子节点N[1-1-1]和N[1-1-2],分别对应于x3 = 1和x3 = 0。在确定了这两个节点的参数后,您看到N[1-1-1]的上限是84,而N[1-1-2]的上限是82,因此您继续分支节点N[1-1-1]。
- 节点N[1-1-1]有两个子节点N[1-1-1-1]和N[1-1-1-2],分别对应于x4 = 1和x4 = 0。这些是两个叶节点(代表选项),因为对于每个节点,已选择的包裹数量。其中节点N[1-1-1-1]代表选项x1 = 3,x2 = 0,x3 = 1和x4 = 1,值为83,而节点N[1-1-1-2]代表选项x1 = 3,x2 = 0,x3 = 1和x4 = 01,值为81。因此,这里的临时最大值为83。
- 回到节点N[1-1-2],您发现N[1-1-2]的上限为82 < 83,因此您修剪节点N[1-1-2]。
- 回到节点N2,您发现N2的上限为84 > 83,因此您继续分支节点N2。节点N2有两个子节点N[2-1]和N[2-2],分别对应于x2 = 1和x2 = 0。在计算了N[2-1]和N[2-2]的参数后,您发现N[2-1]的上限是83,而N[2-2]的上限是75.25。这两个值均不大于83,因此两个节点都被修剪。
- 最后,节点N3和N4也被修剪。
- 因此,树上的所有节点都已分支或被修剪,因此找到最有效的临时解决方案。因此,您需要选择3个包裹{i = 2}、1个包裹{i = 4}和1个包裹{i = 3},总价值为83,总重量为36。
通过对第二个示例进行相同的分析,您得到了结果:选择包裹4(3次)和包裹5(3次)。
贪心三号的Python3代码
- 首先,定义类 KnapsackPackage。
class KnapsackPackage(object):
""" Knapsack Package Data Class """
def __init__(self, weight, value):
self.weight = weight
self.value = value
self.cost = value / weight
def __lt__(self, other):
return self.cost < other.cost
- 然后创建一个函数来执行算法贪心三号。
class FractionalKnapsack(object):
def __init__(self):
def knapsackGreProc(self, W, V, M, n):
packs = []
for i in range(n):
packs.append(KnapsackPackage(W[i], V[i]))
packs.sort(reverse = True)
remain = M
result = 0
i = 0
stopProc = False
while (stopProc != True):
if (packs[i].weight <= remain):
remain -= packs[i].weight;
result += packs[i].value;
print("Pack ", i, " - Weight ", packs[i].weight, " - Value ", packs[i].value)
if (packs[i].weight > remain):
i += 1
if (i == n):
stopProc = True
print("Max Value:\t", result)

代码解释
- 为每个背包包裹初始化重量和价值。
- 按成本降序对背包包裹进行排序。
- 如果选择包裹i。
- 如果选择的包裹i数量足够。
- 浏览所有包裹时停止。
这是使用第一个示例运行上述程序的 Python3 代码
if __name__ == "__main__":
W = [15, 10, 2, 4]
V = [30, 25, 2, 6]
M = 37
n = 4
proc = FractionalKnapsack()
proc.knapsackGreProc(W, V, M, n)
输出结果
Pack 0 - Weight 10 - Value 25 Pack 0 - Weight 10 - Value 25 Pack 0 - Weight 10 - Value 25 Pack 2 - Weight 4 - Value 6 Pack 3 - Weight 2 - Value 2 Max Value: 83
贪心三号的C#代码
- 首先,定义类 KnapsackPackage。
using System;
namespace KnapsackProblem
{
public class KnapsackPackage
{
private double weight;
private double value;
private double cost;
public KnapsackPackage(double weight, double value)
{
this.weight = weight;
this.value = value;
this.cost = (double) value / weight;
}
public double Weight
{
get { return weight; }
}
public double Value
{
get { return value; }
}
public double Cost
{
get { return cost; }
}
}
}
- 然后创建一个函数来执行算法贪心三号。
using System;
namespace KnapsackProblem
{
public class FractionalKnapsack
{
public FractionalKnapsack()
{
}
public void KnapsackGreProc(int[] W, int[] V, int M, int n)
{
KnapsackPackage[] packs = new KnapsackPackage[n];
for (int k = 0; k < n; k++)
packs[k] = new KnapsackPackage(W[k], V[k]);
Array.Sort<KnapsackPackage>(packs, new Comparison<KnapsackPackage>(
(kPackA, kPackB) => kPackB.Cost.CompareTo(kPackA.Cost)));
double remain = M;
double result = 0d;
int i = 0;
bool stopProc = false;
while (!stopProc)
{
if (packs[i].Weight <= remain)
{
remain -= packs[i].Weight;
result += packs[i].Value;
Console.WriteLine("Pack " + i + " - Weight " + packs[i].Weight + " - Value " + packs[i].Value);
}
if (packs[i].Weight > remain)
{
i++;
}
if (i == n)
{
stopProc = true;
}
}
Console.WriteLine("Max Value:\t" + result);
}
}
}

代码解释
- 为每个背包包裹初始化重量和价值。
- 按成本降序对背包包裹进行排序。
- 如果选择包裹i。
- 如果选择的包裹i数量足够。
- 浏览所有包裹时停止。
这是使用第一个示例运行上述程序的 C# 代码
public void run()
{
/*
* Pack and Weight - Value
*/
int[] W = new int[]{15, 10, 2, 4};
//int[] W = new int[] { 12, 2, 1, 1, 4 };
int[] V = new int[]{30, 25, 2, 6};
//int[] V = new int[] { 4, 2, 1, 2, 10 };
/*
* Max Weight
*/
int M = 37;
//int M = 15;
int n = V.Length;
/*
* Run the algorithm
*/
KnapsackGreProc(W, V, M, n);
}
输出结果
Pack 0 - Weight 10 - Value 25 Pack 0 - Weight 10 - Value 25 Pack 0 - Weight 10 - Value 25 Pack 2 - Weight 4 - Value 6 Pack 3 - Weight 2 - Value 2 Max Value: 83
贪心三号的反例
贪心三号的算法在某些情况下可以快速解决并且是最优的。但是,在某些特殊情况下,它不能给出最优解。这里有一个反例
- 问题参数:n = 3;M = 10。
- 包裹:{i = 1; W[i] = 7; V[i] = 9; Cost = 9/7}; {i = 2; W[i] = 6; V[i] = 6; Cost = 1}; {i = 3; W[i] = 4; V[i] = 4; Cost = 1}。
- 算法将选择(包裹1),总价值为9,而问题的最优解是(包裹2,包裹3),总价值为10。
这是使用反例运行上述程序的 Java 代码
public void run() {
/*
* Pack and Weight - Value
*/
int W[] = new int[]{7, 6, 4};
int V[] = new int[]{9, 6, 4};
/*
* Max Weight
*/
int M = 10;
int n = V.length;
/*
* Run the algorithm
*/
knapsackGreProc(W, V, M, n);
}
结果如下
Pack 0 - Weight 7.0 - Value 9.0 Max Value: 9.0
这就是分数背包问题。





