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