【蓝桥杯】 历届试题 国王的烦恼(并查集)

历届试题 国王的烦恼

问题描述

C 国由 n n n 个小岛组成,为了方便小岛之间联络,C 国在小岛间建立了 m m m 座大桥,每座大桥连接两座小岛。两个小岛间可能存在多座桥连接。然而,由于海水冲刷,有一些大桥面临着不能使用的危险。

如果两个小岛间的所有大桥都不能使用,则这两座小岛就不能直接到达了。然而,只要这两座小岛的居民能通过其他的桥或者其他的小岛互相到达,他们就会安然无事。但是,如果前一天两个小岛之间还有方法可以到达,后一天却不能到达了,居民们就会一起抗议。

现在 C 国的国王已经知道了每座桥能使用的天数,超过这个天数就不能使用了。现在他想知道居民们会有多少天进行抗议。

输入格式

输入的第一行包含两个整数 n , m n, m n,m,分别表示小岛的个数和桥的数量。
接下来 m m m 行,每行三个整数 a , b , t a, b, t a,b,t,分别表示该座桥连接 a a a 号和 b b 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 1\leq n \leq 20,1 \leq m \leq 100 1n201m100
对于 50% 的数据, 1 ≤ n ≤ 500 , 1 ≤ m ≤ 10000 1 \leq n \leq 500,1 \leq m \leq 10000 1n5001m10000
对于 100% 的数据, 1 ≤ n ≤ 10000 , 1 ≤ m ≤ 100000 , 1 ≤ a , b ≤ n , 1 ≤ t ≤ 100000 1 \leq n \leq 10000,1 \leq m \leq 100000,1\leq a, b \leq n, 1 \leq t \leq 100000 1n100001m1000001a,bn1t100000



—— 分割线之初入江湖 ——


分析:
这道题一定要认真读题
题中有句话:“如果前一天两个小岛之间还有方法可以到达,后一天却不能到达了,居民们就会一起抗议”。这句话针对的仅仅是某两座小岛,而并非所有的小岛,举个例子,如下图所示:
第1天
第1天,所有小岛均正常,无任何居民抗议
第2天,桥d损坏(如下图所示),此时小岛3和小岛4的居民由于在第1天能互通而现在不能互通,于是开始抗议。这是抗议的第1次。
第2天
第3天,桥c损坏(如下图所示),此时小岛1和小岛3仍能互通(通过小岛2),因此无任何居民抗议。此时你肯定有疑惑了,为什么小岛3和小岛4的居民不抗议呢?如果你产生了这样的疑惑,那么恭喜你,你和我犯了一样的错!兄弟,看清题目:“如果前一天两个小岛之间还有方法可以到达,后一天却不能到达了,居民们就会一起抗议”。在第2天的时候,小岛3和小岛4就已经不互通了,现在到了第3天它们依然不互通,所以根本不会抗议!
第3天
因此我们得出结论:抗议与否是由两个小岛在两天之间是否发生了连通分支数增加的事件所决定的,换言之,一旦某个桥的损坏导致了两个小岛不互通,就仅仅是会抗议那一天,之后就没它们的什么事儿了。
都走到这步了,我们就继续演示这个过程吧
第4天,桥a损坏(如下图所示),此时小岛1和小岛3的居民由于在第3天能互通而现在不能互通,于是开始抗议。这是抗议的第2次。
第4天
第5天,此时桥b仍未损坏(如上图所示),由于在第4天的时候小岛1与小岛2、小岛1与小岛3、小岛3与小岛4均不能互通,而在第5天的情况依然如此,所以此时没有任何居民会抗议
第6天,桥b损坏(如下图所示),显然此时小岛2与小岛3的居民会抗议,这是抗议的第3次
第5天

于是对于我给出的这个测试样例,程序最终应该输出 3。

上述演示过程实际上也给我们提供了一种解题的思路:模拟
怎么个模拟法?很简单,首先我们定义一个结构体Bridge(包含了桥的可用天数day,以及其连接的两座小岛的序号x和y),如下:

struct Bridge		//代表桥 
{
	int x,y;		//表示桥连接的两个地方 
	int day;		//表示这个桥的可用时限(天数)
	Bridge(){ }
	Bridge(int a,int b,int c):x(a),y(b),day(c){ } 
};

然后把输入的多个桥的数据放进一个Bridge型数组中,并根据这些桥的可用时限进行一个升序排序
在录入桥的信息后,我们通过并查集来将把这些桥的联通情况记录下来
接着,就开始模拟上面的过程了
首先是一个循环i,该循环的次数为录入的桥的数量,循环里的内容如下:
模拟当前的桥bridge[i]损坏,并判断此时整个小岛所构成的并查集中是否有发生连通分支数的增加
如果是:说明桥bridge[i]的损坏会导致其连接的两个小岛的居民抗议,于是执行ans++
否则:说明桥bridge[i]的损坏不会使其连接的两个小岛的居民抗议,因此不执行ans++
最后输出答案即可
不过有一个情况似乎被遗漏掉了,接下来我们看一下另一组测试数据(情况如下):
特殊情况
注意到可用时限为1天的桥有两座(桥b和桥c),那么在第2天时,小岛1和小岛3、小岛3和小岛4的居民就由第1天的能互通变成了现在的不互通,因此ans++。但是我们的程序是顺序执行的(通过一个循环),因此在执行的时候会将这种情况作为两次不同的情况来处理,于是会导致程序ans++两次。而实际上,尽管那一天是两座桥损坏,但是却是再同一天啊!因此居民只会抗议那一天!
处理这个情况很简单,我们只需要在i循环内,预先探测下一个桥的可用时限(bridge[i+1].day)。这样在循环时,我们每次都先用bridge[i+1].day来和当前桥的使用时限(bridge[i].day)进行对比,如果两个天数不一致,则直接模拟当前桥被损坏的过程;否则,跳过当前循环。这样一来就能避免在这种情况下执行两次(甚至更多的)ans++。

这样的思路很清晰,也很简单。但是往往想法很美好,现实却很残酷。
对于i循环,其循环次数为桥的数量,题目给的最大的桥的数量为105
而在i循环内部,模拟桥bridge[i]损坏却相当麻烦,因为每当某座桥损坏,我们都必须重新将剩下的所有小岛进行一个Unite操作,然后再扫描所有小岛构成的并查集,以检测其连通分支数是否发生改变。在这个过程中,将剩下的小岛进行Unite操作是一个O(n)级别,扫描小岛构成的并查集也是一个O(n)级别。即,在i循环内部是一个2*O(n)级别的操作
综上,此种方法下,程序的时间复杂度在O(n2)级别,那么在105的极限情况下,程序必然超时无疑
因此我们必须换一种思路(个人认为,当换了这样的思路之后,整个题的大门就立刻被打开了)



—— 分割线之踏雪寻梅 ——


题目要我们做的是求居民抗议的天数。如果从一开始去模拟一座桥一座桥的坏掉,那有可能遇到很坏的情况——在很后面才遇到那个割线(离散数学术语,表示当当前边被除去后,整个图的连通分支数就由1变为了2)。而你在每一次坏掉一座桥的时候都需要从头去联合这些桥以判断所有小岛的连通性,这样必然会超时。那我们完全可以逆向思维,不去模拟桥的坏掉,而是去模拟“桥的修建”!!!这样一来我们的程序就只需要循环一次去联合(unite)这些桥,一旦出现某座桥在进行Unite操作时,该桥的Unite操作导致了其连接的两个小岛的代表元发生变化(说明该桥是一条割线),就表示这个桥的损坏会使某两个小岛的居民由前一天的互通变为后一天的不互通,也就是说需要执行一次ans++。采用这样逆向修建的思路,我们就需要将给出的测试数据由桥的使用时限进行降序排序(此时使用时间最长的就是最开始被枚举的!因为它最晚坏,所以在逆向看来它是最先修建的)。
接下来我们用题目给的测试数据进行一个演示(已经进行了降序排序):
测试数据
首先一开始(第4天),是4座互相孤立的小岛,此时所有居民均不互通,如下:
第4天
① 时间来到第3天,小岛3和小岛4之间的桥被修建好(如下图所示),那么此时小岛3和小岛4的居民就由第4天的不互通变成了现在的互通。反过来就是在第3天时小岛3和小岛4的居民互通,但是在第4天就不互通了,因此这两座小岛上的居民会抗议,即ans++。
第3天
② 接下来是第2天,修建了小岛1和小岛2之间的桥(如下图所示)
第2天
此时小岛1和小岛2的居民由第3天的不互通变成了现在的互通。反过来就是在第2天时小岛1和小岛2的居民互通,但是在第3天就不互通了,因此这两座小岛上的居民会抗议,即ans++。
现在有个问题,在天数为2的小桥中,还有一座啊!接下来程序继续扫描,遇到了同样是使用时限为2的另一座小桥,但是它连接的是小岛1与小岛3(此时情况如下)。
第2天
根据上面的分析:小岛1和小岛3的居民由第3天的不互通变成了现在的互通。反过来就是在第2天时小岛1和小岛3的居民互通,但是在第3天就不互通了,因此这两座小岛上的居民会抗议,即ans++。
但是实际上,这一天依然还是第2天,也就是说实际情况是:第2天,桥a和桥b都在,小岛1和小岛2、小岛1和小岛3都是联通的,但是在第3天的时候,这两座桥都损坏了,于是小岛1、2、3彼此被分割开,因此这3座小岛的居民就开始抗议,但是仅抗议这1天,即ans++只执行一次。
由于我们的程序是顺序执行的,因此其会将这个情况视为两天不同的情况。我们如何规避?
通过一个lastDay变量即可,这个变量的作用是记录前一次某个桥的使用天数,如果在循环中,检测到当前桥的使用天数和lastDay不相等,并且将当前桥连接的两个小岛进行Unite操作后其确实使得这两个岛的代表元发生了改变,就说明此时需要执行ans++,否则一律不执行。

③ 接下来时间来到第1天,此时所有小岛的连通情况如下图所示:
第1天
这一天,所有小岛之间是保持着连通性的,和第2天情况一样,所以这里不执行ans++。
此时i循环结束,输出ans=2
以上便是通过转换思路后得到的解题方法,可见其时间复杂度仅为O(n),这在m=105的极限情况下仍能保持较好的性能,因此完全可行。



—— 分割线之巫山见云 ——


下面直接给出本题的完整代码:

#include<iostream>
#include<algorithm>
using namespace std;

const int N=10010;
const int M=100010;
struct Bridge		//代表桥 
{
	int x,y;		//表示桥连接的两个地方 
	int day;		//表示这个桥的可用时限(天数)
	Bridge(){ }
	Bridge(int a,int b,int c):x(a),y(b),day(c){ } 
}; 
Bridge bridge[M];	//用于存储所有的桥 
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,a,b,t;
	cin>>n>>m;
	init(n);
	for(int i=1;i<=m;i++){
		cin>>a>>b>>t;
		bridge[i]=Bridge(a,b,t);
	}
	sort(bridge+1,bridge+1+m,cmp);
	int ans=0,lastDay=0;							//lastDay用于探测一次某个桥的生命时限 
	for(int i=1;i<=m;i++)
	{
		bool flag=unite(bridge[i].x,bridge[i].y);	//如果为真表示当前这两个岛未联通
		if(flag && bridge[i].day!=lastDay)	 		//未连通,且此桥的天数是第一次出现,那么就增加了抗议的天数 
		{
			ans++;
			lastDay=bridge[i].day;
		}
	}
	cout<<ans<<endl;
	return 0;
}

END


  • 189
    点赞
  • 239
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 40
    评论
青蛙跳杯子是蓝桥杯历届试题中的一道典型题目。题目要求模拟一个青蛙跳杯子的游戏,给定n个杯子和m次操作,每次操作可以选择一个杯子,并将其放置在当前位置前面或者后面的位置。最后,要求输出经过m次操作后,杯子从左到右的顺序。 下面是一个可能的Java代码实现: ```java import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int n = scanner.nextInt(); // 杯子的数量 int m = scanner.nextInt(); // 操作的次数 int[] cups = new int[n]; // 初始化杯子的初始顺序 for (int i = 0; i < n; i++) { cups[i] = i+1; } // 执行m次操作 for (int i = 0; i < m; i++) { int index = scanner.nextInt(); // 选择的杯子的位置 int direction = scanner.nextInt(); // 移动的方向,1表示前面,2表示后面 // 将选择的杯子移动到指定位置 if (direction == 1) { for (int j = index-1; j > 0; j--) { int temp = cups[j]; cups[j] = cups[j-1]; cups[j-1] = temp; } } else if (direction == 2) { for (int j = index-1; j < n-1; j++) { int temp = cups[j]; cups[j] = cups[j+1]; cups[j+1] = temp; } } } // 输出最终的杯子顺序 for (int i = 0; i < n; i++) { System.out.print(cups[i] + " "); } } } ``` 该代码首先读取输入的杯子数量n和操作次数m,然后创建一个数组用来保存杯子的顺序。从1到n依次填充数组的初始顺序。接下来,执行m次操作,根据输入的位置和方向调整杯子的顺序,最后输出调整后的杯子顺序。 需要注意的是,该代码没有进行输入合法性的验证,实际应用中可能需要对输入进行适当的验证和处理。
评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

theSerein

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值