文章目录
前言
由于图论学习总结内容过多,全放在一篇博客过于冗长现进行拆分,本文是网络流初步部分,其他部分地址见:图论学习总结(For XCPC)
注:在本篇博客中,我并没有介绍求最大流和费用流的方法,主要聚焦于一些常用的网络流建模方法,同时对于问题的建模也是解决网络流问题的核心。
五、网络流初步
- 网络流的核心在于建图。建图是精髓也是难点。
- 网络流的建图方法一定程度上刻画了贪心问题的内在性质,从而简便地支持了反悔,不需要我们为每道贪心问题都寻找反悔策略。
最大流(Maximum flow,简称 M F MF MF)
e g 1 : eg1: eg1: P2764【网络流24题】最小路径覆盖问题 - 洛谷
题目大意
给定有向图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E) 。设
P
P
P 是
G
G
G 的一个简单路(顶点不相交)的集合。如果
V
V
V 中每个定点恰好在
P
P
P 的一条路上,则称
P
P
P 是
G
G
G 的一个路径覆盖。
P
P
P 中路径可以从
V
V
V 的任何一个定点开始,长度也是任意的,特别地,可以为
0
0
0。
G
G
G 的最小路径覆盖是
G
G
G 所含路径条数最少的路径覆盖。设计一个有效算法求一个 DAG(有向无环图)
G
G
G 的最小路径覆盖。
输入格式
第一行有两个正整数
n
n
n 和
m
m
m。
n
n
n 是给定 DAG(有向无环图)
G
G
G 的顶点数,
m
m
m 是
G
G
G 的边数。接下来的
m
m
m 行,每行有两个正整数
i
i
i 和
j
j
j 表示一条有向边
(
i
,
j
)
(i,j)
(i,j)。
输出格式
从第一行开始,每行输出一条路径。文件的最后一行是最少路径数。
提示
对于
100
%
100\%
100% 的数据,
1
≤
n
≤
150
1\leq n\leq 150
1≤n≤150,
1
≤
m
≤
6000
1\leq m\leq 6000
1≤m≤6000。
由 @FlierKing 提供 SPJ
题目解析
- 在最小路径覆盖问题中,每个点只能有一个前驱和一个后继。在这种情况下,我们可以把一个点拆成一个入点和一个出点。 a a a→ b b b 有边则 a 出 a出 a出→ b 入 b入 b入,容量为 1。
- S向出点点集连容量为1的边,入点点集向T连边,若a出到b入流过了1的流量,那么在路径覆盖中b在a的后面,b不是路径的起点。
- 若S到T的最大流为x,则有x个点不是路径覆盖的起点,但剩下的 n − x n-x n−x个点一定是起点,所以 a n s = n − x ans=n-x ans=n−x。
启发:对于限制了点的出入度都为1的时候,我们可以将一个点拆成一个入点和一个出点
ac代码参考
import sys
from types import GeneratorType
from math import inf
from collections import deque
def bootstrap(f, stack=[]):
def wrappedfunc(*args, **kwargs):
if stack:
return f(*args, **kwargs)
else:
to = f(*args, **kwargs)
while True:
if type(to) is GeneratorType:
stack.append(to)
to = next(to)
else:
stack.pop()
if not stack:
break
to = stack[-1].send(to)
return to
return wrappedfunc
input = lambda : sys.stdin.readline().rstrip("\r\n")
out = sys.stdout.write
def S():
return input()
def I():
return int(input())
def MI():
return map(int, input().split())
def MF():
return map(float, input().split())
def MS():
input().split()
def LS():
return list(input().split())
def LI():
return list(MI())
def print1(x):
return out(str(x)+"\n")
def print2(x,y):
return out(str(x)+" "+str(y)+"\n")
def print3(x,y,z):
return out(str(x) + " " + str(y) + " " + str(z) + "\n")
def Query(i,j):
print3('?',i,j)
sys.stdout.flush()
qi = I()
return qi
def print_arr(arr):
out(' '.join(map(str,arr)))
out(str("\n"))
#---------------------------------#----------------------------#
# Dinic算法求最大流
# 用法示例
# 创建一个 Maxflow 实例
# mf = Maxflow(100) # n 是节点数
# 添加边,例如:
# mf.add_edge(u, v, cap)
# 计算最大流
# max_flow = mf.max_flow(source, sink)
class Maxflow:
# n 节点数, 从0开始编号
def __init__(self, n):
self.n = n
self.G = [[] for _ in range(n)]
self.depth = [0] * n
# cur[u] 表示当前弧下标
self.cur = [0] * n
# 添加一条从 u 到 v 的容量为 cap 的边
def add_edge(self, u, v, cap):
# 正向边 u -> v
self.G[u].append([v, cap, len(self.G[v])]) # 分别存(该边的终点,容量,反向边的idx)
# 反向边 v -> u,初始容量为 0
self.G[v].append([u, 0, len(self.G[u]) - 1]) # 分别存(该边的终点,容量,正向边的idx)
# 从源点出发进行一次bfs,计算出每个点的深度
# 对当前残差图的BFS构造一个level图(或为顶点分配深度), 并检查是否更多的流量是可能的。
def bfs(self, s, t):
self.depth = [-1] * self.n
self.depth[s] = 0
q = deque([s])
while q:
u = q.popleft()
for v, cap, _ in self.G[u]:
if self.depth[v] == -1 and cap > 0:
self.depth[v] = self.depth[u] + 1
q.append(v)
return self.depth[t] != -1
# 从 u 出发,沿着深度优先搜索的路径,不断增广
def dfs(self, u, t, f=inf):
if u == t:
return f
max_flow = 0
while self.cur[u] < len(self.G[u]):
v, cap, rev = self.G[u][self.cur[u]]
# 只有当 u 的深度加 1 之后等于 v 的深度时,我们才让他流 [不能走回头路]
if cap > 0 and self.depth[v] == self.depth[u] + 1:
# 流多少呢,能装多少流就流多少
df = self.dfs(v, t, min(f - max_flow, cap))
if df > 0:
self.G[u][self.cur[u]][1] -= df
self.G[v][rev][1] += df
max_flow += df
if max_flow == f:
break
else:
self.depth[v] = 0
# 只有当前面的边都已经流满/不可达,我们才更新 cur[u],下一次直接从第cur[u]条边开始枚举
self.cur[u] += 1
return max_flow
# s 是源点,t 是汇点
def max_flow(self, s, t):
total_flow = 0
# 当找不到增广路时,bfs 返回 false
while self.bfs(s, t):
self.cur = [0] * self.n
flow = self.dfs(s, t)
while flow > 0:
total_flow += flow
flow = self.dfs(s, t)
return total_flow
# 用手写栈优化dfs
@bootstrap
def dfs_bootstrap(self, u, t, f=inf):
if u == t:
yield f
max_flow = 0
while self.cur[u] < len(self.G[u]):
v, cap, rev = self.G[u][self.cur[u]]
# 只有当 u 的深度加 1 之后等于 v 的深度时,我们才让他流 [不能走回头路]
if cap > 0 and self.depth[v] == self.depth[u] + 1:
# 流多少呢,能装多少流就流多少
df = yield self.dfs_bootstrap(v, t, min(f - max_flow, cap))
if df > 0:
self.G[u][self.cur[u]][1] -= df
self.G[v][rev][1] += df
max_flow += df
if max_flow == f:
break
else: self.depth[v] = 0
# 只有当前面的边都已经流满/不可达,我们才更新 cur[u],下一次直接从第cur[u]条边开始枚举
self.cur[u] += 1
yield max_flow
def max_flow_bootstrap(self, s, t):
total_flow = 0
# 当找不到增广路时,bfs 返回 false
while self.bfs(s, t):
self.cur = [0] * self.n
flow = self.dfs_bootstrap(s, t)
while flow > 0:
total_flow += flow
flow = self.dfs_bootstrap(s, t)
return total_flow
def solve():
def path(x,flag):
if flag: res.insert(0,x+1)
else: res.append(x+1)
vis[x] = 1
for y,cap,rev in mf.G[x]:
if y == s or y == t: continue
y = y-n
if not cap and not vis[y]: path(y,0)
for y,cap,rev in mf.G[x+n]:
if y == s or y == t: continue
if cap and not vis[y]: path(y,1)
n,m = MI()
mf = Maxflow(n+n+2)
s,t = n+n,n+n+1
for _ in range(m):
x,y = MI()
mf.add_edge(x-1,n+y-1,1)
for i in range(n):
mf.add_edge(s,i,1)
mf.add_edge(i+n,t,1)
ans = n-mf.max_flow_bootstrap(s,t)
vis = [0]*(n+1)
for i in range(n):
if not vis[i]:
res = []
path(i,0)
print_arr(res)
print1(ans)
solve()
e g 2 : eg2: eg2: P2765【网络流24题】魔术球问题 - 洛谷
题目大意
假设有
n
n
n 根柱子,现要按下述规则在这
n
n
n 根柱子中依次放入编号为
1
1
1,
2
2
2,
3
3
3,…的球
-
每次只能在某根柱子的最上面放球。
-
同一根柱子中,任何 2 2 2 个相邻球的编号之和为完全平方数。
试设计一个算法,计算出在 n n n 根柱子上最多能放多少个球。例如,在 4 4 4 根柱子上最多可放 11 11 11 个球。
对于给定的 n n n,计算在 n n n 根柱子上最多能放多少个球。
输入格式
只有一行一个整数 n n n,代表柱子数。
输出格式
本题存在 Special Judge。
请将
n
n
n 根柱子上最多能放的球数以及相应的放置方案输出。
输出的第一行是球数。
接下来的 n n n 行,每行若干个整数,代表一根柱子上的球的编号,数字间用单个空格隔开。
数据规模与约定
对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 55 1 \le n \le 55 1≤n≤55。
题目解析
本题如果把能相邻的球连边,就是最小路径覆盖问题。但本题求的是
n
n
n 条路径最多能容纳多少个点。在本题中,如果一个球可放在多根柱子上,通过后面的寻找增广路的过程也可以反悔(退流),以贪心的求得最优解。
所以我们可以一个点一个点的增加,如果加了一个点流量+1,则不用新增柱子(其可以放到一个另一个球的上面),否则加一根柱子,直到柱子加到n。
每加入一个数,最大流要么不变,要么+1
- MF+1:之前某个无后继的出点流了1流量到该新点(不增加柱子)。
- MF不变:新点接不到之前任何一个点后面(放不到任何一个柱子上)。
ac代码参考:
import sys
from types import GeneratorType
from math import inf,sqrt
from collections import deque
def bootstrap(f, stack=[]):
def wrappedfunc(*args, **kwargs):
if stack:
return f(*args, **kwargs)
else:
to = f(*args, **kwargs)
while True:
if type(to) is GeneratorType:
stack.append(to)
to = next(to)
else:
stack.pop()
if not stack:
break
to = stack[-1].send(to)
return to
return wrappedfunc
input = lambda : sys.stdin.readline().rstrip("\r\n")
out = sys.stdout.write
def S():
return input()
def I():
return int(input())
def MI():
return map(int, input().split())
def MF():
return map(float, input().split())
def MS():
input().split()
def LS():
return list(input().split())
def LI():
return list(MI())
def print1(x):
return out(str(x)+"\n")
def print2(x,y):
return out(str(x)+" "+str(y)+"\n")
def print3(x,y,z):
return out(str(x) + " " + str(y) + " " + str(z) + "\n")
def Query(i,j):
print3('?',i,j)
sys.stdout.flush()
qi = I()
return qi
def print_arr(arr):
out(' '.join(map(str,arr)))
out(str("\n"))
#---------------------------------#----------------------------#
# Dinic算法求最大流
# 用法示例
# 创建一个 Maxflow 实例
# mf = Maxflow(100) # n 是节点数
# 添加边,例如:
# mf.add_edge(u, v, cap)
# 计算最大流
# max_flow = mf.max_flow(source, sink)
class Maxflow:
# n 节点数, 从0开始编号
def __init__(self, n):
self.n = n
self.G = [[] for _ in range(n)]
self.depth = [0] * n
# cur[u] 表示当前弧下标
self.cur = [0] * n
# 添加一条从 u 到 v 的容量为 cap 的边
def add_edge(self, u, v, cap):
# 正向边 u -> v
self.G[u].append([v, cap, len(self.G[v])]) # 分别存(该边的终点,容量,反向边的idx)
# 反向边 v -> u,初始容量为 0
self.G[v].append([u, 0, len(self.G[u]) - 1]) # 分别存(该边的终点,容量,正向边的idx)
# 从源点出发进行一次bfs,计算出每个点的深度
# 对当前残差图的BFS构造一个level图(或为顶点分配深度), 并检查是否更多的流量是可能的。
def bfs(self, s, t):
self.depth = [-1] * self.n
self.depth[s] = 0
q = deque([s])
while q:
u = q.popleft()
for v, cap, _ in self.G[u]:
if self.depth[v] == -1 and cap > 0:
self.depth[v] = self.depth[u] + 1
q.append(v)
return self.depth[t] != -1
# 从 u 出发,沿着深度优先搜索的路径,不断增广
def dfs(self, u, t, f=inf):
if u == t:
return f
max_flow = 0
while self.cur[u] < len(self.G[u]):
v, cap, rev = self.G[u][self.cur[u]]
# 只有当 u 的深度加 1 之后等于 v 的深度时,我们才让他流 [不能走回头路]
if cap > 0 and self.depth[v] == self.depth[u] + 1:
# 流多少呢,能装多少流就流多少
df = self.dfs(v, t, min(f - max_flow, cap))
if df > 0:
self.G[u][self.cur[u]][1] -= df
self.G[v][rev][1] += df
max_flow += df
if max_flow == f:
break
else:
self.depth[v] = 0
# 只有当前面的边都已经流满/不可达,我们才更新 cur[u],下一次直接从第cur[u]条边开始枚举
self.cur[u] += 1
return max_flow
# s 是源点,t 是汇点
def max_flow(self, s, t):
total_flow = 0
# 当找不到增广路时,bfs 返回 false
while self.bfs(s, t):
self.cur = [0] * self.n
flow = self.dfs(s, t)
while flow > 0:
total_flow += flow
flow = self.dfs(s, t)
return total_flow
# 用手写栈优化dfs
@bootstrap
def dfs_bootstrap(self, u, t, f=inf):
if u == t:
yield f
max_flow = 0
while self.cur[u] < len(self.G[u]):
v, cap, rev = self.G[u][self.cur[u]]
# 只有当 u 的深度加 1 之后等于 v 的深度时,我们才让他流 [不能走回头路]
if cap > 0 and self.depth[v] == self.depth[u] + 1:
# 流多少呢,能装多少流就流多少
df = yield self.dfs_bootstrap(v, t, min(f - max_flow, cap))
if df > 0:
self.G[u][self.cur[u]][1] -= df
self.G[v][rev][1] += df
max_flow += df
if max_flow == f:
break
else: self.depth[v] = 0
# 只有当前面的边都已经流满/不可达,我们才更新 cur[u],下一次直接从第cur[u]条边开始枚举
self.cur[u] += 1
yield max_flow
def max_flow_bootstrap(self, s, t):
total_flow = 0
# 当找不到增广路时,bfs 返回 false
while self.bfs(s, t):
self.cur = [0] * self.n
flow = self.dfs_bootstrap(s, t)
while flow > 0:
total_flow += flow
flow = self.dfs_bootstrap(s, t)
return total_flow
def solve():
@bootstrap
def path(x):
res.append(x + 1)
vis[x] = 1
for y, cap, rev in mf.G[x]:
if y == s or y == t: continue
y = y - n
if not cap and not vis[y]: yield path(y)
yield None
N = I()
n = 1570
mf = Maxflow(n+n+2)
s,t = n+n,n+n+1
ans,cnt = 0, 0
total_flow = 0
for i in range(n):
ans += 1
mf.add_edge(s,i,1)
mf.add_edge(i+n,t,1)
for j in range(i):
tmp = i+1+j+1
if int(sqrt(tmp))**2 == tmp:
mf.add_edge(j, i+n, 1)
tmp = total_flow
while mf.bfs(s, t):
mf.cur = [0] * mf.n
flow = mf.dfs_bootstrap(s, t)
while flow > 0:
total_flow += flow
flow = mf.dfs_bootstrap(s, t)
if tmp == total_flow: cnt += 1
if cnt > N:
ans -= 1
break
print1(ans)
vis = [0]*(ans+1)
for i in range(ans):
if not vis[i]:
res = [i]
path(i)
print_arr(res)
solve()