寒假训练营 第十节 搜索与图论(一)总结

DFS(深度优先搜索)

深度优先搜索(DFS,Depth First Search)简称深搜或者爆搜,DFS 的搜索顺序是按照深度优先搜索,简单来说就是 “一条路走到黑”,搜索是把所有方案都试一遍,再判断是不是一个可行解。搜索与 “递归” 和 “栈” 有很大的联系,递归是实现搜索的一种方式,而栈则是计算机实现递归的方式。每个搜索过程都对应着一棵递归搜索树,递归搜索树可以让我们更加容易的理解 DFS。 整个搜索过程就是基于该搜索树完成的,为了不重复遍历每个结点,会对每个结点进行标记,也可以对树中不可能是答案的分支进行删除,从而更高效的找到答案,这种方法被称为剪枝。如果搜索树在某个子树中搜索到了叶结点,想继续搜索只能返回上个或多个状态,返回的过程称为回溯,回溯要记得恢复状态,才能保证接下来的搜索过程可以正常进行。

寒假训练营 第十节 搜索与图论(一)总结_第1张图片

基本思路

回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的方法为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

深度优先遍历图的方法是,从图中某顶点v出发:

  1. 访问顶点v。
  2. 依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问。
  3. 若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

DFS一般用于求解问题有多少种情况,多少条路径,最大路径等等。

基本模板如下:
void dfs(int step)
{
        if(满足回溯条件) 
        {
            对应操作
            return;
        }
        尝试每一种可能
        {
              if(满足选择条件)
               {
                   进行选择与标记
               	   继续下一步dfs(step+1)
                   撤销选择与标记
               }
        }
}
/*
也可以这样理解:
void dfs(int k) { // k代表递归层数,或者说要填第几个空
	if (所有空已经填完了) {
		判断最优解/记录答案;
		return;
	}
	for (枚举这个空能填的选项)
		if (这个选项是合法的) {
 			记录下这个空(保存现场);
 			dfs(k + 1);
	 		取消这个空(恢复现场);
	 	}
}
*/   
//	1.算法竞赛中,如果无法找到高效求解的方法(如贪心、递推、动态规划、公式推导等),使用搜索也可以解决一些规模较小的情况。
//	2.但不管怎么说,时间复杂度往往是指数级别的,效率相比于多项式时间复杂度还是要低。

题目详情:洛谷P1605 迷宫

给定一个 N×M 方格的迷宫,迷宫里有 T 处障碍,障碍处不可通过。

在迷宫中移动有上下左右四种方式,每次只能移动一个方格。数据保证起点上没有障碍。

给定起点坐标和终点坐标,每个方格最多经过一次,问有多少种从起点坐标到终点坐标的方案。

#include
using namespace std;
int n,m,t,a[10][10],v[10][10],ans;
int d[][2]={{0,1},{1,0},{0,-1},{-1,0}};
struct point
{
    int x,y;
};
point start,fi;
void dfs(point s)
{
    point temp;
    if(s.x==fi.x&&s.y==fi.y)//到达终点,可达路径加一
    {
        ans++;
        return;//返回上个点
    }
    for(int i=0;i<4;i++)//遍历每个点的四个方向
    {
        temp.x=s.x+d[i][1];
        temp.y=s.y+d[i][0];
        if(temp.y>=1&&temp.y<=n&&temp.x>=1&&temp.x<=m&&a[temp.y][temp.x]==0&& v[temp.y][temp.x]==0)//若下个点在迷宫范围内且为没有被访问过的非障碍点
        {
            v[temp.y][temp.x]=1;//标记选择的点,防止重复遍历
            dfs(temp);//以选择的点进行新的遍历
            v[temp.y][temp.x]=0;//撤销标记
        }
    }
}
int main()
{
    cin >> n >> m>> t;
    cin >> start.x >> start.y >> fi.x >> fi.y;
    point temp;
    for(int i=0;i<t;i++)
    {
        cin >> temp.x >> temp.y;
        a[temp.y][temp.x]=1;
    }
    v[start.y][start.x]=1;
    dfs(start);
    cout << ans;
    return 0;
}

题目详情:洛谷P1706 全排列问题

按照字典序输出自然数 1 到 n 所有不重复的排列,即 nn 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。

#include
using namespace std;
int gd[15],a[15],n;
void pt(){
	for(int i=1;i<=n;i++) 
		printf("%5d",a[i]);
	printf("\n"); 
}
void dfs(int dee){
	if(dee>n){
		pt();
		return;
	}
	for(int i=1;i<=n;i++){
		if(!gd[i]){
			gd[i]=1;
			a[dee]=i;
			dfs(dee+1);
			gd[i]=0;
		}
	}
	return;
}
int main(){
	scanf("%d",&n);
	dfs(1);
	return 0;
} 
//	详解版:
#include
using namespace std;
int n,a[10],q[50000],v[10],p;
void dfs(int step)
{
    if(step==n+1)//第n+1步时,表示已经选择了n个数
    {
        for(int i=1; i<=n; i++)//进行输出
        {
           printf("%5d",q[i]);//%5d是保留五个场宽
        }
    	cout << '\n';
        return;//返回上步继续选择
    }
    for(int i=1; i<=n; i++)//遍历所有可选择的数字
    {
        if(v[i]==0)//满足未被使用的条件
        {
            q[step]=a[i];//选择数字i,放入数组q
            v[i]=1;//将数字i进行标记
            dfs(step+1);//进行下步选择
            v[i]=0;//撤销数字i的标记
        }
    }
    return ;
}
int main()
{
    cin >>n;
    for(int i=1; i<=n; i++)a[i]=i;
    dfs(1);
    return 0;
}

补充:

争对全排列问题,我们还可以使用函数——next_permutation(),该函数位于头文件algorithm中。
next_permutation()三个参数(默认是字典序排列):序列的首地址、序列的尾地址、比较函数(可选)
C++ STL 全排列

BFS(宽 / 广度优先搜索)

宽度优先搜索(Breadth First Search,简称BFS):同样是一种遍历搜索树或图的算法。遍历方式为选定一个节点,接着访问所有与当前节点连接的满足条件的点。接着从这些可访问点中,按照相同的遍历方式访问每个节点,直到所有节点都被访问,这与树的层次遍历相同,时间复杂度与DFS相同,与搜索树和图的节点树相关。

寒假训练营 第十节 搜索与图论(一)总结_第2张图片

基本思路

深度优先搜索用(stack)来实现,整个过程可以想象成一个倒立的树形:
1、把根节点压入栈中。
2、每次从栈中弹出一个元素,搜索所有在它下一级的元素,把这些元素压入栈中。并把这个元素记为它下一级元素的前驱。
3、找到所要找的元素时结束程序。
4、如果遍历整个树还没有找到,结束程序。

广度优先搜索使用队列(queue)来实现,整个过程也可以看做一个倒立的树形:
1、把根节点放到队列的末尾。
2、每次从队列的头部取出一个元素,查看这个元素所有的下一级元素,把它们放到队列的末尾。并把这个元素记为它下一级元素的前驱。
3、找到所要找的元素时结束程序。
4、如果遍历整个树还没有找到,结束程序。

BFS一般用于解决最短路径,最短步骤等最优问题。

基本模板如下:

以下两种模板是用队列的形式来实现的:

Q.push(初始状态); // 将初始状态入队
while (!Q.empty()) {
	State u = Q.front(); // 取出队首
	Q.pop();//出队
	for (枚举所有可扩展状态) // 找到u的所有可达状态v
		if (是合法的) // v需要满足某些条件,如未访问过、未在队内等
		Q.push(v); // 入队(同时可能需要维护某些必要信息)
}
struct node //结构体用于保存每一状态信息 
{
	......	
};

void bfs(){
	queue<node> Q; // 定义存放结构体的队列Q 
	起点入队
	标记起点
	while(!Q.empty()) // 队非空 
	{
		node u=Q.front(); //获取队首信息(结构体)
		for(拓展接下来所有可能的状态) 
		{
			得到并记录新的状态信息
			判断状态是否合法
			若合法
			{
				当前标记为已访问 
				Q.push(合法节点);//状态入队
				判断是否到达目标
				若满足,输出答案,return ; 	 
			} 
		}
		Q.pop();//每次从队首把所有可能的状态走完,队首要出队  
	}
}

下面一种模板是用栈的形式来实现的:

 /**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */

//深度优先
int minDepth(struct TreeNode* root)
{	
	if(!root) return 0;
	if(!root->left && !root->right) return 1;
	int min_dep = pow(2, sizeof(min_dep) * 8) - 1;
	if(root->left)
	{
		int tmp = minDepth(root->left);
		min_dep = tmp < min_dep ? tmp : min_dep;
	}
	if(root->right)
	{
		int tmp = minDepth(root->right);
		min_dep = tmp < min_dep ? tmp : min_dep;
	}
	return min_dep + 1;

}

题目详情:洛谷P1135 奇怪的电梯

呵呵,有一天我做了一个梦,梦见了一种很奇怪的电梯。大楼的每一层楼都可以停电梯,而且第 i层楼( 1≤ i ≤ N )上有一个数字 K i K_{i} Ki(0 ≤ K i K_{i} Ki≤ N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例如: 3, 3, 1, 2, 5 代表了 K i K_{i} Ki K 1 K_{1} K1=3, K 2 K_{2} K2=3,……),从 1 楼开始。在 1 楼,按“上”可以到 4 楼,按“下”是不起作用的,因为没有 −2 楼。那么,从 A 楼到 B 楼至少要按几次按钮呢?

思路:

本题使用 STL 的 queue 实现队列。建立结构体数组存储扩展的结点
起点入队,然后在队列逐个扩展。每个点被扩展到时步数最小

#include
using namespace std;
int n,a,b,s[205],visited[205];
struct node
{
    int id,step;
};

int bfs(node start)
{
    queue<node> q;
    q.push(start);//将起始楼层入队
    visited[start.id]=1;//标记楼层
    while (!q.empty())
    {
        node front=q.front(),down,up;//获取现在所在楼层的状态
        down.id=front.id-s[front.id];//当前楼层向下可到达的楼层
        up.id=front.id+s[front.id];//当前楼层向上课到达的楼层
        down.step=front.step+1;//步数加一
        up.step=front.step+1;
        if (front.id==b)//如果当前楼层为目标楼层,返回结果
        {
            return front.step;
        }
        
        if (down.id>=1 && visited[down.id]==0)//判断当前楼层向上是否是可达的
        {
            visited[down.id]=1;
            q.push(down);//入队
        }
        if (up.id<=n && visited[up.id]==0)//与上相同
        {
            visited[up.id]=1;
            q.push(up);
        }
        q.pop();//在遍历了所有可能后,将队首元素出队
    }
    return -1;
}
int main()
{
    cin >> n >> a >> b;
    for (int i = 1; i <= n; i++)
    {
        cin >> s[i];
    }
    node s;
    s.id=a;
    s.step=0;
    cout << bfs(s);
    return 0;
}

题目详情:洛谷P1443 马的遍历

有一个 n × m 的棋盘,在某个点 (x, y) 上有一个马,要求你计算出马到达棋盘上任意一个点最少要走几步。

求 “ 最少 ” 步数,使用洪泛法。广度优先搜索使用队列实现。每次从队首取出元素,将该元素所能扩展到的结果插入队尾。这样即可保证,在同一层的其他元素均被取出之前,不会访问到下层的新元素。

本题的思路与上一题一样。仅是空间由一维变为二维,以及移动规则由给定的数字变为马步。

struct coord { //一个结构体存储x,y两个坐标
	int x, y;
};

queue<coord> Q;//队列
int ans[maxn][maxn];//记录答案,-1表示未访问
int walk[8][2] = {{2, 1}, {1, 2}, {-1, 2}, {-2, 1},{-2, -1}, {-1, -2}, {1, -2}, {2, -1}}

coord tmp = {sx, sy};
Q.push(tmp); // 使起点入队扩展
ans[sx][sy] = 0;
while (!Q.empty()) { // 循环直到队列为空
	coord u = Q.front(); // 拿出队首以扩展
	int ux = u.x, uy = u.y;
	Q.pop();
	for (int k = 0; k < 8; k++) {
		int x = ux + walk[k][0], y = uy + walk[k][1];
		int d = ans[ux][uy];
		if (x < 1 || x > n || y < 1 || y > m || ans[x][y] != -1)
			continue; // 若坐标超过地图范围或者该点已被访问过则无需入队
		ans[x][y] = d + 1; // 记录答案,是上一个点多走一步的结果。
		coord tmp = {x, y};
		Q.push(tmp);
	}
}
//	复杂度是 O(mn)

题目详情:洛谷P1746 离开中山路

爱与愁大神买完东西后,打算坐车离开中山路。现在爱与愁大神在 x 1 x_{1} x1 y 1 y_{1} y1处,车站在 x 2 x_{2} x2 y 2 y_{2} y2处。现在给出一个 n × n ( n ≤ 1000) 的地图,0 表示马路,1 表示店铺(不能从店铺穿过),爱与愁大神只能垂直或水平着在马路上行进。爱与愁大神为了节省时间,他要求最短到达目的地距离(每两个相邻坐标间距离为 1 )。你能帮他解决吗?

#include 
using namespace std;
int a, b, c, d, n, vis[1005][1005];
int dc[] = {0, 0, -1, 1};
int dr[] = {-1, 1, 0, 0};

char s[1005][1005];
struct point
{
    int c, r, step;
};

int bfs(point start)
{
    queue<point> q;
    q.push(start);
    vis[start.r][start.c] = 1;
    while (!q.empty())
    {
        point front = q.front(),p;
        q.pop();
        for (int i = 0; i < 4; i++)
        {
            
            p.r = front.r + dr[i];
            p.c = front.c + dc[i];
            p.step = front.step + 1;
            if (p.r >= 0 && p.r <= n && p.c >= 0 && p.c <= n && s[p.r][p.c] == '0' && vis[p.r][p.c] == 0)
            {
                vis[p.r][p.c] = 1;
                q.push(p);
            }
            if (p.r == c && p.c == d)
            {
                return p.step;
            }
        }
    }
    return 0;
}
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
        {
            cin >> s[i][j];
        }
    }

    cin >> a >> b >> c >> d;
    point s;
    s.r = a;
    s.c = b;
    s.step = 0;
    cout << bfs(s);
    return 0;
}

你可能感兴趣的:(图论,深度优先,算法)