列生成算法(Column generation)
列生成广泛应用于大规模整数规划问题的求解中,尤其在装箱和切割问题、并行机调度问题、车辆路线问题的相关研究中。另外列生成算法也被用于乘务员调度、通信网络中的信道分配、多商品流等问题的研究中。(Gilmore and Gomory,1961年)首次将列生成技术用于求解一维切割问题(Cutting Stock Problem);(Desrosiers et al,1984)将列生成与分支定界结合以求解带时间窗的车辆路径问题;事实上,列生成得以广泛应用在于其求解(混合)整数规划问题所表现的卓越性能。
列生成算法思想
考虑下面的线性规划问题,称为主问题(Master problem,MP):
MP中有|J|=n个变量和m个约束条件,若n>>m,则想要直接求解是不可能的。相反的,考虑一个限制主问题(Restricted master problem,RMP),其变量为子集
J
′
J'
J′
⊆
\subseteq
⊆
J
J
J,令为
λ
∗
λ^{*}
λ∗是RMP问题的一个最优解(并不一定是MP问题的最优解)。在单纯形法的每一次迭代中,需要找一个检验数为负的非基变量入基,在列生成算法中则是通过一个定价子问题(Pricing problem,PP)实现的。
P
P
:
m
i
n
{
c
j
−
π
∗
a
j
∣
j
∈
J
}
PP: min \{c_j-\pi^*a_j|j∈J \}
PP:min{cj−π∗aj∣j∈J}
若
P
P
<
0
PP<0
PP<0,则将对应的变量
λ
i
\lambda_i
λi和其系数列
(
c
j
,
a
j
)
(c_j,a_j)
(cj,aj)加入到RMP中,然后求解新的RMP得到新的最优对偶变量值,重复这个过程直至找不到新的入基变量。此时,
λ
∗
\lambda^*
λ∗也是主问题MP的最优解了。列生成的优势在于无需考虑所有候选变量的迭代,而是通过其他的隐式搜索(implicit search)来寻找负消减成本(negative reduced cost,对应于单纯形法迭代中的额检验数)的列作为候选的入基变量。
切料问题(Cutting Stock Problem)
数学模型
列生成算法的一个经典例子是一维切割问题:给定宽度为
W
W
W的原料,需要切割
d
i
d_i
di个宽度为
w
i
w_i
wi的坯料,如何切割才能使用料最省?
主问题(Mater Problem):
限制主问题(Restricted Mater Problem):
定价子问题(Pricing Problem):
完整代码
分为3个类:Data类存放已知数据;CuttingStockProblem 算法类;Test程序运行。
package colgen_csp;
//class Data {
// static double _rollWidth = 17;// 木材长度
// static double[] _size = new double[] { 3, 5, 9 };;// 木材需求尺寸
// static double[] _amount = new double[] { 25, 20, 15 };;// 需求量
//}
class Data {
// 木材长度
static double _rollWidth = 5600;
// 木材需求尺寸
static double[] _size = new double[]{1380, 1520, 1560, 1710, 1820, 1880, 1930, 2000, 2050, 2100, 2140, 2150, 2200};
// 需求量
static double[] _amount = new double[]{22, 25, 12, 14, 18, 18, 20, 10, 12, 14, 16, 18, 20};
}
package colgen_csp;
import ilog.concert.*;
import ilog.cplex.*;
import static net.mindview.util.Print.*;
public class CuttingStockProblem {
static double _width;
static double[] _size;
static double[] _amount;
static void init() {
_width = Data._rollWidth;
_size = Data._size;
_amount = Data._amount;
print("width of the roll: " + _width);
print("demand width:");
for (double size : _size)
printnb(size + " ");
print();
print("demand amount:");
for (double amount : _amount)
printnb(amount + " ");
}
static class IloNumVarArray {
int _num = 0;
IloNumVar[] _array = new IloNumVar[32];
void add(IloNumVar ivar) {
if (_num >= _array.length) {
IloNumVar[] array = new IloNumVar[_array.length * 2];
System.arraycopy(_array, 0, array, 0, _num);
_array = array;
}
_array[_num++] = ivar;
}
IloNumVar getElement(int index) {
return _array[index];
}
int getSize() {
return _num;
}
}
static void writeAnsRMP(IloCplex RMP, IloNumVarArray vars, IloRange[] Fill) throws IloException {
print();
print("using" + RMP.getObjValue() + " rolls");
print();
for (int i = 0; i < vars.getSize(); i++) {
System.out.format("using pattern" + i + "=" + RMP.getValue(vars.getElement(i)) + "times");
}
print();
for (int i = 0; i < Fill.length; i++) {
print("Duals" + i + "=" + RMP.getDual(Fill[i]));
}
print();
}
static void writeAnsPP(IloCplex PP, IloNumVar[] Use) throws IloException {
print();
print("reduced cost = " + PP.getObjValue());
print();
/*PP.getObjValue() <= -RC_EPS*/
if (PP.getObjValue() < 0) {
for (int i = 0; i < Use.length; i++)
System.out.println(" Use" + i + " = " + PP.getValue(Use[i]));// 输出新的切法
System.out.println();
}
}
static void writeAns(IloCplex RMP, IloNumVarArray vars) throws IloException {
print();
print("Best solution uses " + RMP.getObjValue() + "rolls");
for (int i = 0; i < vars.getSize(); i++) {
print(RMP.getValue(vars.getElement(i)));
}
}
public static void solution() {
init();
try {
限制主问题 //
// 建立Cplex参数
IloCplex RMP = new IloCplex();
RMP.setOut(null);
// 最小值问题
IloObjective RMP_Obj = RMP.addMinimize();
// 建立cplex需求量约束数组,添加约束
IloRange[] Fill = new IloRange[_amount.length];
for (int i = 0; i < _amount.length; i++) {
Fill[i] = RMP.addRange(_amount[i], Double.MAX_VALUE);
}
IloNumVarArray vars = new IloNumVarArray();
int len = _size.length;
for (int j = 0; j < len; j++) {
IloColumn col = RMP.column(RMP_Obj, 1.0).
and(RMP.column(Fill[j], (int) (_width / _size[j])));
vars.add(RMP.numVar(col, 0.0, Double.MAX_VALUE));
}
//选择控制器
RMP.setParam(IloCplex.Param.RootAlgorithm, IloCplex.Algorithm.Primal);
定价子问题 //
IloCplex PP = new IloCplex();
PP.setOut(null);
IloObjective ReducedCost = PP.addMinimize();
//定义决策变量取值范围
IloNumVar[] Use = PP.numVarArray(len, 0, Double.MAX_VALUE, IloNumVarType.Int);
PP.addRange(-Double.MAX_VALUE, PP.scalProd(_size, Use), _width);
列生成 //
double[] newPatt;
for (; ; ) {
限制主问题 //
RMP.solve();
writeAnsRMP(RMP, vars, Fill);
double[] price = RMP.getDuals(Fill);
定价子问题 //
ReducedCost.setExpr(PP.diff(1., PP.scalProd(Use, price)));
PP.solve();
writeAnsPP(PP, Use);
if (PP.getObjValue() >= 0)// PP.getObjValue() > -RC_EPS
break;
newPatt = PP.getValues(Use);
IloColumn column = RMP.column(RMP_Obj, 1.);
for (int i = 0; i < newPatt.length; i++) {
column = column.and(RMP.column(Fill[i], newPatt[i]));
}
vars.add(RMP.numVar(column, 0., Double.MAX_VALUE));
}
for (int i = 0; i < vars.getSize(); i++) {
RMP.add(RMP.conversion(vars.getElement(i), IloNumVarType.Int));
}
RMP.solve();
// 输出最优切法所需木材数以及每种切法所需木材数
writeAns(RMP, vars);
System.out.println("Solution status: " + RMP.getStatus());
RMP.end();
PP.end();
} catch (IloException e) {
e.printStackTrace();
}
}
}
package colgen_csp;
public class Test {
public static void main(String[] args) {
CuttingStockProblem csp = new CuttingStockProblem();
csp.solution();
}
}
Reference
- Carvalho, J.M.V., 1998. Exact Solution of Cutting Stock Problems Using Column Generation and Branch-and-Bound. Int Trans Operational Res 5, 35–44.
- https://blog.csdn.net/u014090505/article/details/89019327