算法:用Java尝试解决图论算法中的网络流问题,以求得最大流

这篇博文,我尝试用Java解决图论算法中的网络流问题,以求得最大流的流量和与其对应的所有增广路径组合(认识什么是网络流问题和最大流:点我学习)。这次要解决的就是下面这幅网络流图的最大流:
在这里插入图片描述
我们构建上面网络流图的邻接矩阵,上面的图可以看做是一个有向图,其邻接矩阵如下:
在这里插入图片描述
我的解决思路如下:

  1. 首先得到示例网络流图中的所有从S到T的路径(也叫增广路径,就是在尝试减掉S到T的路径的过程中有效减去的路径)
  2. 开始依次尝试放入路径,并预先算出该增广路径所有边的权重最小值
  3. 如果增广路径的边的权重最小值大于零,则加入该增广路径,最大流量增加这个最小值,然后该增广路径上的所有边的权重都减去这个最小值(在邻接矩阵里面实现),此时该增广路径上至少有一条边的权重为0,我们认为此时权重为0的边已经不存在于网络流图中了
  4. 如果增广路径的边的权重最小值等于零,则这条增广路径不可用,不加入该增广路径,最大流量也不变
  5. 增广路径不重复使用,如果之前已经加入过,则之后不再考虑该条增广路径
  6. 求出了一条有效的增广路径并加入了之后,就计算剩下的所有没尝试过的(当前分支没有加入的)、且边的权重最小值大于零的增广路径,遍历递归调用本算法
  7. 到了算法的某一时刻,剩下的所有增广路径的边的权重最小值都等于零,则结束本次递归分支
  8. 当所有的递归分支全部结束之后,程序输出计算结果,得到示例网络流图的最大流量值,以及组成最大流量的所有增广路径顺序集合(增广路径的顺序集合不一定唯一,可能存在多种组合情况)

由于示例网络流图比较复杂,所以输出的结果很多。我把执行Java算法程序的cmd命令,放在了命令行窗口执行,并将控制台输出结果重定位到了文件里。所以下面我的实现代码,你在控制台里面执行会打印很多东西,导致你都看不到全部的输出。你也可以在命令行窗口里面执行算法,然后重定位控制台输出结果。具体做法如下:

C:\Users\Administrator\Desktop>{Java执行代码} > {输出文件的全路径}

好了,下面是我用Java实现的求上面示例网络流图的最大流,以及组成该最大流的增广路径的组合。算法的思路和精髓都在代码和其间的详细注释中:

import java.util.*;

/**
 * @author LiYang
 * @ClassName NetworkFlowProblem
 * @Description 网络流问题解决实现类
 * @date 2019/11/19 13:19
 */
public class NetworkFlowProblem {

    /**
     * 返回示例网络流图的邻接矩阵
     * @return 示例网络流图的邻接矩阵
     */
    public static int[][] initNetworkFlowMatrix() {
        //创建示例网络流图的邻接矩阵
        return new int[][] {
                {0, 29, 23, 0, 0, 0, 0},
                {0, 0, 7, 7, 6, 0, 0},
                {0, 0, 0, 5, 10, 0, 0},
                {0, 0, 0, 0, 6, 13, 11},
                {0, 4, 0, 8, 0, 7, 8},
                {0, 0, 0, 0, 0, 0, 17},
                {0, 0, 0, 0, 0, 0, 0}
        };
    }

    /**
     * 增广路径类
     */
    static class ArgmentingPath {
        
        //增广路径的顶点顺序
        public List<Integer> vertexIndexList = new ArrayList<>();

        /**
         * 深度拷贝增广路径类的实例
         * @param argmentingPath 待拷贝的增广路径类实例
         * @return 拷贝的增广路径类实例副本
         */
        static ArgmentingPath copyPath(ArgmentingPath argmentingPath) {
            //新建增广路径类实例
            ArgmentingPath newArgmentingPath = new ArgmentingPath();
            
            //将路径顶点顺序逐一拷贝
            for (Integer order : argmentingPath.vertexIndexList) {
                newArgmentingPath.vertexIndexList.add(order);
            }
            
            //返回拷贝副本
            return newArgmentingPath;
        }

        /**
         * 根据当前的邻接矩阵,计算增广路径上面的所有边的最小权重
         * @param matrix 当前的邻接矩阵
         * @return 该增广路径的所有边的最小权重
         */
        public int calcFlowVolume(int[][] matrix) {
            if (vertexIndexList.size() < 2) {
                throw new IllegalStateException("增广路径太短,状态不正常,请检查");
            }
            
            //先将最小权重设为无穷大
            int flowVolume = Integer.MAX_VALUE;

            //遍历增广路径的所有边
            for (int i = 1; i < vertexIndexList.size(); i++) {
                
                //取出当前边的两个顶点
                int startVertex = vertexIndexList.get(i - 1);
                int endVertex = vertexIndexList.get(i);
                
                //将当前边的权重,与最小流量做min运算,哪个小就赋给最小权重
                flowVolume = Math.min(matrix[startVertex][endVertex], flowVolume);
            }
            
            //最终求得增广路径在当前邻接矩阵的最小权重,返回
            return flowVolume;
        }

        /**
         * 在当前邻接矩阵中,做减去该增广路径的操作
         * 具体做法是,求出该路径最小权重,所有边都减去该权重值
         * @param matrix 当前的邻接矩阵
         * @return 减去路径最小权重后的邻接矩阵,是独立的副本
         */
        public int[][] decreaseMatrixPath(int[][] matrix) {
            //求出该增广路径的最小权重
            int flowVolumeToBeDecreased = calcFlowVolume(matrix);
            
            //创建新的邻接矩阵
            int[][] newMatrix = new int[matrix.length][matrix.length];
            
            //拷贝原来的邻接矩阵
            for (int i = 0; i < matrix.length; i++) {
                for (int j = 0; j < matrix.length; j++) {
                    newMatrix[i][j] = matrix[i][j];
                }
            }

            //遍历增广路径的所有边
            for (int i = 1; i < vertexIndexList.size(); i++) {

                //取出当前边的两个顶点
                int startVertex = vertexIndexList.get(i - 1);
                int endVertex = vertexIndexList.get(i);
                
                //将所有边,都减去路径当前邻接矩阵的最小权重
                newMatrix[startVertex][endVertex] = newMatrix[startVertex][endVertex] 
                        - flowVolumeToBeDecreased;
            }
            
            //返回减去路径最小权重后的邻接矩阵,是独立的副本
            return newMatrix;
        }
    }

    /**
     * 根据示例网络流图的邻接矩阵,求得所有的增广路径
     * 我们用深度优先遍历(DFS)来求增广路径,从源点S出发,
     * 如果到了汇点T,则路径求得。如果某一时刻没有下一个
     * 顶点,且最后一个顶点并不是汇点T,则不是增广路径
     * @param matrix 示例网络流图的邻接矩阵
     * @return 示例网络流图的所有增广路径,所有增广路径
     *               均已编号,并且为Map的key
     */
    public static Map<Integer, ArgmentingPath> getAllArgmentingPath(int[][] matrix) {
        //示例网络流图的所有S到T的路径List
        List<ArgmentingPath> argmentingPathList = new ArrayList<>();
        
        //运用深度优先搜索DFS,递归求出所有的S到T的路径(增广路径)
        argmentingPathDFS(matrix, new ArgmentingPath(), 0, argmentingPathList);
        
        //编号与路径的Map
        Map<Integer, ArgmentingPath> argmentingPathMap = new HashMap<>();
        
        //从1开始,编号所有的增广路径
        for (int i = 0; i < argmentingPathList.size(); i++) {
            argmentingPathMap.put(i + 1, argmentingPathList.get(i));
        }
        
        //返回编号与增广路径的Map
        return argmentingPathMap;
    }

    /**
     * 运用深度优先搜索DFS,递归求出示例网络流图的所有S到T的路径(增广路径)
     * @param matrix 当前的邻接矩阵
     * @param currentPath 当前的排序组合
     * @param nextOrder 下一个将要执行的顶点下标
     * @param pathList 记录完成的增广路径的List
     */
    public static void argmentingPathDFS(int[][] matrix, ArgmentingPath currentPath, 
                                         int nextOrder, List<ArgmentingPath> pathList) {
        
        //如果到了汇点T
        if (nextOrder == matrix.length - 1) {
            
            //路径顶点组合加入T
            currentPath.vertexIndexList.add(nextOrder);
            
            //将完成的增广路径,加入pathList
            pathList.add(currentPath);
            
            //结束递归
            return;
        }
        
        //装入下一个顶点
        currentPath.vertexIndexList.add(nextOrder);
        
        //求出再下一个顶点的下标值集合
        List<Integer> nextVertexList = new ArrayList<>();

        //通过当前的邻接矩阵,求出再下一个顶点的下标值集合
        for (int i = 0; i < matrix.length; i++) {
            
            //网络流图的所有边权重都大于零
            //等于0的都是不存在的边,或者是到自己,都不考虑
            if (matrix[nextOrder][i] > 0) {
                nextVertexList.add(i);
            }
        }
        
        //如果已经没有下一个顶点了,且nextOrder不是汇点
        if (nextVertexList.size() == 0 && nextOrder != matrix.length - 1) {
            //那就是找到了偏门路径,结束递归
            return;
        }
        
        //将所有的下一次递归的顶点进行遍历递归
        for (Integer next : nextVertexList) {
            
            //如果下一个顶点是没有遍历过的
            if (!currentPath.vertexIndexList.contains(next)) {
                
                //才新建增广路径类
                ArgmentingPath newArgmentingPath = ArgmentingPath.copyPath(currentPath);
                
                //并采用深度优先遍历DFS方式来寻找增广路径
                argmentingPathDFS(matrix, newArgmentingPath, next, pathList);
            }
        }
    }

    /**
     * 深度拷贝Set<Integer>对象
     * @param source 拷贝源
     * @return 深度拷贝的副本
     */
    public static Set<Integer> copySet(Set<Integer> source) {
        //拷贝副本
        Set<Integer> copy = new HashSet<>();
        
        //加入所有元素
        for (Integer item : source) {
            copy.add(item);
        }
        
        //返回拷贝副本
        return copy;
    }

    /**
     * 深度拷贝邻接矩阵二维数组对象
     * @param matrix 邻接矩阵拷贝源
     * @return 深度拷贝的邻接矩阵副本
     */
    public static int[][] copyMatrix(int[][] matrix) {
        //拷贝副本
        int[][] newMatrix = new int[matrix.length][matrix.length];
        
        //复制所有值
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix.length; j++) {
                newMatrix[i][j] = matrix[i][j];
            }
        }

        //返回拷贝副本
        return newMatrix;
    }

    /**
     * 递归求解示例网络流图的最大流流量
     * @param allArgmentingPath 增广路径的编号Map,通过编号查找增广路径
     * @param decreasedPath 当前的增广路径编号的组合顺序
     * @param nextpath 下一个将要尝试的增广路径编号
     * @param matrix 当前的临界矩阵
     * @param flowVolume 当前的累加最大流流量
     * @param results 算法结果描述字符串的集合(有重复,需要Set去重)
     */
    public static void calcMaxFlowVolume(Map<Integer, ArgmentingPath> allArgmentingPath, Set<Integer> decreasedPath, 
                                         int nextpath, int[][] matrix, int flowVolume, Set<String> results) {
        
        //根据编号,得到当前需要尝试的增广路径
        ArgmentingPath argmentingPath = allArgmentingPath.get(nextpath);
        
        //算出当前增广路径的最小权重,也就是即将在邻接矩阵中路径消减的量
        int decreasedAmount = argmentingPath.calcFlowVolume(matrix);
        
        //最大流流量累加decreasedAmount
        flowVolume = flowVolume + decreasedAmount;

        //邻接矩阵中消减当前增广路径的量,返回独立的消减后的邻接矩阵
        int[][] decreasedMatrix = argmentingPath.decreaseMatrixPath(matrix);
        
        //当前增广路径消减后,加入组合顺序集合
        decreasedPath.add(nextpath);
        
        //计算剩下可以继续的增广路径
        List<Integer> availablePath = new ArrayList<>();

        //遍历所有的增广路径
        for (int i = 1; i <= allArgmentingPath.size(); i++) {
            
            //如果是已经计算过的增广路径,则略过,不重复计算
            if (decreasedPath.contains(i)) {
                continue;
            }
            
            //如果该增广路径在当前的邻接矩阵中,可以消减一定的量
            if (allArgmentingPath.get(i).calcFlowVolume(decreasedMatrix) > 0) {
                
                //才作为继续计算的路径,否则就略过
                availablePath.add(i);
            }
        }
        
        //如果接下来存在可以继续消减的增广路径
        if (availablePath.size() > 0) {
            
            //遍历所有的可以继续消减的增广路径
            for (Integer nextPath : availablePath) {
                
                //递归调用,继续往下消减
                calcMaxFlowVolume(allArgmentingPath, copySet(decreasedPath), nextPath, 
                        copyMatrix(decreasedMatrix), flowVolume, results);
            }
            
        //如果没有可以继续消减的增广路径,当前的邻接矩阵
        //(也就是对应的网络流图)已经消减不动了
        } else {
            
            //完成当前增广路径组合顺序的计算,并将该路径组合
            //消减掉的总流量和,加上路径组合编辑为字符串记录
            String result = String.format("最大流为:%d,用到的所有增广路径:%s", flowVolume, decreasedPath.toString());
            
            //记录本次组合的结果
            results.add(result);
        }
    }

    /**
     * 求解示例网络流图的最大流流量的驱动程序
     * @param allArgmentingPath 增广路径的编号Map,通过编号查找增广路径
     * @param matrix 示例网络流图的初始邻接矩阵
     * @return 算法结果描述字符串的集合
     */
    public static Set<String> calcMaxFlowVolume(Map<Integer, ArgmentingPath> allArgmentingPath, int[][] matrix) {
        //最后记录算法结果描述字符串的集合
        Set<String> results = new HashSet<>();
        
        //遍历所有的增广路径
        for (int i = 1; i <= allArgmentingPath.size(); i++) {
            
            //将所有的增广路径,每个都作为组合的第一个,驱动递归算法
            calcMaxFlowVolume(allArgmentingPath, new HashSet<>(), i, matrix, 0, results);
            
            //输出程序进度
            System.out.println(String.format("%d开头的路径已计算完毕,共有%d条路径……", i, allArgmentingPath.size()));
        }
        
        //返回算法结果描述字符串的集合
        return results;
    }

    /**
     * 运行求示例网络流图的最大流流量和增广路径组合的算法程序
     * @param args
     */
    public static void main(String[] args) {
        //示例图的邻接矩阵
        int[][] matrix = initNetworkFlowMatrix();
        
        //所有编好号的增广路径Map
        Map<Integer, ArgmentingPath> allArgmentingPath = getAllArgmentingPath(matrix);

        //递归求出示例网络流图的最大流流量和增广路径组合
        Set<String> results = calcMaxFlowVolume(allArgmentingPath, matrix);
        
        //整理结果字符串
        List<String> resultList = new ArrayList<>();
        
        //先将结果装入List
        resultList.addAll(results);
        
        //将结果字符串排序
        Collections.sort(resultList);
        
        //将List反转,然后最大流量大的,就在前面了
        Collections.reverse(resultList);

        System.out.println();
        System.out.println("=================算法结束=================");
        
        //输出所有的最大流流量和增广路径组合
        for (String result : resultList) {
            System.out.println(result);
        }

        System.out.println("==========================================");
        
        //示例网络流图的顶点名字
        String[] vertexName = new String[]{"S", "A", "B", "C", "D", "E", "T"};
        
        //遍历增广路径Map
        for (Map.Entry<Integer, ArgmentingPath> item : allArgmentingPath.entrySet()) {
            //路径名List
            List<String> vertexNameList = new ArrayList<>();
            
            //将路径顶点下标顺序,变成路径顶点名称顺序
            for (Integer index : item.getValue().vertexIndexList) {
                vertexNameList.add(vertexName[index]);
            }
            
            //输出所有的增广路径,以及对应的编号,好对比上面的输出结果
            System.out.println(String.format("%d号路径:%s", item.getKey(), vertexNameList));
        }
    }
    
}

因为本次求网络流的最大流的示例图有些复杂,顶点多且边也多,所以我运行后输出的内容太多了。我按照上面的方法,在cmd的命令行窗口里面执行上述算法代码,然后将控制台输出结果重定向到了文件中。下面是我摘选的输出内容的一部分:最大的流量为28(这就是我们需要的答案,示例网络流图的最大流量值),最小的流量为24,每一种流量总和值都有多种增广路径组合顺序,我只是从中摘选了一部分。最后我也给出了所有增广路径的编号对应的路径图,大家可以结合着看,再对比示例的网络流图,算法运行测试通过:

=================算法结束=================
最大流为:28,用到的所有增广路径:[4, 20, 5, 26, 11, 15]
最大流为:28,用到的所有增广路径:[3, 5, 26, 12, 15]
最大流为:28,用到的所有增广路径:[20, 25, 26, 11, 13]
最大流为:28,用到的所有增广路径:[2, 3, 23, 8, 12, 15]
最大流为:28,用到的所有增广路径:[19, 6, 9, 26, 12, 13]
最大流为:28,用到的所有增广路径:[16, 1, 17, 5, 8, 24, 12]

最大流为:27,用到的所有增广路径:[3, 20, 22, 25, 26, 12, 15]
最大流为:26,用到的所有增广路径:[19, 4, 21, 23, 10, 15]
最大流为:25,用到的所有增广路径:[16, 19, 21, 6, 9]
最大流为:24,用到的所有增广路径:[1, 18, 22, 23, 12, 15]
==========================================
1号路径:[S, A, B, C, D, E, T]
2号路径:[S, A, B, C, D, T]
3号路径:[S, A, B, C, E, T]
4号路径:[S, A, B, C, T]
5号路径:[S, A, B, D, C, E, T]
6号路径:[S, A, B, D, C, T]
7号路径:[S, A, B, D, E, T]
8号路径:[S, A, B, D, T]
9号路径:[S, A, C, D, E, T]
10号路径:[S, A, C, D, T]
11号路径:[S, A, C, E, T]
12号路径:[S, A, C, T]
13号路径:[S, A, D, C, E, T]
14号路径:[S, A, D, C, T]
15号路径:[S, A, D, E, T]
16号路径:[S, A, D, T]
17号路径:[S, B, C, D, E, T]
18号路径:[S, B, C, D, T]
19号路径:[S, B, C, E, T]
20号路径:[S, B, C, T]
21号路径:[S, B, D, A, C, E, T]
22号路径:[S, B, D, A, C, T]
23号路径:[S, B, D, C, E, T]
24号路径:[S, B, D, C, T]
25号路径:[S, B, D, E, T]
26号路径:[S, B, D, T]
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值