本文参考Google OR-Tools官网文档介绍OR-Tools的使用方法。
装箱问题的描述是要将一组给定尺寸的物品放置到具有固定容量的容器中,一般情况下由于容器有容量限制,不可能放入所有物品,那么装箱问题的目标是要找到限定约束下使得总价值最大或总花费最小的装箱方法。根据我们具体的目标情况,装箱问题又可分为两类:
OR-Tools针对典型的单背包问题提供了专用的接口,对于更一般的多背包问题和Bin-packing问题则需要使用通用的整数规划接口来计算。这篇文章里我就参考官方教程演示用OR-Tools对单背包问题、多背包问题和Bin-Packing问题进行建模。
如果背包问题中只有一个背包,那么就是单背包问题,一般也成为0-1背包问题,因为它可以用下面的方程式来描述问题:
用0-1变量 x i x_i xi表示第 i i i个物件是否放入背包, v i v_i vi表示物件 i i i的价值, w i w_i wi则表示物件的重量, W W W则是背包的最大承重,那么整个问题可表示为:
m a x i m i z e ∑ i = 1 n v i x i s u b j e c t t o ∑ i = 1 n w i x i ≤ W x i ∈ { 0 , 1 } \begin{aligned} maximize\quad &\sum_{i=1}^n v_ix_i\\ subject\ to\quad& \sum_{i=1}^nw_ix_i\leq W \\ & x_i \in \{0,1\} \end{aligned} maximizesubject toi=1∑nvixii=1∑nwixi≤Wxi∈{0,1}
当然实际生活中可能限制背包存放物体数量的不仅是重量,还有体积等属性,因此上面的定义更严格的来说应该是单维单背包问题,如果有多维限定,那么在每一维存放物体的总量都不能超过背包的限定值。
我们新建一个.NET Core控制台应用,自定义一个单背包问题的数据:
//values[i], the value of item i
long[] values = { 360, 83, 59, 130, 431, 67, 230, 52, 93,
125, 670, 892, 600, 38, 48, 147, 78, 256,
63, 17, 120, 164, 432, 35, 92, 110, 22,
42, 50, 323, 514, 28, 87, 73, 78, 15,
26, 78, 210, 36, 85, 189, 274, 43, 33,
10, 19, 389, 276, 312 };
//weights[i,j], the weight of weights[i][j]
long[,] weights = { { 7, 0, 30, 22, 80, 94, 11, 81, 70,
64, 59, 18, 0, 36, 3, 8, 15, 42,
9, 0, 42, 47, 52, 32, 26, 48, 55,
6, 29, 84, 2, 4, 18, 56, 7, 29,
93, 44, 71, 3, 86, 66, 31, 65, 0,
79, 20, 65, 52, 13 } };
long[] capacities = { 850 };
OR-Tools提供了一个KnapsackSolver来专门处理单背包问题,我们定义这个KnapsackSolver对象,并且指定使用分支界定算法
KnapsackSolver solver = new KnapsackSolver(
KnapsackSolver.SolverType.KNAPSACK_MULTIDIMENSION_BRANCH_AND_BOUND_SOLVER,
"KnapsackExample");
然后就可以初始化KnapsackSolver对象并求解了
solver.Init(values, weights, capacities);
long computedValue = solver.Solve();
Solve()方法的返回值就是算法的最终目标值,而如果要知道那些物件被放置,则需要调用BestSolutionContains()方法查看。
Console.WriteLine("Optimal Value = " + computedValue);
string selectItems = $"Selected item indexs : ";
for (int i = 0; i < values.Length; i++)
{
if (solver.BestSolutionContains(i))
{
selectItems += $"{i}({values[i]}), ";
}
}
Console.WriteLine(selectItems);
完整的程序
using System;
using Google.OrTools.Algorithms;
namespace SingleKnapsackProblem
{
class Program
{
static void Main(string[] args)
{
//Create a knapsack solver, use Branch And Bound algorithm
KnapsackSolver solver = new KnapsackSolver(
KnapsackSolver.SolverType.KNAPSACK_MULTIDIMENSION_BRANCH_AND_BOUND_SOLVER,
"KnapsackExample");
//values[i], the value of item i
long[] values = { 360, 83, 59, 130, 431, 67, 230, 52, 93,
125, 670, 892, 600, 38, 48, 147, 78, 256,
63, 17, 120, 164, 432, 35, 92, 110, 22,
42, 50, 323, 514, 28, 87, 73, 78, 15,
26, 78, 210, 36, 85, 189, 274, 43, 33,
10, 19, 389, 276, 312 };
//weights[i,j], the weight of weights[i][j]
long[,] weights = { { 7, 0, 30, 22, 80, 94, 11, 81, 70,
64, 59, 18, 0, 36, 3, 8, 15, 42,
9, 0, 42, 47, 52, 32, 26, 48, 55,
6, 29, 84, 2, 4, 18, 56, 7, 29,
93, 44, 71, 3, 86, 66, 31, 65, 0,
79, 20, 65, 52, 13 } };
long[] capacities = { 850 };
solver.Init(values, weights, capacities);
long computedValue = solver.Solve();
Console.WriteLine("Optimal Value = " + computedValue);
string selectItems = $"Selected item indexs : ";
for (int i = 0; i < values.Length; i++)
{
if (solver.BestSolutionContains(i))
{
selectItems += $"{i}({values[i]}), ";
}
}
Console.WriteLine(selectItems);
}
}
}
把单个背包扩展到多个背包,就是多背包问题,对于多背包问题,我们可以采用下面的定义方式:
给出 n n n个物件和 m m m个背包( m ≤ n m\leq n m≤n),并且按如下定义(仍然只是单维):
v j : 物 件 j 的 价 值 w j : 物 件 j 的 重 量 c i : 第 i 个 背 包 的 容 量 \begin{aligned} &v_j : 物件j的价值\\ &w_j : 物件j的重量\\ &c_i : 第i个背包的容量\\ \end{aligned} vj:物件j的价值wj:物件j的重量ci:第i个背包的容量
要让每个背包不超过容量限制的情况下最大化放置的物件的价值总量,即:
m a x i m i z e ∑ i = 1 m ∑ j = 1 n v j x i j s u b j e c t t o ∑ j = 1 n w j x i j ≤ c i , i ∈ M = { 1 , . . . m } ∑ i = 1 m x i j ≤ 1 , j ∈ N = { 1 , . . . n } x i j ∈ { 0 , 1 } \begin{aligned} maximize\quad &\sum_{i=1}^m\sum_{j=1}^n v_jx_{ij}\\ subject\ to\quad& \sum_{j=1}^nw_jx_{ij}\leq c_i, i\in M=\{1,...m\} \\ & \sum_{i=1}^mx_{ij}\leq 1, j\in N=\{1,...n\} \\ & x_{ij} \in \{0,1\} \end{aligned} maximizesubject toi=1∑mj=1∑nvjxijj=1∑nwjxij≤ci,i∈M={1,...m}i=1∑mxij≤1,j∈N={1,...n}xij∈{0,1}
其中 x i j x_{ij} xij是0-1变量,表示物件 j j j是否分配到背包 i i i中。
对于多背包问题,OR-Tools没有提供直接的接口,不过从它的建模方程来看,这是一很典型的整数规划问题,因此我们可以例用OR-Tools的整数规划接口来建模和计算。
我们先自定义一个问题的数据源,假设有15个物件和5个背包,每个背包的最大承重都是100
class DataModel
{
public double[] Weights =
{48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36};
public double[] Values =
{10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25};
public double[] BinCapacities = { 100, 100, 100, 100, 100 };
private int numItems;
public int NumItems
{
get
{
numItems = Weights.Length;
return numItems;
}
set
{
numItems = value;
}
}
public int NumBins = 5;
}
新建一个整数规划求解对象,这里指定内部连接CBC求解器
// Create the linear solver with the CBC backend.
Solver solver = Solver.CreateSolver("SimpleMipProgram", "CBC_MIXED_INTEGER_PROGRAMMING");
然后定义求解变量,即 i × j i\times j i×j个0-1变量
// Create variables, x[i][j]=1 means item i is packed in bin j
Variable[,] x = new Variable[data.NumItems,data.NumBins];
for (int i = 0; i < data.NumItems; i++)
{
for (int j = 0; j < data.NumBins; j++)
{
x[i,j] = solver.MakeIntVar(0, 1, String.Format("x_{0}_{1}", i, j));
}
}
接着是两个约束,约束一是对一个物件,最多只会被分配在一个背包;约束二是每个背包内的物件的重量和不会超过限制
//Item i can't be packed in more than one bins
for (int i = 0; i < data.NumItems; ++i)
{
LinearExpr sum=new LinearExpr();
for (int j = 0; j < data.NumBins; ++j)
{
sum += x[i,j];
}
solver.Add(sum <= 1.0);
}
//The amount packed in each bin cannot exceed its capacity
for (int j = 0; j < data.NumBins; ++j)
{
LinearExpr Weight=new LinearExpr();
for (int i = 0; i < data.NumItems; ++i)
{
Weight += data.Weights[i] * x[i,j];
}
solver.Add(Weight <= data.BinCapacities[j]);
}
最后就可以定义目标并求解了
//Objective
LinearExpr totalValue=new LinearExpr();
for (int i = 0; i < data.NumItems; ++i)
{
for (int j = 0; j < data.NumBins; ++j)
{
totalValue += data.Values[i] * x[i,j];
}
}
solver.Maximize(totalValue);
//Solve it
Solver.ResultStatus resultStatus = solver.Solve();
完整的代码
using System;
using Google.OrTools.LinearSolver;
namespace MultipleKnapsackProblem
{
class Program
{
class DataModel
{
public double[] Weights =
{48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36};
public double[] Values =
{10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25};
public double[] BinCapacities = { 100, 100, 100, 100, 100 };
private int numItems;
public int NumItems
{
get
{
numItems = Weights.Length;
return numItems;
}
set
{
numItems = value;
}
}
public int NumBins = 5;
}
static void Main(string[] args)
{
DataModel data = new DataModel();
// Create the linear solver with the CBC backend.
Solver solver = Solver.CreateSolver("SimpleMipProgram", "CBC_MIXED_INTEGER_PROGRAMMING");
// Create variables, x[i][j]=1 means item i is packed in bin j
Variable[,] x = new Variable[data.NumItems,data.NumBins];
for (int i = 0; i < data.NumItems; i++)
{
for (int j = 0; j < data.NumBins; j++)
{
x[i,j] = solver.MakeIntVar(0, 1, String.Format("x_{0}_{1}", i, j));
}
}
//Item i can't be packed in more than one bins
for (int i = 0; i < data.NumItems; ++i)
{
LinearExpr sum=new LinearExpr();
for (int j = 0; j < data.NumBins; ++j)
{
sum += x[i,j];
}
solver.Add(sum <= 1.0);
}
//The amount packed in each bin cannot exceed its capacity
for (int j = 0; j < data.NumBins; ++j)
{
LinearExpr Weight=new LinearExpr();
for (int i = 0; i < data.NumItems; ++i)
{
Weight += data.Weights[i] * x[i,j];
}
solver.Add(Weight <= data.BinCapacities[j]);
}
//Objective
LinearExpr totalValue=new LinearExpr();
for (int i = 0; i < data.NumItems; ++i)
{
for (int j = 0; j < data.NumBins; ++j)
{
totalValue += data.Values[i] * x[i,j];
}
}
solver.Maximize(totalValue);
//Solve it
Solver.ResultStatus resultStatus = solver.Solve();
// Check that the problem has an optimal solution.
if (resultStatus != Solver.ResultStatus.OPTIMAL)
{
Console.WriteLine("The problem does not have an optimal solution!");
return;
}
Console.WriteLine("Total packed value: " + solver.Objective().Value());
double TotalWeight = 0;
for (int j = 0; j < data.NumBins; ++j)
{
double BinWeight = 0;
double BinValue = 0;
Console.WriteLine("Bin " + j);
for (int i = 0; i < data.NumItems; ++i)
{
if (x[i,j].SolutionValue() == 1)
{
Console.WriteLine("Item " + i + " weight: " + data.Weights[i]
+ " values: " + data.Values[i]);
BinWeight += data.Weights[i];
BinValue += data.Values[i];
}
}
Console.WriteLine("Packed bin weight: " + BinWeight);
Console.WriteLine("Packed bin value: " + BinValue);
TotalWeight += BinWeight;
}
Console.WriteLine("Total packed weight: " + TotalWeight);
}
}
}
和背包问题不同,Bin-Packing问题是站在容器优化的角度来定义的,我们不再考虑放置物件的价值,而是希望用最少的容器装下所有的物件。Bin-Packing问题相比背包问题更加一般化,生活中物流等领域也常会遇到这种问题,比如用最少的运输车辆递送快递以最小化成本。Bin-Packing问题也可以用整数规划模型来表示:
给出足够多的箱子(Bin) S 1 S_1 S1, S 2 S_2 S2, … , S m , S_m, Sm,每个箱子都有相同的容量 C C C; n n n个物件需要装箱,每个物件有各自的权重 w 1 w_1 w1, w 2 w_2 w2,…, w n w_n wn;我们希望用最少的箱子把所有物件打包:
m i n i m i z e B = ∑ i = 1 n y i s u b j e c t t o ∑ j = 1 n w j x i j ≤ C y i , i ∈ M = { 1 , . . . m } ∑ i = 1 m x i j = 1 , j ∈ N = { 1 , . . . n } x i j ∈ { 0 , 1 } y i ∈ { 0 , 1 } \begin{aligned} minimize\quad& B=\sum_{i=1}^ny_i\\ subject\ to\quad& \sum_{j=1}^nw_jx_{ij}\leq Cy_i, i\in M=\{1,...m\} \\ & \sum_{i=1}^mx_{ij}= 1, j\in N=\{1,...n\} \\ & x_{ij} \in \{0,1\} \\ & y_{i}\in \{0,1\} \end{aligned} minimizesubject toB=i=1∑nyij=1∑nwjxij≤Cyi,i∈M={1,...m}i=1∑mxij=1,j∈N={1,...n}xij∈{0,1}yi∈{0,1}
其中 y i y_i yi表示箱子 i i i是否用到, x i j x_{ij} xij表示物件 j j j是否装到箱子 i i i。
我们使用OR-Tools的整数规划求解接口来处理这个问题。首先自定义数据,这里我用一个类封装问题数据,numItems表示需要打包的物件数量,binCapacity表示每个箱子的容量,numBins表示可供使用的箱子数量,如果置空则就用物件的数量代替;每个物件的权重随机设置为小于容量的值。
class DataModel
{
public DataModel(int numItems, double binCapacity, int? numBins=null)
{
this.numItems = numItems;
this.binCapacity = binCapacity;
if (numBins.HasValue)
{
this.numBins = numBins.Value;
}
else
{
this.numBins = numItems;
}
weights = new double[numItems];
for(int i=0;i<numItems;i++)
{
weights[i] = new Random(Guid.NewGuid().GetHashCode()).Next(1, Convert.ToInt32(binCapacity));
}
}
private double[] weights;
public double[] Weights
{
get
{
return weights;
}
set
{
weights = value;
}
}
private double binCapacity;
public double BinCapacity
{
get
{
return binCapacity;
}
set
{
binCapacity = value;
}
}
private int numItems;
public int NumItems
{
get
{
return numItems;
}
set
{
numItems = value;
}
}
private int numBins;
public int NumBins
{
get
{
return numBins;
}
set
{
numBins = value;
}
}
}
创建CBC整数规划求解器对象,定义求解目标
// Create the linear solver with the CBC backend.
GoogleLinearSolver.Solver solver = GoogleLinearSolver.Solver.CreateSolver("SimpleMipProgram", "CBC_MIXED_INTEGER_PROGRAMMING");
// Create variables, x[i][j]=1 means item i is packed in bin j
GoogleLinearSolver.Variable[,] x = new GoogleLinearSolver.Variable[data.NumItems, data.NumBins];
for (int i = 0; i < data.NumItems; i++)
{
for (int j = 0; j < data.NumBins; j++)
{
x[i, j] = solver.MakeIntVar(0, 1, String.Format("x_{0}_{1}", i, j));
}
}
//y[j]=1 means bin j is used
GoogleLinearSolver.Variable[] y = new GoogleLinearSolver.Variable[data.NumBins];
for (int j = 0; j < data.NumBins; j++)
{
y[j] = solver.MakeIntVar(0, 1, string.Format("y_{0}", j));
}
定义约束。约束一,每个物件必须被分配到某一个箱子里;约束二,每个箱子里分配的物件不能超出容量
//Each item must be in exactly one bin.
for (int i = 0; i < data.NumItems; ++i)
{
GoogleLinearSolver.LinearExpr sum = new GoogleLinearSolver.LinearExpr();
for (int j = 0; j < data.NumBins; ++j)
{
sum += x[i, j];
}
solver.Add(sum == 1.0);
}
//The amount packed in each bin cannot exceed its capacity
for (int j = 0; j < data.NumBins; ++j)
{
GoogleLinearSolver.LinearExpr Weight = new GoogleLinearSolver.LinearExpr();
for (int i = 0; i < data.NumItems; ++i)
{
Weight += data.Weights[i] * x[i, j];
}
solver.Add(Weight <= data.BinCapacity * y[j]);
}
最后定义目标并求解
//Objective
GoogleLinearSolver.LinearExpr numBinsUsed = new GoogleLinearSolver.LinearExpr();
for (int j = 0; j < data.NumItems; ++j)
{
numBinsUsed += y[j];
}
solver.Minimize(numBinsUsed);
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
GoogleLinearSolver.Solver.ResultStatus resultStatus = solver.Solve();
stopwatch.Stop();
Console.WriteLine($"Calculate time: {stopwatch.ElapsedMilliseconds / 1000} s , {stopwatch.ElapsedMilliseconds} ms");
// Check that the problem has an optimal solution.
if (resultStatus != GoogleLinearSolver.Solver.ResultStatus.OPTIMAL)
{
Console.WriteLine("The problem does not have an optimal solution!");
return;
}
Console.WriteLine("Number of bins used: " + solver.Objective().Value());
double TotalWeight = 0;
for (int j = 0; j < data.NumBins; ++j)
{
double BinWeight = 0;
if (y[j].SolutionValue() == 1)
{
Console.WriteLine("Bin " + j);
for (int i = 0; i < data.NumItems; ++i)
{
if (x[i, j].SolutionValue() == 1)
{
Console.WriteLine("Item " + i + " weight: " + data.Weights[i]);
BinWeight += data.Weights[i];
}
}
Console.WriteLine("Packed bin weight: " + BinWeight);
TotalWeight += BinWeight;
}
Console.WriteLine("Total packed weight: " + TotalWeight);
}
另一方面,我们也可以把Bin Packing问题看成一个约束满足模型,求解变量和约束关系和上面的整数规划表达式里的一致,我们可以用OR-Tools的SAP求解器来计算这个问题。因为使用的算法存在本质区别,我们可以检验一下这两种求解方式的效率差异
下面是完整的程序代码,SolveItWithMixedIntegerSolver方法是就是上面用整数规划求解接口计算的方式,而SolveItWithSATSolver则是用SAP求解接口的方式。
using System;
using System.Collections.Generic;
using GoogleLinearSolver=Google.OrTools.LinearSolver;
using GoogleSat = Google.OrTools.Sat;
namespace BinPackingProblem
{
class Program
{
class DataModel
{
public DataModel(int numItems, double binCapacity, int? numBins=null)
{
this.numItems = numItems;
this.binCapacity = binCapacity;
if (numBins.HasValue)
{
this.numBins = numBins.Value;
}
else
{
this.numBins = numItems;
}
weights = new double[numItems];
for(int i=0;i<numItems;i++)
{
weights[i] = new Random(Guid.NewGuid().GetHashCode()).Next(1, Convert.ToInt32(binCapacity));
}
}
private double[] weights;
public double[] Weights
{
get
{
return weights;
}
set
{
weights = value;
}
}
//public double[] Weights = { 48, 30, 19, 36, 36, 27, 42, 42, 36, 24, 30 };
private double binCapacity;
public double BinCapacity
{
get
{
return binCapacity;
}
set
{
binCapacity = value;
}
}
private int numItems;
public int NumItems
{
get
{
return numItems;
}
set
{
numItems = value;
}
}
private int numBins;
public int NumBins
{
get
{
return numBins;
}
set
{
numBins = value;
}
}
}
static void Main(string[] args)
{
DataModel data = new DataModel(10,100,10);
Console.WriteLine("###################Use Mixed Integer Solver##########################");
SolveItWithMixedIntegerSolver(data);
Console.WriteLine("###################Use SAT Solver##########################");
SolveItWithSATSolver(data);
}
static void SolveItWithMixedIntegerSolver(DataModel data)
{
// Create the linear solver with the CBC backend.
GoogleLinearSolver.Solver solver = GoogleLinearSolver.Solver.CreateSolver("SimpleMipProgram", "CBC_MIXED_INTEGER_PROGRAMMING");
// Create variables, x[i][j]=1 means item i is packed in bin j
GoogleLinearSolver.Variable[,] x = new GoogleLinearSolver.Variable[data.NumItems, data.NumBins];
for (int i = 0; i < data.NumItems; i++)
{
for (int j = 0; j < data.NumBins; j++)
{
x[i, j] = solver.MakeIntVar(0, 1, String.Format("x_{0}_{1}", i, j));
}
}
//y[j]=1 means bin j is used
GoogleLinearSolver.Variable[] y = new GoogleLinearSolver.Variable[data.NumBins];
for (int j = 0; j < data.NumBins; j++)
{
y[j] = solver.MakeIntVar(0, 1, string.Format("y_{0}", j));
}
//Each item must be in exactly one bin.
for (int i = 0; i < data.NumItems; ++i)
{
GoogleLinearSolver.LinearExpr sum = new GoogleLinearSolver.LinearExpr();
for (int j = 0; j < data.NumBins; ++j)
{
sum += x[i, j];
}
solver.Add(sum == 1.0);
}
//The amount packed in each bin cannot exceed its capacity
for (int j = 0; j < data.NumBins; ++j)
{
GoogleLinearSolver.LinearExpr Weight = new GoogleLinearSolver.LinearExpr();
for (int i = 0; i < data.NumItems; ++i)
{
Weight += data.Weights[i] * x[i, j];
}
solver.Add(Weight <= data.BinCapacity * y[j]);
}
//Objective
GoogleLinearSolver.LinearExpr numBinsUsed = new GoogleLinearSolver.LinearExpr();
for (int j = 0; j < data.NumBins; ++j)
{
numBinsUsed += y[j];
}
solver.Minimize(numBinsUsed);
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
GoogleLinearSolver.Solver.ResultStatus resultStatus = solver.Solve();
stopwatch.Stop();
Console.WriteLine($"Calculate time: {stopwatch.ElapsedMilliseconds / 1000} s , {stopwatch.ElapsedMilliseconds} ms");
// Check that the problem has an optimal solution.
if (resultStatus != GoogleLinearSolver.Solver.ResultStatus.OPTIMAL)
{
Console.WriteLine("The problem does not have an optimal solution!");
return;
}
Console.WriteLine("Number of bins used: " + solver.Objective().Value());
double TotalWeight = 0;
for (int j = 0; j < data.NumBins; ++j)
{
double BinWeight = 0;
if (y[j].SolutionValue() == 1)
{
Console.WriteLine("Bin " + j);
for (int i = 0; i < data.NumItems; ++i)
{
if (x[i, j].SolutionValue() == 1)
{
Console.WriteLine("Item " + i + " weight: " + data.Weights[i]);
BinWeight += data.Weights[i];
}
}
Console.WriteLine("Packed bin weight: " + BinWeight);
TotalWeight += BinWeight;
}
Console.WriteLine("Total packed weight: " + TotalWeight);
}
}
static void SolveItWithSATSolver(DataModel data)
{
GoogleSat.CpModel cpModel = new GoogleSat.CpModel();
GoogleSat.IntVar[,] x = new GoogleSat.IntVar[data.NumItems, data.NumBins];
// Create variables, x[i][j]=1 means item i is packed in bin j
for (int i = 0; i < data.NumItems; i++)
{
for (int j = 0; j < data.NumBins; j++)
{
x[i, j] = cpModel.NewBoolVar(String.Format("x_{0}_{1}", i, j));
}
}
//y[j]=1 means bin j is used
GoogleSat.IntVar[] y = new GoogleSat.IntVar[data.NumBins];
for (int j = 0; j < data.NumBins; j++)
{
y[j] = cpModel.NewBoolVar(string.Format("y_{0}", j));
}
//Each item must be in exactly one bin.
for (int i = 0; i < data.NumItems; ++i)
{
List<GoogleSat.IntVar> items = new List<GoogleSat.IntVar>();
for (int j = 0; j < data.NumBins; ++j)
{
items.Add(x[i, j]);
}
cpModel.Add(GoogleSat.LinearExpr.Sum(items) == 1);
}
//The amount packed in each bin cannot exceed its capacity
for (int j = 0; j < data.NumBins; ++j)
{
List<GoogleSat.IntVar> itemWeights = new List<GoogleSat.IntVar>();
for (int i = 0; i < data.NumItems; ++i)
{
var currentWeight = cpModel.NewIntVar(0, Convert.ToInt32(data.Weights[i]), "");
cpModel.Add(currentWeight == Convert.ToInt32(data.Weights[i]) * x[i, j]);
itemWeights.Add(currentWeight);
}
var currentMaxCapacity = cpModel.NewIntVar(0, Convert.ToInt32(data.BinCapacity), "");
cpModel.Add(currentMaxCapacity == Convert.ToInt32(data.BinCapacity) * y[j]);
cpModel.Add(GoogleSat.LinearExpr.Sum(itemWeights) <= currentMaxCapacity);
}
//Objective
List<GoogleSat.IntVar> numBinsUsed = new List<GoogleSat.IntVar>();
for (int j = 0; j < data.NumBins; ++j)
{
numBinsUsed.Add(y[j]);
}
cpModel.Minimize(GoogleSat.LinearExpr.Sum(numBinsUsed));
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
var solver = new GoogleSat.CpSolver();
var resultStatus = solver.Solve(cpModel);
stopwatch.Stop();
Console.WriteLine($"Calculate time: {stopwatch.ElapsedMilliseconds / 1000} s , {stopwatch.ElapsedMilliseconds} ms");
// Check that the problem has an optimal solution.
if (resultStatus != GoogleSat.CpSolverStatus.Optimal || resultStatus == GoogleSat.CpSolverStatus.Feasible)
{
Console.WriteLine("The problem does not have an optimal solution!");
return;
}
Console.WriteLine("Number of bins used: " + solver.ObjectiveValue);
double TotalWeight = 0;
for (int j = 0; j < data.NumBins; ++j)
{
double BinWeight = 0;
if (solver.Value(y[j]) == 1)
{
Console.WriteLine("Bin " + j);
for (int i = 0; i < data.NumItems; ++i)
{
if (solver.Value(x[i, j]) == 1)
{
Console.WriteLine("Item " + i + " weight: " + data.Weights[i]);
BinWeight += data.Weights[i];
}
}
Console.WriteLine("Packed bin weight: " + BinWeight);
TotalWeight += BinWeight;
}
Console.WriteLine("Total packed weight: " + TotalWeight);
}
}
}
}
for (int i = 0; i < data.NumItems; ++i)
{
Weight += data.Weights[i] * x[i, j];
}
solver.Add(Weight <= data.BinCapacity * y[j]);
}
//Objective
GoogleLinearSolver.LinearExpr numBinsUsed = new GoogleLinearSolver.LinearExpr();
for (int j = 0; j < data.NumItems; ++j)
{
numBinsUsed += y[j];
}
solver.Minimize(numBinsUsed);
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
GoogleLinearSolver.Solver.ResultStatus resultStatus = solver.Solve();
stopwatch.Stop();
Console.WriteLine($"Calculate time: {stopwatch.ElapsedMilliseconds / 1000} s , {stopwatch.ElapsedMilliseconds} ms");
// Check that the problem has an optimal solution.
if (resultStatus != GoogleLinearSolver.Solver.ResultStatus.OPTIMAL)
{
Console.WriteLine("The problem does not have an optimal solution!");
return;
}
Console.WriteLine("Number of bins used: " + solver.Objective().Value());
double TotalWeight = 0;
for (int j = 0; j < data.NumBins; ++j)
{
double BinWeight = 0;
if (y[j].SolutionValue() == 1)
{
Console.WriteLine("Bin " + j);
for (int i = 0; i < data.NumItems; ++i)
{
if (x[i, j].SolutionValue() == 1)
{
Console.WriteLine("Item " + i + " weight: " + data.Weights[i]);
BinWeight += data.Weights[i];
}
}
Console.WriteLine("Packed bin weight: " + BinWeight);
TotalWeight += BinWeight;
}
Console.WriteLine("Total packed weight: " + TotalWeight);
}
}
static void SolveItWithSATSolver(DataModel data)
{
GoogleSat.CpModel cpModel = new GoogleSat.CpModel();
GoogleSat.IntVar[,] x = new GoogleSat.IntVar[data.NumItems, data.NumBins];
// Create variables, x[i][j]=1 means item i is packed in bin j
for (int i = 0; i < data.NumItems; i++)
{
for (int j = 0; j < data.NumBins; j++)
{
x[i, j] = cpModel.NewBoolVar(String.Format("x_{0}_{1}", i, j));
}
}
//y[j]=1 means bin j is used
GoogleSat.IntVar[] y = new GoogleSat.IntVar[data.NumBins];
for (int j = 0; j < data.NumBins; j++)
{
y[j] = cpModel.NewBoolVar(string.Format("y_{0}", j));
}
//Each item must be in exactly one bin.
for (int i = 0; i < data.NumItems; ++i)
{
List<GoogleSat.IntVar> items = new List<GoogleSat.IntVar>();
for (int j = 0; j < data.NumBins; ++j)
{
items.Add(x[i, j]);
}
cpModel.Add(GoogleSat.LinearExpr.Sum(items) == 1);
}
//The amount packed in each bin cannot exceed its capacity
for (int j = 0; j < data.NumBins; ++j)
{
List<GoogleSat.IntVar> itemWeights = new List<GoogleSat.IntVar>();
for (int i = 0; i < data.NumItems; ++i)
{
var currentWeight = cpModel.NewIntVar(0, Convert.ToInt32(data.Weights[i]), "");
cpModel.Add(currentWeight == Convert.ToInt32(data.Weights[i]) * x[i, j]);
itemWeights.Add(currentWeight);
}
var currentMaxCapacity = cpModel.NewIntVar(0, Convert.ToInt32(data.BinCapacity), "");
cpModel.Add(currentMaxCapacity == Convert.ToInt32(data.BinCapacity) * y[j]);
cpModel.Add(GoogleSat.LinearExpr.Sum(itemWeights) <= currentMaxCapacity);
}
//Objective
List<GoogleSat.IntVar> numBinsUsed = new List<GoogleSat.IntVar>();
for (int j = 0; j < data.NumItems; ++j)
{
numBinsUsed.Add(y[j]);
}
cpModel.Minimize(GoogleSat.LinearExpr.Sum(numBinsUsed));
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
var solver = new GoogleSat.CpSolver();
var resultStatus = solver.Solve(cpModel);
stopwatch.Stop();
Console.WriteLine($"Calculate time: {stopwatch.ElapsedMilliseconds / 1000} s , {stopwatch.ElapsedMilliseconds} ms");
// Check that the problem has an optimal solution.
if (resultStatus != GoogleSat.CpSolverStatus.Optimal || resultStatus == GoogleSat.CpSolverStatus.Feasible)
{
Console.WriteLine("The problem does not have an optimal solution!");
return;
}
Console.WriteLine("Number of bins used: " + solver.ObjectiveValue);
double TotalWeight = 0;
for (int j = 0; j < data.NumBins; ++j)
{
double BinWeight = 0;
if (solver.Value(y[j]) == 1)
{
Console.WriteLine("Bin " + j);
for (int i = 0; i < data.NumItems; ++i)
{
if (solver.Value(x[i, j]) == 1)
{
Console.WriteLine("Item " + i + " weight: " + data.Weights[i]);
BinWeight += data.Weights[i];
}
}
Console.WriteLine("Packed bin weight: " + BinWeight);
TotalWeight += BinWeight;
}
Console.WriteLine("Total packed weight: " + TotalWeight);
}
}
}
}
当物件数量为10时,两种求解方式的耗时差不多,分别为24ms和127ms
当把物件数量设为15时,用整数规划求解器耗时为100ms,而用SAT的耗时则加大到了4秒
可见对于标准的Bin Packing问题,整数规划模型和整数规划求解算法(例如分支界定)是较为合适的。这也符合求解优化问题的一般化思路,尽量往线性规划模型的方向建立模型,除非是无法用统一的表达式表述的约束满足或组合优化问题。