题解 P5507 机关

传送门

毕设老师让我做 MAPF,由于学了好多 A* 算法的变形,就过来做一做了。

这题用的是 EPEA*(Enhanced Partial Expansion A*)算法


【分析】

显然搜索能过,但是状态空间太大了。一眼可 A*。

考虑所有机关的状态为 \(s_i\) 时:

若没有连锁反应,最少步数为 \(\displaystyle h_1(\boldsymbol s)=\sum_i((5-s_i)\bmod 4)\)

现在由于有连锁反应,我们设计估价函数的时候需要保证估价函数的结果不大于真实步数;

为此,我们考虑最优情况下,每次转动一个机关会导致另一个机关跟着转动;也就是说,这种情况下等价于没有连锁反应的情况下转两次。

因此估价函数可以设计为 \(\displaystyle h_2(\boldsymbol s)={1\over 2}h_1(\boldsymbol s)\)

当然,我们知道,估价函数在不超过真实值的情况下,估价函数的值越大,搜索效率越高。因此,对于奇数的 \(h_1(\boldsymbol s)\) ,我们直接取上取整,因此设计出估价函数:

\(\displaystyle h_2(\boldsymbol s)=\lceil{1\over 2}h_1(\boldsymbol s)\rceil\)


我们考虑一下一般的 A* 算法,在状态 \(S\) 下,通过维护当前耗散 \(g_S\) 和对于后续操作的估计耗散 \(h_S\) ,共同组合成总期望耗散 \(f_S=g_S+h_S\)

通过对待扩展状态的总期望耗散进行从小到大排序,优先拓展总期望耗散较小的状态。

EPEA* 则是在此基础上动态调整自身的后续估价耗散,从而达到动态调整总期望耗散的结果。

在拓展一个状态 \(S\) 时,EPEA* 对于其所有后继状态 \(\{T_i\}\) ,EPEA* 只拓展满足条件 \(f_S=f_{T_i}\) 的后继状态。

由于 A* 中,估价函数的结果是不超过真实值的,因此不可能存在 \(f_S>f_{T_i}\) 的情况。

而对于其他总期望耗散更大的后继状态,可能也能产生对答案的贡献。为此,我们修正 \(h_S\) 使得 \(f_S\) 更新为未选择的后继状态中,\(f_{T_i}\) 最小的结果;之后,将修正后的当前节点重新扔进待拓展的队列中。


如对于搜索到的某个状态 \(S\) ,其当前耗散 \(g_S=5\) 而根据估价函数产生了初始的后续估价耗散 \(h_S=4\) ,于是 \(f_S=5+4=9\)

假设它存在 \(3\) 个后续状态 \(T_1, T_2, T_3\) ,他们根据估价函数产生后续估价耗散 \(h_{T_1}=3, h_{T_2}=4, h_{T_3}=5\) ;那么,由于他们的当前耗散均为 \(6\) ,因此有 \(f_{T_1}=9, f_{T_2}=10, f_{T_3}=11\)

这一次的拓展中,我们仅拓展了 \(T_1\) 状态。而对于没拓展的后继状态中,最小的结果为 \(f_{T_2}=10\) ;因此在拓展后,我们修正 \(h_S=5\) 使得 \(f_S=10\)

当然,在具体的实现中,我们不必修正 \(h_S\) 的结果,直接修改 \(f_S\) 的结果也是等价的。


可以发现,EPEA* 适用于遍历后继状态较快,且评估后继状态的估价函数较快的问题。

对于这题而言,由于所有状态的后继状态都最多为 \(12\) 种,而估价函数对于每个状态可以在 \(O(1)\) 的时间内算出结果;恰好适用于这题。

最后,关于答案的维护,我们直接维护 \(fr_S, pace_S\) 数组分别表示状态 \(S\) 是从状态 \(fr_S\) 沿边 \(pace_S\) 转移而来的。

当然,为了避免搜索重复状态,可以再维护数组 \(vis_S\) 表示是否已经访问过了状态 \(S\)

因为 EPEA* 算法中保证了对每个状态,优先拓展总期望耗散和自身一致的后继。故当一个后继状态一旦被拓展到,后续的拓展方案中,必然不存在总期望耗散更优的拓展方案,可以直接记录 \(fr_S,pace_S,vis_S\) 结果。

论文里描述该算法在 MAPF 中对于状态空间的数量产生了 dramatically reduce 。但在算法竞赛题中,实测好像没那么强,甚至会更慢。

就当作作为一个拓展吧。


【代码】

为了实现方便,代码中将机关的状态 \(1,2,3,4\) 分别映射成了 \(0,1,2,3\) ,将机关 \(1\)~\(12\) 分别映射成了 \(0\)~\(11\)

#include <bits/stdc++.h>
using namespace std;
int a[12][4];
inline int h(int sta) {
	//Heuristic Function
	/*
	  12 * 2 = 24
	  24 / 4 = 6
	*/
	int s1 = (0x444444 - (sta & 0x333333)) & 0x333333;
	int s2 = (0x1111110 - (sta & 0xcccccc)) & 0xcccccc;
	int tmp = __builtin_popcount((s1 | s2) & 0x555555);
	int pace = __builtin_popcount(s1 | s2) * 2 - tmp;
	return pace + 1 >> 1;
}
inline int move(int sta, int id) {
	//Move the id-th mechanism while the state is sta.
	int single_sta = ((sta >> id + id)&3);
	int chain = a[id][single_sta];
	int chain_sta = ((sta >> chain + chain)&3);
	
	single_sta = (single_sta + 1)&3;
	chain_sta = (chain_sta + 1)&3;
	int msk = ((3 << id + id) | (3 << chain + chain));
	return sta&(~msk) | (single_sta << id + id) | (chain_sta << chain + chain);
}

struct node {
	int sta;
	int gv, hv, fv;
	inline node(int sta_=0, int gv_=0): sta(sta_), gv(gv_) {
		hv=h(sta_);
		fv=gv+hv;
	}
	inline friend bool operator < (const node &a, const node &b) {
		return a.fv > b.fv;
	}
};
priority_queue<node> pq;
int fr[1<<24], pace[1<<24];
bool vis[1<<24];
inline void EPEA_star(int sta) {
	while(!pq.empty()) pq.pop();
	pq.emplace(sta, 0);
	fr[sta]=-1;
	pace[sta]=-1;
	vis[sta]=1;
	
	while(!pq.empty()) {
		node now = pq.top(); pq.pop();
		if(now.sta == 0)//target state
			break;
		
		int nxtf=-1;
		for(int i=0; i<12; ++i) {
			int nxtsta = move(now.sta, i);
			if(vis[nxtsta])
				continue;
			int f = now.gv + 1 + h(nxtsta);
			if(f < now.fv)//expanded
				continue;
			else if(f == now.fv) {//expand
				pq.emplace(nxtsta, now.gv+1);
				fr[nxtsta] = now.sta;
				pace[nxtsta] = i;
				vis[nxtsta] = 1;
			}
			else if(nxtf == -1)//update the next f-value of this state
				nxtf = f;
			else
				nxtf = min(nxtf, f);
		}
		if(nxtf == -1)//expanded all the successors
			continue;
		now.fv = nxtf;
		pq.push(now);
	}
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int ini_sta=0;
	for(int i=0; i<12; ++i) {
		int s;
		cin>>s;
		ini_sta |= (s-1<<i+i);
		for(int j=0; j<4; ++j) {
			cin>>a[i][j];
			--a[i][j];
		}
	}
	EPEA_star(ini_sta);
	
	vector<int> v;
	for(int sta=0; ~fr[sta]; sta=fr[sta])
		v.push_back(pace[sta]);
	reverse(v.begin(), v.end());
	cout<<v.size()<<"\n";
	for(auto e : v) cout<<e+1<<" ";
	return 0;
}
posted @ 2023-02-01 13:07  JustinRochester  阅读(60)  评论(0编辑  收藏  举报