2.1BFS的多种模型(算法提高课)

目录

一,flood fill 算法

1,池塘统计

2,城堡问题

3,山峰和山谷

二,bfs中的最短路模型

1,迷宫问题

2,武士风度的牛

3,抓住那头牛

4,Jerry

 三,多源bfs

1,矩阵距离

四,最小步数模型

1,魔板

五,双端队列广搜

1,电路维修

六,双向广搜

1,字串变换

七,A*算法

1,八数码

2,第k短路


一,flood fill 算法

flood fill用bfs实现,作用是可以在线性的时间复杂度内,找到某个点所在的连通块

1,池塘统计

题目链接:https://www.luogu.com.cn/problem/P1596

用bfs的方式找到每一个连通块即可

代码如下:

#include<iostream>
#include<algorithm>
#include<queue>
#include<cstdio>

#define x first
#define y second

using namespace std;

typedef pair<int, int>pii;

const int N = 1010;

int n, m, cnt;
queue<pii>q;//队列存点
char str[N][N];
bool st[N][N];//表示该点是否已经访问过

void bfs(int sx, int sy)
{
	q.push({ sx,sy });
	st[sx][sy] = true;
	while (q.size())
	{
		auto t = q.front();
		q.pop();//弹出队列
		for (int i = t.x - 1; i <= t.x + 1; i++)//遍历包围t.x和t.y 的上下左右八个方向的八个点,该题是八连通
		{
			for (int j = t.y - 1; j <= t.y + 1; j++)
			{
				if (i == t.x && j == t.y)continue;//首先排除自己
				if (i < 0 || i >= n || j < 0 || j >= m)continue;//注意边界
				if (str[i][j] == '.' || st[i][j])continue;//如果是高地或者是已经访问过的点就continue

				st[i][j] = true;//所有条件都满足,说明找到了一个连通块内的点
				q.push({ i,j });//加入队列
			}
		}
	}
}
int main()
{
	scanf("%d%d", &n, &m);
	for (int i = 0; i < n; i++)
		scanf("%s", str[i]);
	for (int i = 0; i < n; i++)//遍历所有点
	{
		for (int j = 0; j < m; j++)
		{
			if (str[i][j] == 'W' && !st[i][j])
			{
				bfs(i, j);//如果是水洼,将这个点所在的连通块全部遍历,设置已访问
				cnt++;//连通块的数量加一
			}
		}
	}
	printf("%d", cnt);
	return 0;
}

2,城堡问题

题目链接:城堡问题 - OpenJ_NOI CH0205-1817 - Virtual Judge (vjudge.net)

 这题跟上题一样,但是在输入上面要稍微复杂一点,要用位运算判断一个点的四个方向是否能走过去,以及这题要多一个问题,找到最大连通块的数量,上面那题用STL的queue写的,这题就用数组模拟队列来写吧

代码如下:

#include<iostream>
#include<algorithm>

#define x first
#define y second
using namespace std;

typedef pair<int, int>pii;

const int N = 55, M = N * N;

int n, m;
pii q[M];
int a[N][N];
bool st[N][N];

int dx[4] = { 0,-1,0,1 };//以左上右下定义偏移量,该题为四连通
int dy[4] = { -1,0,1,0 };//方便后面位运算判断哪边有墙

int bfs(int sx, int sy)
{
	int hh = 0, tt = 0,area=0;
	q[0] = { sx,sy };
	st[sx][sy] = true;
	while (hh <= tt)
	{
		pii t = q[hh++];
		area++;//连通的数量加1
		for (int i = 0; i < 4; i++)
		{
			int fx = t.x + dx[i], fy = t.y + dy[i];
			if (fx < 0 || fx >= n || fy < 0 || fy >= m)continue;
			if (st[fx][fy])continue;
			if (a[t.x][t.y] >> i & 1)continue;//这里是重点,用位运算判断哪边有墙,有墙的话就不能走过去

			st[fx][fy] = true;
			q[++tt] = { fx,fy };
		}
	}
	return area;
}
int main()
{
	cin >> n >> m;
	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			cin >> a[i][j];
	int area = 0,cnt=0;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
		{
			if (!st[i][j])
			{
				area = max(area, bfs(i, j));//找到最大的连通块
				cnt++;
			}
		}
	}
	cout << cnt << endl << area << endl;
	return 0;
}

3,山峰和山谷

题目链接:https://vjudge.net/problem/HYSBZ-1102

这题也是用flood fill来找到所有连通块,并且在找的过程中判断有没有比他高的山峰和比他矮的山峰,如果整个连通块找完了,都没有比他高的山峰,说明这个连通块是山谷,如果整个连通块都找完了,都没有比他矮的山,说明这个连通块是山峰

代码如下:

#include<iostream>
#include<algorithm>

#define x first
#define y second

using namespace std;

typedef pair<int, int>pii;

const int N = 1010, M = N * N;

int n;
pii q[M];
int a[N][N];
bool st[N][N];

void bfs(int sx, int sy, bool& has_higher, bool& has_lower)
{
	int hh = 0, tt = 0;
	st[sx][sy] = true;
	q[0] = { sx,sy };
	while (hh <= tt)
	{
		pii t = q[hh++];
		for (int i = t.x - 1; i <= t.x + 1; i++)//这题是八连通图
		{
			for (int j = t.y - 1; j <= t.y + 1; j++)
			{
				if (i == t.x && j == t.y)continue;
				if (i < 0 || i >= n || j < 0 || j >= n)continue;
				if (a[i][j] != a[t.x][t.y])//如果不相等就判断这个山比他高还是矮
				{
					if (a[i][j] > a[t.x][t.y])has_higher = true;//比他高说明我们找到了比他高的山
					if (a[i][j] < a[t.x][t.y])has_lower = true;//比他矮说明我们找到了比他矮的山
				}
				else if (!st[i][j])
				{
					q[++tt] = { i,j };
					st[i][j] = true;
				}
			}
		}
	}

}
int main()
{
	scanf("%d", &n);
	for (int i = 0; i < n; i++)
		for (int j = 0; j < n; j++)
			scanf("%d", &a[i][j]);
	int peak = 0, valley = 0;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			if (!st[i][j])
			{
				bool has_higher = false, has_lower = false;
				bfs(i, j, has_higher, has_lower);
				if (!has_higher)peak++;//如果说整个连通块都遍历完了,都没有找到比他高的,说明他是山谷
				if (!has_lower)valley++;//这里要注意一下,可能所有山都是一样的高度,那么这个山既是山峰也是山谷,所以这里要用else if而不能用else
			}
		}
	}
	printf("%d %d\n", peak, valley);
	return 0;
}

二,bfs中的最短路模型

bfs是具有最短路的性质的,也就是说,当用bfs搜索,所有找到的点一定是距离源点的距离最短的点

1,迷宫问题

题目链接:https://vjudge.net/problem/POJ-3984

这题就是一个很简单的bfs模型,只不过要求我们把最短路径打印出来,所以我们多开一个数组来存储每个点是由哪个点走过来的就可以了,由于存的是逆序,所以我们从终点往起点走,得到的路径就是正序

代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>

#define x first
#define y second
using namespace std;

typedef pair<int, int>pii;

const int N = 10, M = N * N;

int a[N][N];
pii pre[N][N];//存储每个点由从哪个点走过来的
pii q[M];//数组模拟队列

int dx[4] = { 0,1,0,-1 };//定义偏移量,右下左上
int dy[4] = { 1,0,-1,0 };
void bfs(int sx, int sy)
{
	memset(pre, -1, sizeof pre);//首先初始话所有点的都没有点转移过来
	int hh = 0,tt = 0;
	q[0] = { sx,sy };
	pre[sx][sy] = { 0,0 };//将第一个点转移过来的点随便设置一个,这里设{0,0};
	while (hh <= tt)
	{
		pii t = q[hh++];
		for (int i = 0; i < 4; i++)
		{
			int fx = t.x + dx[i], fy = t.y + dy[i];
			if (fx < 0 || fx>4 || fy < 0 || fy>4)continue;//判断边界
			if (pre[fx][fy].x!=-1)continue;//如果这个点已经被走过了
			if (a[fx][fy])continue;//如果这个点是墙

			pre[fx][fy] = t;//记录路径
			q[++tt] = { fx,fy };//将走过的点入队
		}
	}
}
int main()
{
	for (int i = 0; i < 5; i++)
		for (int j = 0; j < 5; j++)
			scanf("%d", &a[i][j]);
	bfs(4, 4);//因为路径村的是逆序的,所以从终点往起点走,存的路径就是正序的

	pii end(0, 0);//初始化一个pair,为(0,0)
	while (1)
	{
		printf("(%d,%d)\n", end.x, end.y);
		if (end.x == 4 && end.y == 4)break;
		end = pre[end.x][end.y];
	}
	return 0;
}

2,武士风度的牛

题目链接:https://www.acwing.com/problem/content/190/

这题也是一个很简单的bfs模型,唯一不同的就是这个题有八个方向,定义八个偏移量就可以了

代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>

#define x first
#define y second
using namespace std;

typedef pair<int, int>pii;

const int N = 160, M = N * N;

int n, m;
pii q[M];//数组模拟队列
char a[N][N];
int dist[N][N];//存储起点到每个点的距离

int dx[8] = {-2,-1,1,2,2,1,-1,-2};//定义偏移量,八个方向
int dy[8] = {1,2,2,1,-1,-2,-2,-1};

int bfs()
{
	int sx, sy;
	for(int i=0;i<n;i++)
		for (int j = 0; j < m; j++)
			if (a[i][j] == 'K')
				sx=i, sy = j;//找到起点的位置
	int hh = 0, tt = 0;
	q[0] = { sx,sy };//起点入队
	dist[sx][sy] = 0;//起点到起点的距离为0
	while (hh <= tt)
	{
		pii t = q[hh++];//记录起点,并将起点出队
		for (int i = 0; i < 8; i++)
		{
			int fx = t.x + dx[i], fy = t.y + dy[i];
			if (fx < 0 || fx >= n || fy < 0 || fy >= m)continue;//边界问题
			if (a[fx][fy] == '*')continue;//如果是障碍就不能走
			if (dist[fx][fy])continue;//如果这个点走过了
			if (a[fx][fy] == 'H')return dist[t.x][t.y] + 1;//如果找到终点了

			dist[fx][fy] = dist[t.x][t.y] + 1;//更新距离
			q[++tt] = { fx,fy };//将走过的点入队
		}
	}
}
int main()
{
	cin >> m >> n;
	for (int i = 0; i < n; i++)cin >> a[i];

	cout << bfs() << endl;

	return 0;
}

3,抓住那头牛

题目链接:https://www.acwing.com/problem/content/description/1102/

这题看似跟bfs没有关系,实际上就是用bfs做,因为每次移动都只花费一分钟,这个题目可以看成一个点可以由三个点走过去。

同时我们要注意一下边界问题

农夫是有可能先走到牛的右边再往左边走的,所以数据范围不能只开到题目要求那么大,根据题意,当农夫在牛的右边时,只能向左一步一步走抓到牛,所以农夫最多只能走到2倍题目给的范围的距离,如果她超出了这个范围,想要抓到牛,一定不是最优解,因为会做很多无用功。同时因为牛的位置一定是在正坐标上的,所以如果农夫走到了负坐标上,只能通过向右一步一步走到正坐标上在进行其他步数操作,这是无用功,所以农夫最多只能走到0号坐标的位置

代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N=2e5+10;//一定要注意这个边界范围,可能存在农夫先走到牛的右边再往左边走,这样走的步数是最小的情况
                    //但是农夫最多只能走到2倍题给范围,否则会做无用功
int n,k;
int q[N];//数组模拟队列
int dist[N];//存储点到到起点的距离

int bfs()
{
    memset(dist,-1,sizeof dist);//初始化将所有点都没有点走过来
    int hh=0,tt=0;;
    q[0]=n;//将起点入队
    dist[n]=0;//起点到起点的距离为0
    while(hh<=tt)
    {
        int t=q[hh++];
        
        if(t==k)return dist[k];//说明找到牛了
        if(t+1<=k&&dist[t+1]==-1)//如果当前农夫在牛的左侧,那就可以通过向前走一步走过去,否则就是再做无用功
        {
            dist[t+1]=dist[t]+1;//更新距离
            q[++tt]=t+1;//将走过的点入队
        }
        //因为牛的位置是在正坐标上的,如果农夫往后走走到负数坐标上了
        //想要找到只能一步一步先走到正坐标上,所以如果往后走走到负坐标上是在做无用功
        if(t-1>=0&&dist[t-1]==-1)
        {
            dist[t-1]=dist[t]+1;
            q[++tt]=t-1;
        }
        //如果农夫走到了牛的右边,只能一步一步往左边走,所以农夫最多只能走到两倍题给数据范围,也就是2e5,否则肯定会多走无用的步数
        if(2*t<N&&dist[2*t]==-1)
        {
            dist[2*t]=dist[t]+1;
            q[++tt]=2*t;
        }
    }
}
int main()
{
    scanf("%d%d",&n,&k);
    printf("%d",bfs());
    return 0;
}

4,Jerry

题目链接:https://vjudge.net/problem/CSG-1177

这题是广东省2021年省赛原题,跟上个题基本上是一样的,只不过上个题一个点可以由三个点转移过去,这个题一个点可以由多个点转移过去,还要注意的是,这题数据范围很大,所以要先用bfs预处理出来0号点到所有点的最短距离,再通过查表得方式就可以得到答案

代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N = 2e5 + 10;

int n;
int q[N];
int dist[N];

void bfs()
{
	memset(dist, -1, sizeof dist);//初始化表示所有点都没有从其他点走过来
	int hh = 0, tt = 0;
	q[0] = 0;
	dist[0] = 0;
	while (hh <= tt)
	{
		int t = q[hh++];
		for (int i = 1; i * i <= 1e5; i++)//遍历所有能走的点
		{
			int x = i * i;
			if (dist[t + x] == -1)//这个点没有走过
			{
				dist[t + x] = dist[t] + 1;
				q[++tt] = t + x;
			}
			if (dist[t - x] == -1 && t - x >= 0)//往回走得话要保证不走到负数得位置上
			{
				dist[t - x] = dist[t] + 1;
				q[++tt] = t - x;
			}
		}
	}
}
int main()
{
	bfs();//预处理出来所有点到源点0号点得距离
	int q;
	scanf("%d", &q);
	while (q--)
	{
		scanf("%d", &n);
		printf("%d\n", dist[n]);
	}
	return 0;
}

 三,多源bfs

多源bfs其实和单源bfs差不多,唯一不同的地方就是单源我们首先是把起点入队,多源的话我们首先要把多个起点入队,剩下的就和单源bfs没区别了

1,矩阵距离

题目链接:https://www.acwing.com/problem/content/description/175/

这题要求离每个0最近的1的距离,可以以0作为源点也可以以1作为源点,但是这题以1作为源点会方便一些,所以这题我们把所有的1作为源点,然后正常做一遍bfs即可得到答案

代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>

#define x first
#define y second
using namespace std;

const int N=1010,M=N*N;

typedef pair<int,int>pii;

int n,m;
char a[N][N];
pii q[M];//数组模拟队列
int dist[N][N];//表示点到源点的距离

int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};
void bfs()
{
    memset(dist,-1,sizeof dist);//初始化为-1
    int hh=0,tt=-1;//这里tt要初始化成-1
    for(int i=0;i<n;i++)
    {   
        for(int j=0;j<m;j++)
        {
            if(a[i][j]=='1')//我们把所有是1的位置作为起点,加入到队列中,找到的0的位置距离1的位置的距离就是最短距离
            {
                q[++tt]={i,j};
                dist[i][j]=0;
            }
        }
    }
    while(hh<=tt)
    {
        pii t=q[hh++];
        for(int i=0;i<4;i++)
        {
            int fx=t.x+dx[i],fy=t.y+dy[i];
            if(fx<0||fx>=n||fy<0||fy>=m)continue;
            if(dist[fx][fy]!=-1)continue;
            
            dist[fx][fy]=dist[t.x][t.y]+1;
            q[++tt]={fx,fy};
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++)
        scanf("%s",a[i]);
    bfs();
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<m;j++)
            printf("%d ",dist[i][j]);
        printf("\n");
    }
    return 0;
}

四,最小步数模型

1,魔板

题目链接:https://www.acwing.com/problem/content/description/1109/

这题如果是第一次做可能很难想到是用bfs,但其实这个是一个bfs模型

我们把序列看成一个字符串,找到目标序列也就是找到目标字符串,对于每一步字符串我们可以进行三种操作,记录走到每个字符串的最小步数,以及走到这个字符串是由哪一步操作和由哪个字符串转换过来的,用bfs搜索第一次搜到的目标字符串一定是最小步数,因为这题是记录走到每个字符串得步数,所以不能用一个数组,可以用STL中得map容器,因为不用保证有序,用unordered_map速度会更快一点

代码如下:

#include<iostream>
#include<algorithm>
#include<queue>
#include<unordered_map>

using namespace std;

unordered_map<string, int>dist;//表示走到字符串的最小步数
unordered_map<string, pair<char, string>>pre;//表示走到字符串是由哪一步操作由哪个字符串转换过来的
string eend;//目标串

string get(string t, int op)//题意得三种操作,暴力看起来比较直观
{
	string k;
	if (op == 0) k = { t[4], t[5], t[6], t[7], t[0], t[1], t[2], t[3] };
	if (op == 1) k = { t[3], t[0], t[1], t[2], t[7], t[4], t[5], t[6] };
	if (op == 2) k = { t[0], t[5], t[1], t[3], t[4], t[6], t[2], t[7] };
	return k;
}

int bfs()
{
	string s = "12348765";//首先将初始状态入队
	queue<string>q;
	dist[s] = 0;//初始状态到初始状态的最小步数为0
	q.push(s);
	
	while (!q.empty())
	{
		auto t = q.front();//取出队首元素
		q.pop();
		if (t == eend)return dist[t];//说明找到目标串了,返回走到目标串的最小步数
		for (int i = 0; i < 3; i++)
		{
			string fs = get(t, i);//经过各种操作得到的字符串
			if (!dist.count(fs))//判断这个字符串出现过没
			{
				dist[fs] = dist[t] + 1;//表示走到这个字符串的最小步数
				pre[fs] = { 'A' + i,t };//表示这个字符串是由哪一种操作由哪一个字符串转换过来的
				q.push(fs);//将这个字符串入队
			}
		}
	}
}
int main()
{
	string start = "12348765";//初始状态
	int a[10];
	
	for (int i = 0; i < 8; i++)cin >> a[i];//一定要注意这题的输入,给的八个数前四个是正序,后四个是逆序
	reverse(a + 4, a + 8);
	for (int i = 0; i < 8; i++)
		eend.push_back(a[i] + '0');//转换成目标字符串
		
	cout << bfs() << endl;//bfs返回最小步数
	string res;//记录答案,但是这是从终点到起点的逆序顺序,最后要翻转一下
	while (eend != start)
	{
		res += pre[eend].first;//记录答案
		eend = pre[eend].second;//表示从到这一步的上一步
	}
	reverse(res.begin(), res.end());//翻转答案
	cout << res ;
	return 0;
}

五,双端队列广搜

1,电路维修

题目链接:https://www.acwing.com/problem/content/177/

这题其实跟dijkstra有点像,我们把不用旋转看成代价为0,要旋转代价为1,我们把图中的点连成一个无向图,那么所有的边权可能为0,也可能为1,不是全为0的情况,所以不能用常规的bfs来做

转换成图以后就相当于让我们求,从源点到终点的最短距离,就可以用dijkstra来做这个题,因为只有两种边权,所以我们可以用双端队列来做,双端队列会比dijkstra更加快一点,而且如果用dijkstra的话我们要建图,代码复杂度会比较高,用双端队列把走过去边权为0的点加到对头,边权为1的点加到队尾,每次取出队首,就相当于dijkstra算法的取出距离源点的最小值

但是这题还有个比较复杂的地方就是我们走的是点,但给的是点与点之间的关系,给的是线路,所以我们走到这个点之前要判断是否有线路,如果有线路说明代价为0,没有的话代价为1

代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<deque>

using namespace std;

typedef pair<int,int>pii;

const int N=510;

int n,m;
char a[N][N];
int dist[N][N];//表示从源点走到这个点的代价
bool st[N][N];//表示这个点是否走过

int dx[4]={-1,-1,1,1},dy[4]={-1,1,1,-1};//定义点偏移量,左上右上右下左下
int ix[4]={-1,-1,0,0},iy[4]={-1,0,0,-1};//这时线路的偏移量,左上右上右下左下
char ch[]={'\\','/','\\','/'};//判断我要走过去的那个点有没有线路,有的话走过去的代价为0,没有为1
        //这里要注意/是一个转义字符,所以要用两个//才能代表/  
int bfs()
{
    deque<pii>q;
    //跟dijkstra算法一样,初始化所有点为最大值,与常规的bfs不同是因为边权不是固定为1,
    //而是有0和1两种边权,因为只有两种边权,我们可以用双端队列优化,比堆优化更快一点
    //否则要用堆优化,这题就跟dijkstra算法一样了
    memset(st,false,sizeof st);
    memset(dist,0x3f,sizeof dist);
    q.push_back({0,0});//起点插入入队尾
    dist[0][0]=0;//起点距离起点的距离为0
    
    //与dijkstra算法差不多,也可以说与常规bfs差不多,队首就是就是离源点最近的点
    while(!q.empty())
    {
        pii t=q.front();//取出队首
        q.pop_front();//从队头出队
        
        int sx=t.first,sy=t.second;//队首的点的坐标
        if(sx==n&&sy==m)return dist[n][m];//表示找到终点了
        
        if(st[sx][sy])continue;//走过的点不能再走了
        st[sx][sy]=true;//表示这个点已经走过了
        
        for(int i=0;i<4;i++)
        {
            int x=sx+dx[i],y=sy+dy[i];//点的移动
            if(x<0||x>n||y<0||y>m)continue;
            int x1=sx+ix[i],y1=sy+iy[i];//点周围的电线的状态
            
            int w=(a[x1][y1]!=ch[i]);//如果走过去的那个点刚好有线路,代价为0,否则代价为1
            if(dist[x][y]>=dist[sx][sy]+w)//如果能更新这个点的距离
            {
                dist[x][y]=dist[sx][sy]+w;//那就更新这个点的距离
                if(w)//如果代价为1,加插入到队列后面
                    q.push_back({x,y});
                else//为0就插入到队列前面
                    q.push_front({x,y});
            }
        }
    }
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        for(int i=0;i<n;i++)scanf("%s",a[i]);
        
        if(n+m&1)//如果终点的坐标下标之后是奇数,表示永远都不可能走过去,因为每次移动到的点都是下标之和为偶数的点
            printf("NO SOLUTION\n");
        else
            printf("%d\n",bfs());
    }
    return 0;
}

六,双向广搜

1,字串变换

题目链接:https://www.acwing.com/problem/content/192/

这题的数据范围非常非常大,如果用常规bfs的话肯定会超时,但是如果用双向bfs的话,我们可以将时间复杂度大大提高,从O(K^10)提升至O(2K^5),这是一个非常大的提升

下面介绍双向bfs,顾名思义,双向bfs就是从两个端点往中间搜,这样会省去很多没必要搜的点,所以我们的时间复杂度会降低很多,我们建立两个队列,一个从起点开始搜的队列,一个从终点开始搜的队列,每次找到队列中元素较少的那一个队列往中间扩展一层,如果他扩展的元素在另一个队列中之间已经扩展到了,说明找到了一条从起点到终点最短的路径,这个点从起点走过来的距离加上从终点走过来的距离即可得到答案

代码如下:

#include<iostream>
#include<algorithm>
#include<queue>
#include<string>
#include<unordered_map>

using namespace std;

const int N=10;

int n;
string a[N],b[N];


int extend(queue<string>& q,unordered_map<string,int>& da,unordered_map<string,int>& db,string a[],string b[])
{
    int d=da[q.front()];//要保证每次只向外扩展一层
    while(q.size()&&da[q.front()]==d)//保证队列不空和每次只向外扩展一层
    {
        auto t=q.front();//取出队首元素
        q.pop();
        for(int i=0;i<n;i++)//枚举规则
        {
            for(int j=0;j<t.size();j++)//对于每种规则,枚举转换字符串的起点
            {
                if(t.substr(j,a[i].size())==a[i])//从j点开始的字符串要符合转换规则
                {
                    string state=t.substr(0,j)+b[i]+t.substr(j+a[i].size());//如果符合,记录转换后的这个字符串
                    //如果这个字符串在另一个队列中已经遍历过,说明这亮哥哥队列走到一个点了,就可以返回他们之间的最小距离了
                    if(db.count(state))return da[t]+1+db[state];
                    if(da.count(state))continue;//如果这个字符串已经搜过了
                    da[state]=da[t]+1;//更新到这个字符串的距离
                    q.push(state);//将这个字符串入队
                }
            }
        }
    }
    return 11;//如果对于这个队列向外扩展了一层还没找到与另一个队列重合的串,就返回一个大于10的值
            //接着扩展寻找
}
int bfs(string A,string B)
{
    if(A==B)//如果两个字符串相等,说明步数为0
        return 0;

    unordered_map<string,int>da,db;//从起点走到一个字符串的最小步数,和从终点走到一个字符串的最小步数
    queue<string>qa,qb;//从起点开始扩展的队列和从终点开始扩展的队列
    qa.push(A);qb.push(B);//将起点入队,将终点入队
    da[A]=0;db[B]=0;//表示起点到起点的距离为0,终点到终点的距离为0
    
    int step=0;//表示扩展的层数
    while(qa.size()&&qb.size())//保证每个队列都不为空,如果一个队列空了,还没找到答案,说明十不连通的,不可能转换倒目标串
    {
        int t;
        if(qa.size()<qb.size())//这是一个小优化,每次对于队列中元素较少的一个队列向外扩展一层,这样会少扩展一点
            t=extend(qa,da,db,a,b);
        else
            t=extend(qb,db,da,b,a);//这里要注意以下,从终点往起点扩展,转换的规则就是反的
            
        if(t<=10)//如果有一次扩展的步数小于10,就可以直接返回了
            return t;
        if(++step==10)return -1;//如果扩展了10层,等于走了十步还没转换过去,说明十步以内转换不了倒目标串了
    }
    return -1;//全部都方案都试完了也没转换成目标串
}
int main()
{
    string A,B;
    cin>>A>>B;//初始串和目标串
    while(cin>>a[n]>>b[n])n++;//字符串的转换规则
    
    int step=bfs(A,B);//返回步数如果是-1,说明不能在10步以内转换过去
    if(step==-1)
        printf("NO ANSWER!");
    else//否则可以,输出答案
        printf("%d",step);
    return 0;
}

七,A*算法

A*算法就是带有估价函数的优先队列BFS算法,只要保证对于任意状态state,都有f(state)<=g(state),(这里的f代表估价函数),A*算法就一定能在目标状态(只能保证目标状态)第一次从堆中被取出时取得最优解,且在有解的情况用A*算法比较合适,否则效率与常规bfs差不多

那么是什么意思呢?什么是估计函数呢?

估价函数就是,以任意“状态”为输入,计算出从该状态到目标状态所需代价的估计值。在搜索中,维护一个堆,不断从堆中取出“当前代价+未来估价”最小的状态进行扩展。

为了保证第一次从堆中取出目标状态时得到得就是最优解,我们设计得估价函数需要满足一个基本准则:

设当前状态state到目标状态所需代价得估计值为f(state)。

设在未来搜索中,实际求出的从当前状态state到目标状态的最小代价为g(state)。

对于任意的state,应该有f(state)<=g(state)。

也就是说,估价函数的估值不能大于未来实际代价,估价比实际代价更优。

估价越准确,越近进g(state),A*算法的效率就越高,如果估价始终为0,就等于普通的优先队列bfs,估价函数在满足上述设计准则的前提下,还应该尽可能反映未来实际代价的变化趋势和相对大小关系,这样搜索才会较快地逼近最优解

因为A*算法有估价函数,所以能在估价函数设计的不错的时候可以很大的提高我们的搜索效率,对于很多没必要搜索的点就不会搜索,但是往往估价函数不是非常好设计,所以不到迫不得已我们一般都不会用A*算法

下面看两个例题:

1,八数码

题目链接:https://www.acwing.com/problem/content/description/181/

这题我们设计的估价就是每个点现在的位置到它应该在的位置的曼哈顿距离,也就是横纵坐标的绝对值差的总和,每次取出估价函数最小的状态进行扩展

代码如下:

#include<iostream>
#include<algorithm>
#include<string>
#include<unordered_map>
#include<queue>

using namespace std;

typedef pair<int,string>pis;//econd表示字符串,first表示该字符串的估价函数的返回值

unordered_map<string,int>dist;//表示从初始状态走到字符串的距离
unordered_map<string,pair<char,string>>pre;//存储走到字符串是由哪个步骤由哪个字符串走过来的
priority_queue<pis,vector<pis>,greater<pis>>heap;//以字符串的估价函数的返回值排序

int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};//点的偏移量
char ch[]="urdl";//四个操作步骤

int f(string s)//返回字符串的估价函数的值
{
    int res=0;
    for(int i=0;i<9;i++)
    {
        if(s[i]!='x')
        {
            int v=s[i]-'1';//先把这字符扣出来
            res+=abs(i/3-v/3)+abs(i%3-v%3);//现在的位置减去应该在的位置的横纵坐标之和
        }
    }
    return res;//所有点的横纵坐标之和
}
string bfs(string start)
{
    string eend="12345678x";//目标状态
    dist[start]=0;//起点到起点的距离为0
    heap.push({f(start),start});//起点入队
    while(heap.size())
    {
        auto t=heap.top();//取出队首,也就是估价函数值最小的字符串
        heap.pop();
        string state=t.second;//估价函数值最小的字符串
        if(state==eend)break;//如果找到目标串了就break
        
        int sx,sy;//找以下x所在的位置,因为我们每次移动的都是x的位置
        for(int i=0;i<9;i++)
            if(state[i]=='x')
                sx=i/3,sy=i%3;
                
        string source=state;//先保存下来要扩展的字符串,因为后面每次扩展都会改变状态
        for(int i=0;i<4;i++)
        {
            int fx=sx+dx[i],fy=sy+dy[i];//移动x的位置
            if(fx<0||fx>=3||fy<0||fy>=3)continue;//判断是否合法
            state=source;//这步操作不能省去,因为每次扩展会改变需要扩展的字符串的状态,所以要先将需要扩展的字符串复制回来
            swap(state[sx*3+sy],state[fx*3+fy]);//相当于移动x后,改变了x在字符串中的位置
            if(dist.count(state)==0||dist[state]>dist[source]+1)//如果这个字符串之前没找到过,或者能更新到它的距离
            {
                dist[state]=dist[source]+1;//更新距离
                pre[state]={ch[i],source};//记录是由哪个字符串的哪一步操作转移过来的
                heap.push({dist[state]+f(state),state});//入队
            }
        }
    }
    string res;//从目标状态往回找操作步骤
    while(eend!=start)
    {
        res+=pre[eend].first;
        eend=pre[eend].second;
    }
    reverse(res.begin(),res.end());//将操作步骤翻转过来就是从初始状态到目标状态的操作步骤
    return res;//返回操作步骤
}
int main()
{
    string start,seq;
    char ch;
    while(cin>>ch)
    {
        start+=ch;
        if(ch!='x')
            seq+=ch;
    }
    int cnt=0;
    for(int i=0;i<8;i++)//先判断以下该八数码逆序对的奇偶性,如果是奇数,说明永远都不可能走到目标状态
        for(int j=i+1;j<8;j++)//要想判断一个奇数码的两个状态是否可达,要判断这两个奇数码的逆序对的奇偶性是否相同
            if(seq[i]>seq[j])
                cnt++;
    if(cnt&1)//如果为奇数,说明不可达
        printf("unsolvable\n");
    else//否则去找最小操作步数
        cout<<bfs(start);
    return 0;
}

2,第k短路

题目链接:https://www.acwing.com/problem/content/description/180/

这题是A*算法非常好的一个应用,我们以各点到终点的最短距离作为我们的估值函数,当终点出队k次后就说明我们找到了第k短路,这样帮助我们少遍历了很多点,大大提高了搜索效率。

并且,在找第k短路的过程中,每个点都最多出队k次,如果有一条第k短路上的点出队了k次以上,一定能找到一条更短的路径到达终点,那么包含这个点的路径就不是第k短路了,所以在这个地方我们可以做一个小优化,如果某个点出队了k次以上,下次遍历到它的时候就不让它入队了

代码如下:

#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>

#define x first
#define y second

using namespace std;

const int N=1010,M=20010;

typedef pair<int,int>pii;
typedef pair<int,pair<int,int>>piii;

int n,m,S,T,K;
int dist[N],cnt[N];//dist表示每个点到终点的距离,cnt表示每个点遍历的次数
int h[N],rh[N],e[M],ne[M],w[M],idx;//正向邻接表与逆向邻接表
bool st[N];//表示每个点是否呗遍历过

void add(int h[],int a,int b,int c)//邻接表存储图
{
    e[idx]=b;ne[idx]=h[a];w[idx]=c;h[a]=idx++;
}
int dijkstra()//以终点做一遍dijkstra算法,相当于求各点到终点的最短距离,各点到终点的最短距离作为我们的估值函数
{
    memset(dist,0x3f,sizeof dist);//堆优化版的dijkstra算法
    priority_queue<pii,vector<pii>,greater<pii>>heap;
    heap.push({0,T});
    dist[T]=0;
    while(heap.size())
    {
        auto t=heap.top();
        heap.pop();
        int ver=t.y;
        st[ver]=true;
        for(int i=rh[ver];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dist[j]>dist[ver]+w[i])
            {
                dist[j]=dist[ver]+w[i];
                heap.push({dist[j],j});
            }
        }
    }
    if(dist[S]==0x3f3f3f3f)//表示起点和终点之间没有路径,可以直接输出答案了
        return -1;
    return 1;
}
int Astar()//每次取出估值函数最小的点,当终点被取出k次以后就说明找到了第k短路到终点的路
{
    priority_queue<piii,vector<piii>,greater<piii>>heap;
    heap.push({dist[S],{0,S}});//first存储该点到终点的估值函数的信息,second.first存储该点到起点的实际距离,second.second存储点
    while(heap.size())
    {
        auto t=heap.top();
        heap.pop();
        int ver=t.y.y,distance=t.y.x;//distance表示从起点到该点的实际距离
        cnt[ver]++;//每次被弹出的点要记录被弹出的次数
        if(cnt[T]==K)return distance;//终点被弹出k次,说明找到了第k短到终点的路
        for(int i=h[ver];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(cnt[j]<K)//被弹出的次数小于K次才继续入队,因为如果弹出k次以上,说明从该点继续扩展到达的终点一定不是第k短路
                heap.push({distance+w[i]+dist[j],{distance+w[i],j}});//将该点入队
        }
    }
    return -1;//如果没有找到第k短路
}
int main()
{
    memset(h,-1,sizeof h);//表头初始化
    memset(rh,-1,sizeof rh);
    scanf("%d%d",&n,&m);
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(h,a,b,c);
        add(rh,b,a,c);//反向建图,用于找到各点到终点的最短距离
    }
    scanf("%d%d%d",&S,&T,&K);
    //如果起点和终点相同,要将k++,因为最短的路径至少要包含一条边
    //例如想让我们求第一短路,当起点与终点重合时,第一短路是0,但是至少要包含一条边,相当于让我们求第二短路
    if(S==T)
        K++;
    if(dijkstra()!=-1)
        cout<<Astar();
    else
        cout<<"-1";
    return 0;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值