之前那篇博客是在入门网络流时写的,现在对网络流重新有了一定的理解。
Ford–Fulkerson 增广,核心即不断找增广路并增广。
// FF brute
#include
#define int long long
using namespace std;
int n, m, s, t, r[205][205], ans;
bool vis[205];
int dfs(int u, int sum) // sum:流入 u 的流量
{
if(sum == 0 or u == t) return sum;
vis[u] = true;
int flow = 0;
for(int i=1; i<=n; i++)
{
if(!vis[i] and r[u][i] > 0)
{
//找 u 到 t 路上的最小限制并增广
int d = dfs(i, min(sum, r[u][i]));
r[u][i] -= d, r[i][u] += d;
flow += d, sum -= d;
}
}
return flow;
}
signed main()
{
cin >> n >> m >> s >> t;
for(int i=1; i<=m; i++)
{
int u, v, w;
cin >> u >> v >> w;
r[u][v] += w;
}
int d;
while(d = dfs(s, 1e9))
{
memset(vis, 0, sizeof(vis));
ans += d;
}
cout << ans;
}
这种方法的复杂度与最大流 f f f 有关,至多执行 f f f 轮 dfs,时间复杂度 O ( n f ) O(nf) O(nf)。
FF 增广的 bfs 实现也称 EK 算法。
// FF EK
#include
#define int long long
using namespace std;
int n, m, s, t, r[205][205], ans, d[205], pre[205];
queue <int> q;
bool bfs()
{
memset(d, 0, sizeof(d)); memset(pre, 0, sizeof(pre));
d[s] = 1, q.push(s);
while(!q.empty())
{
int u = q.front(); q.pop();
for(int i=1; i<=n; i++)
if(d[i] == 0 and r[u][i] > 0)
pre[i] = u, d[i] = d[u] + 1, q.push(i);
}
return d[t];
}
int augment()
{
int flow = 1e9;
for(int a=t; a!=s; a=pre[a]) flow = min(flow, r[pre[a]][a]);
for(int a=t; a!=s; a=pre[a]) r[pre[a]][a] -= flow, r[a][pre[a]] += flow;
return flow;
}
signed main()
{
cin >> n >> m >> s >> t;
for(int i=1; i<=m; i++)
{
int u, v, w;
cin >> u >> v >> w;
r[u][v] += w;
}
while(bfs()) ans += augment();
cout << ans;
}
EK 算法的时间复杂度是 O ( n m 2 ) O(nm^2) O(nm2)。
证明:每条边至多做 n 2 \dfrac n 2 2n 次增广路上的关键边。
OI-wiki
通过 bfs 对图分层标记,每次 dfs 只访问下一层的结点。
同时加上当前弧优化,即不重复访问满流边,维护第一条有必要尝试的边。
// dinic
#include
#define int long long
using namespace std;
struct Edge{
int nxt, to, r;
}e[10005];
int tot = 1, head[205];
void add_edge(int u, int v, int w)
{
e[++tot] = {head[u], v, w};
head[u] = tot;
}
int n, m, s, t, ans, d[205], cur[205];
queue <int> q;
bool bfs()
{
memset(d, 0, sizeof(d));
d[s] = 1, q.push(s);
while(!q.empty())
{
int u = q.front(); q.pop();
for(int i=head[u]; i; i=e[i].nxt)
{
int v = e[i].to;
if(d[v] == 0 and e[i].r > 0)
d[v] = d[u] + 1, q.push(v);
}
}
return d[t];
}
int dfs(int u, int sum)
{
if(sum == 0 or u == t) return sum;
int flow = 0;
for(int i=cur[u]; i; i=e[i].nxt)
{
cur[u] = i;
int v = e[i].to;
if(d[v] == d[u] + 1 and e[i].r > 0)
{
int d = dfs(v, min(sum, e[i].r));
e[i].r -= d, e[i^1].r += d;
flow += d, sum -= d;
}
}
return flow;
}
signed main()
{
cin >> n >> m >> s >> t;
for(int i=1; i<=m; i++)
{
int u, v, w;
cin >> u >> v >> w;
add_edge(u, v, w);
add_edge(v, u, 0);
}
while(bfs())
{
for(int i=1; i<=n; i++) cur[i] = head[i];
ans += dfs(s, 1e9);
}
cout << ans;
}
二分图:对于 G ( V , E ) G(V,E) G(V,E),分成两个点集 V 1 , V 2 V1,V2 V1,V2 ,对于所有的 ( u , v ) ∈ E (u,v) \in E (u,v)∈E, 保证 u , v u,v u,v 属于不同点集。
容易发现二分图中不存在奇环。
二分图的匹配:选定一些边,这些边之间没有公共点。
建网络流模型即可,通过虚拟源点和虚拟汇点限制 1 1 1。
如果要输出方案,可以通过 f ( u , v ) < c ( u , v ) f(u,v) < c(u,v) f(u,v)<c(u,v)判断。如果根据残量网络,需要根据反向边的残量网络判断。
#include
using namespace std;
int n, m, s, t, r[105][105], ans, d[105], cur[105];
queue <int> q;
bool bfs()
{
memset(d, 0, sizeof(d));
d[s] = 1, q.push(s);
while(!q.empty())
{
int u = q.front(); q.pop();
for(int i=1; i<=n+2; i++)
if(d[i] == 0 and r[u][i] > 0)
d[i] = d[u] + 1, q.push(i);
}
return d[t];
}
int dfs(int u, int sum)
{
if(sum == 0 or u == t) return sum;
int flow = 0;
for(int v=cur[u]; v<=n+2; v++)
{
cur[u] = v;
if(d[v] == d[u] + 1 and r[u][v] > 0)
{
int d = dfs(v, min(sum, r[u][v]));
r[u][v] -= d, r[v][u] += d;
flow += d, sum -= d;
}
}
return flow;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> m >> n;
s = n+1, t = n+2;
while(1)
{
int u, v;
cin >> u >> v;
if(u == -1 and v == -1) break;
r[u][v] = 1;
}
for(int i=1; i<=m; i++) r[s][i] = 1;
for(int i=m+1; i<=n; i++) r[i][t] = 1;
while(bfs())
{
for(int i=1; i<=n+2; i++) cur[i] = 1;
ans += dfs(s, 1e9);
}
cout << ans << "\n";
for(int u=1; u<=m; u++)
{
for(int v=m+1; v<=n; v++)
{
if(r[v][u]) cout << u << " " << v << "\n";
}
}
return 0;
}
分成正点和负电,把不选正点看作损失,每一种割对应一种方案代表不选。
答案即为正收益减去最小损失,即最小割,根据最小割最大流,即最大流。
具体可以看我之前的博客 浅谈网络流。
排列建图的套路。
先只考虑一个数列:记 i i i 所在位置 p i p_i pi,将 i → p i i \to p_i i→pi 建边,形成若干个环,每次操作可以让环上少一个点,因此最小操作次数为 n n n 减去环的数量。
如果从反面出发,想最多能保留几个点可以不动,省操作次数,能保留的数量就是环的数量。
考虑两个数列:对于一个环,最多被省一个点。对于一个点,最多被省一次。
按 i i i 所在的两个环建边跑二分图最大匹配即可。