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;
}
  • 点覆盖:选若干点使得所有边都有被选择的点覆盖。
  • 独立集:原图中点集的一个子集,使得子集中所有点没有边相连。(无向图最大团等于其补图的最大独立集)
  • 最小点覆盖等价于最大匹配(前者所需点数 === 后者所需边数),最大独立集等价于 nnn​ 减最小点覆盖。

模型:最小路径覆盖

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

  • 做法:考虑拆点构建二分图。将所有点拆成两个点(记为 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) 上有流量(即残余流量为 000)那么我们合并 x,yx,yx,yx,yx,yx,y 在同一条路径上。最后找每条路径的起点 O(n2)O(n^2)O(n2) 打印即可。

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

Muddy Fields

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

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

模型:二分图博弈

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

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

网络流

一张有向图,每条边有一个容量表示经过这条边的流量的上限。一般有一个源点和汇点(出度 / 入度为 000)。需要满足三条性质:容量限制、斜对称性(设 (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 较为主流。主要思路:不断寻找增广路来提升流量,用反悔贪心做(即可以撤销之前的方案)。

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

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

EK

不断使用 bfs 寻找增广路进行增广。每当我们找到一条增广路,我们就求出当前每条边的剩余流量的最小值,则最大流可以获得这个最小值,然后每条边的剩余流量都减去这个最小值;最后考虑反悔,令每条边的反向边的剩余流量加上这个最小值,初始这些边的剩余流量为 000。理论复杂度 O(nm2)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(n2m)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. 对于问题 PPP 考虑建流网络 GGG
  2. 证明可行解与可行流能互相转化、一一对应。
  3. 最大可行解就转化成最大流问题,最小割类似。

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

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

模型:最大权值闭合图

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

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

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

最大获利

给定 nnn 个点,选择第 iii 个点需要 PiP_iPi 的代价;给出共 mmm 个点对,对于第 iii 个点对 (Ai,Bi)(A_i,B_i)(Ai,Bi),如果 Ai,BiA_i,B_iAi,Bi 同时选择那么可以获得 CiC_iCi 的收益,求收益减代价的最大值。

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

Task Assignment to Two Employees

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

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

  • 对于不同的两点 i,ji,ji,j,连接容量均为 max⁡(s1,iv1,j,s1,jv1,i)+max⁡(s2,iv2,j,s2,jv2,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) 的双向边(反边照常建),这里将每条边的贡献翻倍以便于建边;
  • 对于每个点 iii 记录一个顶标 h1/2.ih_{1/2.i}h1/2.i 表示按照上述规则建边时与第 1/21/21/2 个人的相关的边权之和。
  • S,TS,TS,T 分别为超级源点和超级汇点,最后对于每个任务 iii,连接 S→iS\to iSi 容量为 2p0v1,i+h1,i2p_0v_{1,i}+h_{1,i}2p0v1,i+h1,i,连接 i→Ti\to TiT 容量为 2p0v2,i+h2,i2p_0v_{2,i}+h_{2,i}2p0v2,i+h2,i

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

Sum of Abs

给定一张 nnn 个点 mmm 条边的无向图,第 iii 个点有两个点权 AiA_iAiBiB_iBi;删去第 iii 个点和与之相连的边的代价为 AiA_iAi。定义一个极大连通块的权值为连通块中点的 BBB 权值和的绝对值。最大化删除若干点后,所有极大连通块权值和减去总代价。

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

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

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

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

Special Edges

给定一张 nnn 个点 mmm 条边的网络流,源点和汇点分别为 1,n1,n1,nqqq 次询问每次更改编号为 111kkk 的边的容量(初始容量均为 000),对于每次询问求出此时的最大流。

1≤k≤101\le k\le 101k101≤容量≤251\le \text{容量}\le 251容量25

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

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

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

上下界网络流

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

费用流

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

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

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};
	}
}

### C++ 中 `int day` 和 `int&amp; day` 的区别 #### 1. 基本定义 - **`int day`** 是一个普通的整型变量声明。它表示分配一块内存空间,用于存储一个整数值[^2]。 - **`int&amp; day`** 是一个整型引用变量的声明。引用是一个别名,它并不分配新的内存空间,而是直接指向已存在的变量[^3]。 #### 2. 内存分配与访问 - 当声明 `int day` 时,编译器会为 `day` 分配一块独立的内存空间。对该变量的任何操作都仅限于这块内存中的值[^2]。 - 当声明 `int&amp; day` 时,`day` 并不分配新的内存空间,而是作为另一个变量的别名存在。对 `day` 的修改实际上是对原始变量的修改[^3]。 #### 3. 示例代码 以下代码展示了两者的差异: ```cpp #include &lt;iostream&gt; using namespace std; int main() { int a = 10; // 普通整型变量 int b = a; // 普通赋值,b是a的一个副本 int&amp; ref_a = a; // 引用变量,ref_a是a的别名 cout &lt;&lt; &quot;Before modification:&quot; &lt;&lt; endl; cout &lt;&lt; &quot;a: &quot; &lt;&lt; a &lt;&lt; &quot;, b: &quot; &lt;&lt; b &lt;&lt; &quot;, ref_a: &quot; &lt;&lt; ref_a &lt;&lt; endl; b = 20; // 修改b不会影响a ref_a = 30; // 修改ref_a会影响a cout &lt;&lt; &quot;After modification:&quot; &lt;&lt; endl; cout &lt;&lt; &quot;a: &quot; &lt;&lt; a &lt;&lt; &quot;, b: &quot; &lt;&lt; b &lt;&lt; &quot;, ref_a: &quot; &lt;&lt; ref_a &lt;&lt; endl; return 0; } ``` #### 4. 输出结果 运行上述代码后,输出如下: ``` Before modification: a: 10, b: 10, ref_a: 10 After modification: a: 30, b: 20, ref_a: 30 ``` 从输出可以看到,修改 `b` 不会影响 `a`,而修改 `ref_a` 会直接影响 `a` 的值,因为 `ref_a` 是 `a` 的别名[^3]。 #### 5. 使用场景 - **`int day`** 适用于需要独立存储值的场景。例如,当需要创建一个新的变量并独立对其进行操作时使用普通变量。 - **`int&amp; day`** 适用于需要通过函数参数传递变量并修改原变量值的场景。此外,在返回大对象或避免拷贝时也常用引用[^3]。 #### 6. 注意事项 - 引用必须在声明时初始化,并且一旦绑定到某个变量后,不能重新绑定到其他变量[^3]。 - 引用本身并不是一个新对象,因此不能取地址(即不能使用 `&amp;ref_a` 来获取引用本身的地址)[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值