搜索-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的重点在于状态的标记

通过队列的不断出队、入队和对状态的标记就可以实现对一片区域的搜素

结点除了存储坐标依题目的意思可能需要存储其他的属性(例如步数、月份等),这些属性往往又决定了搜索什么时候停止

posted @ 2021-04-09 15:49  巴啦啦胖魔仙  阅读(68)  评论(0)    收藏  举报