【蓝桥杯】历届试题 剪格子(深度优先搜索dfs、广度优先搜索bfs)—— 酱懵静

历届试题 剪格子

问题描述
如下图所示,3 x 3 的格子中填写了一些整数。
数字方格
我们沿着图中的红线剪开,得到两个部分,每个部分的数字和都是60。
本题的要求就是请你编程判定:对给定的m x n 的格子中的整数,是否可以分割为两个部分,使得这两个区域的数字和相等。
如果存在多种解答,请输出包含左上角格子的那个区域包含的格子的最小数目。
如果无法分割,则输出 0。

输入格式
程序先读入两个整数 m n 用空格分割 (m,n<10)。
表示表格的宽度和高度。
接下来是n行,每行m个正整数,用空格分开。每个整数不大于10000。

输出格式
输出一个整数,表示在所有解中,包含左上角的分割区可能包含的最小的格子数目。

样例输入1
3 3
10 1 52
20 30 1
1 2 3
样例输出1
3

样例输入2
4 3
1 1 1 1
1 30 80 2
1 1 1 100
样例输出2
10



——分割线——



分析:
读完这道后能很明显的感觉到是一个走迷宫类问题(题目中说“包含左上角”等字眼)
再看数据范围 m,n∈[1,10],每个格子中的数值不大于 10000,那说明可以用暴力搜索算法
而搜索算法有两种:
①深度优先搜索(dfs)
②广度优先搜索(bfs)
对于本题而言,其实可以将其抽象为把一个格子阵分为两部分,并且这两部分的数值之和一致
然后我们的程序能把这两部分中格子数量最小的那个给输出(关键点:“格子数量最小的那个”)
这时我们来思考这两个算法各自的特点:
① dfs 主要的着重点在于“能不能将这个格子阵分为相同的两个部分”,即“能不能找到”
② bfs 主要的着重点在于“如果存在两个部分的和相等,输出第一个找到的(即最短的)”
显然,当 bfs 能有输出的时候,那个输出的结果其实就是最小的,即题目所需的
但是仔细思考 bfs 算法的特点,其有一个很关键的点是会将走过的点标记为已遍历,而 bfs 又不存在回溯 的过程,因此这样遍历下去的是有可能导致结果丢失的。下面给出一个例子。
比如有一个格子的内容如下:
表格例子
假设当前设置的遍历方向顺序是:右、下、左、上(且得到整个格子中的总和为 42)


①当采用广度优先搜索算法时,队列中首先是压入格点 6(同时标记为已走),检测 6*2 =12 不等于 42,继 续搜索。此时格子的遍历情况如下:
初始出发点
当前队列中的内容:6(有颜色标记的点表示已经遍历了的格点)


②接着以格点 6 为出发点,遍历其周围的邻接点
首先检测右边的格点 5,未遍历,故将其压入并标记为已遍历,此时(6+5)*2 =22 不等于 42,继续搜索
然后检测下边的格点 7,未遍历,故将其压入并标记为已遍历,此时(6+7)*2 =26 不等于 42,继续搜索
接着再遍历,发现格点 6 的左边和上边都没有点了,于是结束当前点的遍历,并将当前点(即 6)从当前队 列中推出。
此时格子的遍历情况如下:
从格点6出发的遍历情况
当前队列中的内容:5、7(有颜色标记的点表示已经遍历了的,其中蓝色的表示当前队列中的格点)


③接下来取出队列首的格点 5 并以此为出发点继续遍历
首先检测右边的格点 4,未遍历,故将其压入并标记为已遍历,此时(6+5+4)*2 =30 不等于 42,继续搜索
然后检测下边的格点 8,未遍历,故将其压入并标记为已遍历,此时(6+5+8)*2 =38 不等于 42,继续搜索
接着再遍历,发现格点 5 的左边(即格点 6)已经遍历过了,而其上边又没有点,于是结束当前点的遍历,并将当前点从当前队列中推出。此时格子的遍历情况如下:
从格点5出发的遍历情况
当前队列中的内容:7、4、8(有颜色标记的点表示已经遍历了的,其中蓝色的表示当前队列中的格点)


④接下来取出队列首的格点 7 并以此为出发点继续遍历
首先检查右边的点 8,由于此格点已经遍历过了,故跳过该点并继续搜索 标记§
然后检测下边的点 1,未遍历,故将其压入并标记为已遍历,此时(6+7+1)*2 =28 不等于 42,继续搜索
再接着遍历,发现格点 7 的左边没有其他格点,而其上边(即格点 6)已经遍历过了,于是结束当前点的遍 历,并将当前点从当前队列中推出。此时格子的遍历情况如下:
从格点7出发的遍历情况
当前队列中的内容:4、8、1(有颜色标记的点表示已经遍历了的,其中蓝色的表示当前队列中的格点)


⑤接下来取出队列首的格点 4 并以此为出发点继续遍历
首先检查右边的点,由于格点 4 的右边没有任何点,故跳过并继续搜索
然后检测下边的点 6,未遍历,故压入该点并标记为已遍历,此时(6+5+4+6)*2 =42 等于 42,即表示当前遍 历到的这个格子与其前面的共同组成了整个格子的总和的一半,如下图所示:
找到第一条使整个格子被均分的路径
由算法规则来看,这个被找到的第一个结果应该就是格子数量最小的
但,事实真的如此么?
很遗憾不是,因为在这个例子中存在着一个数量更小的格子族(如下图所示)使得整个格子被分为和相等的两部分(6,7,8)
实际上最短的使整个格子被均分的路径
之所以出现这样的情况,是由于在④的“标记§”中,当走到格点 7 并向右遍历时,格点 8 已在前面的③ 中格点 5 那里被遍历过了。因此当停在格点 7 时,其右边的格点 8 早已被遍历过了,故此时就只能向下遍 历,故出现了“跳过正确答案”的情况。
总结看来就是说:虽然 bfs 算法在走迷宫时能将最短路径找出,但是在转化到本题中时,其会由于遍历顺 序的不同而得到不同的遍历方案,从而在“剪格子”的过程中得到非预料的结果(即把某些正确的结果跳 过)。因此,本题并不适用 bfs 算法。

接下来分析深度优先算法
首先要知道,dfs 的主要特点是会遍历完所有的情况。即,其会将地图中的所有遍历策略都过一遍。那么我们就可以利用这一点来对题中所给的格子进行一个遍历,然后将遍历得到的所有结果进行一个对比,从而 得到最小的格子数量(前提是题中所给的数据范围不太大)。
根据这样的思路,我们得到一个本题的解决方案:
首先要知道“使得这两个区域的数字和相等”其实也就是整个格子阵中数值总和的一半。
于是在程序录入格子信息的同时,我们可以先将所有的数字累加并求出总和 sum,之后在 dfs 的过程中我们 就将当前走过的所有格子累加求和并保存进一个变量 now 中,一旦出现了某个 now 的值等于 sum/2,那么就 说明“从起始点(坐标为[1,1])走到当前位置这一连续的格子的总和与另一部分的格子总和相等”,此时 我们用一个变量 ans 记录下这一连续格子的数量(用变量 size 记录从起始点到当前位置的步数) 。但是此 时的 ans 并不一定是最终的答案,因为 dfs 没有结束就意味着仍有可能存在着更小的答案。于是 dfs 还会 继续下去,直到遍历完所有的方案。在这之后的遍历中,一旦出现了某个 size 比 ans 值更小,我们就直接 替换 ans 为 size,否则,就说明前面的某个遍历方案得到的格子数量更小,不能替换。直到结束。

显然这样一直 dfs 下去是有风险的,但是由于 m,n∈[1,10]使得递归树深度不会过分大,于是也是可以接受 的。但是尽管如此我们还是要适当剪枝(就当学习啦) ,剪枝的方案如下:
一旦得出了某个 ans 的值,这个 ans 就能够用于协助 dfs 的剪枝。怎么说呢?比方说你得出了某个遍历方 案下 ans=3,那么之后你继续 dfs 时,当某一步 dfs 到某个位置时,当前 size=4 了,就说明之后就算出现 了“从起始点(坐标为[1,1])走到当前位置这一连续的格子的总和与另一部分的格子总和相等”的情况, 其格子数量也必然大于 3,也就是说在当前点继续 dfs 下去毫无意义。于是在这种情况我们可以直接回溯, 从而大大降低了时间和内存开销。

(备注:题目说的是每个格子中的数值不大于 10000,但有可能会出现比如-10000000000 的值,不放心的 同学在比赛时遇到可以用 long long 型来避免。但是这种几率极低,反正我就用 int 了,事实上 int 是可 以的,但用 long long 是出于一种更细节和长远的角度)。


——分割线——



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

#include<iostream>
using namespace std;
const int MAX=15;
const int INF=0x3f3f3f3f;
int n,m;
int size,sum,now,ans=INF;
int map[MAX][MAX];
int vis[MAX][MAX];
int go[][2]={{1,0},{0,1},{-1,0},{0,-1}};
void dfs(int x,int y)
{
	vis[x][y]=1;
	now+=map[x][y];
	size++;
	if(size>=ans) return;//剪枝:当某种取法的格子数量已经大于前面某次的数量就不必再继续搜索 
	if(now+now==sum){
		ans=size;
		return;
	}
	for(int i=0;i<4;i++)//往四个方向走以寻求可以剪的地方 
	{
		int new_x=x+go[i][0],new_y=y+go[i][1];
		if(vis[new_x][new_y]==0 && new_x>=1 && new_x<=n
			&& new_y>=1 && new_y<=m)
		{
			dfs(new_x,new_y);
			vis[new_x][new_y]=0;	//回溯的时候需要将已访问过的点还原为未访问
			size--;					//同时将用于标记格子数的变量减1 
			now-=map[new_x][new_y];	//以及把用于记录当前访问格子总和的变量还原 
		}
	}
} 
int main()
{
	cin>>m>>n;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++){
			cin>>map[i][j];
			sum+=map[i][j];
		}
	dfs(1,1);
	if(ans==INF) cout<<0<<endl; 
	else cout<<ans<<endl;
	return 0;
}
发布了30 篇原创文章 · 获赞 67 · 访问量 3028

猜你喜欢

转载自blog.csdn.net/the_ZED/article/details/103767907