干货 | 10分钟搞懂branch and bound(分支定界)算法的代码实现附带java代码
Outline
- 前言
- Example-1
- Example-2
- 运行说明
00 前言
前面一篇文章我们讲了branch and bound算法的相关概念。可能大家对精确算法实现的印象大概只有一个,调用求解器进行求解,当然这只是一部分。其实精确算法也好,启发式算法也好,都是独立的算法,可以不依赖求解器进行代码实现的,只要过程符合算法框架即可。
只不过平常看到的大部分是精确算法在各种整数规划模型上的应用,为此难免脱离不了cplex等求解器。这里简单提一下。今天给大家带来的依然是branch and bound算法在整数规划中的应用的代码实现,所以还是会用到部分求解器的。
注:本文代码下载请关注公众号【程序猿声】,后台回复【bbcode】,不包括【】即可下载!

01 Example-1
首先来看第一个代码实例,该代码求解的是整数优化的模型,关于branch and bound求解整数规划的具体原理就不再概述了,和上一篇文章差不多但是有所区别。代码文件层次如下:

其中branch and bound算法主要部分在BnB_Guide.java这个文件。ExampleProblem.java内置了三个整数规划模型的实例。调用的是scpsolver这个求解器的wrapper,实际调用的还是lpsolver这个求解器用以求解线性松弛模型。下面着重讲讲BnB_Guide.java这个文件。
	public BnB_Guide(int demoProblem){
		example = new ExampleProblem(demoProblem);
		LinearProgram lp = new LinearProgram();
		lp = example.getProblem().getLP();
		solver = SolverFactory.newDefault();
		double[] solution = solver.solve(lp); // Solution of the initial relaxation problem
		int maxElement =  getMax(solution); // Index of the maximum non-integer decision variable's value
		if(maxElement == -1 ) // We only got integers as values, hence we have an optimal solution
			verifyOptimalSolution(solution,lp);
		else
			this.solveChildProblems(lp, solution, maxElement); // create 2 child problems and solve them
		printSolution();
	}
该过程是算法主调用过程:
- 首先变量lp保存了整数规划的松弛问题。
- 在调用求解器求解松弛模型以后,判断是否所有决策变量都是整数了,如果是,已经找到最优解。
- 如果不是,根据找出最大的非整数的决策变量,对该变量进行分支,solveChildProblems。
接着是分支子问题的求解过程solveChildProblems如下:
	public void solveChildProblems(LinearProgram lp, double[] solution ,int maxElement){
		searchDepth++;
		LinearProgram lp1 = new LinearProgram(lp);
		LinearProgram lp2 = new LinearProgram(lp);
		String constr_name = "c" + (lp.getConstraints().size() + 1); // Name of the new constraint
		double[] constr_val = new double[lp.getDimension()]; // The variables' values of the new constraint 
		for(int i=0;i<constr_val.length;i++){ // Populate the table
			if(i == maxElement )
				constr_val[i] = 1.0;
			else
				constr_val[i] = 0;
		}
		//Create 2 child problems: 1. x >= ceil(value), 2. x <= floor(value)
		lp1.addConstraint(new LinearBiggerThanEqualsConstraint(constr_val, Math.ceil(solution[maxElement]), constr_name));
		lp2.addConstraint(new LinearSmallerThanEqualsConstraint(constr_val, Math.floor(solution[maxElement]), constr_name));
		solveProblem(lp1);
		solveProblem(lp2);
	}
具体的分支过程如下:
- 首先新建两个线性的子问题。
- 两个子问题分别添加需要分支的决策变量新约束:1. x >= ceil(value), 2. x <= floor(value)。
- 一切准备就绪以后,调用solveProblem求解两个子问题。
而solveProblem的实现代码如下:
	private void solveProblem(LinearProgram lp) {
		double[] sol = solver.solve(lp);
		LPSolution lpsol = new LPSolution(sol, lp);
		double objVal = lpsol.getObjectiveValue();
		if(lp.isMinProblem()) {
			if(objVal > MinimizeProblemOptimalSolution) {
				System.out.println("cut >>> objVal = "+ objVal);
				return;
			}
		}
		else {
			if(objVal < MaximizeProblemOptimalSolution) {
				System.out.println("cut >>> objVal = "+ objVal);
				return;
			}
		}
		System.out.println("non cut >>> objVal = "+ objVal);
		int maxElement = this.getMax(sol);
		if(maxElement == -1 && lp.isFeasable(sol)){ //We found a solution
			solutionFound = true;
			verifyOptimalSolution(sol,lp);
		}
		else if(lp.isFeasable(sol) && !solutionFound) //Search for a solution in the child problems
			this.solveChildProblems(lp, sol, maxElement);
	}
该过程如下:
- 首先调用求解器求解传入的线性模型。
- 然后实行定界剪支,如果子问题的objVal比当前最优解还要差,则剪掉。
- 如果不剪,则判断是否所有决策变量都是整数以及解是否可行,如果是,找到新的解,更新当前最优解。
- 如果不是,根据找出最大的非整数的决策变量,对该变量再次进行分支,进入solveChildProblems。
从上面的逻辑过程可以看出,solveChildProblems和solveProblem两个之间相互调用,其实这是一种递归。该实现方式进行的就是BFS广度优先搜索的方式遍历搜索树。
02 Example-2
再来看看第二个实例:

input是模型的输入,输入的是一个整数规划的模型。由于输入和建模过程有点繁琐,这里就不多讲了。挑一些重点讲讲具体是分支定界算法是怎么运行的就行。
首先该代码用了stack的作为数据结构,遍历搜索树的方式是DFS即深度优先搜索,我们来看BNBSearch.java这个文件:
public class BNBSearch {
	Deque<searchNode> searchStack = new ArrayDeque<searchNode>();
	double bestVal = Double.MAX_VALUE;
	searchNode currentBest = new searchNode();
	IPInstance solveRel = new IPInstance();
	Deque<searchNode> visited = new ArrayDeque<searchNode>();
	public BNBSearch(IPInstance solveRel) {
		this.solveRel = solveRel;
		searchNode rootNode = new searchNode();
		this.searchStack.push(rootNode);
	};
BNBSearch 这个类是branch and bound算法的主要过程,成员变量如下:
- searchStack :构造和遍历生成树用的,栈结构。
- bestVal:记录当前最优解的值,由于求的最小化问题,一开始设置为正无穷。
- currentBest :记录当前最优解。
- solveRel :整数规划模型。
- visited :记录此前走过的分支,避免重复。
然后在这里展开讲一下searchNode就是构成搜索树的节点是怎么定义的:
public class searchNode {
	  HashMap<Integer, Integer> partialAssigned = new HashMap<Integer, Integer>();
	  public searchNode() {
		  super();
	  }
	  public searchNode(searchNode makeCopy) {
		  for (int test: makeCopy.partialAssigned.keySet()) {
		    	this.partialAssigned.put(test, makeCopy.partialAssigned.get(test));
		    }
		  }
}
其实非常简单,partialAssigned 保存的是部分解的结构,就是一个HashMap,key保存的是决策变量,而value对应的是决策变量分支的取值(0-1)。举上节课讲过的例子:

比如:
- 节点1的partialAssigned == { {x3, 1} }。
- 节点2的partialAssigned == { {x3, 0} }。
- 节点3的partialAssigned == { {x3, 1}, {x2, 1} }。
- 节点4的partialAssigned == { {x3, 1}, {x2, 0} }。
- 节点7的partialAssigned == { {x3, 0}, {x1, 1},  {x2, 1}}。
 ……
想必各位已经明白得不能再明白了。
然后就可以开始BB过程了:
	public int solveIP() throws IloException {
		while (!this.searchStack.isEmpty()) {
			searchNode branchNode = this.searchStack.pop();
			boolean isVisited = false;
			for (searchNode tempNode: this.visited) {
				if (branchNode.partialAssigned.equals(tempNode.partialAssigned)){
					isVisited = true;
					break;
				}
			}
			if (!isVisited) {
				visited.add(new searchNode(branchNode));
				double bound = solveRel.solve(branchNode);
				if (bound > bestVal || bound == 0) {
					//System.out.println(searchStack.size());
				}
				if (bound < bestVal && bound!=0) {
					if (branchNode.partialAssigned.size() == solveRel.numTests) {
						//分支到达低端,找到一个满足整数约束的可行解,设置为当前最优解。
						//System.out.println("YAY");
						this.bestVal = bound;
						this.currentBest = branchNode;
					}
				}
				if (bound < bestVal && bound!=0) {
					//如果还没到达低端,找一个变量进行分支。
					if (branchNode.partialAssigned.size() != solveRel.numTests) {
						int varToSplit = getSplitVariable(branchNode);
						if (varToSplit != -1) {
							searchNode left = new searchNode(branchNode);
							searchNode right = new searchNode(branchNode);
							left.partialAssigned.put(varToSplit, 0);
							right.partialAssigned.put(varToSplit, 1);
							this.searchStack.push(left);
							this.searchStack.push(right);
						}
					}
				}
			}
		}
		return (int) bestVal;
	}
首先从搜索栈里面取出一个节点,判断节点代表的分支是否此前已经走过了,重复的工作就不要做了嘛。
如果没有走过,那么在该节点处进行定界操作,从该节点进入,根据partialAssigned 保存的部分解结构,添加约束,建立松弛模型,调用cplex求解。具体求解过程如下:
  public double solve(searchNode node) throws IloException {
	  try {
		  cplex = new IloCplex();
		  cplex.setOut(null);
		  IloNumVarType [] switcher = new IloNumVarType[2];
		  switcher[0] = IloNumVarType.Int;
		  switcher[1] = IloNumVarType.Float;
		  int flag = 1;
	      IloNumVar[] testUsed = cplex.numVarArray(numTests, 0, 1, switcher[flag]);
	      IloNumExpr objectiveFunction = cplex.numExpr();
	      objectiveFunction = cplex.scalProd(testUsed, costOfTest);
	      cplex.addMinimize(objectiveFunction);
	      for (int j = 0; j < numDiseases*numDiseases; j++) {
	    	  if (j % numDiseases == j /numDiseases) {
	    		  continue;
	    	  }
	    	  IloNumExpr diffConstraint = cplex.numExpr();
	    	  for (int i =  0; i < numTests; i++) {
	    		  if (A[i][j/numDiseases] == A[i][j%numDiseases]) {
	    			  continue;
	    		  }
	    		  diffConstraint = cplex.sum(diffConstraint, testUsed[i]);
	    	  }
	    	  cplex.addGe(diffConstraint, 1);
	    	  diffConstraint = cplex.numExpr();
	      }
	      for (int test: node.partialAssigned.keySet()) {
	    	  cplex.addEq(testUsed[test], node.partialAssigned.get(test));
	      }
	      //System.out.println(cplex.getModel());
	      if(cplex.solve()) {
		        double objectiveValue = (cplex.getObjValue()); 
		        for (int i = 0; i < numTests; i ++) {
		        	if (cplex.getValue(testUsed[i]) == 0) {
		        		node.partialAssigned.put(i, 0);
		        	}
		        	else if (cplex.getValue(testUsed[i]) == 1) {
		        		node.partialAssigned.put(i, 1);
		        	}
		        }
		        //System.out.println("LOL"+node.partialAssigned.size());
		        return objectiveValue;
	      }
	  }
	  catch(IloException e) {
	      System.out.println("Error " + e);
	  }
	  return 0;
  }
中间一大堆建模过程就不多讲了,具体分支约束是这一句:
          for (int test: node.partialAssigned.keySet()) {
              cplex.addEq(testUsed[test], node.partialAssigned.get(test));
          }
此后,求解完毕后,把得到整数解的决策变量放进partialAssigned,不是整数后续操作。然后返回目标值。
然后依旧回到solveIP里面,在进行求解以后,得到目标值,接下来就是定界操作了:
- if (bound > bestVal || bound == 0):剪支。
- if (bound < bestVal && bound!=0):判断是否所有决策变量都为整数,如果是,找到一个可行解,更新当前最优解。如果不是,找一个小数的决策变量入栈,等待后续分支。
03 运行说明
Example-1:
运行说明,运行输入参数1到3中的数字表示各个不同的模型,需要在32位JDK环境下才能运行,不然会报nullPointer的错误,这是那份求解器wrapper的锅。怎么设置参数参考cplexTSP那篇,怎么设置JDK环境就不多说了。
然后需要把代码文件夹下的几个jar包给添加进去,再把lpsolve的dll给放到native library里面,具体做法还是参照cplexTSP那篇,重复的内容我就不多说了。
Example-2:
最后是运行说明:该实例运行调用了cplex求解器,所以需要配置cplex环境才能运行,具体怎么配置看之前的教程。JDK环境要求64位,无参数输入。
代码来源GitHub,经过部分修改。
干货 | 10分钟搞懂branch and bound(分支定界)算法的代码实现附带java代码的更多相关文章
- 10分钟搞懂Tensorflow 逻辑回归实现手写识别
		1. Tensorflow 逻辑回归实现手写识别 1.1. 逻辑回归原理 1.1.1. 逻辑回归 1.1.2. 损失函数 1.2. 实例:手写识别系统 1.1. 逻辑回归原理 1.1.1. 逻辑回归 ... 
- 10分钟搞懂toString和valueOf函数(详细版)
		首先要说明的是这两种方法是toPrimitive抽象操作里会经常用到的. 默认情况下,执行这个抽象操作时会先执行valueOf方法,如果返回的不是原始值,会继续执行toString方法,如果返回的还不 ... 
- 花10分钟搞懂开源框架吧 - 【NancyFx.Net】
		NancyFx是什么? Nancy是一个轻量级的独立的框架,下面是官网的一些介绍: Nancy 是一个轻量级用于构建基于 HTTP 的 Web 服务,基于 .NET 和 Mono 平台,框架的目标是保 ... 
- 干货 | 10分钟带你全面掌握branch and bound(分支定界)算法-概念篇
		00 前言 之前一直做启发式算法,最近突然对精确算法感兴趣了.但是这玩意儿说实话是真的难,刚好boss又叫我学学column generation求解VRP相关的内容.一看里面有好多知识需要重新把握, ... 
- 干货 | 10分钟掌握branch and cut(分支剪界)算法原理附带C++求解TSP问题代码
		00 前言 branch and cut其实还是和branch and bound脱离不了干系的.所以,在开始本节的学习之前,请大家还是要务必掌握branch and bound算法的原理. 01 应 ... 
- 干货 | 10分钟教你用column generation求解vehicle routing problems
		OUTLINE 前言 VRPTW description column generation Illustration code reference 00 前言 此前向大家介绍了列生成算法的详细过程, ... 
- c#代码 天气接口  一分钟搞懂你的博客为什么没人看  看完python这段爬虫代码,java流泪了c#沉默了  图片二进制转换与存入数据库相关  C#7.0--引用返回值和引用局部变量  JS直接调用C#后台方法(ajax调用)  Linq To Json  SqlServer 递归查询
		天气预报的程序.程序并不难. 看到这个需求第一个想法就是只要找到合适天气预报接口一切都是小意思,说干就干,立马跟学生沟通价格.  不过谈报价的过程中,差点没让我一口老血喷键盘上,话说我们程序猿的人 ... 
- 【转】让你10分钟搞定Mac--最简单快速的虚拟安装
		文章出处:让你10分钟搞定Mac--最简单快速的虚拟安装http://bbs.itheima.com/thread-106643-1-1.html (出处: 黑马程序员训练营论坛) 首先说明一下. 第 ... 
- [转帖]10分钟看懂Docker和K8S
		10分钟看懂Docker和K8S https://zhuanlan.zhihu.com/p/53260098 2010年,几个搞IT的年轻人,在美国旧金山成立了一家名叫“dotCloud”的公司. 这 ... 
随机推荐
- Hive 系列(二)—— Linux 环境下 Hive 的安装部署
			一.安装Hive 1.1 下载并解压 下载所需版本的 Hive,这里我下载版本为 cdh5.15.2.下载地址:http://archive.cloudera.com/cdh5/cdh/5/ # 下载 ... 
- ASP.Net Jquery 随机验证码 文本框判断
			// 登陆验证 $(function () { var chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'a', 'B' ... 
- 【转】StackTraceElement获取方法调用栈的信息
			本文链接:https://blog.csdn.net/hp910315/article/details/52702199 一.什么是StackTrace StackTrace(堆栈轨迹)存放的就是方法 ... 
- python之路第一天
			2018-07-11星期三 创建自己的博客(博客园): 登陆 我的博客 随笔:所有人在博客中都能看见的文章 文章:别人看不见,只能URL访问--我把网页地址发给你,你才能看到 日志:别人看不到,URL ... 
- 复盘一篇浅谈KNN的文章
			认识-什么是KNN KNN 即 K-nearest neighbors, 是一个hello world级别, 但被广泛使用的机器学习算法, 中文叫K近邻算法, 是一种基本的分类和回归方法. KNN既可 ... 
- laravel项目中通过nvmw安装node.js和npm 开发环境-- windows版
			windows版本安装 此教程执行的时候,网速一定要好.不然可能出现各种错误. 如果本文对你有用,请爱心点个赞,提高排名,帮助更多的人.谢谢大家!❤ git clone nvmw 直接从 githu ... 
- MySQL删除语句
			删除数据(DELETE) 使用前需注意:删除(DELETE),是删除一(条)行数据.假如我们有四条(行)数据,换句话说,你要删除其中一条(行) 名字为“xx”的用户,那么关于他的 i所有数据都会被删除 ... 
- 随笔小skill
			1.用拉链函数zip()将字典转换成元组对!函数中的两个参数必须是序列!p = {'name':'zhangsanfeng','age':18,'gender':'nan'}print(list(zi ... 
- 整型 字符串方法 for循环
			整型 # 整型 -- 数字 (int) # 用于比较和运算的 # 32位 -2 ** 31 ~ 2 ** 31 -1 # 64位 -2 ** 63 ~ 2 ** 63 -1 # + - * / // ... 
- Gtest:参数化
			转自:玩转Google开源C++单元测试框架Google Test系列(gtest)之四 - 参数化 一.前言 在设计测试案例时,经常需要考虑给被测函数传入不同的值的情况.我们之前的做法通常是写一个通 ... 
