【蓝桥杯】历届试题 分考场(着色问题、深度优先搜索dfs)

历届试题 分考场

问题描述

n n n 个人参加某项特殊考试。
为了公平,要求任何两个认识的人不能分在同一个考场。
求至少需要分几个考场才能满足条件。

输入格式

第一行,一个整数 n ( 1 < n < 100 ) n(1<n<100) n(1<n<100),表示参加考试的人数。
第二行,一个整数 m m m,表示接下来有 m m m 行数据。
以下 m m m 行每行的格式为:两个整数 a , b a,b ab,用空格分开 ( 1 < = a , b < = n ) (1<=a,b<=n) (1<=a,b<=n) 表示第 a a a 个人与第 b b b 个人认识。

输出格式

一行一个整数,表示最少分几个考场。

样例输入 1

5
8
1 2
1 3
1 4
2 3
2 4
2 5
3 4
4 5

样例输出 1

4

样例输入 2

5 10
1 2
1 3
1 4
1 5
2 3
2 4
2 5
3 4
3 5
4 5

样例输出 2

5



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


分析:

这道题很明显就是着色问题的变种。我们可以把两个相互认识的同学视为两个相互连接点,那么显然,这两个点不能着相同的颜色(即不能放在同一间教室)。所以,在解决本题之前,先来带同学们熟悉一下关于着色问题的算法,我们还是以一道题来引入。




着色方案

问题描述

存在一个无向图,要求给图中的点涂色,并且有线连接的点之间不能是同一种颜色

输入格式

第一行为两个正整数 n , m ( 0 < n , m < 100 ) n,m( 0<n,m<100) n,m(0<n,m<100),分别表示录入的图的点数以及边数
第二行为一个正整数 n u m num num,表示能用的颜色数(每种颜色用一个正整数来表示,从 1 开始逐渐增 1 的正整数)
之后为 m m m 行,每行两个数 x , y x,y x,y,表示点 x x x y y y 之间有一条边(点从 1 开始,为逐渐增 1 的正整数)

输出格式

输出在当前情形下的上色方案

样例输入 1

3 3
3
1 2
1 3
2 3

样例输出 1

6

样例输入 2

3 2
2
1 2
1 3

样例输出 2

2




分析:

本题的任务是对一个无向图进行涂色,要求有线连接的两点之间不能是同一种颜色。

具体的解题思路是,利用 dfs 对该无向图的行走方式进行一个遍历,此遍历和传统的走迷宫不一样,要求每到一个点,就对该点进行一个着色(这个着色可能是多种的),这是和走迷宫不同的地方。为了避免由于回退而对该点再次着色时出现的重复现象,我们在 dfs 里面通过一个循环来完成对当前点的上色(注:该循环的次数是由颜色数量所决定),这样不断的 dfs 直到涂完最后一个点的颜色。

由于对于前面的每个点而言,在 dfs 过程中都是一个单调递增的方式上色,所以每次到达最后一个点时,均能保证当前的上色方案是唯一的,因此直接 ans++。这样一直下去直到 dfs 结束,ans 存放的就是待求值。

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

#include<iostream>
using namespace std;

const int MAX=105;			//定义最大的顶点数
int n,m,num,ans;			//分别代表点数、边数、可用颜色数和最终的答案
int map[MAX][MAX];			//对于小规模的数量可以直接使用邻接矩阵(也可以用向量优化空间)
int color[MAX]; 			//color[i]=x表示第i个点涂上代号为x的颜色

bool judge(int pos,int col) //检测在pos位上涂颜色代号为col的方案是否可行
{
	for(int i=1;i<=n;i++)
		if(map[pos][i] && color[i]==col) return false;
	return true; 
}
void dfs(int pos)
{
	if(pos>n){
		ans++;
		return;
	}
	for(int i=1;i<=num;i++){
		if(judge(pos,i))	//如果可以在pos位上涂上颜色i
		{
			color[pos]=i;	//那么就给pos位涂上颜色i
			dfs(pos+1);
			color[pos]=0;	//回退时必须把刚才点的着色去掉,否则有可能会影响下次的dfs 
		}
	}
}
int main()
{
	int x,y; 
	cin>>n>>m>>num;
	for(int i=0;i<m;i++)
	{
		cin>>x>>y;
		map[x][y]=map[y][x]=1;
	}
	dfs(1);
	cout<<ans<<endl;
	return 0;
}


—— 分割线之穿荆度棘 ——


回到本题,我们可以把两个相互认识的同学视为两个相互连接点,那么显然,这两个点不能着相同的颜色(即不能放在同一间教室)。基于这样的一种转换,我们就可以参考上一道题用到的着色算法来对本题目进行求解。但不同的是,着色问题给定了可用的颜色数,而本题中可安排教室数却成为了待求值,并且还要求是最小值。

此时,就需要对上面的着色算法进行一些略微的修饰。

要知道我们的任务是确定最少的教室数,那么我们就需要用一个向量来代表这些教室,为什么用向量?因为向量具有随机访问特性,我们除了需要用一个数据结构来保存教室信息外,还需要这个结构能够保存当前这个教室中的学生数。这样才能在 dfs 的过程中把当前被安置的学生与该教室中的所有学生进行比对,以检测是否有其认识的人,所以向量是本题最合适的数据结构。

由于在求这个最小的教室数时,前一次的选择会影响后一步的执行(比如说当第 i i i 个同学无论选没选第 x x x 间教室,其都会对第 i + 1 i+1 i+1 及其之后的同学造成影响),因此我们最好是把被安排学生的序号和当前已经安排了多少间教室作为参数来进行传递,所以 dfs 函数的参数主要有以下两个变量:

  1. 当前正安排第几个学生 (order)
  2. 当前安排了多少间教室 (classNum)

那么第一次调用 dfs 时,其格式为 dfs(1,0),表示:当前安排第 1 个学生,此时还没有安排任何教室。

接着进入 dfs,首先需要判断当前安排学生的序号 order 是否大于学生总人数,大于则说明当前已经将所有的学生安排进了相应的教室,因此紧接着我们需要判断当前安排的教室数量 classNum 是否大于上一次的安排方案 ans,如果大于则说明当前的这个安排方案不合理,否则就更新 ans。

接着是一个循环,用以遍历每一间教室,并且在这循环里再用一层循环来将每一间教室中的学生与当前传递进来的那个学生进行对比,一旦遇到了认识的就跳过当前教室,直到遇到一间没有任何人和他相认识的教室为止,我们就可以将这个学生安置在这里,这时就进入一个新的 dfs,传递参数为 dfs(order+1,classNum)。

那要是遍历完所有的教室,发现每间教室都有人和他相识呢?

那就单独给这个学生开一间新教室,即进入一个新的 dfs,这时传递的参数是 dfs(order+1,classNum+1)。这里需要引起注意的是:dfs(order+1,classNum) 是可选的(有条件的);而 dfs(order+1,classNum+1 )是最坏的情形(是不需要任何条件的)。这两个情况不能单一地认为前者就一定能组合出最佳方案,而后者就不行(这是一种贪心的思想)。比如在某些情况下,即使前面存在某个教室里的所有人都与当前学生不认识,你本可以安排当前学生在那里,但是如果你为他单独再加一间教室或许最终还会获得更小的教室数。所以,我们要把前者放在一个if条件中作为可选,而把后者放在后面作为必选(这样才能保证不会漏掉任何一种情况),从而保证了求解的完整性。

在涉及到 dfs 的地方,我们都需要留意数据范围。本题给的人数最大达到了 100,也就是说我们的递归树在最糟的情况下会有 100 层,显然,这样超时无疑,那么我们就不得不对程序进行剪枝。剪枝的主要目标在 roomNum 变量上,我们知道,如果在某次 dfs 时当前 roomNum 已经大于了之前某次 dfs 时得到的 ans,那么现在的dfs就显得毫无意义了。因为无论你再怎么搜索,都注定了这次 dfs 的最终答案大于 ans,这是在做无用功,因此我们可以在遇到这个情况时直接 return。别小看了这两行代码,有时候,正是这两行代码让你的 70 分代码变成了 100 分。

注:最开始我就是因为没有剪枝而超时了(仅得了 40% 的分),后来加上这代码后就成功 AC 了。



—— 分割线之苦尽甘来 ——


下面直接给出本题完整代码(附有详细注释):

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

const int MAX=105;
bool map[MAX][MAX];			 	//map[x][y]表示x和y认识
int n,m;
int ans=MAX;					//最开始必须给ans赋值一个最大值(为最多的人数即可)
vector<int> classroom[MAX];	 	//二维向量classroom[i]=x表示
void dfs(int order,int roomNum)	//order表示某个学生的序号,roomNum表示当前教室的数量 
{
	if(roomNum>=ans) return;	//当现在安排的教室数量已经大于了最小的教室数量的话放弃搜索并回退 
	if(order>n){				//安排的学生数量已经大于所有的学生,就表示已经安排完了所有的学生
		ans=roomNum;
		return;
	}
	for(int i=1;classroom[i].size();i++)	//遍历所有教室
	{ 
		int j,len=classroom[i].size();
		for(j=0;j<len;j++)					//检测当前教室是否存在一个人与将要被安排的学生认识 
			if(map[order][classroom[i][j]]) break;
		if(j==len)							//说明当前教室中没有人与当前order的学生认识
		{
			classroom[i].push_back(order);	//在当前教室插入此学生 
			dfs(order+1,roomNum); 	   		//继续安排下一个 
			classroom[i].pop_back();   		//回退时需要把这个学生从当前教室清除掉 
		} 
	}
	classroom[roomNum+1].push_back(order);	//开一间新教室给当前学生 
	dfs(order+1,roomNum+1);					//继续安排下一个 
	classroom[roomNum+1].pop_back();		//回退后需要把这个学生从当前教室清除掉 
}
int main()
{
	int x,y;
	cin>>n>>m;
	for(int i=0;i<m;i++)
	{
		cin>>x>>y;
		map[x][y]=map[y][x]=1;
	}
	dfs(1,0);
	cout<<ans<<endl;
	return 0;
}

END


评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

theSerein

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

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

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

打赏作者

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

抵扣说明:

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

余额充值