题解之前的小故事——
细
节
决
定
成
败
细节决定成败
细节决定成败
两天前为了练习并查集,在洛谷做了这道题,刚开始的时候没有思路就去看了题解,找到解题方向后就去自己写了代码,在本地通过样例后就去提交,结果只通过了四个点,就回去检查自己的代码,发现没有什么大问题,就去看了题解的代码,发现解题思想是一致的,就又交了一发,结果还是死活WA了六个点只有40分,又回去检查代码,发现真的没有什么问题啊,我想是不是测试点改了数据,可是这道题恰恰不能下载测试点的数据,就拿题解的代码去提交,结果题解的代码能过。这就难到我了,没有数据可测的,自己检查又检查不出来,一下午就在找bug过程中度过,结果还是没找出来。我发现这种滋味真的不好受,晚上去上课,心里也是痒痒的。第二天来了之后还是不想放弃,又想到是不是洛谷判题机有问题,拿到其他OJ上去提交也是只过了四个点,又重复了昨天的一波发愣后,我实在是有点失落了,叫上实验室的小伙伴看了之后,他也没有发现哪里出错了,我就有点想放弃了。下午去做别的事情没管这道题,但我心里想谁要是帮我找到这个bug,我一定要请他吃顿饭。
这就是这篇题解今天才来写的原因,为了兑现两天前的承诺(只可惜bug是我自己今天又在琢磨这道题时查看别人提交的代码时发现的),我晚上要好好犒劳自己(滑稽 )
——————————分割线
回归正题,写这篇博客就是为了提醒自己写代码时一定要细心谨慎,不该偷的懒一定不能偷!
基本思路:这个题是一个倒过来的并查集。题目的目的是求各个时间联通块的个数,从最后时刻往前做,并查集的初始状态即被破坏后的图。
主要思想是,并查集中处于一个联通块的祖先结点相等,当任意两个祖先不同的联通块联通时,联通块会减少一个(同时当破坏的被恢复时会增加一个),通过这种操作可以知道目前有多少个联通块。而本题只需要知道是否在一个并查集中就可知道是否增删联通块了,因此核心算法不是很难。
具体操作说明附在代码里
看我40分的代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn=400010;
int n,m,k,cnt,num,fa[maxn],head[maxn],broken[maxn],ans[maxn];
bool bro[maxn];
struct node{
int from,to,next;
}e[maxn];
void add(int a,int b){ //链式前向星
e[++cnt].from=a;
e[cnt].to=b;
e[cnt].next=head[a];
head[a]=cnt;
}
int find(int x){ //寻找祖先
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
void unite(int x,int y){ //合并两个连通块
fa[find(x)]=find(y);
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
fa[i]=i;
}
while(m--){
int x,y;
cin>>x>>y;
add(x,y);
add(y,x); //无向图要加两次
}
cin>>k;
for(int i=1;i<=k;i++){ //记录被摧毁的点
cin>>broken[i];
bro[broken[i]]=true;
}
num=n-k; //计算剩余没有被摧毁的点,就有多少个连通块
for(int i=1;i<=2*m;i++){ //一共有2*m条边
if(!bro[e[i].from]&&!bro[e[i].to]&&find(e[i].from)!=find(e[i].to)){ //如果没有被摧毁且不在一个连通块里
unite(e[i].to,e[i].from); //合并
num--; //连通块减一
}
}
ans[k+1]=num; //被摧毁后实际的连通块个数
for(int i=k;i;i--){ //逆序,模拟修复过程
num++; //修复了一个点,就加了一个连通块
bro[broken[i]]=false; //标记被修复了
for(int j=head[broken[i]];j;j=e[j].next){
if(!bro[e[j].to]&&find(broken[i])!=find(e[j].to)){ //如果和已经修复好的连通块存在边
unite(e[j].to,broken[i]); //合并
num--; //连通块减一
}
}
ans[i]=num; //记录修复了这个点后实际的连通块个数
}
for(int i=1;i<=k+1;i++){ //逆序输出
cout<<ans[i]<<endl;
}
return 0;
}
提交结果:
我这两天一共提交的(除了最后一次,中间两次AC的是复制的题解的代码):
惨兮兮的,直到我刚才去这道题所有的提交记录里去看别人的代码,才终于发现我哪里错了。
最后AC的代码(和上面的基本一样,只有一个细节也是最致命的细节有点改动,希望读者们以后写代码的时候也一定要注意细节,该偷懒的时候可以偷懒,但千万也要思考偷懒会不会带来不好的后果,有点像是在谈人生的问题 ):
#include <bits/stdc++.h>
using namespace std;
const int maxn=400010;
int n,m,k,cnt,num,fa[maxn],head[maxn],broken[maxn],ans[maxn];
bool bro[maxn];
struct node{
int from,to,next;
}e[maxn];
void add(int a,int b){
e[++cnt].from=a;
e[cnt].to=b;
e[cnt].next=head[a];
head[a]=cnt;
}
int find(int x){
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
void unite(int x,int y){
fa[find(x)]=find(y);
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
fa[i]=i;
}
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
add(x,y);
add(y,x);
}
cin>>k;
for(int i=1;i<=k;i++){
cin>>broken[i];
bro[broken[i]]=true;
}
num=n-k;
for(int i=1;i<=2*m;i++){
if(!bro[e[i].from]&&!bro[e[i].to]&&find(e[i].from)!=find(e[i].to)){
unite(e[i].to,e[i].from);
num--;
}
}
ans[k+1]=num;
for(int i=k;i;i--){
num++;
bro[broken[i]]=false;
for(int j=head[broken[i]];j;j=e[j].next){
if(!bro[e[j].to]&&find(broken[i])!=find(e[j].to)){
unite(e[j].to,broken[i]);
num--;
}
}
ans[i]=num;
}
for(int i=1;i<=k+1;i++){
cout<<ans[i]<<endl;
}
return 0;
}