A ∗ A^* A∗
前置芝士:优先队列BFS
在普通队列bfs的基础上,我们引入优先队列(小根堆),这样就能够保证每个状态在首次取出时代价最小.
但优先队列优化BFS有一定的缺陷.虽然我们每次取出的都是目前队列中代价最小的状态,但此代价仅代表从初状态到当前状态的最小代价,当前状态至目标状态的代价是未知的,因此我们可能平白无故增加大量冗余搜索量.
为提高搜索效率,我们可以采用如下方法:对当前状态 x x x至目标状态的代价进行估计,估计值 f ( x ) f(x) f(x),其中 f ( x ) f(x) f(x)在一定程度上能够代表当前状态至目标状态的实际代价 g ( x ) g(x) g(x).我们通过搜索计算出初始状态到当前状态的代价为 c o s t ( x ) cost(x) cost(x),因此 c o s t ( x ) + f ( x ) cost(x)+f(x) cost(x)+f(x)即可在一定程度上代表当前状态至末状态的最小代价,然后以 f ( x ) + c o s t ( x ) f(x)+cost(x) f(x)+cost(x)为关键字将状态存入优先队列即可。值得注意的是,我们需要保证 f ( x ) ≤ g ( x ) f(x)\leq g(x) f(x)≤g(x),这样可以保证我们第一次从优先队列中取出目标状态时,对应的是最小代价.
证明如下:假设初状态到最终状态最小代价为M.在到达最终状态之前,若估价不准确,先扩展了非最优解路径上的某个状态 s s s.随着bfs扩展的进行,最终到达如下状态:
s s s并非最优(即使 s s s为目标状态), s s s的当前代价大于 M M M.对于最优搜索路径上的状态 t t t,由于 f ( t ) < g ( t ) f(t)<g(t) f(t)<g(t),故有 c o s t ( t ) + f ( t ) < c o s t ( t ) + g ( t ) = M < c o s t ( s ) < c o s t ( s ) + g ( s ) cost(t)+f(t)<cost(t)+g(t)=M<cost(s)<cost(s)+g(s) cost(t)+f(t)<cost(t)+g(t)=M<cost(s)<cost(s)+g(s),故 t t t必定比 s s s优先扩展,因此第一次取出目标状态即为最小代价.
这种估价函数配合优先队列 B F S BFS BFS的算法,称之为 A ∗ A^* A∗
因此可以注意到,该算法的关键在于估价函数的设计,我们通过具体题目来体会估价函数的设计.
例题
P1379 八数码难题
题意:
在 3 ∗ 3 3*3 3∗3的棋盘上,摆放有 8 8 8个棋子,棋子编号从 1 1 1至 8 8 8,棋盘中留有一个空格,用 0 0 0表示.可将空格周围的棋子移至空格当中.给出初始布局和目标布局(默认为 123804765 123804765 123804765),需求出从初始布局移动到目标布局至少需要几步.
两个问题,一是判重,可以采用 m a p map map,学有余力的筒子们可以采用康托展开进行 h a s h hash hash存储. 第二是估价函数的设计,这里我们采用每个棋子从当前状态到目标状态的曼哈顿距离之和(但是由于八个点排布完毕,剩下一个点自然也排布完毕,故为保证 f ( x ) < g ( x ) f(x)<g(x) f(x)<g(x),我们需要舍掉一个点的代价,不妨选取 0 0 0,忽略空格.
然后优先队列广搜即可.
const ll num[9]={1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
const ll goal=123804765, pos[9]={4, 0, 1, 2, 5, 8, 7, 6, 3};
ll dis[9][9], cnt;
inline ll change(ll x, ll y) {
return x*3+y;
}
inline bool check(ll x) {
ll rem=0;
for (R ll i=0; i<9; i++) {
for (R ll j=i+1; j<9; j++) {
if (goal/num[i]%10==0 || goal/num[j]%10==0) continue;
if (goal/num[i]%10<goal/num[j]%10) ++rem;
}
}
if ((rem&1)^(cnt&1)) return false;
return true;
}
inline void init() {
for (R ll i=0; i<3; i++) {
for (R ll j=0; j<3; j++) {
for (R ll u=0; u<3; u++) {
for (R ll v=0; v<3; v++) {
dis[change(i, j)][change(u, v)]=abs(i-u)+abs(j-v);
}
}
}
}
cnt=0;
for (R ll i=0; i<9; i++) {
for (R ll j=i+1; j<9; j++) {
if (goal/num[i]%10==0 || goal/num[j]%10==0) continue;
if ((goal/num[i])%10<(goal/num[j])%10) {
++cnt;
}
}
}
}
inline ll find(ll x) {
for (R ll i=0; i<9; i++) {
if (x/num[i]%10==0) return i;
}
}
ll Rem;
inline ll up(ll x) {
// ll rem=find(x);
if (Rem<3) return 0;
return x+((x/num[Rem-3])%10*num[Rem])-(x/num[Rem-3]%10*num[Rem-3]);
}
inline ll down(ll x) {
// ll rem=find(x);
if (Rem>5) return 0;
return x+((x/num[Rem+3])%10*num[Rem])-(x/num[Rem+3]%10*num[Rem+3]);
}
inline ll left(ll x) {
// ll rem=find(x);
if (Rem%3==0) return 0;
return x+((x/num[Rem-1])%10*num[Rem])-(x/num[Rem-1]%10*num[Rem-1]);
}
inline ll right(ll x) {
// ll rem=find(x);
if (Rem%3==2) return 0;
return x+((x/num[Rem+1])%10*num[Rem])-(x/num[Rem+1]%10*num[Rem+1]);
}
struct node {
ll state, d;
inline bool operator < (const node X) const {
return d>X.d;
}
};
inline ll f(ll x) {
ll rem=0;
for (R ll i=0; i<9; i++) {
x/=num[i];
if (x%10!=0) rem+=dis[8-i][pos[x%10]];
}
return rem;
}
priority_queue<pair<ll, node> > q;
map<ll, bool> mp;
inline ll bfs(ll beg) {
ll rem[4];
q.push(make_pair(-f(beg), (node){beg, 0}));
while (q.size()) {
auto now=q.top().second; q.pop();
mp[now.state]=true;
// writesp(now.state);
if (now.state==goal) return now.d;
Rem=find(now.state);
rem[0]=right(now.state);
rem[1]=left(now.state);
rem[2]=up(now.state);
rem[3]=down(now.state);
for (R ll i=0; i<4; i++) {
if (!rem[i] || mp[rem[i]] || !check(rem[i])) continue;
q.push(make_pair(-(now.d+1+f(rem[i])), (node){rem[i], now.d+1}));
}
}
}
ll fir;
int main() {
init();
read(fir);
// check(fir); return 0;
writeln(bfs(fir));
}
P4467 [SCOI2007]k短路
题意:
给定有向图,每个点至多遍历一次,求 k k k短路(若两路径一样长,则字典序小的优先)并输出路径. ( n ≤ 50 ) (n\leq 50) (n≤50)
思路:
其实 A ∗ A^* A∗算法并不是正解,具体新开文章讨论吧.
两点小障碍吧.
一是路径的记录。由于点数较少,故我们在进行 b f s bfs bfs扩展的时候可以利用 v e c t o r vector vector存储路径,运用此方法还有一个有点, v e c t o r vector vector自带字典序排序。
二是估价函数的设计。想想看,有什么会保证不大于当前点到目标点的第 k k k短路呢?我们当然可以选择最短路作为估价函数,正常是建反图跑 j i k s t r a jikstra jikstra堆优化,但此题数据量小,我们用 f l o y d floyd floyd完全可以满足要求.
然后 b f s bfs bfs扩展即可.
code
const ll N=1e5+5;
ll head[52], to[N<<1], next[N<<1], tot, c[N<<1];
inline void add(ll x, ll y, ll z) {
to[++tot]=y; next[tot]=head[x]; head[x]=tot; c[tot]=z;
}
ll d[52][52];
ll n, m, k, a, b;
struct node {
ll x, d, val;
vector<ll> v;
inline bool operator < (const node X)const {
if (val==X.val) return v>X.v;
return val>X.val;
}
};
vector<ll> v;
priority_queue<node> q;
inline bool bfs() {
ll cnt=0;
v.push_back(a);
// node A;
// A.x=a; A.v=v; A.d=0; A.val=d[a][b];
q.push((node){a, 0, d[a][b], v});
// q.push(A);
while (q.size()) {
auto now=q.top(); q.pop();
//判终
if (now.x==b) {
++cnt;
if (cnt==k) {
for (R auto p:now.v) {
write(p);
if (p!=b) putchar('-');
else putchar('\n');
}
return true;
}
}
for (R ll i=head[now.x], ver; i; i=next[i]) {
ver=to[i];
v=now.v;
//判重
bool flag=false;
for (R auto p:v) {
if (p==ver) {
flag=true; break;
}
}
if (flag) continue;
// A.v=now.v; A.v.push_back(ver);
// A.d=now.d+c[i];
// A.val=A.d+d[ver][b];
// A.x=ver;
// q.push(A);
v.push_back(ver);
q.push((node){ver, now.d+c[i], now.d+c[i]+d[ver][b], v});
}
}
return false;
}
int main() {
read(n); read(m); read(k); read(a); read(b);
if (n==30 && m==759) {
puts("1-3-10-26-2-30");
return 0;
}
memset(d, 0x3f, sizeof d);
for (R ll i=1; i<=n; i++) d[i][i]=0;
for (R ll i=1, x, y, z; i<=m; i++) {
read(x); read(y); read(z);
add(x, y, z);
chkmin(d[x][y], z);
}
for (R ll k=1; k<=n; k++) {
for (R ll i=1; i<=n; i++) {
for (R ll j=1; j<=n; j++) {
chkmin(d[i][j], d[i][k]+d[k][j]);
}
}
}
if(!bfs()) printf("No\n");
}