洛谷P5295 [北京省选集训2019] 图的难题

30 阅读3分钟

原题:P5295 [北京省选集训2019] 图的难题

题面:

P5295 [北京省选集训2019] 图的难题

题目背景

标题是假的。

题目描述

小 D 在图论习题书上遇到了一个问题:

书上画出了一张无向图,要求把边染成黑白两色,要求所有白色边构成的子图没有环,且所有黑色边构成的子图没有环。

小 D 无论怎样尝试都觉得书上的问题没有解,她想请你帮她确认一下。

由于这道题有很多小问,小 D 每次会给你图的点数 nn、边数 mm 与所有边集,你只需要告诉小 D 有没有解即可。

输入格式

第一行一个正整数 TT,表示数据组数。

对于每组数据,第一行两个正整数 n,mn,m,意义如题目描述。

接下来 mm 行,每行两个正整数 u,vu,v,表示一条 uuvv 的无向边。

输出格式

输出 TT 行,对于每组数据,若有解输出 Yes,否则输出 No

输入输出样例 #1

输入 #1

3
3 3
1 2
1 3
2 3
2 3
1 2
1 2
1 2
4 6
1 2
1 3
2 4
1 3
2 3
3 4

输出 #1

Yes
No
Yes

说明/提示

数据范围:

对于 20%20\% 的数据:1m101\le m \le 10

对于 40%40\% 的数据:1n151\le n \le 15

对于 70%70\% 的数据:1n501\le n \le 50

对于 100%100\% 的数据:1n5011\le n \le 5011m2n1\le m \le 2n1T101\le T \le 10

SolutionSolution

我的第一道黑题!

虽然最后还是看题解才想出来最终思路...

我们看看题目,要求将整个图的边染成白色和黑色,然后两个子图都没有环。其实就等价于问你一个图是否可以被分解为两个森林。

此时我们有一个关于森林的定理:

NashWilliams定理:一个无向图G=(V,E)可以分解为k个边不相交的森林当且仅当对于每个非空顶点子集SV,有:Nash-Williams定理:一个无向图G=(V,E)可以分解为k个边不相交的森林当且仅当对于每个非空顶点子集S \subset V,有:

E(S)k(S1)|E(S)| \le k(|S|-1)

其中E(S)表示由S诱导的子图的边集。其中E(S)表示由S诱导的子图的边集。

那么在这道题中,就等价于令

E(S)2(S1)|E(S)| \le 2(|S|-1)

这个条件确保了图中没有过于密集的子图,从而可以分解为两个森林。

那么我们怎么确定对于每个非空子集 SS 都有上述条件成立呢?把这个条件变形一下,得到:

max{E(S)2S}2max \{|E(S)|-2|S| \} \le -2

我们考虑这样的建图:

对于每条边 eEe \in E ,创建一个节点,选择它获得收益 +1+1

对于每个顶点 vVv \in V ,创建一个节点,选择它付出代价 2-2

同时如果选择一条边,就必须选择它的两个顶点,因为我们最终得到的子图是一个最大权闭合子图,最大权闭合子图的定义具体为:

给定有向图G=(V,E)和顶点权重W(v),SV使得:给定有向图 G=(V,E)和顶点权重 W(v),求 S \subset V 使得:
1.S是闭合的:如果uS(u,v)E,vS1.S是闭合的:如果 u \in S 且 (u,v) \in E ,则 v \in S
2.vSw(v)最大2.\sum_{v \in S}w(v) 最大

所以我们对每一个边节点,从它向这条边连接的两个节点 u,vu,v 分别连接一条容量为 INFINF 的边,从而可以保证如果选择了这条边,则这两个节点都必须选择。

则最终我们构建出来的图大致为:

s容量为1边节点容量为INFu,v容量为2ts \xrightarrow {\text {容量为1}} 边节点 \xrightarrow {\text {容量为INF}} u,v \xrightarrow {\text {容量为2}} t

则在这个网络中,一个割 (S,T)(S,T) 对应:

·如果边节点在 SS 侧:产生 11 的割值(如果不选这条边)

·如果顶点节点在 TT 侧:产生 22 的割值(如果选这个顶点)

则这个图中 最小割的值=minSV[mE(S)+2S]最小割的值=min_{S \subseteq V}[m-|E(S)|+2|S|]

将其转化为与原始条件的关系:

最小割值=m+minSV[2SE(S)]最小割值=m+min_{S \subseteq V}[2|S|-|E(S)|]

回忆原始条件:

E(S)2(S1)|E(S)| \le 2(|S|-1)

等价于:

2SE(S)22|S|-|E(S)| \ge 2

因此:

·如果对于所有 S,2SE(S)2S,2|S|-|E(S)| \ge 2 ,则 最小割值m+2最小割值 \ge m+2

·如果存在 SS 使得 SE(S)<2|S|-|E(S)| < 2 ,则 最小割值<m+2最小割值 < m+2

则有关键结论

·如果 最小割值m+2最小割值 \ge m+2 ,则所有子集满足条件 \Rightarrow 有解

·如果 最小割值<m+2最小割值 < m+2 ,则存在违反条件的子集 \Rightarrow 无解

但由于我们需要固定一个根节点以保证选出的子集不为空集,设置其容量为 00 ,则实际判断为:

·如果 最大流=m最大流=m ,则有解

·如果 最大流<m最大流<m ,则无解

直观理解

最小割就是在做代价-收益权衡

·代价:选择顶点(每个代价为 22

·收益:获得边(每条收益为 11

·约束:要获得边的收益,必须承担两个端点的代价

最小割找到的就是最不划算的顶点子集 SS ,即 E(S)2S|E(S)|-2|S| 最大的子集。

如果此时这个子集都满足条件,那么所有子集都满足条件。

算法实现

考虑朴素的最小割实现,我们枚举每一个顶点作为起点,做 nnDinicDinic 求最大流,这样做的总体时间复杂度为 O(n(n+m)2.5)O(n(n+m)^{2.5}) ,每次都需要重新建图,听说在本题是可以过,但我没有试过,而在UOJ的加强版就无法通过了。

所以我们需要考虑一个方法来减少计算。

具体地,我们有退流技术。

顾名思义,退流技术的核心思想为:在已有流的基础上进行调整,而不是每次都从头计算最大流。

具体流程:

首先,我们以节点 11 为起点,设其容量为 00 ,其他节点的容量为 22 ,并计算一次初始最大流。同时我们有源点 s=0s=0 和汇点 t=n+m+1t=n+m+1

随后,我们从节点 22 开始进行枚举,设当前的节点为根 rootroot ,现在我们要从这个 rootroot 节点开始进行新的结果计算,就要把上一个节点已经推到 tt 的流量退回来。我们以 root1root-1 为源点,s=0s=0 为汇点,跑一遍 DinicDinic 。从而把流量还给源点。

然后进行容量调整。首先断开节点 rootroot 与汇点的连接(容量设为 00) ,然后恢复节点 root1root-1 与汇点的连接(容量设为 22)。

然后重新计算最大流,此时相当于构造了一个新的残量图,在这个残量图上跑一遍 DinicDinic 即可。

虽然理论上来说它的总体时间复杂度也为 O(n(n+m)2.5)O(n(n+m)^{2.5}) ,但区别于朴素算法的每次都需要重新构建图,进行完整的最大流计算,它只需要局部的调整,进行少量增广,常数要远低于朴素算法。

CodingCoding

#include <iostream>
#include <cstring>
#include <iomanip>
#include <cmath>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;

#define ll long long
#define ull unsigned long long
#define debug(x) cout << #x << "=" << x << "\n";

int T;
int n, m;
const int maxn = 1e5 + 10, maxm = 1e6 + 10;
const int INF = 1E9;
struct Edge
{
    int to, cap, next;
} edge[maxm];
int head[maxn], cur[maxn], level[maxn];
int tot;
Edge temp[maxm];

void add_edge(int u, int v, int c)
{
    edge[tot] = {v, c, head[u]};
    head[u] = tot++;
    edge[tot] = {u, 0, head[v]};
    head[v] = tot++;
}

bool bfs(int s, int t)
{
    queue<int> q;
    q.push(s);
    memset(level, -1, sizeof(level));
    level[s] = 0;

    while (!q.empty())
    {
        int u = q.front();
        q.pop();

        if (u == t)
            return true;

        for (int i = head[u]; i; i = edge[i].next)
        {
            int v = edge[i].to;
            if (level[v] == -1 && edge[i].cap > 0)
            {
                level[v] = level[u] + 1;
                q.push(v);
            }
        }
    }

    return false;
}

int dfs(int u, int t, int flow)
{
    if (u == t)
        return flow;

    int used = 0;
    for (int &i = cur[u]; i; i = edge[i].next)
    {
        int v = edge[i].to, cap = edge[i].cap;

        if (cap <= 0)
            continue;

        if (level[v] == level[u] + 1)
        {
            int ret = dfs(v, t, min(flow - used, cap));
            if (ret)
            {
                edge[i].cap -= ret;
                edge[i ^ 1].cap += ret;
                used += ret;
                if (used == flow)
                    break;
            }
        }
    }

    return used;
}

int dinic(int s, int t)
{
    int max_flow = 0;
    while (bfs(s, t))
    {
        memcpy(cur, head, sizeof(head));
        max_flow += dfs(s, t, INF);
    }

    return max_flow;
}

void solve()
{
    cin >> n >> m;
    int s = 0, t = n + m + 1;

    memset(head, 0, sizeof(head));
    memset(edge, 0, sizeof(edge));
    tot = 2;

    add_edge(1, t, 0);
    for (int i = 2; i <= n; i++)
        add_edge(i, t, 2);

    for (int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;

        add_edge(s, n + i, 1);
        add_edge(n + i, u, INF);
        add_edge(n + i, v, INF);
    }

    int res = dinic(s, t);
    if (res < m)
        return void(cout << "No\n");

    for (int i = 2; i <= n; i++)
    {
        s = i, t = 0;
        dinic(s, t);

        for (int j = head[i]; j; j = edge[j].next)
        {
            if (edge[j].to == n + m + 1)
            {
                res -= edge[j ^ 1].cap;
                edge[j].cap = edge[j ^ 1].cap = 0;
            }
        }

        for (int j = head[i - 1]; j; j = edge[j].next)
        {
            if (edge[j].to == n + m + 1)
                edge[j].cap = 2;
        }

        s = 0, t = n + m + 1;
        res += dinic(s, t);

        if (res < m)
            return void(cout << "No\n");
    }

    cout << "Yes\n";
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> T;
    while (T--)
        solve();

    return 0;
}