EasyLearn--JAVA实现32个经典算法设计(四):分支定界法-旅行商(TSP)问题

分支定界法开始看的时候还是很难能通俗易懂地描述出该算法的规则和界限。顾名思义就是按名字来划分,分支可看作一个二叉树,而定界就可理解为对上述二叉树的一种约束。

具体可以参考该连接的描述:只需要看两张图片即可,如何将图转化为一颗树

还有另外一种用法是:一个箱子可装10斤重的货物,其中有三件分别中4 8 5斤可不拆分的货物,请问最多可装几斤的问题。等等可用此算法的应用。

鄙人实现的是TSP问题,模型构建、解决方案、算法实现等。话不多说上源码。

1.分支定界法的实体bean

import java.io.Serializable;
import java.util.List;

/**
 * 分支定界法的实体bean 假设来回的差旅费用不一样
 * 实体包含:城市节点编号、城市节点名称、城市节点所属层级、到达城市节点编号、到达城市节点所需费用
 */
public class BranchBound implements Serializable {
    // 城市节点编号
    private String nodeNo;
    // 城市节点名称
    private String nodeName;
    // 城市节点所属层级
    private Integer nodeLevel;
    // 到达城市节点编号
    private String arrayNodeNo;
    // 到达城市节点所需费用
    private Double arrayNodeFee;
    // 剩余没去的城市节点,节点编号
    private List<String> restNodes;

    public BranchBound(String nodeNo, String nodeName, String arrayNodeNo, Double arrayNodeFee) {
        super();
        this.nodeNo = nodeNo;
        this.nodeName = nodeName;
        this.arrayNodeNo = arrayNodeNo;
        this.arrayNodeFee = arrayNodeFee;
    }

    public String getNodeNo() {
        return nodeNo;
    }

    public void setNodeNo(String nodeNo) {
        this.nodeNo = nodeNo;
    }

    public String getNodeName() {
        return nodeName;
    }

    public void setNodeName(String nodeName) {
        this.nodeName = nodeName;
    }

    public Integer getNodeLevel() {
        return nodeLevel;
    }

    public void setNodeLevel(Integer nodeLevel) {
        this.nodeLevel = nodeLevel;
    }

    public String getArrayNodeNo() {
        return arrayNodeNo;
    }

    public void setArrayNodeNo(String arrayNodeNo) {
        this.arrayNodeNo = arrayNodeNo;
    }

    public Double getArrayNodeFee() {
        return arrayNodeFee;
    }

    public void setArrayNodeFee(Double arrayNodeFee) {
        this.arrayNodeFee = arrayNodeFee;
    }

    public List<String> getRestNodes() {
        return restNodes;
    }

    public void setRestNodes(List<String> restNodes) {
        this.restNodes = restNodes;
    }
}

2.分支界定法的MAP集合

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 构建分支界定法的MAP集合、该算法的难点不在于算法的本身,而在于如何构建完美二叉树和定界的问题
 * 如果城市之间的费用相差不多,那么将会导致该算法变为穷举法,每个分支都将会算到
 * 1.定义城市节点的总数量
 * 2.初始化城市节点信息以及所需的差旅费、剩余可用城市节点
 * 3.获取节点最少费用的上限值(求取思路:计算出所有节点的出边和入边最小的值,而后依据该城市节点总数量*2的边,进行扩展最终求出N*2条线,比较得出最少路径值)
 */
public class BranchBoundMap {
    // 城市数量
    private Integer nodeCount = 8;
    // 城市节点的集合
    private List<BranchBound> bbList = new ArrayList<>();
    // 存储所有出边和入边的最优路径
    private List<BranchBound> bestList = new ArrayList<>();
    // 剩余城市节点的集合,节点编号
    private List<String> restNodes = new ArrayList<>();

    public BranchBoundMap(){
        // 初始化剩余可用城市节点
        for (int i = 1; i <= nodeCount; i++){
            restNodes.add("CN"+i);
        }
        // 初始化城市节点信息以及所需的差旅费
        for (int i = 1; i <= nodeCount; i++){
            List<String> tempNodes = new ArrayList<>(restNodes);
            tempNodes.remove("CN"+i);
            for (int j = 1; j <= nodeCount; j++){
                if (i == j){
                    // 排除自到达
                    continue;
                }
                Double nodeFee = Math.random()*100;
                BranchBound branchBound = new BranchBound("CN"+i,"CNAME"+i,"CN"+j, nodeFee);
                branchBound.setRestNodes(tempNodes);
                bbList.add(branchBound);
            }
        }
        for (int i = 1; i <= nodeCount; i++){
            String nodeNo = "CN"+i;
            // 添加节点编码为nodeNo
            bestList.add(bbList.stream().filter(v->nodeNo.equals(v.getNodeNo())).min((b1,b2)-> b1.getArrayNodeFee() > b2.getArrayNodeFee() ? 1 : -1).get());
            // 添加到达节点编码为nodeNo
            bestList.add(bbList.stream().filter(v->nodeNo.equals(v.getArrayNodeNo())).min((b1,b2)-> b1.getArrayNodeFee() > b2.getArrayNodeFee() ? 1 : -1).get());
        }
        // 去除重复节点
        bestList = bestList.stream().distinct().collect(Collectors.toList());
        // 按节点编码再进行排序
        // Collections.sort(bestList, Comparator.comparing(BranchBound::getNodeNo));
        showBranchBoundMap();
    }

    /**
     * 打印输出起始所有城市节点信息
     */
    public void showBranchBoundMap(){
        bbList.stream().forEach(v -> {
            System.out.println("节点编码"+v.getNodeNo()+",到达节点"+v.getArrayNodeNo()+",节点费用"+v.getArrayNodeFee());
        });
        bestList.stream().forEach(v -> {
            System.out.println("节点编码"+v.getNodeNo()+",到达节点"+v.getArrayNodeNo()+",节点费用"+v.getArrayNodeFee());
        });
    }

    public Integer getNodeCount() {
        return nodeCount;
    }

    public List<BranchBound> getBbList() {
        return bbList;
    }

    public List<BranchBound> getBestList() {
        return bestList;
    }

    public List<String> getRestNodes() {
        return restNodes;
    }
}

3.根据已有的模型计算出分支定界算法对TSP的最优解

import java.util.*;
import java.util.stream.Collectors;

/**
 * 根据已有的模型计算出分支定界算法对TSP的最优解
 * 1.从Map类中获取剩余城市节点的集合
 * 2.从Map类中获取最优出边和入边的集合
 * 3.按出入边的数据集合依次求取每个路径中的值并存储到map中
 * 求取策略为优先从出入边集合中获取,不存在则取子树集合的最小值那个BranchBound
 */
public class BranchBoundTSP {
    // 定义起始节点编码
    private final String initNodeNo = "CN1";
    // 存储所有候选最佳路径的集合
    private Map<String, List<BranchBound>> bestMap = new HashMap<>();
    // Map类
    private BranchBoundMap branchBoundMap;

    public BranchBoundTSP(BranchBoundMap branchBoundMap){
        super();
        this.branchBoundMap = branchBoundMap;
    }

    public void getBestPathOfTSP(){
        // 获取城市节点的集合
        List<BranchBound> bbList = branchBoundMap.getBbList();
        // 获取所有出边和入边的最优路径
        List<BranchBound> bestList = branchBoundMap.getBestList();
        // 剩余城市节点的集合,节点编号
        List<String> restNodes = branchBoundMap.getRestNodes();
        for (BranchBound branchBound : bestList) {
            // 存储当前节点前后追溯路径的集合
            List<BranchBound> tempList = new ArrayList<>();
            // 添加当前节点
            tempList.add(branchBound);
            // 用于后面的节点控制
            List<String> tempNodes = new ArrayList<>(restNodes);
            // 删除当前的城市节点编码
            tempNodes.remove(branchBound.getNodeNo());
            // 删除所要到达的城市节点编码
            tempNodes.remove(branchBound.getArrayNodeNo());
            System.out.println(branchBound.getNodeNo()+"-"+branchBound.getArrayNodeNo());
            if (initNodeNo.equals(branchBound.getNodeNo())){
                // 开始节点为当前节点只需到达节点往后追溯即可
                do {
                    arrayNodeTransToEndNode(branchBound, tempList, tempNodes, bestList, bbList);
                } while (initNodeNo.equals(branchBound.getArrayNodeNo()));
            } else if (initNodeNo.equals(branchBound.getArrayNodeNo())){
                // 开始节点为到达节点只需当前节点往前追溯即可
                do {
                    nowNodeTransToStartNode(branchBound, tempList, tempNodes, bestList, bbList);
                } while (initNodeNo.equals(branchBound.getNodeNo()));
            } else {
                // 当前节点往前追溯到开始节点
                do {
                    nowNodeTransToStartNode(branchBound, tempList, tempNodes, bestList, bbList);
                } while (initNodeNo.equals(branchBound.getNodeNo()));
                // 到达节点往后追溯到结束节点
                do {
                    arrayNodeTransToEndNode(branchBound, tempList, tempNodes, bestList, bbList);
                } while (initNodeNo.equals(branchBound.getArrayNodeNo()));
            }
            // 节点追溯完后存储到map中,key为该最小费用节点的”当前节点-到达节点“,值为候选最优的路径集合
        }
        // 全部路径都已收集完成,展示其最终成果
        showBestPathMap();
    }

    /**
     * 展示各个节点最小 出边和入边的路径集合,从而得出最优路径的集合
     */
    public void showBestPathMap() {
        String bestPath = "";
        Double bestFee = 0.0;
        for (String keyPath : bestMap.keySet()){
            System.out.println(keyPath);
            List<BranchBound> tempList = bestMap.get(keyPath);
            // 对list的第一个节点依次往后排序
            BranchBound temp;
            String nodeNo = initNodeNo;
            Double totalFee = 0.0;
            do {
                temp = getBranchBoundByNodeNo(tempList,nodeNo);
                System.out.println("开始城市节点:"+temp.getNodeNo()
                        +",结束城市节点:"+temp.getArrayNodeNo()
                        +",费用:"+temp.getArrayNodeFee());
                totalFee += temp.getArrayNodeFee();
            } while (temp != null && !temp.getArrayNodeNo().equals(initNodeNo));
            if (totalFee < bestFee || bestFee == 0){
                bestFee = totalFee;
                bestPath = keyPath;
            }
            System.out.println("候选最优路段:"+keyPath+",总费用"+totalFee);
        }
        System.out.println("===========================================================");
        System.out.println("最优路段为:"+bestPath+",总费用"+bestFee);
    }

    /**
     * 根据现有节点来求出路径节点
     * @param nodeNo
     * @return
     */
    public BranchBound getBranchBoundByNodeNo(List<BranchBound> pathList, String nodeNo){
        List<BranchBound> tempList = new ArrayList<>(pathList);
        return tempList.stream().filter(v->v.getNodeNo().equals(nodeNo)).collect(Collectors.toList()).get(0);
    }

    /**
     * 当前节点,需到达节点往后追溯结束城市节点
     * 1.获取到达节点的编码,根据以该编码为开始节点到两城最优路线集合中查找最低的节点
     * 2.如果不存在则到节点集合中查询以该编码为开始节点,剩余未走的城市节点作为到达节点求取最小费用的那个节点
     * 3.如果存在则去该节点,依次递归该方法直到最终追溯到结束城市节点
     * @param branchBound 当前节点
     * @param pathList 存储当前节点前后追溯路径的集合
     * @param restNodes 剩余未走的城市节点
     * @param bestList 两城之前最优的城市节点
     * @param bbList 两城之前的城市节点集合
     */
    private void arrayNodeTransToEndNode(BranchBound branchBound, List<BranchBound> pathList, List<String> restNodes, List<BranchBound> bestList, List<BranchBound> bbList) {
        if (initNodeNo.equals(branchBound.getArrayNodeNo())){
            // 已追溯到结束城市节点,返回
            return;
        }
        Optional<BranchBound> minBBOpt = bestList.stream().filter(v-> {
            // 开始节点等于传入的到达节点 且到达节点在剩余节点内
            if (v.getNodeNo().equals(branchBound.getArrayNodeNo())&&restNodes.contains(v.getArrayNodeNo())){
                return true;
            } else if (v.getNodeNo().equals(branchBound.getArrayNodeNo())&&v.getArrayNodeNo().equals(initNodeNo)){
                // 开始节点等于传入的到达节点且没有剩余节点和结束节点为结束节点
                return true;
            }
            return false;
        }).collect(Collectors.minBy(Comparator.comparingDouble(BranchBound::getArrayNodeFee)));
        if (minBBOpt != null && minBBOpt.isPresent()){
            BranchBound minBB = minBBOpt.get();
            System.out.println(minBB.getNodeNo()+"-"+minBB.getArrayNodeNo());
            restNodes.remove(minBB.getArrayNodeNo());
            arrayNodeTransToEndNode(minBB, pathList, restNodes, bestList, bbList);
        }else{
            // 从两城之前的城市节点集合中获取
            BranchBound minBB = bbList.stream().filter(v->{
                // 开始节点等于传入的到达节点 且到达节点在剩余节点内
                if (v.getNodeNo().equals(branchBound.getArrayNodeNo())&&restNodes.contains(v.getArrayNodeNo())){
                    return true;
                } else if (v.getNodeNo().equals(branchBound.getArrayNodeNo())&&v.getArrayNodeNo().equals(initNodeNo)){
                    // 开始节点等于传入的到达节点且没有剩余节点和结束节点为结束节点
                    return true;
                }
                return false;
            }).collect(Collectors.minBy(Comparator.comparingDouble(BranchBound::getArrayNodeFee))).get();
            System.out.println(minBB.getNodeNo()+"-"+minBB.getArrayNodeNo());
            pathList.add(minBB);
            restNodes.remove(minBB.getArrayNodeNo());
            arrayNodeTransToEndNode(minBB, pathList, restNodes, bestList, bbList);
        }
    }

    /**
     * 到达节点,需当前节点往前追溯到开始城市节点
     * @param branchBound 当前节点
     * @param pathList 存储当前节点前后追溯路径的集合
     * @param restNodes 剩余未走的城市节点
     * @param bestList 两城之前最优的城市节点
     * @param bbList 两城之前的城市节点集合
     */
    private void nowNodeTransToStartNode(BranchBound branchBound, List<BranchBound> pathList, List<String> restNodes, List<BranchBound> bestList, List<BranchBound> bbList) {
        if (initNodeNo.equals(branchBound.getNodeNo())){
            // 已追溯到开始城市节点,返回
            return;
        }
        Optional<BranchBound> minBBOpt = bestList.stream().filter(v->{
            // 开始节点等于传入的到达节点 且到达节点在剩余节点内
            if (v.getArrayNodeNo().equals(branchBound.getNodeNo())&&restNodes.contains(v.getNodeNo())){
                return true;
            } else if (v.getArrayNodeNo().equals(branchBound.getNodeNo())&&v.getNodeNo().equals(initNodeNo)){
                // 开始节点等于传入的到达节点且没有剩余节点和结束节点为结束节点
                return true;
            }
            return false;
        }).collect(Collectors.minBy(Comparator.comparingDouble(BranchBound::getArrayNodeFee)));
        if (minBBOpt != null && minBBOpt.isPresent()){
            BranchBound minBB = minBBOpt.get();
            System.out.println(minBB.getNodeNo()+"-"+minBB.getArrayNodeNo());
            pathList.add(minBB);
            restNodes.remove(minBB.getNodeNo());
            nowNodeTransToStartNode(minBB, pathList, restNodes, bestList, bbList);
        }else{
            // 从两城之前的城市节点集合中获取
            BranchBound minBB = bbList.stream().filter(v->{
                // 开始节点等于传入的到达节点 且到达节点在剩余节点内
                if (v.getArrayNodeNo().equals(branchBound.getNodeNo())&&restNodes.contains(v.getNodeNo())){
                    return true;
                } else if (v.getArrayNodeNo().equals(branchBound.getNodeNo())&&v.getNodeNo().equals(initNodeNo)){
                    // 开始节点等于传入的到达节点且没有剩余节点和结束节点为结束节点
                    return true;
                }
                return false;
            }).collect(Collectors.minBy(Comparator.comparingDouble(BranchBound::getArrayNodeFee))).get();
            System.out.println(minBB.getNodeNo()+"-"+minBB.getArrayNodeNo());
            pathList.add(minBB);
            nowNodeTransToStartNode(minBB, pathList, restNodes, bestList, bbList);
        }
        System.out.println("==================结束");
    }
}

以上就是所有的分支定界法-旅行商(TSP)问题的核心代码,还是那种句算法就是一个思想的结晶,当你去思考后你才能发现它的美,以及美中不足的地方。小伙伴们还是自己去编写一遍吧,还是很有意思的。如果想要源码连接在此。代码

    原文作者:分支限界法
    原文地址: https://blog.csdn.net/MRA__S__/article/details/84779456
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞