Day3 二分图 & 网络路

二分图 & 网络流

二分图

可以考虑黑白染色(即一条边两边端点颜色不同)和没有奇环转化成二分图的模型。

  • 匹配:选择若干条边使得每个点周围至多只有一条边被选。

  • 最大匹配:最多的边。

  • 增广路用于将匹配边替换为一些当前可能更优的非匹配边,增广路中匹配边与非匹配边交替出现。

  • 匈牙利算法:bfs/dfs,从每个点出发找增广路,找不到了就得到了极大匹配,即最大匹配。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1005;
const int maxm = 5e4 + 5;
int L,R,m;
struct Edge {
    int to,nxt;
} e[maxm << 1];
int head[maxn], cnt;	
void addEdge(int u,int v) {
    e[++ cnt] = Edge{v,head[u]};
    head[u] = cnt;
}
int n, f[maxn]; bool vis[maxn];
int dfs(int u) {
    for (int i = head[u];i;i = e[i].nxt) {
        int v = e[i].to;
        if (vis[v]) continue;
        vis[v] = 1;
        if (!f[v] || dfs(f[v]))
            return f[v] = u, 1;
    }
    return 0;
}
int main() {
    scanf("%d%d%d",&L,&R,&m), n = L + R;
    for (int i = 1, u, v;i <= m;i ++) {
        scanf("%d%d",&u,&v);
        addEdge(u,v + L), addEdge(v + L,u);
    }
    int ans = 0;
    for (int i = 1;i <= L;i ++) {
        for (int j = 1;j <= n;j ++)
            vis[j] = 0;
        ans += dfs(i);
    }
    printf("%d",ans);
    return 0;
}
  • 点覆盖:选若干点使得所有边都有被选择的点覆盖。
  • 独立集:原图中点集的一个子集,使得子集中所有点没有边相连。(无向图最大团等于其补图的最大独立集)
  • 最小点覆盖等价于最大匹配(前者所需点数 = = = 后者所需边数),最大独立集等价于 n n n​ 减最小点覆盖。

模型:最小路径覆盖

基本模型:给出一张有向图,要用若干条不相交的简单路径覆盖所有点,求最少路径数。

  • 做法:考虑拆点构建二分图。将所有点拆成两个点(记为 x , x ′ x,x' x,x),对于原图中 ( x , y ) (x,y) (x,y) 的边,在新图中连一条 ( x , y ′ ) (x,y') (x,y) 的边。
  • 定理:最小路径覆盖 = n   − =n~- =n ​ 二分图的最大匹配。
  • 对于输出方案,用网络流跑二分图最大匹配,根据残量网络使用并查集进行维护。具体地,从左边点出发向外走,对于每条经过的形如 ( x , y ′ ) (x,y') (x,y) 的边,如果 ( x , y ′ ) (x,y') (x,y) 上有流量(即残余流量为 0 0 0)那么我们合并 x , y x,y x,y x , y x,y x,y 在同一条路径上。最后找每条路径的起点 O ( n 2 ) O(n^2) O(n2) 打印即可。

变式:路径可以相交。做法:做传递闭包(即对于两个点 u , v u,v u,v,若 u u u 能走到 v v v 则新图中有边 u → v u\to v uv),然后按照不相交的做法做即可。

Muddy Fields

给定一个 R × C R\times C R×C 的矩阵,每个点是草地和泥地中的一个。现在需要放若干条木板使得所有泥地被覆盖。每条木板长度任意宽度为 1 1 1,只能平行于长宽放置且覆盖区域必须全是泥地。求满足条件所需要的最少的木板数量。

一个经典套路:网格图中考虑行列连边。先考虑平行于行的木板,列是同理的;我们将每个点用一条尽可能长的木板,然后对于每条木板,我们都建一个点;那么对于点 ( i , j ) (i,j) (i,j),至少需要行列上两条木板的其中一条进行覆盖。那我们考虑把这两条木板连边,那么就得到了一张二分图,做一遍最小点覆盖即最大匹配即可。

模型:二分图博弈

基本模型:一个棋子在一个矩阵棋盘上,先手后手每次上下左右移动这颗棋子,不能移动到重复的格子上,谁先走不动谁输。

  • 结论:若所有最大匹配都包含起点,先手必胜;否则后手必胜。
  • 判断方法;先求一遍最大匹配,然后去掉棋子起点再求一遍最大匹配。第二遍看答案是否相同即可。

网络流

一张有向图,每条边有一个容量表示经过这条边的流量的上限。一般有一个源点和汇点(出度 / 入度为 0 0 0)。需要满足三条性质:容量限制、斜对称性(设 ( u , v ) (u,v) (u,v) 的流量为 f ( u , v ) f(u,v) f(u,v),那么有 f ( u , v ) = − f ( v , u ) f(u,v)=-f(v,u) f(u,v)=f(v,u)​)、流守恒性(从源点流出的流量与流入汇点的流量相等)。

两个经典的等价问题模型:

  • 最大流
  • 最小割

算法:EK 和 Dinic 较为主流。主要思路:不断寻找增广路来提升流量,用反悔贪心做(即可以撤销之前的方案)。

模型:最大流求二分图最大匹配

建立超级源点 S S S 和超级汇点 T T T S S S 向二分图左边的所有点连一条容量为 1 1 1 的有向边,原图所有边附加上 1 1 1 的容量,二分图右边的所有点向 T T T 连一条容量为 1 1 1 的有向边,最后求一遍最大流即可。

EK

不断使用 bfs 寻找增广路进行增广。每当我们找到一条增广路,我们就求出当前每条边的剩余流量的最小值,则最大流可以获得这个最小值,然后每条边的剩余流量都减去这个最小值;最后考虑反悔,令每条边的反向边的剩余流量加上这个最小值,初始这些边的剩余流量为 0 0 0。理论复杂度 O ( n m 2 ) O(nm^2) O(nm2)

namespace EK {
    int pre[maxn], id[maxn]; Queue q;
    int bfs() {
        memset(pre,-1,sizeof(pre));
        q.clear(); q.push(S);
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int i = head[u], v, w;i;i = e[i].nxt) {
                v = e[i].to, w = e[i].val;
                if (pre[v] != -1 || w == 0) continue;
                pre[v] = u, id[v] = i; q.push(v); 
                if (pre[T] != -1) return 1;
            } 
        } return pre[T] != -1;
    }
    int EK() {
        int ans = 0;
        while (bfs()) {
            int tmp = inf;
            for (int now = T;now != S;now = pre[now])
                tmp = min(tmp,e[id[now]].val);
            for (int now = T;now != S;now = pre[now])
                e[id[now]].val -= tmp, e[id[now] ^ 1].val += tmp;
            ans += tmp;
        } return ans;
    }
}

Dinic

定义残量网络表示求解最大流过程中,剩余流量为正的所有边组成的子图。用 bfs 求出分层图后 dfs 多路增广并像 EK 算法实时更新剩余流量和当前增广得到的流量,使用当前弧优化后时间复杂度 O ( n 2 m ) O(n^2m) O(n2m)

namespace Dinic {
    int dep[maxn]; Queue q;
    int cur[maxn];
    bool bfs() {
        for (int i = S;i <= T;i ++)
            cur[i] = head[i], dep[i] = inf;
        dep[S] = 0; q.clear(), q.push(S);
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int i = head[u], v;i;i = e[i].nxt) {
                if (dep[v = e[i].to] < inf || e[i].val == 0) continue;
                dep[v] = dep[u] + 1; q.push(v);
                if (v == T) return 1;
            }
        } return dep[T] < inf;
    }
    int dfs(int u,int lim) {
        if (u == T || lim == 0) return lim; // 这句不能忘!
        int flow = 0;
        for (int i = cur[u], v, tmp;i;i = e[i].nxt) {
            cur[u] = i; // 当前弧优化
            if (dep[v = e[i].to] == dep[u] + 1 && (tmp = dfs(v,min(lim,e[i].val)))) {
                flow += tmp, lim -= tmp, e[i].val -= tmp, e[i ^ 1].val += tmp;
                if (lim == 0) break;
            }
        } return flow;
    }
    int Dinic() {
        int ans = 0;
        while (bfs()) ans += dfs(S,inf);
        return ans;
    }
}

最大流问题的解题思路

  1. 对于问题 P P P 考虑建流网络 G G G
  2. 证明可行解与可行流能互相转化、一一对应。
  3. 最大可行解就转化成最大流问题,最小割类似。

定理:最大流=最小割。于是可以考虑转化模型从而更好地构建网络流。可以通过将点拆成入点和出点将删点转化为删边。

网络流建模:总是考虑构造出一些限制,在边上用 + ∞ +\infty + 或者 ∞ 2 \infty^2 2 在最小割中进行约束。

模型:最大权值闭合图

基本模型:给定一张有向图,点有点权;求一张子图,使得子图中点权和最大,且子图中的点所有出边指向的点都在该子图中。

做法:建立超级源点 S S S 和汇点 T T T,若节点 u u u 有正点权,则建一条 S → u S\to u Su,边权为点权;否则 u → T u\to T uT 建一条边,边权为点权相反数。将原图上所有边权改为 ∞ \infty ,跑网络最大流,将所有正点权的权值求和后减去最大流(最小割)即为答案。

在网络流中的一个割中,每一种方案都对应原图中一种合法方案,表现为一种割把网络流分成两部分,其中包含 S S S 那部分没有连向 T T T 中的点。由于原图中的边权都是 ∞ \infty ,所以最小割去除的边一定与 S S S T T T 中的一个点相连。不选择一个正点权的点,表现为断开了与 S S S 相连的那条边;选择了一个负点权的点,表现为断开了与 T T T 相连的那条边。

最大获利

给定 n n n 个点,选择第 i i i 个点需要 P i P_i Pi 的代价;给出共 m m m 个点对,对于第 i i i 个点对 ( A i , B i ) (A_i,B_i) (Ai,Bi),如果 A i , B i A_i,B_i Ai,Bi 同时选择那么可以获得 C i C_i Ci 的收益,求收益减代价的最大值。

考虑转化成上述模型,将每个用户群建一个点,权值为 C i C_i Ci;每个基站建一个点,权值为 − P i -P_i Pi;然后用户向基站连边,按照上述模型做法做即可。

Task Assignment to Two Employees

有两个人和若干个任务,第 i i i 个人完成第 j j j 个任务,可以先获得 v i , j × p i v_{i,j}\times p_i vi,j×pi 的利润,其中 p i p_i pi 初始为 p 0 p_0 p0;然后使 p 0 ← p 0 × s i , j p_0\gets p_0\times s_{i,j} p0p0×si,j。每个任务必须且只能由两人中的一人完成。可以任意选择完成任务的顺序和完成的人,求最大利润。

对于两个任务 ( v 1 , s 1 ) , ( v 2 , s 2 ) (v_1,s_1),(v_2,s_2) (v1,s1),(v2,s2),如果把它们交给同一个人做且任务相邻,那么贡献即为 max ⁡ ( v 1 × s 2 , v 2 × s 1 ) \max(v_1\times s_2,v_2\times s_1) max(v1×s2,v2×s1)​,即考虑先后顺序。 考虑转化为最小割,将每个任务都看作一个点,按照以下方法进行建边:

  • 对于不同的两点 i , j i,j i,j,连接容量均为 max ⁡ ( s 1 , i v 1 , j , s 1 , j v 1 , i ) + max ⁡ ( s 2 , i v 2 , j , s 2 , j v 2 , i ) \max(s_{1,i}v_{1,j},s_{1,j}v_{1,i})+\max(s_{2,i}v_{2,j},s_{2,j}v_{2,i}) max(s1,iv1,j,s1,jv1,i)+max(s2,iv2,j,s2,jv2,i) 的双向边(反边照常建),这里将每条边的贡献翻倍以便于建边;
  • 对于每个点 i i i 记录一个顶标 h 1 / 2. i h_{1/2.i} h1/2.i 表示按照上述规则建边时与第 1 / 2 1/2 1/2 个人的相关的边权之和。
  • S , T S,T S,T 分别为超级源点和超级汇点,最后对于每个任务 i i i,连接 S → i S\to i Si 容量为 2 p 0 v 1 , i + h 1 , i 2p_0v_{1,i}+h_{1,i} 2p0v1,i+h1,i,连接 i → T i\to T iT 容量为 2 p 0 v 2 , i + h 2 , i 2p_0v_{2,i}+h_{2,i} 2p0v2,i+h2,i

最后最小割的方案中,保留下来的边即为一组合法最优解。由于贡献翻了一倍进行计算,所以最终答案需要除以二。

Sum of Abs

给定一张 n n n 个点 m m m 条边的无向图,第 i i i 个点有两个点权 A i A_i Ai B i B_i Bi;删去第 i i i 个点和与之相连的边的代价为 A i A_i Ai。定义一个极大连通块的权值为连通块中点的 B B B 权值和的绝对值。最大化删除若干点后,所有极大连通块权值和减去总代价。

考虑每个点对答案的贡献。对于第 i i i 个点,显然如果它被删除则有 − a i -a_i ai 的贡献。如果它最终被归入权值和为正的连通块中则有 b i b_i bi 的贡献;反之则有 − b i -b_i bi 的贡献。不考虑是否合法,理想的最优答案显然是将所有 b i b_i bi 为正的点丢到同一个连通块(下文中称为”正连通块“)中,所有 b i b_i bi 为负的点丢到另一个连通块(下文中称为”负连通块“)中。

现在考虑使答案变得合法所需要的代价。对于第 i i i 个点,如果它被删去则有 a i + ∣ b i ∣ a_i+|b_i| ai+bi 的代价,前者为删除代价,后者为从合法答案中去除的部分;如果 b i b_i bi 为正却被归入了负连通块中,那么代价为 b i × 2 b_i\times 2 bi×2,分别为正连通块中缺少的部分和负连通块中 b i > 0 b_i>0 bi>0 导致减少的部分;如果 b i b_i bi 为负却被归入了正连通块中,那么同理代价为 ( − b i ) × 2 (-b_i)\times 2 (bi)×2。而对于同一个连通块中的点,它们最终的贡献取正取负显然应该是相同的。

于是我们考虑构建网络流中的最小割。建立超级源点 S S S 和超级汇点 T T T,令最终 b i b_i bi 取正的点有连边 S → i S\to i Si,反之有连边 i → T i\to T iT。套路地,对于点 u u u,我们将其拆成入点 u u u 和出点 u ′ u' u,则删除 u u u 就可以表示为删除 u → u ′ u\to u' uu 这条边,故该边边权即为代价 a u + ∣ b u ∣ a_u+|b_u| au+bu。对于原图中的边 ( u , v ) (u,v) (u,v),我们连两条容量无穷大的边 u ′ → v , v ′ → u u'\to v,v'\to u uv,vu 以防通过删边不删点获得错误的答案。

最后按照上述的代价进行建边即可。最终答案即为理想答案减去最小割。

Special Edges

给定一张 n n n 个点 m m m 条边的网络流,源点和汇点分别为 1 , n 1,n 1,n q q q 次询问每次更改编号为 1 1 1 k k k 的边的容量(初始容量均为 0 0 0),对于每次询问求出此时的最大流。

1 ≤ k ≤ 10 1\le k\le 10 1k10 1 ≤ 容量 ≤ 25 1\le \text{容量}\le 25 1容量25

考虑在不知道 k k k 条边的边权时的答案,我们设包含且仅包含这 k k k 条边的边集为 K K K。将最大流转化为最小割,那么我们就可以计算 K K K 的任意子集在最小割中时至少的答案。由于 k ≤ 10 k\le 10 k10 故考虑状压,对于 K K K 中被割的边,我们将容量设为 0 0 0,反之设为 ∞ \infty ,然后跑最大流求解即可。最终对于每轮询问,我们仍然枚举 k k k 条边是否被割去,然后拿着新的容量填进当前 K K K 中被割了的边的边权,把所有答案取 min ⁡ \min min 即为所求。

但显然复杂度爆炸,考虑优化。注意到我们在枚举 k k k 条边割与不割的状态时,可以通过上一个相邻状态的答案推出新一轮的答案。具体地,我们先在最开始算出 K K K 均割去的答案(即容量全 0 0 0);在枚举状态时设 K K K 中当前割去的边集为 A A A,我们找到一个边集 A ′ A' A,满足 A ′ A' A 的答案已经得到且 A ′ A' A A A A 的子集,那么我们在边集为 A ′ A' A 时得出的残量网络中,将 A A A 中少割去的边的容量设为 ∞ \infty ,跑出来的最大流即为从 A ′ A' A A A A 最大流答案增加的流量。这样每次计算量就减少了很多。

但复杂度仍然偏高。注意到边的容量最大值只有 25 25 25,我们考虑令 ∞ = 25 \infty=25 =25,此时跑 EK 算法的复杂度要优于 Dinic。于是我们只在计算 K K K 全部割去的答案时使用 Dinic,后续均使用 EK。这样复杂度就基本正确了。

上下界网络流

https://www.luogu.com.cn/article/28oo8d8l

费用流

在最大流的基础上给每条边附加了一个单位费用,代价为单位费用乘这条边的流量。在流量最大的情况下需要费用最大/小。

做法:将 EK 算法中的 bfs 替换为 spfa,原图中反边的单位费用为正边的相反数(仍然是考虑反悔),spfa 以单位费用为边权跑最短/长路,最后单轮产生的费用即为这一轮产生的流量乘从 S S S T T T 的最短路。大部分和最大流代码类似。

namespace EK {
	int dis[maxn], pre[maxn], flow[maxn], id[maxn];
	bool vis[maxn];
	queue<int> q;
	pair<int,int> spfa() {
		memset(dis, 0x3f, sizeof(dis));
		memset(vis, 0, sizeof(vis));
		memset(flow, 0x3f, sizeof(flow));
		vis[S] = 1, dis[S] = 0; q.push(S);
		while (!q.empty()) {
			int u = q.front(); q.pop(); vis[u] = 0;
			for (int i = head[u], v, w, c;i;i = e[i].nxt) {
				if ((w = e[i].val) == 0 || dis[v = e[i].to] <= dis[u] + (c = e[i].cost))
					continue;
				dis[v] = dis[u] + c, flow[v] = min(flow[u], w), pre[v] = u, id[v] = i;
				if (!vis[v]) { vis[v] = 1; q.push(v); }
			}
		}
		if (dis[T] >= inf) return {-1,114514};
		int lim = flow[T];
		for (int i = T;i != S;i = pre[i]) 
			lim, e[id[i]].val -= lim, e[id[i] ^ 1].val += lim;
		return {lim, dis[T] * lim};
	}
	pair<int,int> EK() {
		int mxflow = 0, ans = 0;
		while (1) {
			auto tmp = spfa();
			if (tmp.first == -1) break;
			ans += tmp.second, mxflow += tmp.first;
		} return {mxflow, ans};
	}
}

  • 12
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值