题面:
P5295 [北京省选集训2019] 图的难题
题目背景
标题是假的。
题目描述
小 D 在图论习题书上遇到了一个问题:
书上画出了一张无向图,要求把边染成黑白两色,要求所有白色边构成的子图没有环,且所有黑色边构成的子图没有环。
小 D 无论怎样尝试都觉得书上的问题没有解,她想请你帮她确认一下。
由于这道题有很多小问,小 D 每次会给你图的点数 、边数 与所有边集,你只需要告诉小 D 有没有解即可。
输入格式
第一行一个正整数 ,表示数据组数。
对于每组数据,第一行两个正整数 ,意义如题目描述。
接下来 行,每行两个正整数 ,表示一条 到 的无向边。
输出格式
输出 行,对于每组数据,若有解输出 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
说明/提示
数据范围:
对于 的数据:。
对于 的数据:。
对于 的数据:。
对于 的数据:,,。
我的第一道黑题!
虽然最后还是看题解才想出来最终思路...
我们看看题目,要求将整个图的边染成白色和黑色,然后两个子图都没有环。其实就等价于问你一个图是否可以被分解为两个森林。
此时我们有一个关于森林的定理:
那么在这道题中,就等价于令
这个条件确保了图中没有过于密集的子图,从而可以分解为两个森林。
那么我们怎么确定对于每个非空子集 都有上述条件成立呢?把这个条件变形一下,得到:
我们考虑这样的建图:
对于每条边 ,创建一个节点,选择它获得收益
对于每个顶点 ,创建一个节点,选择它付出代价
同时如果选择一条边,就必须选择它的两个顶点,因为我们最终得到的子图是一个最大权闭合子图,最大权闭合子图的定义具体为:
所以我们对每一个边节点,从它向这条边连接的两个节点 分别连接一条容量为 的边,从而可以保证如果选择了这条边,则这两个节点都必须选择。
则最终我们构建出来的图大致为:
则在这个网络中,一个割 对应:
·如果边节点在 侧:产生 的割值(如果不选这条边)
·如果顶点节点在 侧:产生 的割值(如果选这个顶点)
则这个图中
将其转化为与原始条件的关系:
回忆原始条件:
等价于:
因此:
·如果对于所有 ,则
·如果存在 使得 ,则
则有关键结论
·如果 ,则所有子集满足条件 有解
·如果 ,则存在违反条件的子集 无解
但由于我们需要固定一个根节点以保证选出的子集不为空集,设置其容量为 ,则实际判断为:
·如果 ,则有解
·如果 ,则无解
直观理解
最小割就是在做代价-收益权衡
·代价:选择顶点(每个代价为 )
·收益:获得边(每条收益为 )
·约束:要获得边的收益,必须承担两个端点的代价
最小割找到的就是最不划算的顶点子集 ,即 最大的子集。
如果此时这个子集都满足条件,那么所有子集都满足条件。
算法实现
考虑朴素的最小割实现,我们枚举每一个顶点作为起点,做 次 求最大流,这样做的总体时间复杂度为 ,每次都需要重新建图,听说在本题是可以过,但我没有试过,而在UOJ的加强版就无法通过了。
所以我们需要考虑一个方法来减少计算。
具体地,我们有退流技术。
顾名思义,退流技术的核心思想为:在已有流的基础上进行调整,而不是每次都从头计算最大流。
具体流程:
首先,我们以节点 为起点,设其容量为 ,其他节点的容量为 ,并计算一次初始最大流。同时我们有源点 和汇点 。
随后,我们从节点 开始进行枚举,设当前的节点为根 ,现在我们要从这个 节点开始进行新的结果计算,就要把上一个节点已经推到 的流量退回来。我们以 为源点, 为汇点,跑一遍 。从而把流量还给源点。
然后进行容量调整。首先断开节点 与汇点的连接(容量设为 ) ,然后恢复节点 与汇点的连接(容量设为 )。
然后重新计算最大流,此时相当于构造了一个新的残量图,在这个残量图上跑一遍 即可。
虽然理论上来说它的总体时间复杂度也为 ,但区别于朴素算法的每次都需要重新构建图,进行完整的最大流计算,它只需要局部的调整,进行少量增广,常数要远低于朴素算法。
#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;
}