2022.02.20 一道题的心得---国王的烦恼(并查集+逆向思维)
0.引言
这篇文章让我醍醐灌顶,特此记录
1.原题呈现
C国由n个小岛组成,为了方便小岛之间联络,C国在小岛间建立了m座大桥,每座大桥连接两座小岛。两个小岛间可能存在多座桥连接。然而,由于海水冲刷,有一些大桥面临着不能使用的危险。
如果两个小岛间的所有大桥都不能使用,则这两座小岛就不能直接到达了。然而,只要这两座小岛的居民能通过其他的桥或者其他的小岛互相到达,他们就会安然无事。但是,如果前一天两个小岛之间还有方法可以到达,后一天却不能到达了,居民们就会一起抗议。
现在C国的国王已经知道了每座桥能使用的天数,超过这个天数就不能使用了。现在他想知道居民们会有多少天进行抗议。
输入格式
输入的第一行包含两个整数n, m,分别表示小岛的个数和桥的数量。
接下来m行,每行三个整数a, b, t,分别表示该座桥连接a号和b号两个小岛,能使用t天。小岛的编号从1开始递增。
输出格式
输出一个整数,表示居民们会抗议的天数。
样例输入
4 4
1 2 2
1 3 2
2 3 1
3 4 3
样例输出
2
样例说明
第一天后2和3之间的桥不能使用,不影响。
第二天后1和2之间,以及1和3之间的桥不能使用,居民们会抗议。
第三天后3和4之间的桥不能使用,居民们会抗议。
数据规模和约定
对于30%的数据,1<=n<=20,1<=m<=100;
对于50%的数据,1<=n<=500,1<=m<=10000;
对于100%的数据,1<=n<=10000,1<=m<=100000,1<=a, b<=n, 1<=t<=100000。
2.审题
题目看着貌似很简单,其实大有玄机,很多人灾祸于此,其实我们忽略了很多东西。
请注意,本题是要我们输出反抗的天数,一天内可能有一组边使得其不连通,也有可能有两组使其不连通,但程序是顺序执行的,我们需要加上一个限制条件。
判断桥断开以后是否分开两片的方法很简单,有了合根植物的题目的基础上,我们自然想到并查集进行维护,即看他们的生成元是否相同来判断是否联通。
3.反思
ps:看题解的请忽略~
首先复盘我最初大概错误的思路,当时看了题解还奇怪,为什么都选择逆向做呢?答案是时间复杂度!
还原一下思路。
这可以还原一个无向图。即输入的是一个顶点到另外一个顶点的边,第三个可以看成是权值,其代表的意义是即将毁灭的天数。
很自然的想法就是输入边,权值以后构成一个无向图。
然后题目说的是抗议,那么抗议无非就是从连通到不联通的转变嘛。那我们引入并查集来维护无向图的联通状态,
- 按照一开始输入的边,先升序预处理以后,通过并查集,即,将其unite,将所有的边unite合并成n-1条边,即kruskal寻找。
- 然后如果能合成n-1条边,就让每个权值-1,然后重复1,直到不连通为止,重复1的次数就是抗议的天数。
首先,我理解错了题意,题目要求我输出抗议的天数,是抗议的总天数,而不是抗议的第一天。
其次,没有其次了,荒谬至极!
正确的逻辑应该是一座桥的毁灭以后,如果存在两个顶点之前相通,限制却不能互通的,那么结果ans+1,请注意,题目是要求前面联通以后后面却不能联通的情况,但如果实现了也不一定能通过全部测试点。
解决两个问题:
桥的毁灭后该怎么重新表示无向图点到点的联通关系??那我们得需要将边重新unite把?因为要遍历整个边,时间复杂度有O(n)
我们输出答案需要判断无向图有没有从联通到不连通的情况把??因为此行为需要遍历整个并查集,判断find(x)==x?这也又是一个O(n)
一座桥毁灭以后我们就需要2*O(n)的时间了。
那按照题目的要求,所有的桥迟早都要毁灭,那外边又得套一个for循环,就是n*(2n),最后的时间复杂度是O(n^2),题目n有10e5,开个方,时间会超出,所以就如大佬所说,这题需要逆向做才会有新世界。
4.题解
其实我们模拟其过程以后,会发现一个问题,经过每一天的流逝,直到所有的桥断裂以后,居民的反抗次数是已经固定的。那我们如果从没有桥开始,然后每当居民反抗以前,桥是好的,也就是状态从不连通到联通了会不会好很多呢??答案是会的!!如果时光流逝的话,我们就需要按损坏时间从大到小逆向排序来建桥。
我们的人生宗旨是不忘初心,这道题也一样,我们此题最主要关注的是,两岛是否联通,所以我们才在图里寻找一个最小生成树,之所以用树状结构是因为树的特点是点到点是可达的且无回路。
所以我们每次判断有一个“帮主”不是自己人的时候,把他拉进同盟(unite),就相当于建桥使其联通,在时间线上就是居民反抗前的时刻。直到此图有顶点数-1条边,此时不管如何,不存在不连通的情况了,至此居民就无反抗的情况。
这里值得注意的是,之前提到过此题的单位是天,也就是每次状态的改变是所有的权值在变化,就是有可能有两个岛或多个岛反抗的情况。所以要加一个限制条件。解决办法也让我醍醐灌顶。就是预知下一个即将建成的桥的天数是否和我这个一样,一样的话直接忽略掉,不算,但是这桥建还是要建的哈。该联通还是要联通。
参考代码,有一个点错了,但是找不到,先放着以便以后学习。。
#include<iostream>
#include<algorithm>
using namespace std;
int n,m;
struct edge
{
int u;
int v;
int w;
};
edge E[(int)10e5+5];
bool cmp(edge a,edge b)
{
return a.w>b.w;
}
int pre[(int)10e6+5];
int depth[(int)10e6+5];
//bingchaji
void init()
{
for(int i=1;i<=m;i++)
{
pre[i]=i;
depth[i]=0;
}
}
int find(int i)
{
return pre[i]==i?i:pre[i]=find(pre[i]);
}
int kruskal()
{
int ans=0,last_day=0;
int num_edge;
sort(E+1,E+m+1,cmp);
init();
for(int i=1;i<=m;i++)
{
int find_u=find(E[i].u);
int find_v=find(E[i].v);
if(find_u!=find_v)
//我们只需要将两个不可到达的顶点使其联通,这是居民就会防抗的先前情况
{
if(depth[find_u]>depth[find_v])
{
pre[find_v]=find_u;
}
else{
if(depth[find_u]==depth[find_v])
{
depth[E[i].v]++;
}
pre[find_u]=find_v;
}
//避免同一天多个居民反抗结果多加的情况
if(E[i].w!=last_day)
{
last_day=E[i].w;
ans++;
}
num_edge++;
if(num_edge==n-1)
break;
}
}
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>E[i].u>>E[i].v>>E[i].w;
}
cout<<kruskal()<<endl;
return 0;
}
这位大佬教会了我在具体情景中如何分析时间复杂度,以及审题的情况,还有考虑的诸多因素,值得再回头看一看。
end...