搜索篇
搜索篇主要介绍深搜、广搜、剪枝和A*算法,下面通过具体的题目进行一一呈现。
Q1(Problem source : 百练2815):
描述
请你编写一个程序,计算城堡一共有多少房间,最大的房间有多大。城堡被分割成mn(m≤50,n≤50)个方块,每个方块可以有0~4面墙。输入程序从标准输入设备读入数据。第一行是两个整数,分别是南北向、东西向的方块数。
在接下来的输入行里,每个方块用一个数字(0≤p≤50)描述。用一个数字表示方块周围的墙,1表示西墙,2表示北墙,4表示东墙,8表示南墙。每个方块用代表其周围墙的数字之和表示。
城堡的内墙被计算两次,方块(1,1)的南墙同时也是方块(2,1)的北墙。输入的数据保证城堡至少有两个房间。输出城堡的房间数、城堡中最大房间所包括的方块数。结果显示在标准输出设备上。
样例输入
4 7 11 6 11 6 3 10 6 7 9 6 13 5 15 5 1 10 12 7 13 7 5 13 11 10 8 10 12 13
样例输出
5 9
分析:很典型的dfs问题,数据规模不大也不会用到剪枝,这里需要注意的一个运算符优先级的小细节是用位运算符&和==运算符的时候,前面的位运算符要加小括号。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn = 51; int Map[maxn][maxn]; int visit[maxn][maxn]; int m , n; int dfs(int i , int j) { int num = 1; visit[i][j] = 1; if((Map[i][j] & 1) == 0 && j - 1 >= 1 && visit[i][j-1] == 0) { num += dfs(i,j-1); } if((Map[i][j] & 2) == 0 && i - 1 >= 1 && visit[i-1][j] == 0) { num += dfs(i-1,j); } if((Map[i][j] & 4) == 0 && j + 1 <= n && visit[i][j+1] == 0) { num += dfs(i,j+1); } if((Map[i][j] & 8) == 0 && i + 1 <= m && visit[i+1][j] == 0) { num += dfs(i+1,j); } return num; } int main() { while(scanf("%d",&m) != EOF) { scanf("%d",&n); for(int i = 1;i <= m;i++) for(int j = 1;j <= n;j++) scanf("%d",&Map[i][j]); memset(visit , 0 , sizeof(visit)); int num = 0; int Max = 0; // printf("%d ",dfs(1,1)); for(int i = 1;i <= m;i++) for(int j = 1;j <= n;j++) { if(visit[i][j] == 0) { num++; Max = max(Max , dfs(i,j)); } else continue; } printf("%d\n%d\n",num,Max); } }
Q2(Problem source : poj 3984 || 百练ACM暑假课练习题10):
Description
int maze[5][5] = { 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, };它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。
Input
Output
Sample Input
0 1 0 0 0 0 1 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0 0 1 0
Sample Output
(0, 0) (1, 0) (2, 0) (2, 1) (2, 2) (2, 3) (2, 4) (3, 4) (4, 4)
分析:能够看到很明显的搜索思路,但是要打印路径。这里如果用bfs,可以直接得到最短路(在队尾一旦发现到达终点,即可终止),另一方面bfs在一个队列中很容易记录路径。打印路径采取递归的方法。
另外这道题目有个小坑在于,队列的长度一定要开够。下面的代码是手动实现队列的,当然STL更方便。
#include<cstdio> #include<cstring> using namespace std; struct node { int x , y , pre; }q[200]; int Map[5][5]; int dx[4] = {1 , 0 , -1 , 0}; int dy[4] = {0 , 1 , 0 ,-1}; void print(int i) { if(q[i].pre == -1) {printf("(0, 0)\n");return;} else { print(q[i].pre); printf("(%d, %d)\n",q[i].x,q[i].y); return; } } void bfs(int i , int j) { int head = 0 , tail = 1; int a , b; q[head].x = i; q[head].y = j; q[head].pre = -1; int flag = 0; while(1) { for(int i = 0;i < 4;i++) { a = q[head].x + dx[i]; b = q[head].y + dy[i]; if(a < 0 || a > 4 || b < 0 || b > 4 || Map[a][b] == 1) continue; q[tail].x = a; q[tail].y = b; q[tail].pre = head; tail++; if( a == 4 && b == 4) {flag = 1;print(tail-1);break;} } if(flag == 1) break; head++; } } int main() { for(int i = 0;i < 5;i++) for(int j = 0;j < 5;j++) scanf("%d",&Map[i][j]); bfs(0,0); return 0; }
Q3: (Problem source : poj 1321):
描述:
在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。输入输入含有多组测试数据。 每组数据的第一行是两个正整数,n k,用一个空格隔开,表示了将在一个n*n的矩阵内描述棋盘,以及摆放棋子的数目。 n <= 8 , k <= n 当为-1 -1时表示输入结束。 随后的n行描述了棋盘的形状:每行有n个字符,其中 # 表示棋盘区域, . 表示空白区域(数据保证不出现多余的空白行或者空白列)。
输出:对于每一组数据,给出一行
输出,输出摆放的方案数目C (数据保证C<2^31)。
分析:基于矩阵图利用dfs求解填充方案数的问题,这里其技巧点就是如何根据题设限制进行“有序”的深度优先搜索,一个要点是不能够重复计数,令一个要点是不能够漏计数。 考虑到它是行列不重复,我们进行“有序”的深搜的策略是在第i行中,选出一个可选位置,标记visit[j]表示访问了该列,随后在第i+1行中重复如上步骤,期间设置参量用于计数。这是我们“有序”的深搜策略。 另一个层面是不能漏,在第i行中,我们可能有x种选择情况,我们要一次遍历然后进入到搜索树的下一层,但是这里一定不要忘记设置一个“可选项”是“该行不选位置”,下面代码注释部分就是笔者一开始的错误写法。笔者一开始想要依次从第i行开始构造可行的方案,但是这样没有给深搜过程中留出“第i行不选该位置”的选项,这就对导致漏掉一些满足的情况。
参考代码如下:
#include<cstdio> #include<cstring> #include<iostream> using namespace std; const int maxn = 10; char Map[maxn][maxn]; int num , cnt; int n , k; int visit[maxn]; void dfs(int i) { if(cnt == k) {num++;return;} if(i >= n) return; for(int j = 0 ;j < n;j++) { if(Map[i][j] == '#' && visit[j] == 0) { visit[j] = 1; cnt++; dfs(i+1); visit[j] = 0; cnt--; } } dfs(i+1);//第i行填棋子 } int main() { while(scanf("%d %d",&n,&k)) { if(n == -1 && k == -1) break; for(int i = 0;i < n;i++) cin>>Map[i]; num = 0; cnt = 0; dfs(0); printf("%d\n",num); //for(int i = 0;i < n;i++) //{ //...初始化 //dfs(i) //} } }
Q4(Problem source : 百练暑假ACM练习题07):
路飞他们伟大航路行程的起点是罗格镇,终点是拉夫德鲁(那里藏匿着“唯一的大秘宝”——ONE PIECE)。而航程中间,则是各式各样的岛屿。
因为伟大航路上的气候十分异常,所以来往任意两个岛屿之间的时间差别很大,从A岛到B岛可能需要1天,而从B岛到A岛则可能需要1年。当然,任意两个岛之间的航行时间虽然差别很大,但都是已知的。
现在假设路飞一行从罗格镇(起点)出发,遍历伟大航路中间所有的岛屿(但是已经经过的岛屿不能再次经过),最后到达拉夫德鲁(终点)。假设他们在岛上不作任何的停留,请问,他们最少需要花费多少时间才能到达终点?
输入:
输入数据包含多行。 第一行包含一个整数N(2 < N ≤ 16),代表伟大航路上一共有N个岛屿(包含起点的罗格镇和终点的拉夫德鲁)。其中,起点的编号为1,终点的编号为N。 之后的N行每一行包含N个整数,其中,第i(1 ≤ i ≤ N)行的第j(1 ≤ j ≤ N)个整数代表从第i个岛屿出发到第j个岛屿需要的时间t(0 < t < 10000)。第i行第i个整数为0。
输出:输出为一个整数,代表路飞一行从起点遍历所有中间岛屿(不重复)之后到达终点所需要的最少的时间。
分析: 其实这道题目非常具有迷惑性,因为它涉及一个最短路,但是需要遍历所有的点,而且也不是典型的回到起点的TSP,因此这里基于dfs进行搜索维护权值最小,只不过需要基于dp子问题将搜索过程中的各个状态记录下来用于最优化剪枝。
这里记录每个状态的一个基本技巧就是利用位运算,设置dp[i][S]表示出发后,当前在i城市,已经走过了集合S(元素是城市)的最短路,这里利用一个整型的二进制形式来表示集合j,最大不会超过1<<17.
最优化剪枝的两个策略:
策略一:这个策略很好理解,当我们已经得到一种方案的权值和是Min的时候,在以后的搜索过程中发现路径权值和已经超过了Min,显然不需要继续搜下去。
策略二:其实是基于标准TSP的递归方程(虽然题设有些不同但是这个递归方程是通用的),dp[i][S] = min{dp[i][S-{j}] + dis[i][j]},因此我们在深搜的过程中,时刻记录dp[i][S],在搜索过程一旦dp[i][S-{j}] + dis[i][j]大于之前保留的dp[i][S],即可进行剪枝。
参考代码如下。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn = 1<<17; int dp[17][maxn]; int Min; int dis[20][20]; int visit[20]; int n; void dfs(int now ,int pass) { if(dp[now][pass] > Min) return; if((pass | (1<<n-1)) == ((1<<n) - 1)) { Min = min(Min,dp[now][pass] + dis[now][n]); return; } for(int i = 2;i < n;i++) { if(visit[i] == 0) { int new_pass = pass | (1 << (i-1)); if(now == 1) { visit[i] = 1; dp[i][new_pass] = dis[1][i]; dfs(i , new_pass); visit[i] = 0; } else { if( dp[i][new_pass] == 0 || dp[i][new_pass] > (dp[now][pass] + dis[now][i])) { visit[i] = 1; dp[i][new_pass] = dp[now][pass] + dis[now][i]; dfs(i , new_pass); visit[i] = 0; } } } } } int main() { while(scanf("%d",&n) != EOF) { Min = 9999999; memset(dp , 0 , sizeof(dp)); memset(visit , 0 , sizeof(visit)); for(int i = 1;i <= n;i++) for(int j = 1;j <= n;j++) scanf("%d",&dis[i][j]); dfs(1,1); printf("%d\n",Min); } return 0; }
Q4(Problem source : 百练 ACM暑假作业题11):
已知一张地图(以二维矩阵的形式表示)以及佐助和鸣人的位置。地图上的每个位置都可以走到,只不过有些位置上有大蛇丸的手下,需要先打败大蛇丸的手下才能到这些位置。鸣人有一定数量的查克拉,每一个单位的查克拉可以打败一个大蛇丸的手下。假设鸣人可以往上下左右四个方向移动,每移动一个距离需要花费1个单位时间,打败大蛇丸的手下不需要时间。如果鸣人查克拉消耗完了,则只可以走到没有大蛇丸手下的位置,不可以再移动到有大蛇丸手下的位置。佐助在此期间不移动,大蛇丸的手下也不移动。请问,鸣人要追上佐助最少需要花费多少时间?
输入
输入的第一行包含三个整数:M,N,T。代表M行N列的地图和鸣人初始的查克拉数量T。0 < M,N < 200,0 ≤ T < 10 后面是M行N列的地图,其中@代表鸣人,+代表佐助。*代表通路,#代表大蛇丸的手下。
输出
输出包含一个整数R,代表鸣人追上佐助最少需要花费的时间。如果鸣人无法追上佐助,则输出-1。
分析:很典型的dfs遍历维护最短路径的问题,这里涉及最优剪枝和可行性剪枝,一般有路径限制的dfs都可以用到可行性剪枝,这里的路径限制参量是查克拉。
初步的代码如下.
#include<cstdio> #include<cstring> #include<iostream> using namespace std; const int maxn = 205; char Map[maxn][maxn]; int visit[maxn][maxn]; int m , n , c; int dx[4] = {1 , -1 , 0 , 0}; int dy[4] = {0 , 0 , 1 , -1}; int xs , ys , xe , ye; int Min; void dfs(int x , int y ,int c,int step) { if(c < 0) return;//可行性剪枝,一旦查克拉少于0 , 停止搜索 if(step > Min) return;//最优性剪枝,一旦当前步数比当前维护的最小值还要大,停止搜索。 for(int i = 0;i < 4;i++)//四个方向 { int xx = x + dx[i]; int yy = y + dy[i]; if(xx < 0 || xx >= m || yy < 0 || yy >= n || visit[xx][yy] == 1) continue;//越界 if(Map[xx][yy] == '#')//遇到大蛇丸 { visit[xx][yy] = 1; dfs(xx , yy ,c - 1 , step + 1); visit[xx][yy] = 0; } if(Map[xx][yy] == '*')//通路 { visit[xx][yy] = 1; dfs(xx , yy , c , step + 1); visit[xx][yy] = 0; } if(Map[xx][yy] == '+')//遇到佐助,深搜到最底层,返回 { Min = min(Min , step + 1); return; } } } int main() { while(scanf("%d%d%d",&m,&n,&c) != EOF) { Min = 9999999999; memset(visit , 0 , sizeof(visit)); for(int i = 0;i < m ; i++) cin>>Map[i]; for(int i = 0;i < m;i++) for(int j = 1;j < n;j++) { if(Map[i][j] == '@') xs = i , ys = j; if(Map[i][j] == '+') xe = i , ye = j; } visit[xs][ys] = 1; dfs(xs , ys , c , 0); printf("%d\n",Min); } }
ps分析:其实这是典型的bfs搜矩阵路径的最短路,这里体现的就是bfs空间换dfs的时间,但是这里需要注意的是bfs去重过程不仅仅是该点坐标两个维度,还有一个维度的限制——查克拉。
参考代码如下:
#include<cstdio> #include<queue> #include<cstring> #include<iostream> using namespace std; const int maxn = 205; struct node { int x , y , ckl , step; node (int xx,int yy, int cc, int ss):x(xx), y(yy), ckl(cc), step(ss){}; }; char Map[maxn][maxn]; int n , m , c; int dx[4] = {1 , -1 , 0 , 0}; int dy[4] = {0 , 0 , 1 , -1}; int xs , ys , Min; int visit[maxn][maxn][12]; bool bfs(int xs , int ys) { queue<node> q; struct node n0(xs , ys , c , 0); q.push(n0); while(!q.empty()) { struct node temp = q.front(); q.pop(); for(int i = 0;i < 4;i++) { int xx = temp.x + dx[i]; int yy = temp.y + dy[i]; //printf("%d %d\n",xx,yy); if(xx<0 || xx >= m || yy <0 ||yy>=n) continue; if(Map[xx][yy] == '+') { Min = temp.step + 1; return true; } if(Map[xx][yy] == '*' && visit[xx][yy][temp.ckl] == 0) { visit[xx][yy][temp.ckl] = 1; q.push(node(xx , yy , temp.ckl , temp.step + 1)); } if(Map[xx][yy] == '#' && visit[xx][yy][temp.ckl-1]== 0 && temp.ckl >= 1) { visit[xx][yy][temp.ckl-1] = 1; q.push(node(xx , yy , temp.ckl - 1 , temp.step + 1)); } } } return false; } int main() { while(scanf("%d%d%d",&m,&n,&c) != EOF) { for(int i = 0;i < m;i++) for(int j = 0;j < n;j++) { cin>>Map[i][j]; if(Map[i][j] == '@') xs = i , ys = j; } //printf("%d %d",xs , ys); memset(visit , 0 , sizeof(visit)); visit[xs][ys][c] = 1; if(bfs(xs , ys) != false) printf("%d\n",Min); else printf("-1\n"); } }