AtCoder Beginner Contest 318 G - Typical Path Problem 题解

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}$ 实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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$ 大小的数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#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 哦~

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#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)的内存占用最小。

个人评价
这道题出得很好,题意简单而内涵丰富。
我赛时甚至没想到还可以网络流

使用 Hugo 构建
主题 StackJimmy 设计