干货 | 10分钟搞懂branch and bound算法的代码实现附带java代码

简介: 干货 | 10分钟搞懂branch and bound算法的代码实现附带java代码


前言

00

前面一篇文章我们讲了branch and bound算法的相关概念。可能大家对精确算法实现的印象大概只有一个,调用求解器进行求解,当然这只是一部分。


其实精确算法也好,启发式算法也好,都是独立的算法,可以不依赖求解器进行代码实现的,只要过程符合算法框架即可。


只不过平常看到的大部分是精确算法在各种整数规划模型上的应用,为此难免脱离不了cplex等求解器。这里简单提一下。


今天给大家带来的依然是branch and bound算法在整数规划中的应用的代码实现,所以还是会用到部分求解器的。


注:本文代码下载请移步留言区。



Example-1

01



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


微信图片_20220421164143.png


其中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();
    }

该过程是算法主调用过程:


1. 首先变量lp保存了整数规划的松弛问题。


2. 在调用求解器求解松弛模型以后,判断是否所有决策变量都是整数了,如果是,已经找到最优解。


3. 如果不是,根据找出最大的非整数的决策变量,对该变量进行分支,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. 首先新建两个线性的子问题。


2. 两个子问题分别添加需要分支的决策变量新约束:1. x >= ceil(value), 2. x <= floor(value)。


3. 一切准备就绪以后,调用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);
    }


该过程如下:


1. 首先调用求解器求解传入的线性模型。


2. 然后实行定界剪支,如果子问题的objVal比当前最优解还要差,则剪掉。


3. 如果不剪,则判断是否所有决策变量都是整数以及解是否可行,如果是,找到新的解,更新当前最优解。


4. 如果不是,根据找出最大的非整数的决策变量,对该变量再次进行分支,进入solveChildProblems。


从上面的逻辑过程可以看出,solveChildProblems和solveProblem两个之间相互调用,其实这是一种递归。


该实现方式进行的就是BFS广度优先搜索的方式遍历搜索树。


Example-2

02



再来看看第二个实例:

微信图片_20220421164145.png


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)。举上节课讲过的例子:

微信图片_20220421164148.png



比如:

节点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;  }


中间一大堆建模过程就不多讲了,具体分支约束是这一句:


     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;
      }


    此后,求解完毕后,把得到整数解的决策变量放进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,经过部分修改。

    相关文章
    |
    1月前
    |
    存储 算法 安全
    探究‘公司禁用 U 盘’背后的哈希表算法与 Java 实现
    在数字化办公时代,信息安全至关重要。许多公司采取“禁用U盘”策略,利用哈希表算法高效管理外接设备的接入权限。哈希表通过哈希函数将设备标识映射到数组索引,快速判断U盘是否授权。例如,公司预先将允许的U盘标识存入哈希表,新设备接入时迅速验证,未授权则禁止传输并报警。这有效防止恶意软件和数据泄露,保障企业信息安全。 代码示例展示了如何用Java实现简单的哈希表,模拟公司U盘管控场景。哈希表不仅用于设备管理,还在文件索引、用户权限等多方面助力信息安全防线的构建,为企业数字化进程保驾护航。
    |
    2月前
    |
    监控 算法 网络协议
    Java 实现局域网电脑屏幕监控算法揭秘
    在数字化办公环境中,局域网电脑屏幕监控至关重要。本文介绍用Java实现这一功能的算法,涵盖图像采集、数据传输和监控端显示三个关键环节。通过Java的AWT/Swing库和Robot类抓取屏幕图像,使用Socket进行TCP/IP通信传输图像数据,并利用ImageIO类在监控端展示图像。整个过程确保高效、实时和准确,为提升数字化管理提供了技术基础。
    84 15
    |
    5天前
    |
    机器学习/深度学习 存储 算法
    近端策略优化(PPO)算法的理论基础与PyTorch代码详解
    近端策略优化(PPO)是深度强化学习中高效的策略优化方法,广泛应用于大语言模型的RLHF训练。PPO通过引入策略更新约束机制,平衡了更新幅度,提升了训练稳定性。其核心思想是在优势演员-评论家方法的基础上,采用裁剪和非裁剪项组成的替代目标函数,限制策略比率在[1-ϵ, 1+ϵ]区间内,防止过大的策略更新。本文详细探讨了PPO的基本原理、损失函数设计及PyTorch实现流程,提供了完整的代码示例。
    108 10
    近端策略优化(PPO)算法的理论基础与PyTorch代码详解
    |
    12天前
    |
    存储 算法 Java
    解锁“分享文件”高效密码:探秘 Java 二叉搜索树算法
    在信息爆炸的时代,文件分享至关重要。二叉搜索树(BST)以其高效的查找性能,为文件分享优化提供了新路径。本文聚焦Java环境下BST的应用,介绍其基础结构、实现示例及进阶优化。BST通过有序节点快速定位文件,结合自平衡树、多线程和权限管理,大幅提升文件分享效率与安全性。代码示例展示了文件插入与查找的基本操作,适用于大规模并发场景,确保分享过程流畅高效。掌握BST算法,助力文件分享创新发展。
    |
    25天前
    |
    存储 人工智能 算法
    解锁分布式文件分享的 Java 一致性哈希算法密码
    在数字化时代,文件分享成为信息传播与协同办公的关键环节。本文深入探讨基于Java的一致性哈希算法,该算法通过引入虚拟节点和环形哈希空间,解决了传统哈希算法在分布式存储中的“哈希雪崩”问题,确保文件分配稳定高效。文章还展示了Java实现代码,并展望了其在未来文件分享技术中的应用前景,如结合AI优化节点布局和区块链增强数据安全。
    |
    26天前
    |
    算法 安全 Java
    Java线程调度揭秘:从算法到策略,让你面试稳赢!
    在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
    66 16
    |
    1月前
    |
    运维 监控 算法
    企业局域网监控软件中 Java 优先队列算法的核心优势
    企业局域网监控软件是数字化时代企业网络安全与高效运营的基石,犹如一位洞察秋毫的卫士。通过Java实现的优先队列算法,它能依据事件优先级排序,确保关键网络事件如异常流量、数据泄露等被优先处理,保障系统稳定与安全。代码示例展示了如何定义网络事件类并使用PriorityQueue处理高优先级事件,尤其在面对疑似风险时迅速启动应急措施。这一核心技术助力企业在复杂网络环境中稳健前行,护航业务腾飞。
    65 32
    |
    1月前
    |
    存储 监控 算法
    剖析基于Java算法驱动的智能局域网管控之道
    本文探讨了基于Java语言的局域网控制方案,结合链表数据结构与令牌桶算法,解决设备管理和流量调度难题。通过链表灵活存储网络设备信息,实现高效设备管理;令牌桶算法则精准控制流量,确保网络平稳运行。二者相辅相成,为校园、企业等局域网提供稳固高效的控制体系,保障业务连续性和数据安全。
    |
    29天前
    |
    算法 搜索推荐 Java
    【潜意识Java】深度解析黑马项目《苍穹外卖》与蓝桥杯算法的结合问题
    本文探讨了如何将算法学习与实际项目相结合,以提升编程竞赛中的解题能力。通过《苍穹外卖》项目,介绍了订单配送路径规划(基于动态规划解决旅行商问题)和商品推荐系统(基于贪心算法)。这些实例不仅展示了算法在实际业务中的应用,还帮助读者更好地准备蓝桥杯等编程竞赛。结合具体代码实现和解析,文章详细说明了如何运用算法优化项目功能,提高解决问题的能力。
    61 6
    |
    29天前
    |
    算法 Java C++
    【潜意识Java】蓝桥杯算法有关的动态规划求解背包问题
    本文介绍了经典的0/1背包问题及其动态规划解法。
    48 5