这应该是昨天某神犇新传上到OJ上的题,看到分类标签是并查集,就欣欣然地去做了一下。
说在前面:我做的时候题目中并没有说明数据范围,导致cheat一般地试出来了数据范围: n <= 20w, m <= 40w。
乍一看好像很复杂的样子,不过想明白了思路还是很简洁的。虽说是某只猴子用手抓另一只猴子,但是效果是一样的,不管是A抓着B还是B抓着A,只要其中有一个或直接或间接抓着1或者被1抓着都不会掉落,所以这个“抓着”是可以看作无向边的。这也是使用并查集的前提。
删边的操作很难,而添加边的操作是很容易的,本身构造并查集的树的过程也就是添边的过程。所以我们把猴子松手的过程倒过来,变成从第m次删的边到第一次删的边不断的添边,而最初构建并查集的过程不用这m条边。
就是说,先用n*2-m条边建出最开始的并查集,这时候与1相连的就是最终答案为-1的不会掉落的猴子。其他猴子掉落时间均设为m-1,之后从m到1不断把边加入,每加入一次,都合并一次并查集,如果有集合被合并到1所在的数上,那么这个集合的所有猴子的掉落时间都需要更新为i-1。
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#define M 200005
using namespace std;
int n, m, r[M][3], des[M<<1][3], f[M], fail[M];
bool unexi[M][3];
vector <int> bel[M];
int find(int x){
return f[x] = f[x]==x ? x : find(f[x]);
}
int main()
{
freopen("monkeya.in","r",stdin);
freopen("monkeya.out","w",stdout);
memset(fail, -1, sizeof fail);
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++){
f[i] = i;
scanf("%d %d", r[i]+1, r[i]+2);
}
for(int i = 1; i <= m; i++){
int u, v;
scanf("%d %d", &u, &v);
des[i][v] = u;
unexi[u][v] = 1;
}
for(int i = 1; i <= n; i++){
int fi = find(i);
for(int k = 1; k < 3; k++){
if(r[i][k] > 0 && !unexi[i][k]){
int fv = find(r[i][k]);
if(fi != fv) f[fv] = fi;
}
}
}
int f1 = find(1);
for(int i = 1; i <= n; i++){
int fi = find(i);
bel[fi].push_back(i);
if(fi != f1) fail[i] = m-1;
}
for(int i = m; i; i--){
int u, v, fu, fv;
if(des[i][1]){
u = des[i][1];
v = r[u][1];
}
else{
u = des[i][2];
v = r[u][2];
}
fu = find(u);
fv = find(v);
f1 = find(1);
if(fu == fv) continue;
int su = bel[fu].size();
int sv = bel[fv].size();
if(fu == f1){
for(int j = 0; j < sv; j++){
fail[bel[fv][j]] = i-1;
}
}
if(fv == f1){
for(int j = 0; j < su; j++){
fail[bel[fu][j]] = i-1;
}
}
if(su < sv){
f[fu] = fv;
for(int j = 0; j < su; j++){
bel[fv].push_back(bel[fu][j]);
}
bel[fu].clear();
}
else{
f[fv] = fu;
for(int j = 0; j < sv; j++){
bel[fu].push_back(bel[fv][j]);
}
bel[fv].clear();
}
}
for(int i = 1; i <= n; i++){
printf("%d\n", fail[i]);
}
return 0;
}