這篇博文,我嘗試用Java解決圖論算法中的網絡流問題,以求得最大流的流量和與其對應的所有增廣路徑組合(認識什麼是網絡流問題和最大流:點我學習)。這次要解決的就是下面這幅網絡流圖的最大流:
我們構建上面網絡流圖的鄰接矩陣,上面的圖可以看做是一個有向圖,其鄰接矩陣如下:
我的解決思路如下:
- 首先得到示例網絡流圖中的所有從S到T的路徑(也叫增廣路徑,就是在嘗試減掉S到T的路徑的過程中有效減去的路徑)
- 開始依次嘗試放入路徑,並預先算出該增廣路徑所有邊的權重最小值
- 如果增廣路徑的邊的權重最小值大於零,則加入該增廣路徑,最大流量增加這個最小值,然後該增廣路徑上的所有邊的權重都減去這個最小值(在鄰接矩陣裏面實現),此時該增廣路徑上至少有一條邊的權重爲0,我們認爲此時權重爲0的邊已經不存在於網絡流圖中了
- 如果增廣路徑的邊的權重最小值等於零,則這條增廣路徑不可用,不加入該增廣路徑,最大流量也不變
- 增廣路徑不重複使用,如果之前已經加入過,則之後不再考慮該條增廣路徑
- 求出了一條有效的增廣路徑並加入了之後,就計算剩下的所有沒嘗試過的(當前分支沒有加入的)、且邊的權重最小值大於零的增廣路徑,遍歷遞歸調用本算法
- 到了算法的某一時刻,剩下的所有增廣路徑的邊的權重最小值都等於零,則結束本次遞歸分支
- 當所有的遞歸分支全部結束之後,程序輸出計算結果,得到示例網絡流圖的最大流量值,以及組成最大流量的所有增廣路徑順序集合(增廣路徑的順序集合不一定唯一,可能存在多種組合情況)
由於示例網絡流圖比較複雜,所以輸出的結果很多。我把執行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]