【知识】网络流模板梳理&题型总结

基础知识OI-Wiki网络流24题

模板:

EK 求最大流

here
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1005, M = 20005,INF=1e8;
int n, m, S, T;
int h[N], e[M], f[M], ne[M], idx;
int q[N], d[N], pre[N];
bool st[N];
void add(int a,int b,int c){
    e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx++;
    e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx++;
}

bool bfs(){
    int hh = 0, tt = 0;
    memset(st, 0, sizeof(st));
    q[0] = S, st[S] = 1,d[S] = INF;

    while(hh<=tt){
        int t = q[hh++];
        for (int i = h[t]; ~i;i=ne[i]){
            int ver = e[i];
            if(!st[ver]&&f[i]){
                st[ver] = 1;
                d[ver] = min(d[t], f[i]);
                pre[ver] = i;
                if(ver==T) return 1;
                    q[++tt] = ver;
            }
        }
    }
    return 0;
}
int EK(){
    int r = 0; 
    while(bfs()){
        r += d[T];
        for (int i = T; i != S;i=e[pre[i]^1])
            f[pre[i]] -= d[T], f[pre[i]^1] += d[T];
    }
    return r;
}
int main(){
    memset(h, -1, sizeof(h));
    cin >> n >> m >> S >> T;
    while(m--){
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    cout << EK() << endl;
    return 0;
}

Dinic 求最大流

here
#include <bits/stdc++.h>
using namespace std;

const int N = 10005, M = 200005, INF = 1e8;
int n, m, S, T;
int h[N], e[M], f[M], ne[M], idx;
int q[N], d[N], cur[N];

void add(int a,int b,int c){
    e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool bfs(){
    int hh = 0, tt = 0;
    memset(d, -1, sizeof(d));
    q[0] = S, d[S] = 0, cur[S] = h[S];
    while(hh<=tt){
        int t = q[hh++];
        for (int i = h[t]; ~i;i=ne[i]){
            int ver = e[i];
            if(d[ver]==-1&&f[i]){
                d[ver] = d[t] + 1;
                cur[ver] = h[ver];
                if(ver==T)
                    return 1;
                q[++tt] = ver;
            }
        }
    }
    return 0;
}

int find(int u,int limit){
    if(u==T)
        return limit;
    int flow = 0;
    for (int i = cur[u]; ~i&&flow<limit;i=ne[i]){
        cur[u] = i;
        int ver = e[i];
        if(d[ver]==d[u]+1&&f[i]){
            int t = find(ver, min(f[i], limit - flow));
            if(!t) d[ver]=-1;
            f[i] -= t, f[i ^ 1] += t;
            flow += t;
        }
    }
    return flow;
}
int dinic(){
    int r = 0, flow;
    while(bfs()){
        while(flow=find(S,INF))
            r += flow;
    }
    return r;
}
int main(){
    memset(h, -1, sizeof(h));
    cin >> n >> m >> S >> T;
    while(m--){
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
        add(b, a, 0);
    }

    cout << dinic() << endl;
    return 0;
}

匈牙利算法

here
#include <bits/stdc++.h>
using namespace std;
const int N = 10005, M = 200005;
int h[N], ne[M], cnt, e[M];
int match[N],vis[N];
int ans;
void add(int a,int b){
    e[++cnt] = b,ne[cnt] = h[a],h[a] = cnt;
}
bool dfs(int x){
    for (int i = h[x]; i;i=ne[i]){
        int to = e[i];
        if(vis[to])
            continue;
        vis[to] = 1;
        if(match[to]==0||dfs(match[to])){
            match[to] = x;
            return true;
        }
    }
    return false;
}
int main(){
    int n, m, e;
    cin >> n >> m >> e;
    for (int i = 1; i <= e;i++){
        int u, v;
        cin >> u >> v;
        add(u, v + n);
        add(v + n, u);
    }
    for (int i = 1; i <= n;i++){
        memset(vis, 0, sizeof(vis));
        if(dfs(i))
            ans++;
    }
    cout << ans << endl;
    return 0;
}

Minimum Cut(最小割)

here
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 10010, M = 200010, INF = 1e8;

int n, m, S, T;
int h[N], e[M], f[M], ne[M], idx;
int q[N], d[N], cur[N];

void add(int a, int b, int c)
{
    e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}

bool bfs()
{
    int hh = 0, tt = 0;
    memset(d, -1, sizeof d);
    q[0] = S, d[S] = 0, cur[S] = h[S];
    while (hh <= tt)
    {
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if (d[ver] == -1 && f[i])
            {
                d[ver] = d[t] + 1;
                cur[ver] = h[ver];
                if (ver == T) return true;
                q[ ++ tt] = ver;
            }
        }
    }
    return false;
}

int find(int u, int limit)
{
    if (u == T) return limit;
    int flow = 0;
    for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    {
        cur[u] = i;
        int ver = e[i];
        if (d[ver] == d[u] + 1 && f[i])
        {
            int t = find(ver, min(f[i], limit - flow));
            if (!t) d[ver] = -1;
            f[i] -= t, f[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic()
{
    int r = 0, flow;
    while (bfs()) while (flow = find(S, INF)) r += flow;
    return r;
}

int main()
{
    scanf("%d%d%d%d", &n, &m, &S, &T);
    memset(h, -1, sizeof h);
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    printf("%d\n", dinic());
    return 0;
}

P3381 【模板】最小费用最大流

here
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 5010, M = 100010, INF = 1e8;

int n, m, S, T;
int h[N], e[M], f[M], w[M], ne[M], idx;
int q[N], d[N], pre[N], incf[N];
bool st[N];

void add(int a, int b, int c, int d)
{
    e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
    e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
}

bool spfa()
{
    int hh = 0, tt = 1;
    memset(d, 0x3f, sizeof d);
    memset(incf, 0, sizeof incf);
    q[0] = S, d[S] = 0, incf[S] = INF;
    while (hh != tt)
    {
        int t = q[hh ++ ];
        if (hh == N) hh = 0;
        st[t] = false;

        for (int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if (f[i] && d[ver] > d[t] + w[i])
            {
                d[ver] = d[t] + w[i];
                pre[ver] = i;
                incf[ver] = min(f[i], incf[t]);
                if (!st[ver])
                {
                    q[tt ++ ] = ver;
                    if (tt == N) tt = 0;
                    st[ver] = true;
                }
            }
        }
    }

    return incf[T] > 0;
}

void EK(int& flow, int& cost)
{
    flow = cost = 0;
    while (spfa())
    {
        int t = incf[T];
        flow += t, cost += t * d[T];
        for (int i = T; i != S; i = e[pre[i] ^ 1])
        {
            f[pre[i]] -= t;
            f[pre[i] ^ 1] += t;
        }
    }
}

int main()
{
    scanf("%d%d%d%d", &n, &m, &S, &T);
    memset(h, -1, sizeof h);
    while (m -- )
    {
        int a, b, c, d;
        scanf("%d%d%d%d", &a, &b, &c, &d);
        add(a, b, c, d);
    }

    int flow, cost;
    EK(flow, cost);
    printf("%d %d\n", flow, cost);

    return 0;
}

题型思路总结:

  • P2756 飞行员配对方案问题

  • 思路:

    • 解法一:
      观察到给定的图是一个二分图,求的就是二分图的最大匹配即可,时间复杂度 \(\mathcal{O}(nm)\)
    • 解法二:
      网络流算法。从源点 \(S\) 向 点集 \(N\) 的每个点连接一个容量为 \(1\) 的边,代表每名飞行员只能匹配一次。 从点集 \(M\) 向汇点 \(T\) 连接一个容量为 \(1\) 的边,因为能量守恒,所以代表 \(M\) 点集里的同一个点不能多次使用,因为一旦多次使用会违背能量守恒定律。建图后跑 dinic 求最大流即可。时间复杂度 \(\mathcal{O}(\sqrt nm)\)
  • 代码:

    here
    #include <bits/stdc++.h>
    using namespace std;
    
    const int N = 10005, M = 200005, INF = 1e8;
    int n, m, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    
    void add(int a,int b,int c){
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx++;
    }
    bool bfs(){
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof(d));
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while(hh<=tt){
    		int t = q[hh++];
    		for (int i = h[t]; ~i;i=ne[i]){
    			int ver = e[i];
    			if(d[ver]==-1&&f[i]){
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if(ver==T)
    					return 1;
    				q[++tt] = ver;
    			}
    		}
    	}
    	return 0;
    }
    
    int find(int u,int limit){
    	if(u==T)
    		return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i&&flow<limit;i=ne[i]){
    		cur[u] = i;
    		int ver = e[i];
    		if(d[ver]==d[u]+1&&f[i]){
    			int t = find(ver, min(f[i], limit - flow));
    			if(!t) d[ver]=-1;
    			f[i] -= t, f[i ^ 1] += t;
    			flow += t;
    		}
    	}
    	return flow;
    }
    int dinic(){
    	int r = 0, flow;
    	while(bfs()){
    		while(flow=find(S,INF))
    			r += flow;
    	}
    	return r;
    }
    int main(){
    	memset(h, -1, sizeof(h));
    	cin >> m >> n;
    	S = 0, T = n + 1;
    	for (int i = 1; i <= m;i++)
    		add(S, i, 1), add(i, S, 0);
    	for (int i = m + 1; i <= n;i++)
    		add(i, T, 1), add(T, i, 0);
    	int a, b;
    	while(cin>>a>>b,a!=-1)
    		add(a, b, 1), add(b, a, 0);
    	cout << dinic() << endl;
    
    	for (int i = 0; i < idx;i+=2){
    		if(e[i]>m&&e[i]<=n&&!f[i])
    			cout << e[i ^ 1] << " " << e[i] << endl;
    	}
    		return 0;
    }
    

  • P3254 圆桌问题

  • 思路:

    • 建一个超级源点,向每个单位连一条为单位人数的边
    • 建个单位向每个桌子连一条为1的边(同一个单位的人不能在同一个桌子上)
    • 建一个超级汇点,每个桌子向汇点连一条为桌子容量的边

    跑一遍最大流,如果最大流量等于所有单位人数之和,则存在解,否则无解。

    输出方案:从每个单位出发的所有满流边指向的桌子就是该单位人员的安排情况。

  • 代码:

    here
      #include <bits/stdc++.h>
      using namespace std;
      const int N = 430, M = (150 * 270 + N) * 2, INF = 1e8;
      int m, n, S, T;
      int h[N], e[M], f[M], ne[M], idx;
      int q[N], d[N], cur[N];
      void add(int a, int b, int c){
      	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
      	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
      }
    
      bool bfs(){
      	int hh = 0, tt = 0;
      	memset(d, -1, sizeof d);
      	q[0] = S, d[S] = 0, cur[S] = h[S];
      	while (hh <= tt){
      		int t = q[hh ++ ];
      		for (int i = h[t]; ~i; i = ne[i]){
      			int ver = e[i];
      			if (d[ver] == -1 && f[i]){
      				d[ver] = d[t] + 1;
      				cur[ver] = h[ver];
      				if (ver == T) return true;
      				q[ ++ tt] = ver;
      			}
      		}
      	}
      	return false;
      }
    
      int find(int u, int limit){
      	if (u == T) return limit;
      	int flow = 0;
      	for (int i = cur[u]; ~i && flow < limit; i = ne[i]){
      		cur[u] = i;
      		int ver = e[i];
      		if (d[ver] == d[u] + 1 && f[i]){
      			int t = find(ver, min(f[i], limit - flow));
      			if (!t) d[ver] = -1;
      			f[i] -= t, f[i ^ 1] += t, flow += t;
      		}
      	}
      	return flow;
      }
    
      int dinic(){
      	int r = 0, flow;
      	while (bfs()) while (flow = find(S, INF)) r += flow;
      	return r;
      }
    
      int main(){
      	scanf("%d%d", &m, &n);
      	S = 0, T = m + n + 1;
      	memset(h, -1, sizeof h);
    
      	int tot = 0;
      	for (int i = 1; i <= m; i ++ ){
      		int c;
      		scanf("%d", &c);
      		add(S, i, c);
      		tot += c;
      	}
      	for (int i = 1; i <= n; i ++ ){
      		int c;
      		scanf("%d", &c);
      		add(m + i, T, c);
      	}
      	for (int i = 1; i <= m; i ++ )
      		for (int j = 1; j <= n; j ++ )
      			add(i, m + j, 1);
    
      	if (dinic() != tot) puts("0");
      	else{
      		puts("1");
      		for (int i = 1; i <= m; i ++ ){
      			for (int j = h[i]; ~j; j = ne[j])
      				if (e[j] > m && e[j] <= m + n && !f[j])
      					printf("%d ", e[j] - m);
      			puts("");
      		}
      	}
    
      	return 0;
      }
    
  • 上下界网络流

    \(\operatorname{l}(u,\,v)\)\(u \to v\)下界函数,特别的,当 \(u \to v \notin {\mathrm E}\) 时,\(\operatorname{l}(u,\,v) = 0\)

    定义\(\mathrm G = \langle \mathrm {V,\,E} \rangle\) 上的流函数 \(\operatorname{f} : \mathrm {V \times V} \to \mathbb R\),满足如下限制:

    • 容量限制: 对于 \(\forall u,\,v \in {\mathrm V}\)\(\operatorname{l}(u,\,v) \le \operatorname{f}(u,\,v) \le \operatorname{c}(u,\,v)\)
    • 流量守恒: 对于 \(\forall u \in {\mathrm{V}}-\{s,\,t\}\),要求:\(\begin{aligned} & \sum_{v \in {\mathrm{V}}}\operatorname{f}(u,\,v)=\sum_{v \in {\mathrm{V}}}\operatorname{f}(v,\,u) \end{aligned}\)

    则称非负值 \(\operatorname{f}(u,\,v)\) 为图 \(\mathrm {G = \langle V,\,E \rangle}\) 的一个可行流

    一个流的流值 \(|f|\) 定义如下:

    \[\begin{aligned} & |f| = \sum_{v \in {\mathrm{V}}}\operatorname{f}(s,\,v)-\sum_{v \in {\mathrm{V}}}\operatorname{f}(v,\,s) \end{aligned} \]

  • 无源汇上下界可行流

    给定一个特殊点 \(s\) 和一个特殊点 \(t\),找出从 \(s\)\(t\) 的一个流 \(\operatorname{f}(s,\,t)\),使得 \(|f|\) 的值最大。

    无源汇上下界可行流就是没有源汇的情况下,求出图 \(\mathrm G = \langle \mathrm{V,\,E} \rangle\) 的一个可行流。

    对于每条边,我们可以用上界减去下界得到差值网络

    建模方法如下:

    • \(\operatorname{in}(u)\)\(u\) 的所有入边的容量的总和,\(\operatorname{out}(u)\)\(u\) 的所有出边的容量的总和。
    • 计算 \(w = \operatorname{in}(u)-\operatorname{out}(u)\)
    • 如果 \(w > 0\),则从超级源点 \(s\)\(u\) 连一条容量为 \(w\) 的边。如果 \(w <0\),则从 \(u\) 向超级汇点 \(t\) 连容量为 \(-w\) 的边。
    • 跑最大流,如果存在附加边没满流,则不存在可行流,否则 \(\mathrm maxflow\) 加上所有边的下界的和即为可行流的流值。

    添加附加边维护了平衡条件,加上 下界网络就相当于得到了一个流量平衡的残余网络。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 210, M = (10200 + N) * 2, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], l[M], ne[M], idx;
    int q[N], d[N], cur[N], A[N];
    
    void add(int a, int b, int c, int d)
    {
        e[idx] = b, f[idx] = d - c, l[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
        e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
        int hh = 0, tt = 0;
        memset(d, -1, sizeof d);
        q[0] = S, d[S] = 0, cur[S] = h[S];
        while (hh <= tt)
        {
            int t = q[hh ++ ];
            for (int i = h[t]; ~i; i = ne[i])
            {
                int ver = e[i];
                if (d[ver] == -1 && f[i])
                {
                    d[ver] = d[t] + 1;
                    cur[ver] = h[ver];
                    if (ver == T) return true;
                    q[ ++ tt] = ver;
                }
            }
        }
        return false;
    }
    
    int find(int u, int limit)
    {
        if (u == T) return limit;
        int flow = 0;
        for (int i = cur[u]; ~i && flow < limit; i = ne[i])
        {
            cur[u] = i;
            int ver = e[i];
            if (d[ver] == d[u] + 1 && f[i])
            {
                int t = find(ver, min(f[i], limit - flow));
                if (!t) d[ver] = -1;
                f[i] -= t, f[i ^ 1] += t, flow += t;
            }
        }
        return flow;
    }
    
    int dinic()
    {
        int r = 0, flow;
        while (bfs()) while (flow = find(S, INF)) r += flow;
        return r;
    }
    
    int main()
    {
        scanf("%d%d", &n, &m);
        S = 0, T = n + 1;
        memset(h, -1, sizeof h);
        for (int i = 0; i < m; i ++ )
        {
            int a, b, c, d;
            scanf("%d%d%d%d", &a, &b, &c, &d);
            add(a, b, c, d);
            A[a] -= c, A[b] += c;
        }
    
        int tot = 0;
        for (int i = 1; i <= n; i ++ )
            if (A[i] > 0) add(S, i, 0, A[i]), tot += A[i];
            else if (A[i] < 0) add(i, T, 0, -A[i]);
    
        if (dinic() != tot) puts("NO");
        else
        {
            puts("YES");
            for (int i = 0; i < m * 2; i += 2)
                printf("%d\n", f[i ^ 1] + l[i]);
        }
        return 0;
    }
    
  • 有源汇上下界可行流

    即使添加了源点 \(s\) 和汇点 \(t\),我们仍然可以通过从 \(t\)\(s\) 添加一条下界为 0、上界为 \(+\infty\) 的边,将问题转化为无源汇上下界可行流问题,直接求解。此时,可行流的容量等于从 \(t\)\(s\) 的边的容量。

    此时,原图的源点和汇点已视为普通点,而添加的超级源点和汇点分别为 \(s'\)\(t'\)。可行流的容量就是从 \(t\)\(s\) 的附加边的容量。

  • 有源汇上下界最大流

    在可行流的基础上,删除所有附加边后,使用原源汇节点求解一次最大流,再加上可行流的值即为最终答案。

    因为如果存在可行流,原网络一定是平衡的,当前网络同样平衡,满足平衡条件,因此是正确的。

    实际上,不必删除所有附加边,只需要删除 \(t\)\(s\) 的附加边,因为这两个入度为 0 或出度为 0 的节点对最大流的计算没有影响。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 210, M = (N + 10000) * 2, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N], A[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    int main()
    {
    	int s, t;
    	scanf("%d%d%d%d", &n, &m, &s, &t);
    	S = 0, T = n + 1;
    	memset(h, -1, sizeof h);
    	while (m -- )
    	{
    		int a, b, c, d;
    		scanf("%d%d%d%d", &a, &b, &c, &d);
    		add(a, b, d - c);
    		A[a] -= c, A[b] += c;
    	}
    
    	int tot = 0;
    	for (int i = 1; i <= n; i ++ )
    		if (A[i] > 0) add(S, i, A[i]), tot += A[i];
    		else if (A[i] < 0) add(i, T, -A[i]);
    
    	add(t, s, INF);
    	if (dinic() < tot) puts("No Solution");
    	else
    	{
    		int res = f[idx - 1];
    		S = s, T = t;
    		f[idx - 1] = f[idx - 2] = 0;
    		printf("%d\n", res + dinic());
    	}
    	return 0;
    }
    
  • 有源汇上下界最小流

    有源汇上下界最小流 思路基本相同。
    反向跑一边最大流,相当于流回去最大,那么就是最小流。
    答案 \(=\) 可行流 \(-\) 反向最大流

    点击查看代码
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 50010, M = (N + 125003) * 2, INF = 2147483647;
    
    int n, m, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N], A[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    int main()
    {
    	int s, t;
    	scanf("%d%d%d%d", &n, &m, &s, &t);
    	S = 0, T = n + 1;
    	memset(h, -1, sizeof h);
    	while (m -- )
    	{
    		int a, b, c, d;
    		scanf("%d%d%d%d", &a, &b, &c, &d);
    		add(a, b, d - c);
    		A[a] -= c, A[b] += c;
    	}
    
    	int tot = 0;
    	for (int i = 1; i <= n; i ++ )
    		if (A[i] > 0) add(S, i, A[i]), tot += A[i];
    		else if (A[i] < 0) add(i, T, -A[i]);
    
    	add(t, s, INF);
    
    	if (dinic() < tot) puts("No Solution");
    	else
    	{
    		int res = f[idx - 1];
    		S = t, T = s;
    		f[idx - 1] = f[idx - 2] = 0;
    		printf("%d\n", res - dinic());
    	}
    
    	return 0;
    }
    
  • 多源汇最大流

    • 建立一个超级源点 \(S\),向每个 \(s\) 连一条容量为 $+\infty $ 的边。
    • 建立一个超级汇点 \(T\),每个 \(t\)\(T\) 连一条容量为 $+\infty $ 的边。

    \(S \to T\) 的最大流即为原图多源汇最大流。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 10010, M = (100000 + N) * 2, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    int main()
    {
    	int sc, tc;
    	scanf("%d%d%d%d", &n, &m, &sc, &tc);
    	S = 0, T = n + 1;
    	memset(h, -1, sizeof h);
    	while (sc -- )
    	{
    		int x;
    		scanf("%d", &x);
    		add(S, x, INF);
    	}
    	while (tc -- )
    	{
    		int x;
    		scanf("%d", &x);
    		add(x, T, INF);
    	}
    
    	while (m -- )
    	{
    		int a, b, c;
    		scanf("%d%d%d", &a, &b, &c);
    		add(a, b, c);
    	}
    
    	printf("%d\n", dinic());
    	return 0;
    }
    
  • Ikki's Story I - Road Reconstruction

    简要题意:改变一条边的容量使得最大流变大。

    先求一遍最大流,可以发现关键边一定是满流的边
    证明:如果存在某条边没有满流且是关键边,即会有流量经过该条边,同样可以整体将流量减少至其原容量其仍然有流量增加,即存在增广路,与最大流不存在增广路矛盾,故关键边只可能是满流的边。
    同时,对于满流的边也不一定是关键边,对于一条 \(u \to v\) 满流的边,增加其容量的同时,要找到一条增广路,即必须找到 \(s \to u\)\(v \to t\) 的路径,且路径上所有边未满流,可以 dfs 预处理这样的点

    时间复杂度:\(\mathcal{O}(n^2m)\)

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 510, M = 10010, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    bool vis_s[N], vis_t[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    void dfs(int u, bool st[], int t)
    {
    	st[u] = true;
    	for (int i = h[u]; ~i; i = ne[i])
    	{
    		int j = i ^ t, ver = e[i];
    		if (f[j] && !st[ver])
    			dfs(ver, st, t);
    	}
    }
    
    int main()
    {
    	scanf("%d%d", &n, &m);
    	S = 0, T = n - 1;
    	memset(h, -1, sizeof h);
    	for (int i = 0; i < m; i ++ )
    	{
    		int a, b, c;
    		scanf("%d%d%d", &a, &b, &c);
    		add(a, b, c);
    	}
    
    	dinic();
    	dfs(S, vis_s, 0);
    	dfs(T, vis_t, 1);
    
    	int res = 0;
    	for (int i = 0; i < m * 2; i += 2)
    		if (!f[i] && vis_s[e[i ^ 1]] && vis_t[e[i]])
    			res ++ ;
    
    	printf("%d\n", res);
    	return 0;
    }
    
  • P1674 [USACO05FEB] Secret Milking Machine G

    二分 + dinic

    二分,把边权小于等于 mid 的边的边权改为 1,大于的改为 0。

    设源点为 1,汇点为 N,将无向图变为有向图(就是无向图中的每一条边都建正反两边),然后每次在二分时判断一下新建好的这个图中的最大流是否大于 T 即可。

    但是,这里有一个问题:在题目中,要求一条路只能走一次,但在我们新建的有向图中,无向图中的边都已转化成两条边,所以相当于每一条路都有可能走两边。

    其实我们不用担心,因为在网络流中,正向流一次,再反向流一次后,就相当于没流,抵消了。

    进一步,我们发现,在代码上有一个空间上的小优化:在建残余网络时,我们还需要原网络中的每一条边再建一个正边和反边。也就是说题目中无向图的每一条边对应到我们的残余网络中,都有 \(4\) 条边。所以为了节省一下空间,我们可以将方向相同的两条边合并。

    时间复杂度:\(\mathcal{O}(n^2 \times m \times \log m)\)

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 210, M = 80010, INF = 1e8;
    
    int n, m, K, S, T;
    int h[N], e[M], f[M], w[M], ne[M], idx;
    int q[N], d[N], cur[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, w[idx] = c, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    bool check(int mid)
    {
    	for (int i = 0; i < idx; i ++ )
    		if (w[i] > mid) f[i] = 0;
    		else f[i] = 1;
    
    	return dinic() >= K;
    }
    
    int main()
    {
    	scanf("%d%d%d", &n, &m, &K);
    	S = 1, T = n;
    	memset(h, -1, sizeof h);
    	while (m -- )
    	{
    		int a, b, c;
    		scanf("%d%d%d", &a, &b, &c);
    		add(a, b, c);
    	}
    
    	int l = 1, r = 1e6;
    	while (l < r)
    	{
    		int mid = l + r >> 1;
    		if (check(mid)) r = mid;
    		else l = mid + 1;
    	}
    
    	printf("%d\n", r);
    
    	return 0;
    }
    
  • P2754 [CTSC1999] 家园 / 星际转移问题

    分层图+dinic。

    注意到时间不好处理,加上数据非常小,考虑分层图。所有的点代表第 \(i\) 个星际站或星体在第 \(t\) 个时刻的情况。

    建立超级源点和汇点 \(S,T\)

    • 源点向初始地球连边,初始月球向汇点连边,地球月球和空间站向它们下一个时刻的位置连边,以上边容量都是 \(+\infty\)
    • 星际飞船 \(i\),从当前位置连向下一个时刻的下一个位置连一条容量为可容纳的人数 \(H_i\) 的边。

    每次图会扩展一层,重新跑一遍 dinic,若最大流 \(\geq k\) 则当前时刻可行,输出即可。

    初始的时候用并查集判一下月地是否连通,不连通则无解。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 1101 * 50 + 10, M = (N + 1100 + 20 * 1101) + 10, INF = 1e8;
    
    int n, m, k, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    struct Ship
    {
    	int h, r, id[30];
    }ships[30];
    int p[30];
    
    int find(int x)
    {
    	if (p[x] != x) p[x] = find(p[x]);
    	return p[x];
    }
    
    int get(int i, int day)
    {
    	return day * (n + 2) + i;
    }
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    int main()
    {
    	scanf("%d%d%d", &n, &m, &k);
    	S = N - 2, T = N - 1;
    	memset(h, -1, sizeof h);
    	for (int i = 0; i < 30; i ++ ) p[i] = i;
    	for (int i = 0; i < m; i ++ )
    	{
    		int a, b;
    		scanf("%d%d", &a, &b);
    		ships[i] = {a, b};
    		for (int j = 0; j < b; j ++ )
    		{
    			int id;
    			scanf("%d", &id);
    			if (id == -1) id = n + 1;
    			ships[i].id[j] = id;
    			if (j)
    			{
    				int x = ships[i].id[j - 1];
    				p[find(x)] = find(id);
    			}
    		}
    	}
    	if (find(0) != find(n + 1)) puts("0");
    	else
    	{
    		add(S, get(0, 0), k);
    		add(get(n + 1, 0), T, INF);
    		int day = 1, res = 0;
    		while (true)
    		{
    			add(get(n + 1, day), T, INF);
    			for (int i = 0; i <= n + 1; i ++ )
    				add(get(i, day - 1), get(i, day), INF);
    			for (int i = 0; i < m; i ++ )
    			{
    				int r = ships[i].r;
    				int a = ships[i].id[(day - 1) % r], b = ships[i].id[day % r];
    				add(get(a, day - 1), get(b, day), ships[i].h);
    			}
    			res += dinic();
    			if (res >= k) break;
    			day ++ ;
    		}
    
    		printf("%d\n", day);
    	}
    
    	return 0;
    }
    
  • P2891 [USACO07OPEN] Dining G

    拆点 + dinic

    构建图时,左边是食物,中间是奶牛,右边是饮料。源点 \(S\) 连向所有食物,汇点 \(T\) 连向所有饮料,每条边的容量为 \(1\)。奶牛与食物、饮料的连接按其喜好建立反向和正向边,容量为 \(1\)

    由于每只奶牛只能使用一次流量,我们将每个奶牛节点拆成两个节点:入点和出点。食物连向入点,出点连向饮料,入点和出点之间建一条容量为 1 的边,确保流量只能经过一个奶牛节点一次。

    最后,通过最大流算法求解最大配对数,即最大流量。

    时间复杂度为 \(\mathcal{O}(n^2 m)\)

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 410, M = 40610, INF = 1e8;
    
    int n, F, D, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    int main()
    {
    	scanf("%d%d%d", &n, &F, &D);
    	S = 0, T = n * 2 + F + D + 1;
    	memset(h, -1, sizeof h);
    	for (int i = 1; i <= F; i ++ ) add(S, n * 2 + i, 1);
    	for (int i = 1; i <= D; i ++ ) add(n * 2 + F + i, T, 1);
    	for (int i = 1; i <= n; i ++ )
    	{
    		add(i, n + i, 1);
    		int a, b, t;
    		scanf("%d%d", &a, &b);
    		while (a -- )
    		{
    			scanf("%d", &t);
    			add(n * 2 + t, i, 1);
    		}
    		while (b -- )
    		{
    			scanf("%d", &t);
    			add(i + n, n * 2 + F + t, 1);
    		}
    	}
    	printf("%d\n", dinic());
    	return 0;
    }
    
  • P2766 最长不下降子序列问题

    Task 1:

    经典 DP\(f(i)\) 表示选择 \(a_i\) 作为递增子序列的最后一个数所能得到的最长长度,易得

    \[f(i) = \max_{j < i, a_j \le a_i}\lbrace f(j)\rbrace + 1 \]

    \(\max\lbrace f(i)\rbrace\) 记作 \(s\)

    Task 2:
    我们考虑子序列是怎么生成的:设当前序列的结尾是 \(a_i\),往后拓展序列时,我们会选择\(a_i \le a_j, i < j\)\(a_j\)

    于是我们将所有\(a_i \le a_j, i < j\)的点对\((i, j)\)连边,形成的图记作 G

    对于所有入度为0的点

    从超级源点向其连边,容量均为 \(1\)。表示可以从这个点开始进行子序列的选择。
    从这个点进行BFS,找出 G 中从该点经\(s - 1\)个点能到达的全部点,打上标记。表示该点与搜索出的点能形成 \(s\) 个点组成的路径,即能构造出长度为 \(s\) 的子序列
    接着对于打上标记的点,向超级汇点连边,容量均为 \(1\)

    跑一遍最大流即可

    Task 3:

    这就简单了,将超级源点向 \(1\) 的边与 \(n\) 向超级汇点的容量设为 \(+\infty\) 即可

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 1010, M = 251010, INF = 1e8;
    
    int n, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    int g[N], w[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    int main()
    {
    	scanf("%d", &n);
    	S = 0, T = n * 2 + 1;
    	memset(h, -1, sizeof h);
    	for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
    	int s = 0;
    	for (int i = 1; i <= n; i ++ )
    	{
    		add(i, i + n, 1);
    		g[i] = 1;
    		for (int j = 1; j < i; j ++ )
    			if (w[j] <= w[i])
    				g[i] = max(g[i], g[j] + 1);
    		for (int j = 1; j < i; j ++ )
    			if (w[j] <= w[i] && g[j] + 1 == g[i])
    				add(n + j, i, 1);
    		s = max(s, g[i]);
    		if (g[i] == 1) add(S, i, 1);
    	}
    
    	for (int i = 1; i <= n; i ++ )
    		if (g[i] == s)
    			add(n + i, T, 1);
    
    	printf("%d\n", s);
    	if (s == 1) printf("%d\n%d\n", n, n);
    	else
    	{
    		int res = dinic();
    		printf("%d\n", res);
    		for (int i = 0; i < idx; i += 2)
    		{
    			int a = e[i ^ 1], b = e[i];
    			if (a == S && b == 1) f[i] = INF;
    			else if (a == 1 && b == n + 1) f[i] = INF;
    			else if (a == n && b == n + n) f[i] = INF;
    			else if (a == n + n && b == T) f[i] = INF;
    		}
    		printf("%d\n", res + dinic());
    	}
    
    	return 0;
    }
    
  • March of the Penguins

    拆点 + dinic
    因为有很多冰块上有企鹅,所以建立一个超级源点,把有源点向冰块连一条边,容量为当前冰块上企鹅的数量。

    把最终的终点看做汇点,由于本题的汇点不确定,而且数据范围很小,所以可以枚举汇点。

    再考虑冰块之间如何连边,如果两个冰块可以相互跳跃,即两个冰块的距离是在企鹅可以跳跃距离的范围内的,就可以连一条双向边,也就是互相连边。

    现在思考如何将次数限制加到流网络中,如果把冰块的起跳次数直接加到当前冰块的出边上,会发现可能会超过冰块起跳的限制,所以不能这样做。

    因为这个问题是对点有流量限制,所以我们考虑如何拆点。

    把每一个点都拆成一个入点和一个出点。

    现在,我们就可以把到这个点的边连到这个点的入点上,把离开这个点的边连到出点上,当然,入点到出点也要连一条边,这条边的容量为这个冰块的可以起跳的次数,这样是为了限制从这个冰块起跳的企鹅数量。

    这样,本题的图就建立完成了。

    以下分析本流网络的可行流是不是与原问题的解是一一对应的。

    我们可以把每只企鹅跳过的边的流量加一,这样就得到了一个可行流。

    判断可行流只需要判断两点,一是流量守恒,还有一个是容量限制。

    容量限制显然是满足的,因为刚才我们已经把所以限制都加到了流网络里了。

    流量守恒也是满足的,因为企鹅不可能在除了源点和汇点停留的,所以流量也是守恒的。

    反推回去也是可以得到一个合法解的,所以原问题任何一个企鹅跳跃的方案都是可以对应到我们建立的流网络里的可行流。

    如何判断一个方案是不是合法的呢?相当于判断流网络里的企鹅是不是都能流到汇点,直接求一下最大可行流是不是企鹅的总数就可以了,如果是,就说明当前汇点是一个可行点。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    #include <vector>
    #define x first
    #define y second
    
    using namespace std;
    
    typedef pair<int, int> PII;
    
    const int N = 210, M = 20410, INF = 1e8;
    const double eps = 1e-8;
    
    int n, S, T;
    double D;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    PII p[N];
    
    bool check(PII a, PII b)
    {
    	double dx = a.x - b.x, dy = a.y - b.y;
    	return dx * dx + dy * dy < D * D + eps;
    }
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    int main()
    {
    	int cases;
    	scanf("%d", &cases);
    	while (cases -- )
    	{
    		memset(h, -1, sizeof h);
    		idx = 0;
    		scanf("%d%lf", &n, &D);
    		S = 0;
    
    		int tot = 0;
    		for (int i = 1; i <= n; i ++ )
    		{
    			int x, y, a, b;
    			scanf("%d%d%d%d", &x, &y, &a, &b);
    			p[i] = {x, y};
    			add(S, i, a);
    			add(i, n + i, b);
    			tot += a;
    		}
    
    		for (int i = 1; i <= n; i ++ )
    			for (int j = i + 1; j <= n; j ++ )
    				if (check(p[i], p[j]))
    				{
    					add(n + i, j, INF);
    					add(n + j, i, INF);
    				}
    
    		int cnt = 0;
    		vector<int> tmp;
    		for (int i = 1; i <= n; i ++ )
    		{
    			T = i;
    			for (int j = 0; j < idx; j += 2)
    			{
    				f[j] += f[j ^ 1];
    				f[j ^ 1] = 0;
    			}
    			if (dinic() == tot)
    			{
    				tmp.push_back(i - 1);
    				cnt ++ ;
    			}
    		}
    		if (!cnt) cout<<-1<<endl;
    		else{
    			for (int i = 0; i < tmp.size()-1;i++)
    				cout << tmp[i] <<" ";
    			cout<<tmp[tmp.size()-1];
    			cout<<endl;
    		}
    	}
    	return 0;
    }
    
    
  • PIGS

    我们总结一下,题目的操作。

    每名顾客按顺序进行

    • 将自己有钥匙的猪舍打开,从中挑选一些不超过自己想买的数量的猪。
    • 同时可以将,打开的猪舍中的猪进行调整

    这里面,我们需要一个逆向思维

    我们考虑打开的猪舍中的所有猪都先被顾客取走,然后只拿走自己想要的部分,其余部分在自由的返回给打开的猪舍中

    我们首先考虑的是,那我们是否可以直接,从顾客能打开的猪舍向顾客连一条边,接着再从顾客向所有能打开的猪舍连一条边,容量都是 \(+\infty\)

    不行,这显然是不对的,因为我们需要考虑顺序问题,每一位顾客是在上一位的基础上挑选的。是不是发现了什么, 我们发现,这是逐层递推的,那我们可以建立分层图,以顾客的顺序建立,这样就能一步步递推了。

    以每个买猪的作为点,并建立原点和汇点。

    每个猪圈的第一个访客和原点连一条该猪圈中猪数量的边,每个猪圈的访客(除了以第一个)和上一个访问这个猪圈的人连一条 \(+\infty\) 的边,每个人向汇点连一条最多买的数量的边。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 110, M = 100200 * 2 + 10, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    int w[1010], belong[1010];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs())
    	{
    		r += find(S, INF);
    		flow = find(S, INF);
    		if (flow) puts("!");
    		r += flow;
    	}
    	return r;
    }
    
    int main()
    {
    	scanf("%d%d", &m, &n);
    	S = 0, T = n + 1;
    	memset(h, -1, sizeof h);
    	for (int i = 1; i <= m; i ++ ) scanf("%d", &w[i]);
    	for (int i = 1; i <= n; i ++ )
    	{
    		int a, b;
    		scanf("%d", &a);
    		while (a -- )
    		{
    			int t;
    			scanf("%d", &t);
    			if (!belong[t]) add(S, i, w[t]);
    			else add(belong[t], i, INF);
    			belong[t] = i;
    		}
    		scanf("%d", &b);
    		add(i, T, b);
    	}
    
    	printf("%d\n", dinic());
    	return 0;
    }
    
  • 最小割

  • 网络战争

    本题要求的是最小化一个形如 \(\frac{\sum_{e \in C} w_e}{|C|}\) 的表达式,其中 \(C\) 是图中的一个边割集,\(w_e\) 是边 \(e\) 的权值,\(|C|\) 是边割集 \(C\) 的大小。我们可以利用 01 分数规划的思想来求解。

    首先,我们考虑一个值 \(x\),并通过这个值 \(x\) 来判断边割集 \(C\) 的平均权值 \(\frac{\sum_{e \in C} w_e}{|C|}\)\(x\) 的关系。为了简化问题,我们先从以下两个情况进行分析:

    1. 假设 \(\frac{\sum_{e \in C} w_e}{|C|} > x\),我们可以将其变形为:

    \[\sum_{e \in C} w_e - |C| \cdot x > 0 \quad \Longrightarrow \quad \sum_{e \in C} (w_e - x) > 0 \]

    这个不等式表示边割集 \(C\) 的加权和减去 \(x\) 后的权值和大于 0。

    1. 假设 \(\frac{\sum_{e \in C} w_e}{|C|} < x\),同样可以得到:

    \[\sum_{e \in C} (w_e - x) < 0 \]

    根据上述推导,我们可以通过判断 \(\sum_{e \in C} (w_e - x)\) 是否大于 0 来确定 \(\frac{\sum_{e \in C} w_e}{|C|}\)\(x\) 的大小关系。这个关系是二段性的,适合使用二分法进行求解。

    二分

    在整个范围内进行二分,每次计算中间值 \(x\),并根据 \(\sum_{e \in C} (w_e - x)\) 的符号来调整二分的区间。如果 \(\sum_{e \in C} (w_e - x) > 0\),则说明 \(\frac{\sum_{e \in C} w_e}{|C|} > x\),此时我们可以继续对右半区间进行二分;反之,如果 \(\sum_{e \in C} (w_e - x) < 0\),则说明 \(\frac{\sum_{e \in C} w_e}{|C|} < x\),此时对左半区间进行二分。通过不断缩小区间,最终可以找到最小值。

    计算 \(\sum_{e \in C} (w_e - x)\)

    为了计算 \(\sum_{e \in C} (w_e - x)\),我们可以将图中的每条边的权值减去 \(x\),得到新的边权。在这个新图中,边权为 \(w_e - x\) 的边如果 \(w_e - x \leq 0\) 时,我们一定会选上这些边,因为这些边会减少边割集的总权值,从而使得 \(\sum_{e \in C} (w_e - x)\) 更小。

    接下来,对于剩下的边(即 \(w_e - x > 0\) 的边),我们需要根据最小割的性质来决定哪些边应该选择。由于流网络中的割与边割集有所不同,边割集中的边要么连接两个集合中的节点,要么不连接。为了最小化总权值和,我们需要选择能够有效将图割开的边,而这些边正好对应于流网络中的最小割。

    答案:边割集的权值和 \(=\) 非正边的权值和 \(+\) 正边的权值和(最小割)

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 110, M = 810, INF = 1e8;
    const double eps = 1e-8;
    
    int n, m, S, T;
    int h[N], e[M], w[M], ne[M], idx;
    double f[M];
    int q[N], d[N], cur[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, w[idx] = c, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i] > 0)
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    double find(int u, double limit)
    {
    	if (u == T) return limit;
    	double flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i] > 0)
    		{
    			double t = find(ver, min(f[i], limit - flow));
    			if (t < eps) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    double dinic(double mid)
    {
    	double res = 0;
    	for (int i = 0; i < idx; i += 2)
    		if (w[i] <= mid)
    		{
    			res += w[i] - mid;
    			f[i] = f[i ^ 1] = 0;
    		}
    		else f[i] = f[i ^ 1] = w[i] - mid;
    
    	double r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r + res;
    }
    
    int main()
    {
    	scanf("%d%d%d%d", &n, &m, &S, &T);
    	memset(h, -1, sizeof h);
    	while (m -- )
    	{
    		int a, b, c;
    		scanf("%d%d%d", &a, &b, &c);
    		add(a, b, c);
    	}
    
    	double l = 0, r = 1e7;
    	while (r - l > eps)
    	{
    		double mid = (l + r) / 2;
    		if (dinic(mid) < 0) r = mid;
    		else l = mid;
    	}
    
    	printf("%.2lf\n", r);
    	return 0;
    }
    
  • OPTM - Optimal Marks

    本题给我们一个无向图,然后我们给没有标号的点进行标号,使得所有边的费用和最小。

    每条边的费用是两个端点标号的异或和,异或运算是按位运算,因此我们可以每一位单独看,最终的费用和应该是每一位的和加起来。

    由于每一位完全独立,因此最终费用和的最小值其实就是求每一位的最小值,再合在一起,就是答案。

    由于每一位只有 \(0\)\(1\) 两种可能,因此所有点可以分成两类,一类是 \(0\),一类是 \(1\)

    然后看一下点与点之间的边有哪些情况,\(0\)\(0\) 之间的边费用就是 \(0\)\(1\)\(1\) 之间的边费用就是 \(0\)\(0\)\(1\) 之间的边费用就是 \(1\)

    由此可以发现,当两类集合中的点确定之后,整个费用和的最小值就等于两个集合之间的边的数量最小值,也就是割。

    假设现在要计算第 \(k\) 位,若第 \(k\) 位中的两个集合之间的边的最小数量是 \(m\),也就是有 \(m\) 个数第 \(k\) 位是 \(1\),一个数第 \(k\) 位是 \(1\) 的费用是 \(2^k\),那么 \(m\) 个的费用就是 \(m \cdot 2^k\)

    因此如果考虑第 \(k\) 位,就是要将所有点的第 \(k\) 位分成两类,使得两类之间的边的数量最小,这个问题和流网络中的割非常相似。

    我们将原图的无向边都在流网络里建两条有向边。然后我们对点集做一个划分,可以对应到流网络里割的划分。原问题中两个点集之间的边的权值之和就可以对应到两个割之间的割的容量。由于割的容量只会算一个方向,所以权值和也只会算一次,因此原问题中对所有点的划分方式都可以对应到割的划分方式,因此最小费用和就等于最小割(将中间的边的容量设为 \(1\))。

    求最小割还需要一个源点和汇点,我们可以建立虚拟源点和虚拟汇点。

    有些点最开始是有编号的,如果有些点的标号是 \(0\),说明他一定要和源点在一个集合,那么我们就从源点向这些点连一条容量是 \(+\infty\) 的边,这样这些点就一定能从源点走到,这些点就必然不会和汇点在同一个集合,否则源点和汇点就在同一个集合,就矛盾了。如果有些点的标号是 \(1\),说明这些点就必须和汇点在一个集合,同理从这些点向汇点连一条容量是 \(+\infty\) 的边。

    至于剩下的没有标号的点,有可能和源点一个集合也有可能和汇点一个集合,我们就不做多余的操作了,求最小割的时候按照最优情况分配即可。

    综上所述,我们只需要对于每一位分别去求一下最小割,那么每一位的费用就一定是最小的,把每一位的费用加到一块就是总费用的最小值。

    方案:每次遍历一下 \(n\) 个点属于哪个集合,属于 \(1\) 的加上 \(2^k\) 即可。

    注意,本题是无向图,无向边我们就建两条有向边即可,但是在残量网络中每条边有一个反向边,一条无向边会变成四条边,这里和前面一样采用合并的方式合成两条边。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    #define x first
    #define y second
    
    using namespace std;
    
    typedef long long LL;
    typedef pair<int, int> PII;
    
    const int N = 510, M = (3000 + N * 2) * 2, INF = 1e8;
    
    int n, m, k, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    int p[N];
    PII edges[3010];
    int anss[N];
    void add(int a, int b, int c1, int c2)
    {
    	e[idx] = b, f[idx] = c1, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = c2, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    void build(int k)
    {
    	memset(h, -1, sizeof h);
    	idx = 0;
    	for (int i = 0; i < m; i ++ )
    	{
    		int a = edges[i].x, b = edges[i].y;
    		add(a, b, 1, 1);
    	}
    	for (int i = 1; i <= n; i ++ )
    		if (p[i] >= 0)
    		{
    			if (p[i] >> k & 1) add(S, i, INF, 0);
    			else add(i, T, INF, 0);
    		}
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    void dinic(int k)
    {
    	build(k);
    	while (bfs())
    		find(S, INF);
    	for(int i=1;i<=n;i++) if(d[i]!=-1) anss[i] += 1 << k;
    }
    
    int main()
    {
    	int Q;
    	scanf("%d", &Q);
    	while(Q--){
    		scanf("%d%d", &n, &m);
    		S = 0, T = n + 1;
    		for (int i = 0; i < m; i ++ ) scanf("%d%d", &edges[i].x, &edges[i].y);
    		scanf("%d", &k);
    		memset(p, -1, sizeof p);
    		memset(anss, 0, sizeof(anss));
    		while (k -- )
    		{
    			int a, b;
    			scanf("%d%d", &a, &b);
    			p[a] = b;
    		}
    
    		for (int i = 0; i <= 30; i ++ )
    			dinic(i);
    		for (int i = 1; i <= n;i++)
    			cout << anss[i] << endl;
    	}
    	return 0;
    }
    
  • 最小割之最大权闭合图

  • P4174 [NOI2006] 最大获利

    先来解释一下什么是最大权闭合子图:

    对于一个有向图的子图,若从其中的任意一个点出发都无法到达子图外的点,则称这个子图为闭合子图。最大权闭合子图则为所有闭合子图中点权和最大的一个。其实际意义为事件间的依赖关系,一个事件要发生,它需要的所有前提也都一定要发生。

    考虑如何求出最大权闭合子图的点权和:建立新的源点 \(s\) 和汇点 \(t\),对于原图中的每个点,若其点权为正,将其与源点 \(s\) 连接,否则将其与汇点 \(t\) 连接,容量为其点权的绝对值;对于原图中的边,仍然按照原样连接,容量为 \(+\infty\)。正点权之和减去最小割(即最大流)即为最大权闭合子图的点权和。

    本题就是最大权闭合子图的模板题,我们可以这样建图:

    • 将每个通讯中转站与汇点 \(t\) 相连,边权为成本;
    • 将每个客户与源点 \(s\) 相连,边权为收益;
    • 将每个客户与其对应的两个通讯中转站相连,边权为 \(\infty\)
    • 答案即为收益和减最小割(即最大流)。

    考虑这样做的实际意义:对于图 \(s\rightarrow a\rightarrow x,y\rightarrow t\),有两个选择:

    割去边 \(s\rightarrow a\),代表不满足客户 \(a\) 的要求,减去了满足 \(a\) 所能得到的收益;
    割去边 \(x,y\rightarrow t\),代表满足客户 \(a\) 的要求,减去了建设 \(x,y\) 的成本,得到了满足 \(a\) 的收益。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 55010, M = (50000 * 3 + 5000) * 2 + 10, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    int main()
    {
    	scanf("%d%d", &n, &m);
    	S = 0, T = n + m + 1;
    	memset(h, -1, sizeof h);
    	for (int i = 1; i <= n; i ++ )
    	{
    		int p;
    		scanf("%d", &p);
    		add(m + i, T, p);
    	}
    
    	int tot = 0;
    	for (int i = 1; i <= m; i ++ )
    	{
    		int a, b, c;
    		scanf("%d%d%d", &a, &b, &c);
    		add(S, i, c);
    		add(i, m + a, INF);
    		add(i, m + b, INF);
    		tot += c;
    	}
    
    	printf("%d\n", tot - dinic());
    
    	return 0;
    }
    
  • 最小割之最大密度子图

  • Hard Life

    给定无向图 \(G=(V,E)\) ,其子图记为 \(G’=(V’,E’)\) ,在所有子图构成的集合中,密度 \(D=\frac{|E’|}{|V’|}\) 最大的元素称为最大密度子图

    首先我们进行二分答案,二分出一个 \(g\) ,对于 \(\max(|E’|-g|V’|)\) ,如果其大于 \(0\) ,那么说明答案更大,否则说明答案更小,如此下去便可求出答案。

    故下面我们来考察 \(\max(|E’|-g|V’|)\) ,方便起见,将它写成一个以 \(g\) 为自变量的函数: \(h(g) = |E’|-g|V’|\) ,最大化 \(h(g)\) 即可。

    注意到目前的式子没有太好的性质(可以和网络流中的最小割建立联系),考虑进一步变换。

    对于 \(|E’|\) ,我们有:

    \[|E’| = \frac{\sum_{u\in V’}d_u - cnt[V’,\overline{V’}]}{2} \]

    其中 \(d_u\) 表示 \(u\) 的度数,\(cnt[V’,\overline{V’}]\) 表示非子图 \(G’\) 内部边的个数,也就是 \(cnt[V’,\overline{V’}]\) 之间相连的边数。

    我们有 \(h(g) = \frac{\sum_{u\in V’}d_u - cnt[V’,\overline{V’}]}{2}-g|V’|\) ,整理一下

    \[h(g) = \frac{1}{2}(\sum_{u\in V’}d_u - cnt[V’,\overline{V’}]-2g|V’|) \]

    \(2g|V’| = \sum_{u\in V’}2g\) ,进一步有:

    \[h(g) = \frac{1}{2}(\sum_{u\in V’}{(d_u-2g)} - cnt[V’,\overline{V’}]) \]

    因为需要和最小割建立联系,所以问题变成最小化 \(-h(g)\)

    \[-h(g) = \frac{1}{2}(\sum_{u\in V’}{(2g-d_u)} + cnt[V’,\overline{V’}]) \]

    根据式子的特征,我们这样建图:\(V\) 中的点之间连接容量为 \(1\) 的边,\(V\) 中的点 \(u\) 向汇点 \(t\) 连接容量为 \(2g-d_u+U\) 的边(\(U\) 为一个足够保证容量非负的数),源点 \(s\)\(V\) 中的点 \(u\) 连接容量为 \(U\) 的边。

    我们将从接下来的公式推导中看出如此建图是合理而且正确的。

    注意到割的容量由且只由 \(s \rightarrow \overline{V’}\)\(V’ \rightarrow t\) 以及 \(V’\rightarrow \overline{V’}\) 构成。

    因此对于一个割,其容量满足:

    \[c[s,t]=\sum_{u\in \overline{V’}}U + \sum_{u\in V’}(2g-d_u+U) + \sum_{u\in V’}\sum_{v\in \overline{V’}}c(u,v) \]

    进一步化为:

    \[c[s,t]=|V|U + \sum_{u\in V’}(2g-d_u) + \sum_{u\in V’}\sum_{v\in \overline{V’}}c(u,v) \]

    \(\sum_{u\in V’}(-d_u) + \sum_{u\in V’}\sum_{v\in \overline{V’}}c(u,v)\) 恰好为 \(-2|E’|\) ,故上式可进而得到:

    \[c[s,t]=|V|U + \sum_{u\in V’}(2g) - 2|E’| \]

    整理一下,有:

    \[c[s,t]=|V|U + 2(g|V’| - |E’|) \]

    这意味着最小割对应着最小的 \(-h(g) = g|V’|-|E’|\) ,也就是最大的 \(h(g) = |E’|-g|V’|\)

    答案:\(g|V^{\prime}|-|E^{\prime}|=\frac{U\cdot n-C(S,T)}{2}\)

    那么在具体实现的时候, \(U\) 应该如何选取呢?

    注意到 \(U\) 是为了保证 \(2g-d_u\) 非负,所以取 \(U = |E’|\) 即可。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 110, M = (1000 + N * 2) * 2, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], ne[M], idx;
    double f[M];
    int q[N], d[N], cur[N];
    int dg[N];
    
    struct Edge
    {
    	int a, b;
    }edges[M];
    
    int ans;
    bool st[N];
    
    void add(int a, int b, double c1, double c2)
    {
    	e[idx] = b, f[idx] = c1, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = c2, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    void build(double g)
    {
    	memset(h, -1, sizeof h);
    	idx = 0;
    	for (int i = 0; i < m; i ++ ) add(edges[i].a, edges[i].b, 1, 1);
    	for (int i = 1; i <= n; i ++ )
    	{
    		add(S, i, m, 0);
    		add(i, T, m + g * 2 - dg[i], 0);
    	}
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i] > 0)
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    double find(int u, double limit)
    {
    	if (u == T) return limit;
    	double flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i] > 0)
    		{
    			double t = find(ver, min(f[i], limit - flow));
    			if (t <= 0) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    double dinic(double g)
    {
    	build(g);
    	double r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    void dfs(int u)
    {
    	st[u] = true;
    	if (u != S) ans ++ ;
    	for (int i = h[u]; ~i; i = ne[i])
    	{
    		int ver = e[i];
    		if (!st[ver] && f[i] > 0)
    			dfs(ver);
    	}
    }
    
    int main()
    {
    
    	while((scanf("%d%d",&n,&m)!=EOF)){
    		S = 0, T = n + 1;
    		memset(h, -1, sizeof(h));
    		memset(dg, 0, sizeof(dg));
    		memset(st, 0, sizeof(st));
    		idx = 0, ans = 0;
    		for (int i = 0; i < m; i ++ )
    		{
    			int a, b;
    			scanf("%d%d", &a, &b);
    			dg[a] ++, dg[b] ++ ;
    			edges[i] = {a, b};
    		}
    
    		double l = 0, r = m;
    		while (r - l > 1e-8)
    		{
    			double mid = (l + r) / 2;
    			double t = dinic(mid);
    			if (m * n - t > 0) l = mid;
    			else r = mid;
    		}
    
    		dinic(l);
    		dfs(S);
    
    		if (!ans) puts("1\n1\n");
    		else
    		{
    			printf("%d\n", ans);
    			for (int i = 1; i <= n; i ++ )
    				if (st[i])
    					printf("%d\n", i);
    		}
    
    	}
    
    	return 0;
    }
    
  • 最小割之最小点覆盖集

  • Destroying The Graph

    本题给定一个有向图,每次可以指定一个点,然后将这个点出发的所有边删掉或者将这个点到达的所有边删掉。对于每个点都有这样两种操作。每个点的两种操作都会有不同的花费。

    然后问我们将所有边删除需要的最小花费。

    这里和最小权点覆盖集的证明有点出入,证明中讨论的是无向图,本题则是有向图。对于每一条有向边 \(a\) -> \(b\),都有两种选择,一种是删除从 \(a\) 出发的所有边时删掉,花费是 \(W_a^-\);一种是删除到达 \(b\) 的所有边时删掉,花费是 \(W_b^+\)。因此对于每条边来说,这两个操作至少要选择一个。这个要求和点覆盖问题非常像。

    到这已经和最小权点覆盖集问题有关联了,但是每个点有两种操作,因此可以将每个点进行拆点处理,左部节点是所有费用为 \(W^+\) 的点,即删除到达的边,右部节点是所有费用为 \(W^-\) 的点,即删除出去的边。对于一条边 \(a\) -> \(b\),这条边对应的两个点至少要选一个,所以对应的从 \(b^+\)\(a^-\) 连一条边,这样就能构造出一个二分图,对于原问题来说,\(a^-\)\(b^+\) 至少选一个;对于二分图中对应的这条边来说,两个端点至少选一个。所以这两个问题是完全一致的。然后还需要考虑数量关系,原问题每个操作都有一个权值,对应到二分图中每个点都有一个点权,且点权非负,因此这就完全转化成了一个二分图的点权非负的最小权点覆盖问题,按照固定的求法解决即可。

    然后还需要求出操作方案,而原问题的每个操作都对应到二分图中的每个点,所以求操作方案其实等价于求最小权的点覆盖集。这里结合证明过程中根据最小割构造点覆盖集的方法。先从源点开始往下搜,所有能走到的点在 \(S\) 集合,所有走不到的点在 \(T\) 集合,然后枚举所有正向边,找出所有割边(起点在 \(S\),终点在 \(T\) 的边),然后将所有割边中除了源点、汇点的点都找出来,就是最小权的点覆盖集。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 210, M = 5200 * 2 + 10, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    bool st[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    void dfs(int u)
    {
    	st[u] = true;
    	for (int i = h[u]; ~i; i = ne[i])
    		if (f[i] && !st[e[i]])
    			dfs(e[i]);
    }
    
    int main()
    {
    	scanf("%d%d", &n, &m);
    	S = 0, T = n * 2 + 1;
    	memset(h, -1, sizeof h);
    	for (int i = 1; i <= n; i ++ )
    	{
    		int w;
    		scanf("%d",  &w);
    		add(S, i, w);
    	}
    	for (int i = 1; i <= n; i ++ )
    	{
    		int w;
    		scanf("%d", &w);
    		add(n + i, T, w);
    	}
    
    	while (m -- )
    	{
    		int a, b;
    		scanf("%d%d", &a, &b);
    		add(b, n + a, INF);
    	}
    
    	printf("%d\n", dinic());
    	dfs(S);
    
    	int cnt = 0;
    	for (int i = 0; i < idx; i += 2)
    	{
    		int a = e[i ^ 1], b = e[i];
    		if (st[a] && !st[b]) cnt ++ ;
    	}
    
    	printf("%d\n", cnt);
    	for (int i = 0; i < idx; i += 2)
    	{
    		int a = e[i ^ 1], b = e[i];
    		if (st[a] && !st[b])
    		{
    			if (a == S) printf("%d +\n", b);
    		}
    	}
    	for (int i = 0; i < idx; i += 2)
    	{
    		int a = e[i ^ 1], b = e[i];
    		if (st[a] && !st[b])
    		{
    			if (b == T) printf("%d -\n", a - n);
    		}
    	}
    
    	return 0;
    }
    
  • 最小割之最大点权独立集

  • P4474 王者之剑

    结论:最大权独立集 = 点权和 - 最小权覆盖集

    独立集:集合中任意两个点之间没有一条边

    建图和前提都和最小权覆盖集是相同的。

    证明:
    我们设点集为 \(V\),点覆盖集为 \(V'\),对于点覆盖集的补集 \(\overline{V'}\) 来讲,它是一个独立集,那么这样是不是一定成立呢,实际上是成立的。
    如果点覆盖集的补集它不是一个独立集的话,说明有一条边它的两个端点都不在覆盖集里面,那么对于点覆盖集的定义来讲,任意一条边都有一个端点在点覆盖集里面,这样就矛盾了,所以我们可以证明,点覆盖集的补集一定是一个独立集。

    那么为什么最大权独立集 = 点权和 - 最小权覆盖集呢,
    我们假设点权和为 \(W\),它是一个定值,\(\sum_{v\in\overline{V'}}W_v=W-\sum_{v\in V'}W_v\),我们想让 \(\sum_{v\in\overline{V'}}W_v\) 最大,即让 \(\sum_{v\in V'}W_v\) 最小,即为最小权点覆盖,通过求最小割的方法得到答案。

    本题让我们选一个起点,每一秒都会进行三种操作,综合看来每一秒都能走一步或不走,从而形成各种各样的走法,然后需要求出一种走法使拿走的宝石总价值最大

    我们假设这个人并不傻,每走到一个格子,如果格子上有宝石,一定会拿走。

    结合题意,偶数秒会使上、下、左、右的格子上的宝石消失,因此得出第一个性质:只能在偶数秒拿宝石。

    和第一个性质一样的原理,还能得到第二个性质:不能同时拿走相邻格子上的宝石。如果将相邻两个格子之间都连一条边,则能拿的宝石一定是一个独立集。而每个格子上都有一个权值,又是求获得宝石的最大值,可以发现本题已经非常像最大权独立集问题。

    到此已经能将任意一个合法方案对应到二分图中的一个独立集。但是还需要证明任意一个二分图中的独立集都能对应到一个合法方案。其实对于任意一个独立集都能构造出一个合法方案,可以从最左上角的一个有宝石的格子开始走,依次去取别的宝石,假设当前距离下一个宝石还剩两步,停下来判断一下,如果当前是偶数秒,直接走过去拿宝石,如果当前是奇数秒,原地停一秒再走过去拿宝石。且保证每次都优先取最近的宝石。这样的行走方案一定能将独立集中的所有宝石拿走,可以自行按照以上思路证明,这里的构造方式非常多,只要掌握好停顿一秒的精髓就能随便构造。

    由此得出任意一个合法方案和任意一个独立集都是一一对应的,因此要想求最大能取的宝石价值就等价于求最大权独立集,而最大权独立集 \(=\) 总权值 \(-\) 最小权点覆盖集。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 10010, M = 60010, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    
    int get(int x, int y)
    {
    	return (x - 1) * m + y;
    }
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    int main()
    {
    	scanf("%d%d", &n, &m);
    	S = 0, T = n * m + 1;
    	memset(h, -1, sizeof h);
    
    	int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
    
    	int tot = 0;
    	for (int i = 1; i <= n; i ++ )
    		for (int j = 1; j <= m; j ++ )
    		{
    			int w;
    			scanf("%d", &w);
    			if (i + j & 1)
    			{
    				add(S, get(i, j), w);
    				for (int k = 0; k < 4; k ++ )
    				{
    					int x = i + dx[k], y = j + dy[k];
    					if (x >= 1 && x <= n && y >= 1 && y <= m)
    						add(get(i, j), get(x, y), INF);
    				}
    			}
    			else
    				add(get(i, j), T, w);
    			tot += w;
    		}
    
    	printf("%d\n", tot - dinic());
    	return 0;
    }
    
  • 最小割杂题

  • 题意:给定一张 \(n\) 个点 \(m\) 条边的无向图,求最少去掉多少个点,可以使图不连通。

    如果不管去掉多少个点,都无法使原图不连通,则直接返回 \(n\)
    若无向图不连通,则图中必有两个点不连通,至于是哪两个点不连通,这是可以选择的。

    因此我们可以枚举起点 \(S\) 和终点 \(T\),然后求在剩余 \(n-2\) 个节点中最少去掉多少个,可以使 \(S\)\(T\) 不连通。

    注意:\(S\)\(T\) 不能直接相连,否则不可能达到要求。

    在每次枚举的结果中去最小值就是本题的答案。

    接下来就是要去掉最少的点使 \(S\)\(T\) 不连通,这一个要求和网络的最小割很类似,最小割割的是边,而本题是点。

    我们可以用以下方法去构造网络。

    1. 把原来无向图中的每个点 \(x\),拆成 \(x\)\(x' = x + N\) 两个点
    2. 对任意中间点 \(x\),连有向边 \((x, x')\),容量为 \(1\)
    3. 对于原无向图的每条边 \((x, y)\),在网络中连有向边 \((x', y), (y' x)\),容量为正无穷.

    \(S'\) 为网络的起点,\(T'\) 为网络的终点,求最小割,即可得到最少需要去掉的点数。

    为什么呢,这里给出解释:

    在上面的网络中,\((x, x')\) 这条有向边对应原无向图的节点 \(x\)。无向图中任意经过点 \(x\) 的路径,对应在网络中必须先到达 \(x\)
    然后通过 \((x, x')\),再从 \(x'\) 离开,我们一般称 \(x\) 为入点,\(x'\) 为出点。可以发现,在无向图中删去一个点,等价于在网络中断开 \((x, x')\)

    另外,我们只能删掉无向图的节点,不能删掉无向图的边,所以网络中其他边的容量都设为正无穷。最小割必定不会包含这些
    容量为正无穷的边,因为去掉所有容量为 1 的边,就足以使 \(S\)\(T\) 不连通了

    综上所述,网络的最小割就是由最少数量的形如 \((x, x')\) 的边构成的边集,也就是原图最少需要去掉的点数。

    然后我们可以用最大流求出最小割的边的容量和,因此我们只需要将边的容量设置为 \(1\),那么求出的边的容量和就等价于边的数量。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 110, M = 5210, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    int main()
    {
    	while (cin >> n >> m)
    	{
    		memset(h, -1, sizeof h);
    		idx = 0;
    		for (int i = 0; i < n; i ++ ) add(i, n + i, 1);
    		while (m -- )
    		{
    			int a, b;
    			scanf(" (%d,%d)", &a, &b);
    			add(n + a, b, INF);
    			add(n + b, a, INF);
    		}
    		int res = n;
    		for (int i = 0; i < n; i ++ )
    			for (int j = 0; j < i; j ++ )
    			{
    				S = n + i, T = j;
    				for (int k = 0; k < idx; k += 2)
    				{
    					f[k] += f[k ^ 1];
    					f[k ^ 1] = 0;
    				}
    				res = min(res, dinic());
    			}
    		printf("%d\n", res);
    	}
    
    	return 0;
    }
    
  • P2762 太空飞行计划问题

    本题给了很多实验,完成每个实验能获得一定的收益,还有很多器材,每个实验需要对应的一些器材,每个器材都有一个花费。

    可以发现想完成每个实验都需要购买对应的器材,如果将所有器材的权值设置成负数,所有实验的权值设置成正数,如果将所有实验向对应的器材连一条边,那么可以发现任何一个原问题的可行方案都会对应到图中的一个闭合子图。

    因此本题求的最大净收益其实就是图中的最大权闭合子图,因此可以用求最大权闭合子图的方法来求,从源点向所有实验连一条容量是收益的边,从所有器材向汇点连一条容量是花费的边,从所有实验向对应的器材连一条容量是 \(+\infty\) 的边。

    因此本题是经典的最大权闭合子图问题的应用,最大权闭合子图 \(=\) 正权点的权值和 \(-\) 最小割。

    本题还要输出方案,可以从最小割中推出最大权闭合子图,就是 \(S - \lbrace s \rbrace\),即所有从源点能搜到的点(除源点)。

    here
    #include <iostream>
    #include <cstring>
    #include <sstream>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 110, M = 5210, INF = 1e8;
    
    int m, n, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    bool st[N];
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    void dfs(int u)
    {
    	st[u] = true;
    	for (int i = h[u]; ~i; i = ne[i])
    		if (!st[e[i]] && f[i])
    			dfs(e[i]);
    }
    
    int main()
    {
    	scanf("%d%d", &m, &n);
    	S = 0, T = m + n + 1;
    	memset(h, -1, sizeof h);
    	getchar();  // 过滤掉第一行最后的回程
    
    	int tot = 0;
    	for (int i = 1; i <= m; i ++ )
    	{
    		int w, id;
    		string line;
    		getline(cin, line);
    		stringstream ssin(line);
    		ssin >> w;
    		add(S, i, w);
    		while (ssin >> id) add(i, m + id, INF);
    		tot += w;
    	}
    	for (int i = 1; i <= n; i ++ )
    	{
    		int p;
    		cin >> p;
    		add(m + i, T, p);
    	}
    
    	int res = dinic();
    	dfs(S);
    
    	for (int i = 1; i <= m; i ++ )
    		if (st[i]) printf("%d ", i);
    	puts("");
    	for (int i = m + 1; i <= m + n; i ++ )
    		if (st[i]) printf("%d ", i - m);
    	puts("");
    	printf("%d\n", tot - res);
    	return 0;
    }
    
  • P3355 骑士共存问题

    本题是要求放最多的马使得所有马不会相互攻击,如果将所有能相互攻击的马之间连一条边,将所有奇数格作为左部节点,将所有偶数格作为右部节点,这就是一个二分图,且要求选中的点之间都不存在边,也就是求二分图的最大独立集,可以用匈牙利算法来求。

    但是本题的数据比较大,用匈牙利算法会被卡常数,因此需要用更快的算法实现,可以用网络流求最大权独立集的方法来求,而 dinic 算法比匈牙利算法快很多,不用担心超时。

    最大权独立集 \(=\) 总权值 \(-\) 最小权点覆盖集,因此我们需要求出最小权点覆盖集,用固定做法就行,从源点向所有左部节点连一条容量是权值的边,从所有右部节点向汇点连一条容量是权值的边,左部节点和右部节点之间的边容量是 \(+\infty\)。最小割就是最小权点覆盖。

    本题每个点是没有权值的,求的也是最大独立集的点数,因此可以认为每个点的权值都是 \(1\)。这样就能求出最小点覆盖的点数,对应求出最大独立集的点数,两者是互补的。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 40010, M = 400010, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], ne[M], idx;
    int q[N], d[N], cur[N];
    bool g[210][210];
    
    int get(int x, int y)
    {
    	return (x - 1) * n + y;
    }
    
    void add(int a, int b, int c)
    {
    	e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool bfs()
    {
    	int hh = 0, tt = 0;
    	memset(d, -1, sizeof d);
    	q[0] = S, d[S] = 0, cur[S] = h[S];
    	while (hh <= tt)
    	{
    		int t = q[hh ++ ];
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (d[ver] == -1 && f[i])
    			{
    				d[ver] = d[t] + 1;
    				cur[ver] = h[ver];
    				if (ver == T) return true;
    				q[ ++ tt] = ver;
    			}
    		}
    	}
    	return false;
    }
    
    int find(int u, int limit)
    {
    	if (u == T) return limit;
    	int flow = 0;
    	for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    	{
    		cur[u] = i;
    		int ver = e[i];
    		if (d[ver] == d[u] + 1 && f[i])
    		{
    			int t = find(ver, min(f[i], limit - flow));
    			if (!t) d[ver] = -1;
    			f[i] -= t, f[i ^ 1] += t, flow += t;
    		}
    	}
    	return flow;
    }
    
    int dinic()
    {
    	int r = 0, flow;
    	while (bfs()) while (flow = find(S, INF)) r += flow;
    	return r;
    }
    
    int main()
    {
    	scanf("%d%d", &n, &m);
    	S = 0, T = n * n + 1;
    	memset(h, -1, sizeof h);
    	while (m -- )
    	{
    		int x, y;
    		scanf("%d%d", &x, &y);
    		g[x][y] = true;
    	}
    
    	int dx[] = {-2, -1, 1, 2, 2, 1, -1, -2};
    	int dy[] = {1, 2, 2, 1, -1, -2, -2, -1};
    
    	int tot = 0;
    	for (int i = 1; i <= n; i ++ )
    		for (int j = 1; j <= n; j ++ )
    		{
    			if (g[i][j]) continue;
    			if (i + j & 1)
    			{
    				add(S, get(i, j), 1);
    				for (int k = 0; k < 8; k ++ )
    				{
    					int x = i + dx[k], y = j + dy[k];
    					if (x >= 1 && x <= n && y >= 1 && y <= n && !g[x][y])
    						add(get(i, j), get(x, y), INF);
    				}
    			}
    			else add(get(i, j), T, 1);
    			tot ++ ;
    		}
    
    	printf("%d\n", tot - dinic());
    	return 0;
    }
    
  • 费用流

    定义

    费用流: 所有最大可行流中,费用最小或最大的值(因为可行流是不唯一的)

    费用的计算:我们定流网络里的每条边除了容量 \(C\) 外还有一个费用值 \(w\) ,因此一条可行流的费用为 \(Cost(f) = \sum{f * w}\)

    问题描述

    给定一个包含\(n\)个点\(m\)条边的有向图,并给每条边的容量和费用,边的容量非负,图中可能存在重边和自环,保证费用不会出现负环

    求: 从\(S \to T\)的最大流,以及流量最大时的最小费用。

    Solution

    先给出一个直观可想的算法:
    我们在求最大流的EK算法的基础上,将找增广路经的 BFS 换成 SPFA 算法即可。
    如果要求最小费用最大流,就用 SPFA 算法找最短增广路;
    如果要求最大费用最大流,就用 SPFA 算法求最长增广路。

    注意:由于 SPFA 的局限性,用这种算法求最小费用最大流时,是不能处理有负圈的情况的。
    处理方法:消圈法,Capacity Scaling

    下面证明一下这种算法的正确性

    以求最小费用最大流为例:

    假设当前可行流是费用最小的,记为 \(f_1\),经过 SPFA求最短增广路后,得到的可行流为 \(f_2\) ,即当前最短的增广路经的长度的可行流为 \(f_2\)

    我们在求最大流时,可得到一个可行流 \(f = f_1 + f_2\)

    如何证明这个最大可行流是费用最小的呢?

    我们可以用反证法进行证明:

    假设存在另一个可行流 \(f’\),有 \(|f’| = |f|\)\(f’\)的费用 < \(f\) 的费用,
    为了方便表达,我们记为 \(Cost(f’) < Cost(f)\)
    \(f’\)才是最优解

    可行流的加减法与向量类似,是两个可行流间的边权相加减即可。
    有 $$f_2’ = f’ - f_1$$
    因为 \(f_2’\) 是由 \(f’ - f_1\) 得到的,因此 \(f_2’\) 也是从\(f_1\)的残留网络 \(G_{f_1}\) 上增广出来的。
    因此有$$f’ = f_1 + f_2’$$

    又因为 \(|f’| = |f|\) ,所以 \(|f_2| = |f_2’|\)
    又因为 \(Cost(f’) < Cost(f)\)
    我们知道任何一条增广路经的费用 \(Cost(f) = |f| * 边权和 = |f| * 增广路经长度\)
    因此 $$Cost(f_2’) < Cost(f_2)$$

    所以 \(f_2’\) 的增广路经长度 < \(f_2的增广路经长度\),说明找到比 \(f_2\) 更短的增广路经长度,
    与“当前的最短增广路径的可行流是 \(f_2\)”这个前提条件矛盾,因此不存在比 \(f\) 更优的 \(f’\)

    建图方式

    容量的建图方式与普通网络流一致,
    关于费用的建图,我们正向边建立的费用为 \(w(u,v)\) ,则反向边的费用设为 \(-w(u,v)\) 即可。

    原理: 可行流的费用:
    \(Cost(f_{u,v}) = f(u,v) * w(u,v)\)

    退流的时候设费用为负就可以抵消这个费用的产生的贡献了
    \(Cost(f_{u,v}) = f(u,v) * w(u,v) + f(u,v) * (-w(u,v)) = 0\)

  • P4015 运输问题

    费用流裸题

    构图:

    • 源点向每一个仓库连接一条流量为 \(a_i\),费用为 \(0\) 的边。

    • 仓库i向销售点j连接一条流量为无穷大,费用为\(c_{i,j}\)
      销售点j向汇点连接一条流量为 \(b_i\),费用为 \(0\) 的边

    • 跑一个最小费用最大流

    • 第二个任务只要清空图,然后重新连接为费用是 \(-c_i\) 的边即可。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 160, M = 5150 * 2 + 10, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], w[M], ne[M], idx;
    int q[N], d[N], pre[N], incf[N];
    bool st[N];
    
    void add(int a, int b, int c, int d)
    {
    	e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool spfa()
    {
    	int hh = 0, tt = 1;
    	memset(d, 0x3f, sizeof d);
    	memset(incf, 0, sizeof incf);
    	q[0] = S, d[S] = 0, incf[S] = INF;
    	while (hh != tt)
    	{
    		int t = q[hh ++ ];
    		if (hh == N) hh = 0;
    		st[t] = false;
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (f[i] && d[ver] > d[t] + w[i])
    			{
    				d[ver] = d[t] + w[i];
    				pre[ver] = i;
    				incf[ver] = min(incf[t], f[i]);
    				if (!st[ver])
    				{
    					q[tt ++ ] = ver;
    					if (tt == N) tt = 0;
    					st[ver] = true;
    				}
    			}
    		}
    	}
    	return incf[T] > 0;
    }
    
    int EK()
    {
    	int cost = 0;
    	while (spfa())
    	{
    		int t = incf[T];
    		cost += t * d[T];
    		for (int i = T; i != S; i = e[pre[i] ^ 1])
    		{
    			f[pre[i]] -= t;
    			f[pre[i] ^ 1] += t;
    		}
    	}
    	return cost;
    }
    
    int main()
    {
    	scanf("%d%d", &m, &n);
    	S = 0, T = m + n + 1;
    	memset(h, -1, sizeof h);
    	for (int i = 1; i <= m; i ++ )
    	{
    		int a;
    		scanf("%d", &a);
    		add(S, i, a, 0);
    	}
    	for (int i = 1; i <= n; i ++ )
    	{
    		int b;
    		scanf("%d", &b);
    		add(m + i, T, b, 0);
    	}
    	for (int i = 1; i <= m; i ++ )
    		for (int j = 1; j <= n; j ++ )
    		{
    			int c;
    			scanf("%d", &c);
    			add(i, m + j, INF, c);
    		}
    
    	printf("%d\n", EK());
    
    	for (int i = 0; i < idx; i += 2)
    	{
    		f[i] += f[i ^ 1], f[i ^ 1] = 0;
    		w[i] = -w[i], w[i ^ 1] = -w[i ^ 1];
    	}
    	printf("%d\n", -EK());
    
    	return 0;
    }
    
  • P4016 负载平衡问题

    我们对每个仓库创建两个节点 \(X_i\)\(Y_i\),前者为供应节点,后者为需求节点。求出最终要达到的货物值即平均数,然后把初始值处理成偏移值(初始值减去平均数)。

    • 偏移小于 \(0\)
      表明这个节点需要运入货物,将节点 \(Y_i\) 与汇点 \(T\) 相连,容量为偏移的绝对值,费用为 \(0\)

    • 偏移大于 \(0\)
      表明这个节点需要运出货物,将节点 \(X_i\) 与源点 \(S\) 相连,容量为偏移值,费用为 \(0\)

    然后再考虑相邻节点的关系:

    • 相邻节点互相补充
      \(X_i\)\(Y_j\) 相连,容量为 \(+\infty\),费用为 \(1\),表示运输单位货物需要 \(1\) 的费用。

    • 不是直接补充,而是作为中间节点转运
      \(X_i\)\(X_j\) 相连,容量为 \(+\infty\),费用为 \(1\),意义同上。

    通过以上方式建图后,运行最小费用最大流算法即可:

    • 最大流保证能够平衡货物。
    • 最小费用流能保证运输的货物最少。
    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 110, M = 610, INF = 1e8;
    
    int n, S, T;
    int s[N];
    int h[N], e[M], f[M], w[M], ne[M], idx;
    int q[N], d[N], pre[N], incf[N];
    bool st[N];
    
    void add(int a, int b, int c, int d)
    {
    	e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool spfa()
    {
    	int hh = 0, tt = 1;
    	memset(d, 0x3f, sizeof d);
    	memset(incf, 0, sizeof incf);
    	q[0] = S, d[S] = 0, incf[S] = INF;
    	while (hh != tt)
    	{
    		int t = q[hh ++ ];
    		if (hh == N) hh = 0;
    		st[t] = false;
    
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (f[i] && d[ver] > d[t] + w[i])
    			{
    				d[ver] = d[t] + w[i];
    				pre[ver] = i;
    				incf[ver] = min(f[i], incf[t]);
    				if (!st[ver])
    				{
    					q[tt ++ ] = ver;
    					if (tt == N) tt = 0;
    					st[ver] = true;
    				}
    			}
    		}
    	}
    	return incf[T] > 0;
    }
    
    int EK()
    {
    	int cost = 0;
    	while (spfa())
    	{
    		int t = incf[T];
    		cost += t * d[T];
    		for (int i = T; i != S; i = e[pre[i] ^ 1])
    		{
    			f[pre[i]] -= t;
    			f[pre[i] ^ 1] += t;
    		}
    	}
    	return cost;
    }
    
    int main()
    {
    	scanf("%d", &n);
    	S = 0, T = n + 1;
    	memset(h, -1, sizeof h);
    
    	int tot = 0;
    	for (int i = 1; i <= n; i ++ )
    	{
    		scanf("%d", &s[i]);
    		tot += s[i];
    		add(i, i < n ? i + 1 : 1, INF, 1);
    		add(i, i > 1 ? i - 1 : n, INF, 1);
    	}
    
    	tot /= n;
    	for (int i = 1; i <= n; i ++ )
    		if (tot < s[i])
    			add(S, i, s[i] - tot, 0);
    		else if (tot > s[i])
    			add(i, T, tot - s[i], 0);
    
    	printf("%d\n", EK());
    	return 0;
    }
    
  • 二分图最优匹配

  • P4014 分配问题

    \(n\) 个人和 \(n\) 件工作,每个人做每个工作都有不同的效率,将 \(n\) 个工作分配给 \(n\) 个人,问我们可以获得的最大效率和最小效率。

    我们可以将 \(n\) 个人看作左部节点,\(n\) 个工作看作右部节点,这就是一个二分图。将每个人做对应工作的效率看作两个节点之间的边的权值,那么其实就是要我们求一个二分图的匹配,使得选中的所有边的权值和最大,这就是一个二分图最优匹配问题。

    如果每条边上没有权值,单单是一个二分图最大匹配问题就非常简单,从源点向左部节点连边,右部节点向汇点连边,每个左部节点向每个右部节点连边,所有边的容量都是 \(1\)。这样的问题非常直观且常见,因此这里就不再证明原题的方案和流网络的可行流是一一对应的。

    可以发现,任意一个可行方案都保证每个员工都能匹配一个工作,意味着任意一个可行方案对应到流网络中都是一个满流,即整数值最大流。

    现在再加上每条边上有权值(效率),那么本题就是要我们在所有整数值最大流中找出权值和最大的一个流,就是最大费用最大流。

    最大费用最大流只需要在 spfa 算法求最长增广路即可,最长路要求不能有正环,而二分图中不存在正环,因此不会有问题。

    另外还要求最小费用最大流,不用再写一遍用 spfa 算法求最短增广路的 EK 算法,可以将所有边的费用取反,这样新图的最大费用最大流再取反回来就是原图的最小费用最大流。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 110, M = 5210, INF = 1e8;
    
    int n, S, T;
    int h[N], e[M], f[M], w[M], ne[M], idx;
    int q[N], d[N], pre[N], incf[N];
    bool st[N];
    
    void add(int a, int b, int c, int d)
    {
    	e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool spfa()
    {
    	int hh = 0, tt = 1;
    	memset(d, 0x3f, sizeof d);
    	memset(incf, 0, sizeof incf);
    	q[0] = S, d[S] = 0, incf[S] = INF;
    	while (hh != tt)
    	{
    		int t = q[hh ++ ];
    		if (hh == N) hh = 0;
    		st[t] = false;
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (f[i] && d[ver] > d[t] + w[i])
    			{
    				d[ver] = d[t] + w[i];
    				pre[ver] = i;
    				incf[ver] = min(f[i], incf[t]);
    				if (!st[ver])
    				{
    					q[tt ++ ] = ver;
    					if (tt == N) tt = 0;
    					st[ver] = true;
    				}
    			}
    		}
    	}
    	return incf[T] > 0;
    }
    
    int EK()
    {
    	int cost = 0;
    	while (spfa())
    	{
    		int t = incf[T];
    		cost += t * d[T];
    		for (int i = T; i != S; i = e[pre[i] ^ 1])
    		{
    			f[pre[i]] -= t;
    			f[pre[i] ^ 1] += t;
    		}
    	}
    	return cost;
    }
    
    int main()
    {
    	scanf("%d", &n);
    	S = 0, T = n * 2 + 1;
    	memset(h, -1, sizeof h);
    	for (int i = 1; i <= n; i ++ )
    	{
    		add(S, i, 1, 0);
    		add(n + i, T, 1, 0);
    	}
    	for (int i = 1; i <= n; i ++ )
    		for (int j = 1; j <= n; j ++ )
    		{
    			int c;
    			scanf("%d", &c);
    			add(i, n + j, 1, c);
    		}
    
    	printf("%d\n", EK());
    
    	for (int i = 0; i < idx; i += 2)
    	{
    		f[i] += f[i ^ 1], f[i ^ 1] = 0;
    		w[i] = -w[i], w[i ^ 1] = -w[i ^ 1];
    	}
    	printf("%d\n", -EK());
    
    	return 0;
    }
    
  • 费用流之最大权不相交路径

  • P4013 数字梯形问题

    解释一下题意,边不相交指的是不能有两条路径同时经过 \(u \rightarrow~v\) 的路径。

    先考虑限制 \(3\),也就是没有限制的情况,做法非常显然:

    上一层向下一层的数字连边,容量为无穷代表这条边可以走无穷次,花费为 \(0\)

    每个数字都拆一下点,两个点之间连边容量为无穷,代表可以选这个点无数次,花费为这个点的权值代表经过他付出的代价;

    考虑限制 \(2\),一条边只能经过一次,于是将边的容量置为 \(1\) 即可。

    考虑限制 \(1\),同理将点的容量置成 \(1\) 即可。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 1200, M = 4000, INF = 1e8;
    
    int m, n, S, T;
    int h[N], e[M], f[M], w[M], ne[M], idx;
    int q[N], d[N], pre[N], incf[N];
    bool st[N];
    int id[40][40], cost[40][40];
    
    void add(int a, int b, int c, int d)
    {
    	e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool spfa()
    {
    	int hh = 0, tt = 1;
    	memset(d, -0x3f, sizeof d);
    	memset(incf, 0, sizeof incf);
    	q[0] = S, d[S] = 0, incf[S] = INF;
    	while (hh != tt)
    	{
    		int t = q[hh ++ ];
    		if (hh == N) hh = 0;
    		st[t] = false;
    
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (f[i] && d[ver] < d[t] + w[i])
    			{
    				d[ver] = d[t] + w[i];
    				pre[ver] = i;
    				incf[ver] = min(f[i], incf[t]);
    				if (!st[ver])
    				{
    					q[tt ++ ] = ver;
    					if (tt == N) tt = 0;
    					st[ver] = true;
    				}
    			}
    		}
    	}
    	return incf[T] > 0;
    }
    
    int EK()
    {
    	int cost = 0;
    	while (spfa())
    	{
    		int t = incf[T];
    		cost += t * d[T];
    		for (int i = T; i != S; i = e[pre[i] ^ 1])
    		{
    			f[pre[i]] -= t;
    			f[pre[i] ^ 1] += t;
    		}
    	}
    	return cost;
    }
    
    int main()
    {
    	int cnt = 0;
    	scanf("%d%d", &m, &n);
    	S = ++ cnt;
    	T = ++ cnt;
    	for (int i = 1; i <= n; i ++ )
    		for (int j = 1; j <= m + i - 1; j ++ )
    		{
    			scanf("%d", &cost[i][j]);
    			id[i][j] = ++ cnt;
    		}
    
    	// 规则1
    	memset(h, -1, sizeof h), idx = 0;
    	for (int i = 1; i <= n; i ++ )
    		for (int j = 1; j <= m + i - 1; j ++ )
    		{
    			add(id[i][j] * 2, id[i][j] * 2 + 1, 1, cost[i][j]);
    			if (i == 1) add(S, id[i][j] * 2, 1, 0);
    			if (i == n) add(id[i][j] * 2 + 1, T, 1, 0);
    			if (i < n)
    			{
    				add(id[i][j] * 2 + 1, id[i + 1][j] * 2, 1, 0);
    				add(id[i][j] * 2 + 1, id[i + 1][j + 1] * 2, 1, 0);
    			}
    		}
    	printf("%d\n", EK());
    
    	// 规则2
    	memset(h, -1, sizeof h), idx = 0;
    	for (int i = 1; i <= n; i ++ )
    		for (int j = 1; j <= m + i - 1; j ++ )
    		{
    			add(id[i][j] * 2, id[i][j] * 2 + 1, INF, cost[i][j]);
    			if (i == 1) add(S, id[i][j] * 2, 1, 0);
    			if (i == n) add(id[i][j] * 2 + 1, T, INF, 0);
    			if (i < n)
    			{
    				add(id[i][j] * 2 + 1, id[i + 1][j] * 2, 1, 0);
    				add(id[i][j] * 2 + 1, id[i + 1][j + 1] * 2, 1, 0);
    			}
    		}
    	printf("%d\n", EK());
    
    	// 规则3
    	memset(h, -1, sizeof h), idx = 0;
    	for (int i = 1; i <= n; i ++ )
    		for (int j = 1; j <= m + i - 1; j ++ )
    		{
    			add(id[i][j] * 2, id[i][j] * 2 + 1, INF, cost[i][j]);
    			if (i == 1) add(S, id[i][j] * 2, 1, 0);
    			if (i == n) add(id[i][j] * 2 + 1, T, INF, 0);
    			if (i < n)
    			{
    				add(id[i][j] * 2 + 1, id[i + 1][j] * 2, INF, 0);
    				add(id[i][j] * 2 + 1, id[i + 1][j + 1] * 2, INF, 0);
    			}
    		}
    	printf("%d\n", EK());
    
    	return 0;
    }
    
  • 费用流之网格图

  • P2045 方格取数加强版

    • 点边转化:把每个格子 \((i,j)\) 拆成一个入点一个出点。

    • 从每个入点向对应的出点连两条有向边:一条容量为 \(1\) ,费用为格子 \((i,j)\) 中的数;另一条容量为 \(k-1\) ,费用为 \(0\)

    • \((i,j)\) 的出点到 \((i,j+1)\)\((i+1,j)\) 的入点连有向边,容量为 \(k\) ,费用为 \(0\)

    • \((1,1)\) 的入点为源点, \((n,n)\) 的出点为汇点,求最大费用最大流。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 5010, M = 20010, INF = 1e8;
    
    int n, k, S, T;
    int h[N], e[M], f[M], w[M], ne[M], idx;
    int q[N], d[N], pre[N], incf[N];
    bool st[N];
    
    int get(int x, int y, int t)
    {
    	return (x * n + y) * 2 + t;
    }
    
    void add(int a, int b, int c, int d)
    {
    	e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
    	e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool spfa()
    {
    	int hh = 0, tt = 1;
    	memset(d, -0x3f, sizeof d);
    	memset(incf, 0, sizeof incf);
    	q[0] = S, d[S] = 0, incf[S] = INF;
    	while (hh != tt)
    	{
    		int t = q[hh ++ ];
    		if (hh == N) hh = 0;
    		st[t] = false;
    
    		for (int i = h[t]; ~i; i = ne[i])
    		{
    			int ver = e[i];
    			if (f[i] && d[ver] < d[t] + w[i])
    			{
    				d[ver] = d[t] + w[i];
    				pre[ver] = i;
    				incf[ver] = min(incf[t], f[i]);
    				if (!st[ver])
    				{
    					q[tt ++ ] = ver;
    					if (tt == N) tt = 0;
    					st[ver] = true;
    				}
    			}
    		}
    	}
    	return incf[T] > 0;
    }
    
    int EK()
    {
    	int cost = 0;
    	while (spfa())
    	{
    		int t = incf[T];
    		cost += t * d[T];
    		for (int i = T; i != S; i = e[pre[i] ^ 1])
    		{
    			f[pre[i]] -= t;
    			f[pre[i] ^ 1] += t;
    		}
    	}
    	return cost;
    }
    
    int main()
    {
    	scanf("%d%d", &n, &k);
    	S = 2 * n * n, T = S + 1;
    	memset(h, -1, sizeof h);
    	add(S, get(0, 0, 0), k, 0);
    	add(get(n - 1, n - 1, 1), T, k, 0);
    	for (int i = 0; i < n; i ++ )
    		for (int j = 0; j < n; j ++ )
    		{
    			int c;
    			scanf("%d", &c);
    			add(get(i, j, 0), get(i, j, 1), 1, c);
    			add(get(i, j, 0), get(i, j, 1), INF, 0);
    			if (i + 1 < n) add(get(i, j, 1), get(i + 1, j, 0), INF, 0);
    			if (j + 1 < n) add(get(i, j, 1), get(i, j + 1, 0), INF, 0);
    		}
    
    	printf("%d\n", EK());
    
    	return 0;
    }
    
  • P4012 深海机器人问题

    从超级源点往每个出发点连一条边,容量 \(k_i\),费用 \(0\),表示 \(k_i\) 个机器人从第 \(i\) 个出发点出发。

    从每个目的地往超级汇点连一条边,容量 \(r_i\),费用 \(0\),表示 \(r_i\) 个机器人将会最终到达第 \(i\) 个目的地。

    如果每个标本可以无限次采集,那就很好办,网格图中的每个点向它东边、北边的点连一条边,费用为对应标本价值,容量为 \(+\infty\),表示每条边可以无限次走。

    但是这里每个标本只能被采集一次。于是我们想到把一条边拆成两条边,一条费用为这条边上的标本价值,容量为 \(1\),另一条费用为 \(0\),容量为 \(+\infty\)

    跑一个最大费用最大流即可。

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 260, M = 2000, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], w[M], ne[M], idx;
    int q[N], d[N], pre[N], incf[N];
    bool st[N];
    
    int get(int x, int y)
    {
        return x * (m + 1) + y;
    }
    
    void add(int a, int b, int c, int d)
    {
        e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
        e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool spfa()
    {
        int hh = 0, tt = 1;
        memset(d, -0x3f, sizeof d);
        memset(incf, 0, sizeof incf);
        q[0] = S, d[S] = 0, incf[S] = INF;
        while (hh != tt)
        {
            int t = q[hh ++ ];
            if (hh == N) hh = 0;
            st[t] = false;
            for (int i = h[t]; ~i; i = ne[i])
            {
                int ver = e[i];
                if (f[i] && d[ver] < d[t] + w[i])
                {
                    d[ver] = d[t] + w[i];
                    pre[ver] = i;
                    incf[ver] = min(f[i], incf[t]);
                    if (!st[ver])
                    {
                        q[tt ++ ] = ver;
                        if (tt == N) tt = 0;
                        st[ver] = true;
                    }
                }
            }
        }
        return incf[T] > 0;
    }
    
    int EK()
    {
        int cost = 0;
        while (spfa())
        {
            int t = incf[T];
            cost += t * d[T];
            for (int i = T; i != S; i = e[pre[i] ^ 1])
            {
                f[pre[i]] -= t;
                f[pre[i] ^ 1] += t;
            }
        }
        return cost;
    }
    
    int main()
    {
        int A, B;
        scanf("%d%d%d%d", &A, &B, &n, &m);
        S = (n + 1) * (m + 1), T = S + 1;
        memset(h, -1, sizeof h);
        for (int i = 0; i <= n; i ++ )
            for (int j = 0; j < m; j ++ )
            {
                int c;
                scanf("%d", &c);
                add(get(i, j), get(i, j + 1), 1, c);
                add(get(i, j), get(i, j + 1), INF, 0);
            }
        for (int i = 0; i <= m; i ++ )
            for (int j = 0; j < n; j ++ )
            {
                int c;
                scanf("%d", &c);
                add(get(j, i), get(j + 1, i), 1, c);
                add(get(j, i), get(j + 1, i), INF, 0);
            }
        while (A -- )
        {
            int k, x, y;
            scanf("%d%d%d", &k, &x, &y);
            add(S, get(x, y), k, 0);
        }
        while (B -- )
        {
            int r, x, y;
            scanf("%d%d%d", &r, &x, &y);
            add(get(x, y), T, r, 0);
        }
    
        printf("%d\n", EK());
    
        return 0;
    }
    
  • P1251 餐巾计划问题

    先只考虑买入餐巾,也就是不考虑送洗,不难得到一下建模方法:

    将每天抽象成一个点 \(i\),与汇点 \(T\) 连边,容量为 \(r_i\) ,费用为 \(0\),代表每天需要 \(r_i\) 条餐巾。由源点 \(S\) 向每天连边,容量为 \(+\infty\),费用为 \(p\),代表买入餐巾。得到下面的网络 \(G\)

    graph.png

    发现这个图缺少信息,我们需要把送洗的信息加入图中:

    对于每一天,再建一个点 \(i'\),由源点 \(S\) 向其连边,容量为 \(r_i\),代表每天产生了 \(r_i\) 条脏餐巾。若可以送洗,则连边 \(i' \to (i+k)\) ,其中 \(k\) 代表快洗或慢洗所需的天数,容量为 \(+\infty\),费用为此次送洗的费用。由于可以延迟送洗,连接 \(i' \to (i+1)'\),容量 \(+\infty\)。得到下面的网络 \(G'\)

    graph (3).png

    \(G'\) 上跑最小费用最大流即可。

    here
      #include <iostream>
      #include <cstring>
      #include <algorithm>
    
      using namespace std;
      #define int long long
      const int N = 261000, M = 2000000, INF = 1e8;
    
      int n, p, x, xp, y, yp, S, T;
      int h[N], e[M], f[M], w[M], ne[M], idx;
      int q[N], d[N], pre[N], incf[N];
      int r[N];
      bool st[N];
    
      void add(int a, int b, int c, int d)
      {
      	e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
      	e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
      }
    
      bool spfa()
      {
      	int hh = 0, tt = 1;
      	memset(d, 0x3f, sizeof d);
      	memset(incf, 0, sizeof incf);
      	q[0] = S, d[S] = 0, incf[S] = INF;
      	while (hh != tt)
      	{
      		int t = q[hh ++ ];
      		if (hh == N) hh = 0;
      		st[t] = false;
      		for (int i = h[t]; ~i; i = ne[i])
      		{
      			int ver = e[i];
      			if (f[i] && d[ver] > d[t] + w[i])
      			{
      				d[ver] = d[t] + w[i];
      				pre[ver] = i;
      				incf[ver] = min(f[i], incf[t]);
      				if (!st[ver])
      				{
      					q[tt ++ ] = ver;
      					if (tt == N) tt = 0;
      					st[ver] = true;
      				}
      			}
      		}
      	}
      	return incf[T] > 0;
      }
    
      int EK()
      {
      	int cost = 0;
      	while (spfa())
      	{
      		int t = incf[T];
      		cost += t * d[T];
      		for (int i = T; i != S; i = e[pre[i] ^ 1])
      		{
      			f[pre[i]] -= t;
      			f[pre[i] ^ 1] += t;
      		}
      	}
      	return cost;
      }
    
      signed main()
      {
      	scanf("%d",&n);
      	S = 0, T = n * 2 + 1;
      	memset(h, -1, sizeof h);
      	for (int i = 1; i <= n; i ++ )
      		scanf("%d", &r[i]);
      	scanf("%d%d%d%d%d", &p, &x, &xp, &y, &yp);
      	for (int i = 1; i <= n; i ++ ){
      		add(S, i, r[i], 0);
      		add(n + i, T, r[i], 0);
      		add(S, n + i, INF, p);
      		if (i < n) add(i, i + 1, INF, 0);
      		if (i + x <= n) add(i, n + i + x, INF, xp);
      		if (i + y <= n) add(i, n + i + y, INF, yp);
      	}
      	printf("%lld\n", EK());
      	return 0;
      }
    
  • 费用流之上下界可行流

  • P3980 [NOI2008] 志愿者招募

    思路一:

    有一个 \(n\) 天的项目,每天至少需要 \(a_i\) 个人,一共有 \(m\) 类打工人,每类人能从第 \(s_i\) 天工作到第 \(t_i\) 天,且需要 \(c_i\) 的费用,现在要求我们设计一个合理的方案让费用最小。

    我们可以用一条边来表示一天,用 \(1\)\(2\) 的边表示第 \(1\) 天需要的人数,用 \(2\)\(3\) 的边表示第 \(2\) 天需要的人数,以此类推。

    由于每天是至少需要 \(a_i\) 个人,是可以大于 \(a_i\) 的,相当于每条边都有一个下界的限制,并且本题还要求费用,因此本题是一个无源汇上下界可行费用流问题。

    然后我们考虑如何在图中把志愿者表示出来,假设现在有一个志愿者可以从第 \(2\) 天工作到第 \(3\) 天,相当于可以从第 \(2\) 天连续流过两条边,第 \(4\) 天开始该志愿者就不能继续工作了,对于流网络来说相当于从点 \(4\) 开始这段流量就消失了,但是我们要保证整个流网络是流量守恒的,所以我们可以从点 \(4\) 向点 \(2\) 连一条边,让点 \(2\) 到点 \(4\) 之间形成一个循环,保证该支援者在第 \(3\) 天工作完之后将流量回流,保证流量守恒。通过这样的思路我们就可以表示所有类型的志愿者,并且每个志愿者对应的回流的那条边的流量就是该类志愿者的人数,因此这些回流边的容量就是 \(+\infty\),费用是该类志愿者的费用。

    最小费用最大流即可。

    时间复杂度 \(\mathcal{O}(n^2m)\)

    here
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 1010, M = 24010, INF = 1e8;
    
    int n, m, S, T;
    int h[N], e[M], f[M], w[M], ne[M], idx;
    int q[N], d[N], pre[N], incf[N];
    bool st[N];
    
    void add(int a, int b, int c, int d)
    {
        e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
        e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
    }
    
    bool spfa()
    {
        int hh = 0, tt = 1;
        memset(d, 0x3f, sizeof d);
        memset(incf, 0, sizeof incf);
        q[0] = S, d[S] = 0, incf[S] = INF;
        while (hh != tt)
        {
            int t = q[hh ++ ];
            if (hh == N) hh = 0;
            st[t] = false;
    
            for (int i = h[t]; ~i; i = ne[i])
            {
                int ver = e[i];
                if (f[i] && d[ver] > d[t] + w[i])
                {
                    d[ver] = d[t] + w[i];
                    pre[ver] = i;
                    incf[ver] = min(f[i], incf[t]);
                    if (!st[ver])
                    {
                        q[tt ++ ] = ver;
                        if (tt == N) tt = 0;
                        st[ver] = true;
                    }
                }
            }
        }
        return incf[T] > 0;
    }
    
    int EK()
    {
        int cost = 0;
        while (spfa())
        {
            int t = incf[T];
            cost += t * d[T];
            for (int i = T; i != S; i = e[pre[i] ^ 1])
            {
                f[pre[i]] -= t;
                f[pre[i] ^ 1] += t;
            }
        }
        return cost;
    }
    
    int main()
    {
        scanf("%d%d", &n, &m);
        S = 0, T = n + 2;
        memset(h, -1, sizeof h);
        int last = 0;
        for (int i = 1; i <= n; i ++ )
        {
            int cur;
            scanf("%d", &cur);
            if (last > cur) add(S, i, last - cur, 0);
            else if (last < cur) add(i, T, cur - last, 0);
            add(i, i + 1, INF - cur, 0);
            last = cur;
        }
        add(S, n + 1, last, 0);
    
        while (m -- )
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            add(b + 1, a, INF, c);
        }
    
        printf("%d\n", EK());
        return 0;
    }
    

    思路二:

    设一些新的变量:

    • \(B_i (B_i \ge 0)\),表示第 \(i\) 天实际招了 \(A_i + B_i\) 人。

    • \(D_i (D_i \ge 0)\),表示实际上第 \(i\) 类志愿者招了 \(D_i\) 类人

    这样我们就可以列出 \(n\) 个等式,对于第 \(i\) 个等式(针对第 \(i\) 天的匹配情况)

    \[A_i + B_i = \displaystyle \sum_{S_j \le i \le T_j} D_j \]

    但是为了让每个变量在流网络中、在每个等式中都相等,所以我们得让每个变量至多出现在两个式子中,(如果出现在一个式子,就可以将其到源汇点的费用改了,这样就是费用对应上了,如果两个式子,可以从本该连向汇点的边直接连向本该从源点出的边,这样费用对应。这里本人实力还是非常菜,很可能讲了一些玄学的东西,求大佬们轻喷。)

    由于每个 \(j\) 影响的 \(i\) 是连续的一段,所以我们可以将式子前后加入两个 \(0 = 0\),然后将式子差分(这是一步等价变换)这样每个 \(D_j, B_i\) 都恰好会出现在两个式子之中。

    对于第 \(i\) 个等式而言,差分后的式子:

    \[-A_{i-1} - B_{i-1} + A_i + B_i = \displaystyle -\sum_{i-1=T_j}D_j+\sum_{i=S_j}D_j \]

    我们把式子移项,让每一项都是正的:

    \[A_i + B_i + \displaystyle\sum_{i-1=T_j} D_j = A_{i-1}+B_{i-1}+\sum_{i=S_j}D_j \]

    这样,我们可以把等式看作一个点的流量守恒等式,等式左右两侧分别是流入该点/流出改点的流量,我们建立流网络:

    • 对于常量 \(A\),在左侧则连一条自虚拟源点出发,到 \(i\) 点,流量为 \(A_i\),无费用的边,右侧连到汇点,即对称的。

    • 对于变量 \(B\),从 \(B_i\) 所在右侧等式的点向 \(B_i\) 所在左侧的点,即 \(i\) 连向 \(i-1\),这条边流量无限,意味着自由选择的 \(B\),而这条边 + 流量守恒保证了 \(B_i\) 在两个式子中不变

    • 对于变量 \(D\) 同理,即 \(S_j\) 连向 \(T_j + 1\),流量无限,费用为 \(C_j\) 的边。

    这样,在流网络跑到的最大流 = 从 \(S\) 出发所有容量(满足常量的强行限制) \(\Leftrightarrow\) 差分等式成立 \(\Leftrightarrow\) 原始等式成立 \(\Leftrightarrow\) 一个满足条件的方案

    所以,原问题最小费用 \(\Leftrightarrow\) 最小费用最大流

    时间复杂度\(\mathcal{O}(n^2m)\)

    here
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #define rint register int
    typedef long long LL;
    
    using namespace std;
    
    const int N = 1005, M = (N * 3 + 10000) * 2, INF = 0x3f3f3f3f;
    
    int n, m, a[N], incf[N], d[N], q[N], pre[N]; 
    int head[N], numE = 1, S, T;
    bool vis[N];
    LL ans;
    struct E{
        int next, v, w, c;
    } e[M];
    
    void inline add(int u, int v, int w, int c) {
        e[++numE] = (E) { head[u], v, w, c };
        head[u] = numE;
        e[++numE] = (E) { head[v], u, 0, -c };
        head[v] = numE;
    }
    
    bool inline spfa() {
        memset(d, 0x3f, sizeof d);
        rint hh = 0, tt = 1;
        d[S] = 0, q[0] = S, incf[S] = INF;
        while (hh != tt) {
            rint u = q[hh++]; vis[u] = false;
            if (hh == N) hh = 0;
            for (rint i = head[u]; i; i = e[i].next) {
                rint v = e[i].v;
                if (d[u] + e[i].c < d[v] && e[i].w) {
                    d[v] = d[u] + e[i].c, pre[v] = i, incf[v] = min(incf[u], e[i].w);
                    if (!vis[v]) {
                        vis[v] = true;
                        q[tt++] = v;
                        if (tt == N) tt = 0;
                    }
                }
            }
        }
        return d[T] != INF;
    }
    
    void inline update() {
        int x = T;
        while (x != S) {
            int i = pre[x];
            e[i].w -= incf[T], e[i ^ 1].w += incf[T]; 
            x = e[i ^ 1].v;
        }
        ans += (LL)d[T] * incf[T];
    }
    
    int main() {
        scanf("%d%d", &n, &m); 
        S = n + 2, T = n + 3;
        for (rint i = 1, A; i <= n; i++) {
            scanf("%d", &A);
            add(S, i, A, 0), add(i + 1, T, A, 0);
            add(i + 1, i, INF, 0);
        }
        for (rint i = 1, s, t, c; i <= m; i++) {
            scanf("%d%d%d", &s, &t, &c);
            add(s, t + 1, INF, c);
        }
        while (spfa()) update();
        printf("%lld\n", ans);
        return 0;
    }
    
posted @ 2024-11-30 15:04  Star_F  阅读(276)  评论(0编辑  收藏  举报