历届试题 国王的烦恼
问题描述
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。
——分割线——
分析:
读完这道题的第一感觉是,可不可以模拟+枚举?
就是说我去模拟从第一天开始,某(几)个桥坏掉,然后检测此时所有的小岛是否联通;然后接着第二天,某(几)个桥坏掉,然后检测此时所有的小岛是否联通……直到检测出某一天所有的小岛不再联通。
那么在这样的思路下,具体做法即为:
首先输入所有的a,b,t(a、b表示桥连接的两个小岛,而t代表这个桥的可存在时限),这个可以用一个结构体Bridge(桥)来表示。由于录入的桥有多个,所以这里就把这些桥用一个数组bridge[100010]来存储。然后把所有的桥的数据,利用并查集来判断其一开始是否是可通的。怎么利用并查集呢?即我们先把上面存储各个桥的信息的数组bridge根据其中的生命t参数进行升序排序,然后从第一个开始循环for(int i=0; i < m ;i++),去模拟当前第i个桥坏掉的前提下,接下来的桥能否使得所有的剩余小岛联通。
即,在这个i循环的内部可以再利用一层循环for(int j=i;j < m;j++)来进行并查集的unite操作,看最终是否能够使得所有小岛连通。如果能,说明当前桥坏掉之后居民不会抗议;否则,居民抗议。这样直到最后,即能算得居民抗议的天数了。
这个思路很清晰也很好理解,但看一下这道题的数据范围(m<100000),ok!不用想了,超时无疑。
虽然说上面的分析不能实质性的解决问题,但是它却给我们提供了一种解题的思路。接下来我们换一种思路(个人认为,当换了这样的思路之后,整个题的大门就立刻被打开了):
题目要我们做的是求居民抗议的天数。我们如果从一开始去模拟一座桥一座桥的坏掉,那有可能遇到很坏的情况——在很后面才遇到那个割线(离散数学术语,表示当当前边被除去后,整个图的连通分支数就由1变为了2)的出现。而你在每一次坏掉一座桥的时候都需要从头去联合这些桥以判断所有小岛的连通性,这样必然会超时。那我们完全可以逆向思维,不去模拟桥的坏掉,而是去模拟“桥的修建”!!!这样一来我们的程序就只需要循环一次去联合(unite)这些桥,一旦出现某座桥在联合进当前的并查集时,整个图的连通分支数变为了1,就表示这一天是居民开始抗议的转折点,因为当天数大于等于这一天之后的每一天都将会使得当前小岛的连通分支数再次变为大于1。换言之,就找到了居民的抗议天数。
接下来就以本题的例子来演绎一下这个思路:
首先给出桥的信息如下:
小岛a | 小岛b | 生命时限 |
---|---|---|
3 | 4 | 3 |
1 | 2 | 2 |
1 | 3 | 2 |
2 | 3 | 1 |
最开始(int ans=0),也就第4天时,整个图是各自孤立的四座小岛(连通分支数为4,如下图):
第3天,此时修建了岛3和岛4之间的桥,(连通分支数为3,如下图):
由于此时所有的小岛没有连通,那么在第3天居民会抗议,ans++(此时ans=1)
接下来来到第2天,此时修建了岛1和岛2之间的桥,(连通分支数为2,如下图):
由于此时所有小岛仍然没有连通,也就是说在第2天居民们还是会抗议,ans++(此时ans=2)
但需要注意的是,生命时限为2天的有两座桥!也就是说其实第二天的真实情况应该如下(应该还多修建了一座岛1和岛3之间的桥):
这时候其实所有小岛是连通的!那么在这种情况下刚才的ans应不应该++呢?
你仔细想,我们这是逆向修建的过程,在正向中,真实情况是这一天这两座桥都没了!也就是说这其实就是那个转折点,因此这里需要++,但是只++一次。
接下来时间来到第1天,这时候小岛的连通情况(也是初始情况)如下图所示:
这天所有小岛之间是保持着连通性的(连通分支数为1),换言之这里ans不应该++
然后退出循环,得到居民反抗天数为2
下面直接给出本题的完整代码:
---分割线---
#include<iostream>
#include<algorithm>
using namespace std;
const int N=10000;
const int MAX=100010;
struct Bridge //代表桥
{
int x,y; //表示桥连接的两个地方
int day; //表示这个桥的可存在的时限(天数)
};
Bridge bridge[MAX];//用于存储所有的桥
int pre[N]; //用于存储每个小岛的“上级”
void init(int n)
{
for(int i=1;i<=n;i++)
pre[i]=i;
}
int find_pre(int n)
{
if(pre[n]==n) return n;
else return pre[n]=find_pre(pre[n]);
}
bool unite(int x,int y)
{
int rootx=find_pre(x);
int rooty=find_pre(y);
if(rootx!=rooty){
pre[rootx]=rooty;
return true;
}
else return false;
}
bool cmp(Bridge a,Bridge b)
{ return a.day>b.day; }
int main()
{
int n,m;
cin>>n>>m;
init(n);
for(int i=1;i<=m;i++)
cin>>bridge[i].x>>bridge[i].y>>bridge[i].day;
sort(bridge+1,bridge+1+m,cmp);
int ans=0,last_dayth=0; //last_dayth用于记录上一次出现的桥的存在时限,并以此来过滤掉相同的生命期的桥(使得其不纳入结果)
for(int i=1;i<=m;i++)
{
bool flag=unite(bridge[i].x,bridge[i].y);//如果为假代表之前已经有桥使得其连通,故不需要再建桥
if(flag && bridge[i].day!=last_dayth) //如果其不连通,并且其抗议的天的序数是第一次出现,那么就增加了抗议的天数
{
ans++;
last_dayth=bridge[i].day;
}
}
cout<<ans<<endl;
return 0;
}