[AcWing算法提高课]之搜索 A*+DFS之搜索顺序(C++题解)

目录

(一)A*

1)第K短路(困难)

A*算法介绍:

2)八数码

(二)DFS之搜索顺序

 3) 分成互质组


(一)A*

1)第K短路(困难)

 

样例解释:

第一行输入 n个点,m条边

接下来m行输入 A->B的 有向边 边权为C

最后一行输入 起点S 终点T  的第K条最短路

 

 特殊情况分析

  1. 首先先从简单的入手,当起点和终点都是同一个点时,由于题目说:起点和终点至少包含一条边,那么就说明上述的特俗情况不成立,所以需要将此时的第 K 短路 变成 第 K+1短路
  2. 当K==1时,这个时我们最熟悉的最短路情况,无需过多介绍

A*算法介绍:

参考自:AcWing 178. 第K短路(A* 反向计算最短路作为到终点的估计值) - AcWing

数学证明我是真的不会,但 我可以学

(一)应用场景:

这道题就是A* 算法的模板题目,注意它的特殊之处,每个点可以被重复走过,那么这就意味着起点和终点可以重复走过,具体可看上面的样例图解

(二)暴力思想:

我的暴力想法是:从起点开始,将距离起点最近的点加入至一个集合当中,然后再从集合中依次扩展距离它们的最短距离的点,然后搞一个计数的,谁先到达终点谁就是最短的,每有一个到达中终点,计数器++,那么就证明它是第cnt短的,当然我没有任何的证明佐证这个是正确的

(三)算法优化:

核心思想--->>>​​​​​​   A* 反向计算最短路作为到终点的估计值

我来说代码的流程把.....

预处理阶段

  1. main函数中输入点数,边数 n,m
  2. main函数中初始化 邻接表的头元素 h[N] ,rh[N]  (正向,反向)
  3. 对于a->b 权重为c 的边  同时建立一条 b->a 权重为c的反向边,注意这里是一定要正向和反向的头节点数组的,而不能像建立无向图一样(只建立正向头节点数组)
  4. 输入起点S,终点T,第K小,特判情况 1  (起点和终点是同一个的情况)
  5. 堆优化版dijkstra 预处理 dist数组,dist数组存的是该点到终点的最小距离
  6. 堆优化版的dijkstra ,因为是点到终点的距离,所以要用 rh[ ] 反向遍历寻找节点​​​​​​​

A*解决阶段

  1. 建立一个存一个节点存三个数 的 小根堆 PIII
  2. 这三个数存的数 <估价值,<起点到该点的距离,该点的编号> >,
  3. 注意这里的估价值,个人理解是:该点到终点的估计距离,(因为估价距离存的原本是这个点到终点的最短距离,但对于本题而言,因为可能多次经过一个点,所以这个最短距离一定是<=真实距离的,那么就一定有下面的这个公式)
  4. 同时还需要一个cnt来表示,我们经过了这个终点几次,每经过一次终点,就让cnt++,第一次经过就表示最短路,第cnt次经过,就表示第cnt个最短路,
  5. 当cnt==k时,即可return 跳出
  6. 正序头节点遍历寻找里源点最小的距离,并将它加入堆中,这里注意一个易错点,堆与队列的区别,(谁的d[u]+f[u]更小 谁先出队列:估价函数的作用),这也是堆的特性,也是A*算法能找到第K最短的核心
#include<iostream>
#include<queue>
#include<cstring>
#include<cstdio>
#include<vector>
using namespace std;

#define x first
#define y second
typedef pair<int, int> PII;
typedef pair<int, PII> PIII;
const int N = 1010;//点数
const int M = 2e5 + 10;//边数
int n, m, S, T, K;//点数,边数,起点,终点,第K小
int h[N], rh[N], e[M], w[M], ne[M], idx;//正向 h[]  反向 rh[]
int dist[N];
bool st[N];//每个点用没用过

void add(int  h[], int a, int b, int c)
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
}

// 在反向图上dijkstra(),保存估价函数(dist[]距离)
// dist存的是该点到终点的最小距离
void dijkstra()
{
	priority_queue<PII, vector<PII>, greater<PII> >heap;
	heap.push({ 0,T });//<距离,编号> -->> 终点到终点的距离初始化为0
	memset(dist, 0x3f, sizeof dist);
	dist[T] = 0;// 终点到终点的距离初始化为0

	while (heap.size())
	{
		auto t = heap.top();
		heap.pop();
		
		int ver = t.y;//取出编号
		if (st[ver]) continue;
		st[ver] = true;

		// 在反向图上遍历,rh[]数组
		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 });
			}
		}
	}
}

int astar()
{
	priority_queue<PIII, vector<PIII>, greater<PIII> > heap;
	//A* 算法,从起点开始搜
	heap.push({ dist[S],{0,S} });//<估价值,<真实值,编号> >
	int cnt = 0;//终点被遍历了几次

	if (dist[S] == 0x3f3f3f3f) return -1;//终点到起点的距离为INF,那么就证明无解,返回-1

	// 谁的d[u]+f[u]更小 谁先出队列:估价函数的作用
	while (heap.size())
	{
		auto t = heap.top();
		heap.pop();

		int ver = t.y.y, distance = t.y.x;//编号---从起点到该点的真实距离
		if (ver == T) cnt++;//遍历一遍终点,cnt++,直到第K次
		if (cnt == K) return distance;//这个点是终点,并且是第k短路,直接返回第k短路的真实距离distance

		// 正向扩展所有的边
		// 用 起点到该点的真实距离+ 该点到终点的估价距离来  作为标准
		// distance + w[i] + dist[j]
		for (int i = h[ver]; i != -1; i = ne[i])
		{
			int j = e[i];
			heap.push({ distance + w[i] + dist[j],{distance + w[i], j} });
		}
	}
	return -1;
}

int main()
{
	cin >> n >> m;
	//初始化表头
	memset(h, -1, sizeof h);
	memset(rh, -1, sizeof rh);
	for (int i = 0; i < m; i++)
	{
		int a, b, c;
		cin >> a >> b >> c;
		add(h, a, b, c);
		add(rh, b, a, c);
	}
	cin >> S >> T >> K;
	if (S == T) K++;//特判:如果起点和终点一样,那么就将第K小的变为第K+1小的:目的是跳过该情况

	dijkstra();//堆优化版迪杰特拉预处理-->>该点到终点的距离-->>估价函数(dist距离)
	cout << astar() << endl;//ans

	return 0;
}

2)八数码

 这个我的注释绝对已经可以无障碍看懂了

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

#define x first
#define y second

using namespace std;

typedef pair<int,string> PIS;

int f(string m)//估计函数
{
    int dt=0;//计算该string的方案得到的距离
    for(int i=0;i<9;i++)//这里1~8对应的下标为0~7
    {
        if(m[i]!='x')//跳过'x'
        {
            int t=m[i]-'1';//对应下标
            //行:i/3   列:i%3   
            dt=dt+abs(i/3-t/3)+abs(i%3-t%3);//曼哈顿距离
        }
    }
    
    return dt;//返回总曼哈顿距离
}

string bfs(string start)
{
    string end="12345678x";//终点

    unordered_map<string,int> d;//存储距离
    priority_queue<PIS, vector<PIS>, greater<PIS>> heap;//小根堆,将元素的估计终点距离从小到大排序
    unordered_map<string,pair<string,char>> last;//存储一个元素由哪种状态,经过哪种操作得来,跟前面几题一样

    heap.push({f(start),start});//<估价距离,对应的string> --->>加入起点
    d[start]=0;//起点到起点的距离为0
    //要将操作数组与坐标变化数组一一对应
    char oper[]="udlr";//操作ans字符数组
    int dx[4]={-1,1,0,0};
    int dy[4]={0,0,-1,1};//方位实际数组

    while(heap.size())
    {
        auto t=heap.top();//队头//取出编号idx->>string
        heap.pop();//弹出
        string state=t.y;//记录

        if(t.y==end) break;如果该编号已经是end状态的编号-->>达到理想情况-->>break跳出

        int x,y;//找到'x'的坐标
        for(int i=0;i<9;i++)
        if(state[i]=='x')
        {
            x=i/3,y=i%3;
            break;
        }

        string init=state;//存储代替
        for(int i=0;i<4;i++)//以'x'的坐标(x,y)为源点 进行bfs
        {
            int a=x+dx[i],b=y+dy[i];
            if(a<0||a>=3||b<0||b>=3) continue;//越界就跳过
            swap(state[a*3+b],state[x*3+y]);//扩展出来的(a,b) 与 扩展之前的(x,y)进行交换
            if(!d.count(state)||d[state]>d[init]+1)//如果没有被记录或者小于记录值
            {
                d[state]=d[init]+1;//更新距离
                heap.push({f(state)+d[state],state});//加入堆中
                last[state]={init,oper[i]};//标记由哪种状态转移而来,并且记录执行的操作
            }
            state=init;//因为要扩展到四个方向,所以要还原
        }
    }

    string ans;
    //跟前面几题原来相同
    while(end!=start)
    {
        ans+=last[end].y;
        end=last[end].x;
    }
    reverse(ans.begin(),ans.end());//将其反转
    return ans;
}

int main()
{
    string start,x,c;
    while(cin>>c)//这样输入可以忽视空格
    {
        start+=c;//start保存 有x的串
        if(c!="x") x+=c;//x保存 没有x的串
    }

    int res=0;//统计逆序对的数量
    for(int i=0;i<8;i++)
     for(int j=i+1;j<8;j++)
      if(x[i]>x[j]) 
       res++;
    
    //末状态为:1 2 3 4 5 6 7 8 x 逆序对的数量为 0 
	//行的交换不会改变逆序对的数量
	//列的交换会改变+2 或 -2 的逆序对的数量变化
	//可以手动模拟:那么即可知道(以下的结论)
    if(res%2) printf("unsolvable\n");//如果逆序对为奇数,就不可能抵达终点
    else cout<<bfs(start)<<endl;//输出答案

    return 0;
}


(二)DFS之搜索顺序

前两题是马走日 和 单词接龙  这里放个链接就不写了

[AcWing算法刷题]之DFS+BFS迷宫模板(简单)_lihua777的博客-CSDN博客

 3) 分成互质组

 

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

const int N = 10;
int a[N];
vector<int> g[N];
int n;
int ans = 10;
int len;

int gcd(int a, int b)
{
	return b ? gcd(b, a % b) : a;
}

bool check(int c, int x)
{
	for (int i = 0; i < g[c].size(); i++)
	{
		if (gcd(g[c][i], x) > 1) return false;
	}
	return true;
}

void dfs(int u)
{
	if (u == n)
	{
		ans = min(ans, len);
		return;
	}

	//每个元素的方法即——放到当前已经存在的组中  或者  放到新开的组中
	for (int i = 0; i < len; i++)
	{
		if (check(i, a[u]))
		{
			g[i].push_back(a[u]);
			dfs(u + 1);
			g[i].pop_back();
		}
	}

	//可见这里的len代表着的是当前开辟数组的个数
	g[len++].push_back(a[u]);
	dfs(u + 1);
	g[--len].pop_back();
}

int main()
{
	cin >> n;
	for (int i = 0; i < n; i++) cin >> a[i];

	dfs(0);
	cout << ans << endl;

	return 0;
}

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值