开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情
[Codeforces] Codeforces Round #847 (Div. 3) G. Tokens on Graph | 图论,并查集
题目链接
题目
题目大意
给一张n个点m条边无向连通图,1号点为终点,n个点中有p个点上有token,并且有b个点上有bonus,bonus和token可以在同一个点上,bonus始终不变,初始状态token不会重复在同一个点上,但token可以重叠在同一个点上。现在规定你可以进行如下操作:
- 最开始你可以做恰好一次小操作:选择一个token移动到它所在点的任意一个相邻点处
- 如果之前的token移动到的点是一个bonus点,你可以选择另一个和上一个点不同的点再进行一次小操作,并重复2的判断
问进行上述操作后是否存在一个token能到达终点(1号点),如果1号点原本有token也算到达。
思路
暂时先不考虑相邻两次小操作的token需要不同的规定,即假设一个token只要在上次遇到bonus点就可以一直走下去。那要让至少一个token能走到1号点,则至少要有一个token可以通过只有bonus点作为中间点的路径到达1号点,这是一个必要条件。
我们如何判断是否存在这样的token呢?
首先排除掉特殊情况,1号点有token 和 1号点的邻近点有token
然后我们构建一个并查集,只对于两端的点都是bonus的边进行merge,这样子就有了一些连通块,同时我们维护每个连通块的bonus点数量以筛去不是由bonus点组成的单点连通块。
之后我们对1号点的所有邻近点进行判断,如果它在某个bonus连通块里,把该连通块的根节点加入集合link。我们再对所有的token点做类似操作,如果一个token点的某个邻近点所在连通块和S中对应的某个连通块一样,说明这个token点可以只经过bonus点走到1号点,把这个点加入集合candidate。
做到这里,我们可以筛选出所有能只通过bonus点到达1号点的token集合。接下来,我们考虑限制:两次小操作对应的token不能是同一个。
第一种情况:candidate.size() = 0 直接NO
第二种情况:candidate.size() > 1 这代表至少有两个token可以走到1号点,它们交替着走,到1号点距离更短的点就能先到达1号点,所以是YES
第三种情况:candidate.size() = 1 这时候只有一个初始token点可以只经过bonus点到达1号点,设这个点是u。我们要比较其他token点能给u提供的小操作次数 和 1号点只经过bonus点到达u的最短距离。后者用一个平凡的最短路算法可以求出,前者则是对于剩下的token点进行如下考虑:
-
如果这个token点的邻近点不存在bonus点,那么这个token点不能提供任何额外次数;
-
如果这个token点的邻近点存在bonus点
- 存在一个bonus点所在的bonus连通块size > 1,那么token点可以在这个连通块里绕圈,可以提供无限的额外次数
- 如果所有bonus点都是单点bonus连通块,那么这个token只能提供1次额外次数
我们对每个点提供的额外次数之和 + 1(u点先走,本身就有一次) 和 u点到1号点的最短距离进行比较,>= 则YES,<则NO。至此判断完毕,全题结束。
代码
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
using namespace std;
const int maxn = 2e6 + 10;
const int mod = 1e9 + 7;
const int inf = 1LL << 60;
#define db 0
#define ttest if (db)
void solve() {
int n, m, p, b;
cin >> n >> m;
vector<vector<int>> edge(n);
cin >> p >> b;
vector<int> tokens(n, 0), bonus(n, 0);
for (int i = 0; i < p; ++i) {int x; cin >> x; --x; tokens[x] = 1;}
for (int i = 0; i < b; ++i) {int x; cin >> x; --x; bonus[x] = 1;}
for (int i = 0; i < m; ++i) {
int x, y;
cin >> x >> y;
-- x; -- y;
edge[x].push_back(y);
edge[y].push_back(x);
}
if (tokens[0]) {cout << "YES\n"; return;}
for (int x : edge[0]) if (tokens[x]) {cout << "YES\n"; return;}
vector<int> f(n), sz(bonus);
iota(f.begin(), f.end(), 0);
function<int(int)> fa = [&](int x) {
return (x == f[x]) ? x : f[x] = fa(f[x]);
};
function merge = [&](int x, int y) {
x = fa(x); y = fa(y);
f[x] = y;
if (x != y) {
sz[y] += sz[x];
}
return;
};
for (int i = 0; i < n; ++i) if (bonus[i]) {
for (int j : edge[i]) if (bonus[j]) {
merge(i, j);
}
}
set<int> link;
for (int x : edge[0]) if (bonus[x] && sz[fa(x)]) link.insert(fa(x));
if (!link.size()) {cout << "NO\n"; return;}
set<int> candidate;
for (int i = 0; i < n; ++i) if (tokens[i]) {
for (int x : edge[i]) if (bonus[x] && link.count(fa(x))) {
candidate.insert(i);
break;
}
}
if (candidate.size() > 1) {cout << "YES\n"; return;}
if (!candidate.size()) {cout << "NO\n"; return;}
assert((int)candidate.size() == 1);
int u = *candidate.begin();
int steps = 0;
for (int i = 0; i < n && steps != inf; ++i) if (i != u && tokens[i]) {
int cc = 0;
for (int x : edge[i]) {
if (sz[fa(x)] > 1) {
steps = inf; break;
}
else if (sz[fa(x)] == 1) cc = 1;
}
if (steps < inf) steps += cc;
}
queue<int> q;
vector<int> vis(n, 0), dis(n, inf);
vis[0] = 1; dis[0] = 0; q.push(0);
while (!q.empty()) {
int x = q.front(); q.pop();
for (int y : edge[x]) if (!vis[y] && (bonus[y] || y == u)) {
dis[y] = dis[x] + 1;
vis[y] = 1;
q.push(y);
}
}
assert(dis[u] != inf);
if (dis[u] <= steps+1) cout << "YES\n"; else cout << "NO\n";
}
void refresh() {
}
signed main() {
IOS
int t;
t = 1;
cin >> t;
while (t--) {
solve();
refresh();
}
return 0;
}