算法:用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]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章