搜索-DFS和BFS
搜索
主要包括深度优先搜索和广度优先搜索
DFS
Depth first search(深度优先搜索)
以“深度”作为前进的关键词,不到底不回头
深度优先搜索每一次“回头”都意味着一条完整路径的形成,深度优先搜索会枚举所有的完整路径,常以递归实现深度优先搜索,非递归实现较递归实现复杂
背包问题
有n件物品,每件物品的重量为w[i],价值为c[i]。现在需要选出若干件物品放入一个容量为V的背包中,使得在选入背包的物品重量和不超过容量V的前提下,让背包中物品的价值之和最大,求最大价值。(1<=n<=20)
输入示例:
5 8
3 5 1 2 2
4 5 2 1 3
物品个数n=5,背包容量v=8
重量w:3 5 1 2 2
价值v:4 5 2 1 3
输出示例:
10
对每件物品的选择可以有两种情况:加入背包和不加入背包
- 加入背包后,背包容量和价值发生改变
- 不加入则不发生改变
由此产生两个递归式
选入背包的物品重量不超过背包容量v 即 递归边界
由此递归函数需要有三个参数:当前处理的商品编号、总重量和总价值
void DFS(int index,int sumW,int sumV){
if(index == n){
// 所有的物品都处理完了
if(sumW <= v && sumV > maxValue){
maxValue = sumV;
}
return ; // 递归回头
}
// 不选
DFS(index + 1,sumW,sumV);
// 选
DFS(index + 1,sumW + weight[index],sumV + values[index]);
}
使用递归实现了深度优先遍历:
实际上0-1背包问题中,每件物品都有选择和不选择两种,将每件物品看作一个结点(第一个物品为顶点),于是这就是个二叉树(对称),然后使用递归实现了先序遍历(这个也算是深度优先遍历)。
对这个递归的理解:
实际上分为“递”和“归”进行,首先前进“递”进入程序DFS(index + 1, sumW, sumC),一直进,直到index=5时(对应于图中的5下面的不选),才满足条件返回,于是最里层的DFS函数退出,按程序顺序执行,则应执行DFS(index+1, sumW + w[index], sumC + c[index]),此时index=5满足条件返回(对应于图中的5下面的选),这时返回到index=4,发现程序执行完了,于是程序继续返回到了index=3,一直这样,遍历完所有。
对于最大价值maxValue是怎么算出来的:
理解完递归,这个就很简单了,因为每次遍历到底时,都会检查if sumW <= v and sumC >maxValue,满足了就maxValue = sumC,也就是说这个递归枚举了所有的可能结果,每次枚举结果时,都更新maxValue,保证它最大,自然这就是正确结果了。
迷惑的问题:
递归调用不成立后,为什么要返回上一层递归?
原因是:一个函数里面调用了另一个函数,里面的函数返回了,就是回到外面的函数继续执行。
而里面的函数 返回有两种可能:对于最底层返回上一层,是因为条件不成立导致的;而对于其他层的返回,是因为程序执行完了。
程序是按语句顺序执行的,递归并不会并发执行语句。
全排列问题:codeup 5872
题目描述
排列与组合是常用的数学方法。
先给一个正整数 ( 1 < = n < = 10 )
例如n=3,所有组合,并且按字典序输出:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
输入
输入一个整数n( 1<=n<=10)
输出
输出所有全排列
每个全排列一行,相邻两个数用空格隔开(最后一个数后面没有空格)
样例输入
3
样例输出
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
// 如果某个数没有被选择(flag[i] = false)就将其加入排列中
// 选中后设置(flag[i] = true)
// 递归回头时需要将其重新设置(falg[i] = false)
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
using namespace std;
int n;
int path[12]; // 存储一个排列
bool flag[12] = {false}; // 标记某数是否已经被选择
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
void dfs(int depth){
if(depth == n){
// 所有数都已被排列,产生一个完整排列
for(int i=0;i!=n;++i){
printf("%d ",path[i]);
}
printf("\n");
}
for(int i=0;i!=n;++i){
if(flag[i] == false){
path[depth] = i+1;
flag[i] = true;
dfs(depth + 1);
flag[i] = false; // 递归回头
}
}
}
int main(int argc, char** argv) {
while(~scanf("%d",&n)){
int depth = 0;
dfs(depth);
}
return 0;
}
next_permutation()
int nums[3] = {3, 2, 1};
do{
cout << nums[0] << " "<< nums[1] << " " << nums[2] << endl;
}while(next_permutation(nums, nums+3)); // 3 2 1
#include <cstdio>
#include <algorithm>
using namespace std;
int main() {
int n; scanf("%d", &n);
int a[n];
for (int i = 1; i <= n; i++) a[i - 1] = i;
do {
for (int i = 0; i < n; i++) {
if (i > 0) printf(" ");
printf("%d", a[i]);
}
printf("\n");
} while (next_permutation(a, a + n));
return 0;
}
BFS
BFS 以广度作为前进方向,整个算法的过程很像一个队列
动画演示过程:
https://cuijiahua.com/blog/2018/01/alogrithm_10.html
BFS常用队列实现,常用于最短路径问题
伪代码:
void BFS(int s){
queue<int> s;
q.push(s);
while(!q.empty()){
取出队首元素;
访问队首元素;
队首元素出队;
队首元素的下一层节点入队,并设置为已入队;
}
}
识别矩阵中的块数
题目描述:
给出一个m*n的矩阵,矩阵中的元素为0或1.称位置(x,y)与其上下左右四个位置是相邻的。如果矩阵中有若干个1相邻,则称这些1构成了一个块。求给定矩阵中的块数。
输入:
6 7
0 1 1 1 0 0 1
0 0 1 0 0 0 0
0 0 0 0 1 0 0
0 0 0 1 1 1 0
1 1 1 0 1 0 0
1 1 1 1 0 0 0
输出:4
首先明确这个题是广度优先搜索的模板题,广度优先搜索常用队列实现,矩阵的存储可以用二维数组实现
定义一个结构体,表示矩阵中元素的位置,设置二维数组inq[x][y]表示该结点是否进入过队列
struct Node{
int x, y;
}node; // 通过结点表示矩阵中的数字
结点和队列的常规操作
// 入队
Q.push(node);
inq[x][y] = true;
// 获得队首元素
Node node = Q.front();
// 出队
Q.pop();
inq[x][y] = false;
将矩阵从左到右从上到下进行遍历,遇到0则跳过,遇到1则将与这个“1”结点同一块且为1的结点的inq置为true(逐一入队)
-
判断结点是否合法
// 判断某点需要入队(是否越界,是否为1,是否访问过) bool judge(int x, int y){ if(x<0 || y<0 || x>=m || x>=n){ // 点坐标越界 return false; } if(matrix[x][y] == 0 || inq[x][y] == true){ // 该点为0 或 已经访问过 return false; } return true; } -
将与"1"结点相邻的合法“1”结点逐一入队和出队,并置该结点inq=true
即同一块的所有1结点
void bfs(int x, int y){ queue<Node> Q; node.x = x; node.y = y; Q.push(node); // 结点入队 inq[x][y] = true; while(!Q.empty()){ Node top = Q.front(); // 获得队首元素 Q.pop(); // 结点出队 for(int i=0; i!=4; ++i){ int newX = top.x + X[i]; int newY = top.y + Y[i]; if(judge(newX, newY)){ node.x = newX; node.y = newY; Q.push(node); inq[newX][newY] = true; } } } }
完整程序清单:
#include<iostream>
#include<stdio.h>
#include<math.h>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn = 100;
int matrix[maxn][maxn];
bool inq[maxn][maxn] = {false}; // 标记结点是否进过队列
// 广度优先搜索
// 遇到没有进入过队列的1结点,就将块数ans+1
// matrix[x][y] == 1
// inq[x][y] == false
// 访问该结点的上下左右结点(前提是上下左右结点存在,即没有越界).判断上下左右结点是否需要入队
// judge() 函数
// 访问该结点周围一块的所有1,并将这些结点的inq置为true
// BFS() 函数
//// 入队
// Q.push(node);
// inq[x][y] = true;
//
//// 获得队首元素
// Node node = Q.front();
// 出队
// Q.pop();
// inq[x][y] = false;
int n,m;
struct Node{
int x, y;
}node; // 通过结点表示矩阵中的数字
// 产生上下左右结点的增量数组
int X[] = {0, 0, -1, 1};
int Y[] = {1, -1, 0 ,0};
// 判断某点需要入队(是否越界,是否为1,是否访问过)
bool judge(int x, int y){
if(x<0 || y<0 || x>=m || y>=n){ // 点坐标越界
return false;
}
if(matrix[x][y] == 0 || inq[x][y] == true){ // 该点为0 或 已经访问过
return false;
}
return true;
}
void bfs(int x, int y){
queue<Node> Q;
node.x = x;
node.y = y;
Q.push(node); // 结点入队
inq[x][y] = true;
while(!Q.empty()){
Node top = Q.front(); // 获得队首元素
Q.pop(); // 结点出队
for(int i=0; i!=4; ++i){
int newX = top.x + X[i];
int newY = top.y + Y[i];
if(judge(newX, newY)){
node.x = newX;
node.y = newY;
Q.push(node);
inq[newX][newY] = true;
}
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for(int i=0; i!=n; ++i){
for(int j=0; j!=m; ++j){
scanf("%d", &matrix[i][j]);
}
}
int blocks; // 块数
for(int i=0; i!=n; ++i){
for(int j=0; j!=m; ++j){
if(matrix[i][j] == 1 && inq[i][j] == false){
// 块数+1
blocks++;
// 上下左右结点
bfs(i, j);
}
}
}
cout << blocks << endl;
return 0;
}
长草
【问题描述】
小明有一块空地,他将这块空地划分为 n 行 m 列的小块,每行和每列的长度都为 1。
小明选了其中的一些小块空地,种上了草,其他小块仍然保持是空地。
这些草长得很快,每个月,草都会向外长出一些,如果一个小块种了草,则它将向自己的上、下、左、右四小块空地扩展,这四小块空地都将变为有草的小块。
请告诉小明,k 个月后空地上哪些地方有草。
【输入格式】
输入的第一行包含两个整数 n, m。
接下来 n 行,每行包含 m 个字母,表示初始的空地状态,字母之间没有空格。如果为小数点,表示为空地,如果字母为 g,表示种了草。
接下来包含一个整数 k。
【输出格式】
输出 n 行,每行包含 m 个字母,表示 k 个月后空地的状态。如果为小数点,表示为空地,如果字母为 g,表示长了草。
【样例输入】
4 5
.g...
.....
..g..
.....
2
【样例输出】
gggg.
gggg.
ggggg
.ggg.
由一个点向四周进行扩展,可以很容易想到BFS的解决思想。
用一个数组存储这个.和g组成的矩阵,然后遍历数组将所有的g结点都加入到队列中,再用BFS去扩展周围的草地,当结点的月份等于给定的月份时不再向外长草
只需要判定结点是否经过队列就可以知道输出g还是点,不必去替换原矩阵中的点
完整程序清单:
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn = 1010;
char grass[maxn][maxn];
bool inq[maxn][maxn] = {false}; // 是否进过队列
struct Node {
int x,y;
int month; // 记录月份
} node;
int n, m, k;
int X[] = {0, 0, -1, 1};
int Y[] = {1, -1, 0, 0};
queue<Node> Q;
bool isValid(int x, int y) {
if(x<0 || y<0 || x>=n || y>=m) {
// 点是否越界
return false;
}
return true;
}
void bfs() {
while(!Q.empty()) {
Node top = Q.front();
Q.pop();
for(int x=0; x!=4; ++x) {
// 向结点的四周长草
if(top.month == k) {
// 结点的月份等于给定月份,不再向外长草
break;
}
int newX = top.x + X[x];
int newY = top.y + Y[x];
if(isValid(newX, newY)) {
node.x = newX;
node.y = newY;
node.month = top.month + 1;
Q.push(node);
inq[newX][newY] = true;
}
}
}
}
int main() {
scanf("%d%d", &n, &m);
for(int x=0; x!=n; ++x) {
getchar(); // 排除换行符的影响
for(int y=0; y!=m; ++y) {
scanf("%c", &grass[x][y]);
}
}
for(int x=0; x!=n; ++x) {
for(int y=0; y!=m; ++y) {
if(inq[x][y] == false && grass[x][y] == 'g') {
// 遍历所有的未被访问的g结点
node.x = x;
node.y = y;
node.month = 0;
Q.push(node);
inq[x][y] = true;
}
}
}
scanf("%d", &k);
bfs();
for(int x=0; x!=n; ++x) {
for(int y=0; y!=m; ++y) {
if(inq[x][y] == true) {
// 进过队列说明长草
cout << 'g';
} else {
cout << '.';
}
}
cout << endl;
}
return 0;
}
迷宫问题
给定一个n*m大小的迷宫,而“.”代表平地,S表示起点,T代表终点。移动过程中,如果当前位置是(x,y)(下标从0开始),且每次只能前往上下左右、(x,y+1)、(x,y-1)、(x-1,y)、(x+1,y)四个位置的平地,求从起点S到达终点T的最少步数。
样例输入:
5 5
.....
.*.*.
.*S*.
.***.
...T*
2 2 4 3
样例输出:
11
样例输入中5 5表示5行5列
2 2 4 3表示S和T的坐标
S为(2,2),T的坐标为(4,3)。
注意如果到不了终点则返回-1
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#include <queue>
using namespace std;
const int maxn = 100;
int matrix[maxn][maxn];
int inq[maxn][maxn] = {false};
int n, m;
struct Node{
int x, y;
int step;
}node,s,t;
int X[] = {0, 0, -1, 1};
int Y[] = {1, -1, 0, 0};
bool isValid(int x, int y){
if(x<0 || y<0 || x >=n || y>=m){
return false;
}
if(inq[x][y] == true || matrix[x][y] == '*'){
return false;
}
return true;
}
int bfs(){
queue<Node> Q;
Q.push(s);
inq[s.x][s.y] = true;
while(!Q.empty()){
Node top = Q.front();
Q.pop();
if(top.x == t.x && top.y == t.y){
return top.step;
}
// 获得结点上下左右的坐标
for(int i=0; i!=4; ++i){
int newX = top.x + X[i];
int newY = top.y + Y[i];
// 判断点的合法性
if(isValid(newX, newY)){
node.x = newX;
node.y = newY;
node.step = top.step + 1;
Q.push(node);
inq[newX][newY] = true;
}
}
}
return -1; // 无法到达终点返回-1
}
int main()
{
scanf("%d%d", &n, &m);
for(int i=0; i!=n; ++i){
getchar();
for(int j=0; j!=m; ++j){
scanf("%c", &matrix[i][j]);
}
}
scanf("%d%d%d%d", &s.x, &s.y, &t.x, &t.y);
s.step = 0;
cout << bfs() << endl;
return 0;
}
小结
BFS的重点在于状态的标记
通过队列的不断出队、入队和对状态的标记就可以实现对一片区域的搜素
结点除了存储坐标依题目的意思可能需要存储其他的属性(例如步数、月份等),这些属性往往又决定了搜索什么时候停止

浙公网安备 33010602011771号