G - Typical Path Problem
题目大意
给定一张 \(N\) 个点、\(M\) 条边的简单无向图 \(G\) 和三个整数 \(A,B,C\)。
是否存在一条从顶点 \(A\) 到 \(C\),且经过 \(B\) 的简单路径?
数据范围:
- \(3\le N\le 2\times 10^5\)
- \(N-1\le M\le \min(\frac{N(N-1)}2,2\times 10^5)\)
- \(1\le A,B,C\le N\)(\(A,B,C\) 互不相同)
什么是 简单路径 ?
简单路径 是不重复经过同一个点的路径。例如,\(1\to 2\to 3\) 是简单路径,但 \(1\to 2\to 1\) 不是简单路径。
解法1:最大流
不难发现,存在一条 \(A\to B\to C\) 的简单路径,当且仅当存在一条 \(B\to A\) 和一条 \(B\to C\) 的路径,使得这两条路径不经过同一个点(\(B\) 除外)。因此,我们可以构建网络流模型来解决此问题。
考虑由 \((2N+2)\) 个点组成的有向图 \(G'\):
- 源点:\(s\)
- 汇点:\(t\)
- \(G\) 中每个点对应的入点:\(x_1,\dots,x_N\)
- \(G\) 中每个点对应的出点:\(y_1,\dots,y_N\)
然后进行连边:
- 对于每个 \(1\le i\le N\),从入点 \(x_i\) 向出点 \(y_i\) 连接一条流量为 \(1\) 的边;
- 从源点 \(s\) 到中转点的入点 \(x_B\) 连接一条流量为 \(2\) 的边;
- 从 \(A\) 和 \(C\) 的出点 \(y_A,y_C\) 向汇点 \(t\) 分别连接一条流量为 \(1\) 的边;
- 最后,\(\forall (u,v)\in E_G\),连接 \(y_u \to x_v\) 和 \(y_v \to x_u\),流量为 \(1\)。
计算 \(s\) 到 \(t\) 的最大流,如果最大流为 \(2\) 则必定有存在不经过同一个顶点的 \(B\to A,B\to C\) 的路径。
证明
显然,如果最大流为 \(2\),必然通过了 \(y_A\) 和 \(y_C\) 向汇点连接的边,则一定分别有 \(B\to A\) 和 \(B\to C\) 的路径。
假设选择的这两条路径经过了同一顶点 \(v\),则两流都必须经过 \(x_v\to y_v\) 这一条流量为 \(1\) 的边,此时最大流不可能超过 \(1\)。而最大流为 \(2\),说明假设不成立,故没有经过同一顶点。
若使用 \(\text{Dinic}\) 算法,由于最大流不超过 \(2\),网络流的时间复杂度为 \(\mathcal O(N+M)\)。
代码实现
在以下的两种实现中,我们规定
- 源点:\(s=0\)
- 汇点:\(t=2n+1\)
- \(i\) 的入点:\(x_i=i\)
- \(i\) 的出点:\(y_i=n+i\)
AC Library 实现
AtCoder Library 内置最大流的 \(\text{Dinic}\) 实现。
#include <cstdio>
#include <atcoder/maxflow>
using namespace std;
int main()
{
int n, m, a, b, c;
scanf("%d%d%d%d%d", &n, &m, &a, &b, &c);
int s = 0, t = (n << 1) + 1;
atcoder::mf_graph<int> G(t + 1);
G.add_edge(s, b + n, 2);
G.add_edge(a + n, t, 1);
G.add_edge(c + n, t, 1);
for(int i=1; i<=n; i++)
G.add_edge(i, i + n, 1);
while(m--)
{
int x, y;
scanf("%d%d", &x, &y);
G.add_edge(x + n, y, 1);
G.add_edge(y + n, x, 1);
}
puts(G.flow(s, t, 2) == 2? "Yes": "No");
return 0;
}
Dinic 手写实现
\(\text{Dinic}\) 算法对于此图的时间复杂度为 \(\mathcal O(N+M)\)。如果不清楚算法原理可以参考 OI Wiki。
关于空间分配问题
由于新图 \(G'\) 包含 \((N+2M+3)\) 条边,若使用静态链式前向星存图,数组大小需要开到 \(2(N+2M+3)\),其理论最大值为 \(1.2\times 10^6+6\)。此处建议使用 \(1.25\times 10^6\) 大小的数组。
#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
#define maxn 400005
#define maxm 1250005
using namespace std;
int n, s, t, head[maxn], cur[maxn], dis[maxn],
cnt, w[maxm], to[maxm], nxt[maxm];
inline void add(int u, int v, int flow)
{
nxt[cnt] = head[u];
head[u] = cnt;
to[cnt] = v;
w[cnt++] = flow;
}
inline void add_flow(int u, int v, int f)
{
add(u, v, f);
add(v, u, 0);
}
inline bool bfs()
{
memset(dis, -1, sizeof(int) * n);
dis[s] = 0, cur[s] = head[s];
queue<int> q;
q.push(s);
while(!q.empty())
{
int v = q.front(); q.pop();
for(int i=head[v]; ~i; i=nxt[i])
if(w[i])
{
int u = to[i];
if(dis[u] == -1)
{
dis[u] = dis[v] + 1, cur[u] = head[u];
if(u == t) return true;
q.push(u);
}
}
}
return false;
}
int dfs(int v, int flow)
{
if(v == t) return flow;
int res = 0;
for(int i=cur[v]; ~i && flow; i=nxt[i])
{
cur[v] = i;
int u = to[i];
if(w[i] && dis[u] == dis[v] + 1)
{
int k = dfs(u, min(flow, w[i]));
w[i] -= k;
w[i ^ 1] += k;
flow -= k;
res += k;
}
}
return res;
}
int main()
{
int n, m, a, b, c;
scanf("%d%d%d%d%d", &n, &m, &a, &b, &c);
s = 0, t = (n << 1) + 1, ::n = t + 1;
memset(head, -1, sizeof(int) * ::n);
add_flow(s, b + n, 2);
add_flow(a + n, t, 1);
add_flow(c + n, t, 1);
for(int i=1; i<=n; i++)
add_flow(i, i + n, 1);
while(m--)
{
int x, y;
scanf("%d%d", &x, &y);
add_flow(x + n, y, 1);
add_flow(y + n, x, 1);
}
int mf = 0;
while(bfs()) mf += dfs(s, 2);
puts(mf == 2? "Yes": "No");
return 0;
}
解法2:圆方树
注意到以下算法的正确性:
- 找到 \(A\to C\) 的任意简单路径。对于经过的每一个点双连通分量,如果 \(B\) 在此点双内,则必然存在 \(A\to B\to C\) 的简单路径;如果 \(B\) 不属于任一经过的点双,则不可能存在 \(A\to B\to C\) 的简单路径。
因此,可以使用 \(\text{Tarjan}\) 算法构造原图的圆方树 \(T\) 来解决此问题。将上述算法转换到圆方树上如下:
- 在 \(T\) 上找到 \(A\to C\) 的唯一简单路径。对于经过的每一个方点,如果 \(B\) 是与其相邻的圆点,则必然存在 \(A\to B\to C\) 的简单路径;如果 \(B\) 不与任一经过的方点相邻,则不可能存在 \(A\to B\to C\) 的简单路径。
总时间复杂度为 \(\mathcal O(N+M)\),实际运行时间优于网络流解法。
代码实现
小贴士:圆方树相关的数组要开到两倍大小,不然会 RE 哦~
#include <cstdio>
#include <cstdlib>
#include <vector>
#define maxn 200005
using namespace std;
inline void setmin(int& x, int y)
{
if(y < x) x = y;
}
vector<int> G[maxn], T[maxn << 1];
inline void add_edge(vector<int>* G, int x, int y)
{
G[x].push_back(y);
G[y].push_back(x);
}
int dfc, dfn[maxn], low[maxn], top, st[maxn], cnt;
void tarjan(int v)
{
low[v] = dfn[v] = ++dfc;
st[++top] = v;
for(int u: G[v])
if(!dfn[u])
{
tarjan(u);
setmin(low[v], low[u]);
if(low[u] == dfn[v])
{
add_edge(T, v, ++cnt);
do add_edge(T, st[top], cnt);
while(st[top--] != u);
}
}
else setmin(low[v], dfn[u]);
}
int n, m, a, b, c, ct[maxn << 1];
void dfs(int v, int par)
{
if(v > n)
for(int u: T[v])
ct[u] ++;
if(v == c)
{
puts(ct[b]? "Yes": "No");
exit(0);
}
for(int u: T[v])
if(u != par)
dfs(u, v);
if(v > n)
for(int u: T[v])
ct[u] --;
}
int main()
{
scanf("%d%d%d%d%d", &n, &m, &a, &b, &c);
while(m--)
{
int x, y;
scanf("%d%d", &x, &y);
add_edge(G, x, y);
}
cnt = n;
tarjan(1);
dfs(a, -1);
return 0;
}
总结
三种解法的对比参见下表:
解法 | 代码长度 | 运行时间 | 内存占用 |
---|---|---|---|
最大流(AC Library)[1] | \(523~\mathrm{B}\) | \(337~\mathrm{ms}\) | \(106480~\mathrm{KB}\) |
最大流(Dinic)[2] | \(1650~\mathrm{B}\) | \(334~\mathrm{ms}\) | \(46980~\mathrm{KB}\) |
圆方树[3] | \(1142~\mathrm{B}\) | \(162~\mathrm{ms}\) | \(57824~\mathrm{KB}\) |
可见,圆方树算法的运行速度最快,最大流(AC Library)的代码最短,最大流(Dinic)的内存占用最小。
个人评价
这道题出得很好,题意简单而内涵丰富。
我赛时甚至没想到还可以网络流