1. 本题相关资料
这是一道求最大流的模板题。
最大流预备知识:dfs,bfs,图论基础(图的存储)。
网上已经有很多最大流的解析,我就不再唠叨了,还不了解的同学可参考这个博客:什么是最大流及常用求解算法。
2. 本题坑点
- 一条边可能会出现多次,所以这里用加等于
map[sc.nextInt()][sc.nextInt()] += sc.nextInt();
- 测试点4数据错误,m的值为10000,实际上只有1000条边,可以用StreamTokenizer输入数据,可过100%测试点。(我代码中没有用这个,因为在自己测试的时候不方便)
这里吐槽一下蓝桥杯练习系统
构建图时:while (in.nextToken()!=StreamTokenizer.TT_EOF)...
3. 代码实现
1. FF(AC-750ms):
就是用dfs查找增广路,更新反向边,反复。
这里我用的是非递归版本,一开始用递归超时,也可能是我代码有问题吧,有兴趣可自行尝试。当然竞赛的话还是上Dinic吧,快太多了。
import java.io.BufferedInputStream;
import java.util.Arrays;
import java.util.Scanner;
import java.util.Stack;
public class 网络流裸题_FF_非递归 {
static int[][] map;//图
static int[] pre;//记录dfs路径,存储dfs搜索树,从汇点可获取路径。
static int n, m;
public static void main(String[] args) {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
n = sc.nextInt();
m = sc.nextInt();
map = new int[n + 1][n + 1];
pre=new int[n+1];
for (int i = 0; i < m; i++) {
map[sc.nextInt()][sc.nextInt()] += sc.nextInt();
}
sc.close();
int res = 0;
int r=0;
while ((r=dfs(1))>0) {
res += r;
int a=n,b=pre[n];
while(a!=1) {
map[a][b]+=r;
map[b][a]-=r;
a=b;
b=pre[b];
}
}
System.out.println(res);
}
// 非递归版
static int dfs(int x) {
Stack<Integer> s = new Stack<>();
boolean vis[]=new boolean[n+1];
s.push(x);
int[] flow=new int[n+1];//记录到每个节点的最大流
flow[1]=0x3f3f3f3f;
while (!s.isEmpty()) {
x=s.pop();
vis[x]=true;
for (int i = 0; i <= n; i++) {
if (!vis[i] && map[x][i] > 0) {
pre[i] = x;
flow[i]=Math.min(flow[x], map[x][i]);
if (i == n)
return flow[n];
s.push(i);
}
}
}
return -1;
};
}
2. EK(AC-670ms):
用bfs代替dfs,因为bfs每次找到的总是最短路径,所以比dfs效率要高些。
这个代码是粘的本站某位博主的,由于后来找不到了,就没贴链接,我自己也写了EK的,但改来改去总是接近超时,直接干Dinic去了。
package 网络流;
import java.util.ArrayDeque; //类
import java.util.Queue; //接口
import java.util.Scanner;
public class Main {
public static int n;
public static int m;
public static void main(String[] args) {
Scanner scn = new Scanner(System.in);
// 输入结点个数和边的个数
n = scn.nextInt();
m = scn.nextInt();
// 因为第一个结点是1不是0,所以我们多申请一些空间,索引为0的位置就不使用了。从索引为1的位置开始放
int[][] edge = new int[n + 1][n + 1];
for (int i = 0; i < m; i++) {
int vertex1 = scn.nextInt();
int vertex2 = scn.nextInt();
int weight = scn.nextInt();
edge[vertex1][vertex2] += weight;// 一条边可能会出现多次
}
// path[i]是第i个结点的父节点。我们通过在BFS(广度优先遍历)中不断更新path[i]里的数,从而保存一条路径。
int[] path = new int[n + 1];
int flow = 0;
while (BFSfindPath(edge, 1, n, path)==1) {
// 如果能够找到增广路径,那么我们在该路径上能够通过的容量就是这个路径上能通过的最小容量。
int min = edge[path[n]][n];
for (int i = path[n]; i != 1; i = path[i]) {
if (edge[path[i]][i] < min && edge[path[i]][i] > 0) {
min = edge[path[i]][i];
}
}
flow += min;
for (int i = n; i != 1; i = path[i]) {
int j = path[i];
edge[j][i] -= min;
edge[i][j] += min;
}
}
System.out.println(a);
System.out.println(flow);
}
static long a=0;
private static int BFSfindPath(int[][] edge, int s, int t, int[] path) {
// path[]是通过记录每个结点的父节点,从而记录下一条完整的路径。
long b=System.currentTimeMillis();
for (int i = 0; i < path.length; i++) {
path[i] = 0;
}
int vertex_num = edge.length;
// 用于标记是否已经访问过该结点
int[] visited = new int[vertex_num];
Queue<Integer> q = new ArrayDeque<Integer>();
q.add(s); //将指定元素插入此双端队列的末尾
visited[s] = 1;
while (!q.isEmpty() ) {
int w = q.poll(); //获取并移除此双端队列所表示的队列的头
for (int i = 1; i < vertex_num; i++) {
if (edge[w][i] > 0 && visited[i] == 0) {
q.add(i);
visited[i] = 1;
path[i] = w;
}
}
}
a=a+(System.currentTimeMillis()-b);
System.out.println(System.currentTimeMillis()-b);
return visited[t];
}
}
3. Dinic(AC-100ms):
参考大佬的Dinic解析,不过是C语言
Dinic的主要思想:
- 分层
用bfs遍历整个图,使用deep数组存储每个节点的深度,然后用dfs查找最大流,dfs时下一个节点必须是当前节点层数+1,大大加快dfs效率。 - dfs优化
而这里的dfs也和FF中的不同,dfs中参数maxL表示到当前节点的最大流,到达汇点后回溯到这个节点,若到汇点的最大流减去到当前节点的最大流大于0,则说明还可以从当前节点继续搜索,知道当前节点流量全部被瓜分或到达不了汇点为止;这样,一次dfs可发挥最大价值。 - 当前弧优化
用一个cur数组存储一次dfs中每个点遍历到了那一条边,下次碰到这个点时就从这条边开始,因为遍历过的边已经发挥了他最大的价值,已经没有在遍历的必要了。
import java.io.IOException;
import java.io.StreamTokenizer;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Queue;
public class Dinic {
static int n, m;//节点数和边数
static int edge_num = -1;
static Edge[] edge;//所有边
static int[] deep;//节点深度
static int[] head, cur; //head存储节点的最后一条边在edge中的索引,cur用于备份head,当前弧优化中需改变cur
static Queue<Integer> q = new ArrayDeque<>();
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(System.in);
in.nextToken();
n = (int) in.nval;
in.nextToken();
m = (int) in.nval;
edge = new Edge[m << 1];
head = new int[n + 1];
deep = new int[n + 1];
Arrays.fill(head, -1);
for (int i = 0; i < m; i++) {
in.nextToken();
int a = (int) in.nval;
in.nextToken();
int b = (int) in.nval;
in.nextToken();
add(a, b, (int) in.nval);
add(b, a, 0);//这里同时增加正边和反边,后面反向边增加的时候方便,异或即可。
}
System.out.println(dinic());
}
static int dinic() {
int ans = 0;
while (bfs())
ans += dfs(1, Integer.MAX_VALUE);
return ans;
}
//寻找增广路
static int dfs(int x, int maxL) {
int res = 0;
int r = 0;
if (maxL == 0 || x == n) {
return maxL;
}
for (int i = cur[x]; i != -1; i = edge[i].next) {
cur[x] = i;
if (deep[edge[i].to] == deep[x] + 1 && (r = dfs(edge[i].to, Math.min(maxL, edge[i].val))) > 0) {
res += r;
maxL -= r;
edge[i].val -= r;
edge[i ^ 1].val += r;
if (maxL == 0)
break;
}
}
return res;
}
//分层
static boolean bfs() {
q.clear();
q.add(1);
Arrays.fill(deep, 0);
cur = head.clone();//拷贝
deep[1] = 1;
while (!q.isEmpty()) {
int a = q.poll();
for (int i = head[a]; i != -1; i = edge[i].next) {
if (deep[edge[i].to] == 0 && edge[i].val > 0) {
q.add(edge[i].to);
deep[edge[i].to] = deep[a] + 1;
}
}
}
return deep[n] != 0;
}
//构建图
private static void add(int a, int b, int c) {
edge[++edge_num] = new Edge(head[a], b, c);
head[a] = edge_num;
}
static class Edge {
int next;
int to;
int val;
public Edge(int next, int to, int val) {
this.next = next;
this.to = to;
this.val = val;
}
public Edge() {
}
}
}