题解 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;
}