这周的题目中设计到的算法:DFS
和 BFS
都是基本的图算法,图是一种数据结构,可以表示出节点之间的关系。
基本搜索算法有两种策略:
我们对一个图进行搜索,无非就是寻找某种状态,深度优先顾名思义,就是寻找某种状态的时候选择一条路走到底,走不通就退回去换另一条路。
就像走迷宫那样,我们把迷宫抽象为一个图,路就是图里面的边,路两端的地方抽象为节点。走出这个迷宫就可以看作是我们要寻找的一个状态,在不清楚迷宫构造和不破换游戏规则的情况下,找到出口只能一个一个去尝试,为了不出现在同一条路上绕来绕去的尴尬情况,我们需要标记过我们到过的地方,然后一直往深处走,走到头若发现此路不通或已经走过,那自然就要原路返回到前一个岔路口去走另一条路。这种方法虽然比较笨比较低效,但只要迷宫有出口就一定可以出的去。
有了这个例子,很容易归纳出深度优先的基本方法:
因为我们很难判断要走多少层,再由第3步很容易联想到递归。
那么找到目标状态和走到不能再走就是递归的结束条件。
DFS 理解起来到是不难,关键在于对实际问题的抽象,下面是几道关于DFS的题
定义一个二维数组:
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
一个5 × 5的二维数组,表示一个迷宫。数据保证有唯一解。
Output
左上角到右下角的最短路径,格式如样例所示。
这道题和之前举得例子相似,只是多了两个条件:1.路要最短 2.要记录走过的路径
那就需要走过所有的路,记录每条路所走过的路径。
代码如下:
#include
#include
#define MAXN 5
int vst[MAXN][MAXN]; // 访问标记,记录是否走过
int map[MAXN][MAXN]; // 坐标范围
int depth = 0; // 路长
int mindh = MAXN * MAXN;
int path[MAXN * MAXN][MAXN * MAXN];
int min, count = 0;
int dir[4][2] =
{
{0, 1}, {0, -1}, {1, 0}, {-1, 0}
};
int CheckEdge(int x, int y) // 边界、约束判断
{
// 如果节点没走过且没有越界
if ((x >= 0 && x < MAXN && y >= 0 && y < MAXN) && !map[x][y] && !vst[x][y])
return 1;
return 0;
}
void dfs(int x, int y)
{
path[count][depth] = x*10 + y;
depth++;
vst[x][y] = 1; // 标记访问过的节点
if (x == MAXN - 1 && y == MAXN - 1)
{
if (depth < mindh)
{
mindh = depth; // 更新最短路径
min = count; // 记录存储最短路径节点的下标
count++;
memcpy(path[count], path[count-1], sizeof(path[count-1]));
}
return;
}
// 往其他的方向尝试
for (int i = 0; i < 4; i++)
{
if (CheckEdge(x + dir[i][0], y + dir[i][1]))
{
dfs(x + dir[i][0], y + dir[i][1]);
vst[x + dir[i][0]][y + dir[i][1]] = 0;
depth--; // 走另一条路
}
}
return;
}
int main(void)
{
int x = 0, y = 0;
for (int i = 0; i < MAXN; i++)
for (int j = 0; j < MAXN; j++)
scanf("%d", &map[i][j]);
dfs(x, y);
for (int i = 0; i < mindh; i++)
printf("(%d, %d)\n", path[min][i]/10, path[min][i]%10);
return 0;
}
2.勘探石油
你刚刚承包了一块大小为 M * N 的油田, 并通过专业器具知道了哪些区块下面蕴藏着石油。 现在你想知道至少需要多少台机器才能将这块油田的所有石油采完。 如果蕴藏着石油的区域如果互相连接, 那么这两块区域可以使用同一台机器。 例如: 若 M = 1, N = 5, 其中(1, 1), (1, 2)和(1, 4)区域有石油, 那么由于(1, 1)和(1, 2)这两块区域连在一起, 所以只需要两台机器即可采完所有石油。
输入
输入的数据有多组,每组数据首先是两个正整数 M 和 N, 接下来是一个 M 行N 列的矩阵, 矩阵表示油田,矩阵中有两种可能的值,一种是“ *”,表示此处没有油,一种是“@”,代表此处蕴藏着石油。 若 M, N 均为 0,则表示输入结束,该组数据不输出任何内容。
输出
对于每组数据, 在一行内输出需要机器的数量
样例输入:
1 1
*
3 5
*@*@*
**@**
*@*@*
1 8
@@****@*
5 5
****@
*@@*@
*@**@
@@@*@
@@**@
0 0
样例输出:
0
1
2
2
分析:
对于一个这样的5x5的油田
****@
*@@*@
*@**@
@@@*@
@@**@
相连的油田同属于一个区域,我们现在只需要求出区域的个数。那么还是暴力解决,先找到一个油田,然后以它为初始节点往下一层一层找,找到并标记与它同属于一个区域的所有节点,如果找不到了就停止。然后寻找下一个和它不属于同一区域的油田,重复上面的步骤。如此继续下去即可。
#include
#include
#define MAXN 105
int vst[MAXN][MAXN]; // 记录是否被查找过
char map[MAXN][MAXN]; // 地图
int count; // 区域数量
int M, N;
int CheckIsEdge(int x, int y) // 检查是否满足条件,未超出范围且是没有被标记过的'@'
{
if ((x >= 0 && x < M && y >= 0 && y < N) && map[x][y] == '@' && vst[x][y] == 0)
return 1;
else
return 0;
}
void DFS(int x, int y) // 递归遍历四周所有的油田并标记
{
vst[x][y] = 1;
for (int dx = -1; dx <= 1; dx++) // 往八个方向搜索
{
for (int dy = -1; dy <= 1; dy++)
{
if (dx == 0 && dy == 0)
continue;
if (CheckIsEdge(x + dx, y + dy))
DFS(x + dx, y + dy);
}
}
return;
}
int main(void)
{
while (scanf("%d %d", &M, &N) == 2 && M && N)
{
while(getchar() != '\n') ;
count = 0;
memset(vst, 0, sizeof(vst));
for (int i = 0; i < M; i++)
{
for (int j = 0; j < N; j++)
scanf("%c", &map[i][j]);
getchar();
}
for (int i = 0; i < M; i++) // 从数组的第i行第j列一个一个找
{
for (int j = 0; j < N; j++)
{
if (map[i][j] == '@' && vst[i][j] == 0)
{
count++;
DFS(i, j);
}
}
}
printf("%d\n", count);
}
}
深度优先搜索是往深处一直走,是纵向搜索,广度优先搜索则是横向搜索,当到达一个状态后,下一次同时走与这个状态相关联的状态,一层一层这样递进,直到找不到下一个状态或目标状态。
广度优先搜索可以解决两种问题:
《算法图解》里关于广度优先搜索有一个简单易懂的例子:
假设你经营着一个芒果农场,需要寻找关系最近的芒果销售商,以便将芒果卖给他。
假设朋友是一度关系,朋友的朋友是二度关系,如此类推..
你应先在一度关系中搜索,确定其中没有芒果销售商后,才在二度关系中搜索。广度优先搜索就是这样做的!在广度优先搜索的执行过程中,搜索范围从起点开始逐渐向外延伸,即先检查一度关系,再检查二度关系。
你也可以这样看,一度关系在二度关系之前加入查找名单你按顺序依次检查名单中的每个人,看看他是否是芒果销售商。这将先在一度关系中查找,再在二度关系中查找,因此找到的是关系最近的芒果销售商。广度优先搜索不仅查找从A到B的路径,而且找到的是最短的路径。
深度优先隐含了栈这种数据结构,广度优先则使用了队列
在上面的例子中,要寻找关系最近的芒果销售商,我们先在一度关系里面查找,如果一度关系没有,那就查找朋友的朋友,即二度关系。我们在查找完一度关系之前,根本不清楚一度关系是否有芒果销售商,所以如果当我们查找一度关系的某个成员后发现他不是芒果销售商,那就把他的朋友放入我们的查找名单里面,放入之后我们下一个查找的依然还是一度关系的成员,当一度成员都不是我们要找的人,就开始查找二度关系的成员。
如此层层查找下去,如果查找到了芒果销售员或者查找列表以及空了,就停止查找。
查找的时候有可能一个人是很多人的朋友,但我们只需要查找它一次,那就需要标记一下我们查找过得成员。不然有可能陷入死循环里面。
下面再举两个可以用广度优先搜索的例子:
闪现!
在某知名游戏中,“闪现” 是一个很有用的技能,某天你在机缘巧合之下在现实生活中也学会了这个技能,它使得你可以突然到达道路上,坐标翻倍的位置上,现在假设你走在一条长为 100000 的道路上, 你目前处在坐标为a的位置上,欲到坐标为 b 的地方去。 你在 1 秒钟内可以移动 1 个单位的距离, 当然也可以使用“ 闪现” 使你目前的坐标翻倍。
输入
输入的数据有多组,每组数据包含两个以空格为分隔符的非负整数 a 和 b。其中, 0 ≤ a ≤ 100000 且 0 ≤ b ≤ 100000。
输出
输出你由 a 出发到达坐标 b 所需的最短时间, 每组数据的输出占一行.样例输入
5 17
10 20
样例输出
41
#include
#include
#include
#define MAXN 100005
using namespace std;
queue<int>q; // 新建队列
int description; // 目的地
int a, b;
int isVst[MAXN * 2];// 标记是否查询过
int length; // 查找的bfs层数
void bfs()
{
int end, flag = 1;
while (!q.empty())
{
if (flag) // 标记bfs层数
{
end = q.back();
flag = 0;
}
int temp = q.front(); // 记录对首元素
if (temp == description)
{
printf("%d\n", length); // 找到直接退出
return;
}
q.pop();
if (!isVst[temp + 1])
{
q.push(temp + 1);
isVst[temp + 1] = 1;
}
if (2 * temp - 2 > a && !isVst[temp - 1]) // 减小搜索范围
{
q.push(temp - 1);
isVst[temp - 1] = 1;
}
if (temp < description + 3 && !isVst[temp * 2])
{
q.push(temp * 2);
isVst[temp * 2] = 1;
}
if (temp == end)
{
length++;
flag = 1;
}
}
}
int main(void)
{
while (scanf("%d %d", &a, &b) == 2)
{
while(!q.empty()) // 清空队列
q.pop();
memset(isVst, 0, sizeof(isVst));
length = 0;
isVst[a] = 1; // 标记初始点
description = b;
if (a >= b) // 如果a >= b, 只能倒着一步一步走
printf("%d\n", a - b);
else
q.push(a);
dfs();
}
return 0;
}
这题直接暴力写会超时,需要简单优化一下,同时也需要注意一下当 a >= b 的时候只能一步一步倒着走。
喝可乐!
夏天到了, 没有什么比喝冰可乐更爽的事情了。 同样的, 和最喜欢的人一起分享可乐也是一件很快乐的事。现在你买了一瓶 S 毫升的可乐, 并且你手边有两个容量为 N 毫升和 M 毫升的杯子( M + N = S), 若你想把这瓶可乐完美的平分为两份,但无论是可乐瓶还是两个杯子都没有刻度, 该怎么办呢?
输入
输入的数据有多组,每组数据在一行内有以空格作为分隔符的三个非负整数
S, N 和 M。 若 S, N, M 均为 0, 表示输入结束,该组数据不输出任何内容。输出
对于每组数据, 若可以平分, 则在一行内输出最少需要倒的次数, 若不能平
分, 则在一行内输出 NO。样例输入
7 4 3
4 1 3
0 0 0
样例输出
NO
3
emmm…这题也可以用数论解决,大佬的代码18行AC…………
题目内容很简单,就是三个瓶子来回倒,最后有两个瓶子里面的可乐体积一样就OK
因为 M + N = S
, 所以当 M == N
的时候只需要一次,如果S是奇数M != N
肯定不行,三个都是整数最后要搞出两个小数肯定没可能。
我们假设 S > N > M, 记三个瓶子现有可乐体积分别为:S` N` M`
那么最后只要判断 S`等不等于 N` 就可以,倒瓶子需要的条件是:有可乐的瓶子倒入可乐没装满的瓶子。
因为三个瓶子来回倒的可能情况有限,所以如果再状态节点里面找不到目标状态就说明不能平分
#include
#include
#include
#define maxn 101
using namespace std;
typedef struct state // 记录两个值
{
int a[3];
}State;
queue q; // 新建队列
int vst[maxn][maxn]; // 标记状态
int count, flag; // flag 标记是否被寻找到
int A[3];
void BFS()
{
int BoE = 1, end[2];
State tmpState;
while(!q.empty())
{
if (BoE) // 标记某一层的队尾来判断倒了几次
{
end[0] = q.back().a[0];
end[1] = q.back().a[1];
BoE = 0;
}
State temp = q.front();
printf("(%d %d %d)\n", temp.a[0], temp.a[1], temp.a[2]);
if (temp.a[0] == temp.a[1] && temp.a[1] * 2 == A[0])
{
flag = 1; // 标记已找到
printf("%d\n", count);
return;
}
q.pop();
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (i == j)
continue;
if (temp.a[i] != 0 && temp.a[j] < A[j])
{
tmpState = temp;
if (temp.a[i] <= (A[j] - temp.a[j]))
{
tmpState.a[i] = 0;
tmpState.a[j] = temp.a[j] + temp.a[i];
}
if (temp.a[i] > (A[j] - temp.a[j]))
{
tmpState.a[i] = temp.a[i] - A[j] + temp.a[j];
tmpState.a[j] = A[j];
}
if (vst[tmpState.a[0]][tmpState.a[1]] == 0)
{
vst[tmpState.a[0]][tmpState.a[1]] = 1;
q.push(tmpState);
}
}
}
}
if (temp.a[0] == end[0] && temp.a[1] == end[1])
{
count++;
BoE = 1;
}
}
}
int main(void)
{
while (scanf("%d %d %d", &A[0], &A[1], &A[2]) == 3 && A[0] + A[1] + A[2] != 0)
{
flag = count = 0; // 清除数据
memset(vst, 0, sizeof(vst));
while (!q.empty())
q.pop();
if (A[1] < A[2])
{
int t = A[1];
A[1] = A[2];
A[2] = t;
}
if (A[1] == A[2])
printf("1\n");
else
{
State head;
head.a[0] = A[0];
head.a[1] = 0;
head.a[2] = 0;
vst[A[0]][0] = 1;
q.push(head);
BFS();
if (!flag)
printf("NO\n");
}
}
}