9.13算法

棋盘3956(有权图的bfs,优先队列)(问题的抽象与转化,状态转移)

Q0:怎么处理颜色?怎么储存结点?

Q1:怎么理解魔法?

A:魔法只能用在无色格子,而且不能连续使用,也就是说走的路不能连续两格都是无色的,在用了一次魔法到中间格子后,这个中间格子必须连接着有色格子

Q2:每次可以怎么走?状态转移方式?

如果走到无色结点,就要2个代价,走到异色结点需要1个代价,同色结点无代价

Q3:怎么求最值?

有权图与无权的bfs

EX:现在从优先队列的头部取出一号结点,那么从一号结点访问到2,3号结点的代价一定是最小的

优先队列,这个思路是先从头部取出结点,在这个结点的基础上,往队列中加入 这个最小代价结点能到达的所有结点,加入后就已经自动排了个序;

即可以确定在访问完这个最小代价结点后,接下来哪个结点是最小代价的,然后去访问那个结点,并加入那个结点可以到达的所有结点

这里需要注意的是,在最小代价上的结点所能访问到的后继直接结点不一定就是最小代价,但需要把它们加入到队列当中,然后全部加入完成之后,在现在所有可以访问的结点当中选择一个访问代价最小的结点,在这个结点的基础上继续访问,一次往后,如果说这个最小代价结点之前没有访问过,那么现在这个代价就是最小的代价,之后再遇到就一定不是。

对于之前已经加入的结点,但不是之前的最小代价,比如第一个结点就可以访问到,但因为权值太大在优先队列的后面,一直没有访问到,那么就会一直不被访问,直到说有通过其他结点可以有更小的代价访问到它,或者其他结点都访问的差不多了,现在它就是最小代价了,才会访问到;

前者的情况是在原先可以访问到的基础上又在其前面插入了一个更小代价的访问方式(优先队列的性质),后者也是插入(或者就没有了,尾结点),只不过插入到了后面

即,优先队列存储的是当前已有的路径及长度,但最终并不一定要选择这个路径,每次只是连接,架起现在队列中的最短路径,并在这条最短路径的基础上,再次寻找后续的最短路径

只有在最短路径的基础上访问到的结点,才会更新结点存储的值(即最小代价),优先队列中存储的并不都是结点存储的代价

故而,在优先队列中会有重复结点(是不同路径到达的方式),如果之前已经访问过这个结点了,那就是已经存储了一个最小代价,再次访问到就要con掉。即只有第一次访问到结点才赋值,其他都直接con掉

?担心的是,之前访问时的结点到一个目标结点,可能权值很大,后续有新结点到这个结点权值很小,那就会得到错误结果

//为了区分无色
//红0黄1,加1,则红1黄2,这样无色就是0
int dx[] = { 0,1,0,-1,1,1,-1,-1,0,2,0,-2 };//12方向及魔法代价 
int dy[] = { 1,0,-1,0,1,-1,1,-1,2,0,-2,0 };
int dw[] = { 0,0,0,0,2,2,2,2,2,2,2,2 };//前四个是基础方向,后面的是用魔法的方向,其要算上用魔法的两个代价
//需注意
struct node {
	int x, y, c, w;//c是颜色,w是代价,即从上一步到这个结点的最小代价
	bool operator<(node b)const {//作为双端队列的排列标准
		return w > b.w;
	}//优先队列默认取出最大元素,这里重载运算符,保证每次取出的是最小元素
};
//w记录的是到这个结点的最小代价,怎么保证?
priority_queue<node>q;
//每次取出代价最小的元素,然后在这个代价最小的元素基础上继续向后延申,
//那么之后延申到的元素也一定是代价最小,因为是建立在前驱代价最小的基础之上的
//如果后续还能到,那么一定不是代价最小的
//即第一次到,就是最小的,因为先到
//这里,对于普通bfs,是图没有权值的情况下,那么先到的一定是最小的代价
//因为逐层延申,每层代价都相同
//如果图有权值,那么先到的不一定是最小的代价
//因为此时先到,可能走的是权值很大的一条边
//这时,就需要建立在前置结点的权值最小的情况下
//在入队时,入的是到前置结点的代价(即前置结点所记录的代价)再加上从前置结点到下一个结点的代价
//即下一个结点记录的代价在其前置结点时就已经确定,而不是在下一个结点的回合
//
int a[105][105], dis[105][105];//a记录当前结点的颜色,dis记录到当前结点的最小代价
memset(dis, 0x3f, sizeof(dis));//0x3f为无穷大
node cur, next;
while (!q.empty()) {
	cur = q.top(); q.pop();//每次取出的是最小代价的元素
	if (dis[cur.x][cur.y] < cur.w) { continue; }//如果到当前结点的代价已经小于当前结点的代价,就跳过
	for (int i = 0; i < 12; i++) {
		next.x = cur.x + dx[i], next.y = cur.y + dy[i];
		next.w = cur.w + dw[i];//在前一个的基础上
		if () { continue; }
		next.c = a[next.x][next.y];
		if (!next.c) { continue; }//下个地板无色,由于魔法已经等价转化为状态转移方法,所以遵循一般判断规律,只要是无色就不能到
		if (cur.c != next.c) { next.w++; }//如果颜色不一致,就会使权值增加
		if (dis[next.x][next.y] > next.w) {
			dis[next.x][next.y] = next.w;
			q.push(next);
		}
	}
}
//需注意,用了魔法后总归是要往后接着走的,而往后接着走就是继续向用了魔法后的四周走
//即只要那个用魔法的格子不是终点,就一定会往后走,
//但绝对不会往回走,因为注定不是最优解
//那么就是说,用魔法,就是连走两格
//如果用了魔法但只走一格,即四个基础方向,那么魔法还没结束,因为魔法格子还没消失
//要完整用完一次魔法就要再走出那个魔法格子
// 这样就不需要再另行判断修改魔法格子的颜色,而只需要判断原本的颜色
// 那么在每个格子就有12种选择,4个基础方向是基础的,是不用魔法的,因为如果用魔法是没办法停留在四个基础格子的,也不会再回到原始格子
// 所以如果最后停在四个基础格子,那就是没用魔法直接走,需要保证格子有颜色,而且不会增加额外的支付
// 如果最后停在8个延申格子,那就是用了魔法,因为用了魔法后不能停留,总归要往后走,而往后走的所有方向就是这8个
// 所以如果最后停在这8个格子都是用了魔法,要额外支付两个费用
// 而且由于魔法不能连用,这8个格子也不能是无色的,因为要到这8个格子,就需要到之前的4个前置无色格子,
// 而如果这8个是无色格子,那就意味着连续两个无色格子,所以就无法停留
// 由此,完成了对魔法的等价转换,即拓展了每个格子的状态转移方式,而又不影响原本的判别方式
// 
//如果目标格子是无色的,那只能由其紧邻的两个格子得到,而且这两个格子必须有色,不能无色,
// 而且需要用魔法
//

struct node {
	int x, y, c, w;
	bool operator <(node b)const {
		return w > b.w;
	}//优先队列默认为最大的在顶部
	//所以通过重载小于号,把最小的顶上去
};
priority_queue<node>q;//队列存储的是之前结点可以到达的其他结点
//其排列依据为到达那个结点的路径长度,不对
//排列依据不是路径长度,而是已有结点与路径到那个目标结点的代价
// 这个代价包括了前驱所有结点的代价以及连接其的权值
// 应当注意的是,队列头部元素的代价在访问过程中是单调不减的,即要么相等,要么逐渐增加
// 因为后续结点的代价(下一个的前缀和)=前驱结点的代价(前缀和)+权值(单项)
//不一定是最小的;如果是第一次访问到,那么是最小的,如果不是,那么一定不是最小的,就是由于这个性质
//即优先队列干的就是不断访问在前面结点的基础上,不断访问到接下来代价最小的结点,直到全部结点都被访问过
//
int dx[] = {},dy[]={},dw[]={};
int a[105][105], dis[105][105];
int n, m;
void bfs() {
	memset(dis, 0x3f, sizeof(dis)); dis[1][1] = 0;
	q.push((node) { 1, 1, a[1][1], dis[1][1] });
	node cur, next;
	while (!q.empty()) {
		cur = q.top();//得到当前队列的头结点
		q.pop();
		if (dis[cur.x][cur.y] < cur.w)continue;//这个结点之前访问过
		for (int i = 0; i < 12; i++) {
			next.x = cur.x + dx[i], next.y = cur.y + dy[i];
			next.w = cur.w + dw[i];//通过这个当前最小结点得到后续结点的代价,不一定是最小
			if(){}//保证结点合法
			next.c = a[next.x][next.y];
			if (!next.c) { continue; }
			if (cur.c != next.c) { next.w++; }
			if (dis[next.x][next.y] > next.w) {//如果大于,只能是之前还没到过,不然不可能在现在才找到更小代价的,错误,不对
				dis[next.x][next.y] = next.w;//在访问过程中,第一次添加进优先队列中的不一定是最小代价
				//可能在后续结点中又有新方式到,而且代价更小,所以就是不断更新,遇到更小的就更新,而且能保证最小的总是能在后续访问中第一个被访问到,而且此时dis存储的是它(这种路径,方式)的代价
				//这里是不断更新,而不是一锤定音,即在队列中对同一结点可能有不同的到达方式
				//其是建立在不同的前驱结点基础上的,但加入队列后,访问只会是访问到最小代价的那个
//后面再访问到同一结点,就会因为dis[cur.x][cur.y]<cur.w而被con掉(队列中的cur并不都是最小的)
				q.push(next);//只有没到过这个结点,才把这个结点
			}
		}
	}
}
int x, y, c;
for (int i = 1; i <= n; i++) {
	cin >> x >> y >> c;
	a[x][y] = c + 1;
}
bfs();
if (!a[m][m]) {
	int ans = min(dis[m][m - 1], dis[m - 1][m]) + 2;//此时只能由紧邻的两个方块通过不完全的魔法走到
	if (ans >= inf) { cout << -1; }
	else {
		cout << ans;
	}
}


#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
struct node {
	int x, y, c, w;
	bool operator<(node b)const {
		return w > b.w;
	}
	node(int a=0, int b=0, int e=0, int d=0) :x(a), y(b), c(e), w(d) {};
};//结点只是辅助,目的是要把dis数组填上
int a[105][105];
int dis[105][105];
int m, n;
priority_queue<node>q;


node cur, nxt;
int dx[] = { 0,1,0,-1,1,1,-1,-1,0,2,0,-2 };//12方向及魔法代价 
int dy[] = { 1,0,-1,0,1,-1,1,-1,2,0,-2,0 };
int dw[] = { 0,0,0,0,2,2,2,2,2,2,2,2 };
void bfs() {
	memset(dis, 0x3f, sizeof(dis));
	dis[1][1] = 0;//在初始化之后,而不应该是在之前,初始就在第一个结点,代价为0
	q.push(node(1,1,a[1][1],dis[1][1]));

	while (!q.empty()) {
		cur = q.top();//得到当前的最小结点(建立在前驱的基础上)
		q.pop();
		if (dis[cur.x][cur.y] < cur.w) { continue; }//这个结点之前已经访问过了
		for (int i = 0; i < 12; i++) {
			nxt.x = cur.x + dx[i], nxt.y = cur.y + dy[i];
			if (nxt.x >= 1 && nxt.x <= m && nxt.y >= 1 && nxt.y <= m) {
				nxt.c = a[nxt.x][nxt.y];
				nxt.w = cur.w + dw[i];//在前驱地块的基础上,加上这个选择的代价(边的权值)
				if (!nxt.c) { continue; }//下个地块是无色,是不可达的
				if (cur.c != nxt.c) { nxt.w++; }//如果颜色不一样,代价还要再+1
				//至此,通过前驱结点访问这个结点的方式才算正式被确定,接下来就是判断入队与更新
				if (dis[nxt.x][nxt.y] > nxt.w) {//只有大于才会入队,即要么还没被访问过,是无穷大,要么是之前有,但是代价大,还在队列的后面没被访问,现在有了新的方式,就插入到它前面被优先访问,从而不选择之前的那种方式
					dis[nxt.x][nxt.y] = nxt.w;//更新新的访问方式并入队
					q.push(nxt);
				}
			}
			}
	}
}
int main() {
	cin >> m >> n;
	int col,xx,yy;
	while (n--) {
		cin >> xx >> yy >> col;
		a[xx][yy] = col + 1;
	}
	bfs();
	int ans = 0;
	if (!a[m][m]) {
		ans = min(dis[m - 1][m], dis[m][m - 1]) + 2;
		if (ans >= 0x3f) {
			cout << -1;
		}
		else {
			cout << ans;
		}
	}
	else {
		ans = dis[m][m];
		if (ans >= 0x3f) {
			cout << -1;
		}
		else {
			cout << ans;
		}
	}
	
	return 0;
}

题解最终版

0x3f表最大,最大还得是0x3f3f3f

相关推荐
XH华4 小时前
初识C语言之二维数组(下)
c语言·算法
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
不想当程序猿_4 小时前
【蓝桥杯每日一题】求和——前缀和
算法·前缀和·蓝桥杯
落魄君子4 小时前
GA-BP分类-遗传算法(Genetic Algorithm)和反向传播算法(Backpropagation)
算法·分类·数据挖掘
菜鸡中的奋斗鸡→挣扎鸡4 小时前
滑动窗口 + 算法复习
数据结构·算法
Lenyiin5 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
郭wes代码5 小时前
Cmd命令大全(万字详细版)
python·算法·小程序
scan7245 小时前
LILAC采样算法
人工智能·算法·机器学习
菌菌的快乐生活5 小时前
理解支持向量机
算法·机器学习·支持向量机
大山同学5 小时前
第三章线性判别函数(二)
线性代数·算法·机器学习