这篇博文,我尝试用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]