Thomasguo666
2019-03-06 21:06:13
前置芝士:堆优化Dijkstra/优先队列bfs(其实本质上相同)
A*算法是一种常见的搜索算法,可以用于在搜索中更快找到并判定最优解。
最短路问题,大家应该都会解吧。堆优化dij其实就是优先队列bfs。
但是,优先队列bfs的策略有一个缺点:当前代价最小的状态,接下来可能有很大的代价。这就导致了最优解可能反而出现的比较晚。
于是我们很自然地想到一个对策:定义一个估价函数
而这个估价函数
为什么呢?我们举个例子看看:(
显然,最短路应该是走最左边这条,代价为
而如果保证
综上所述,
而且我们可以想到,f(x)越接近g(x),就能越快找到最优解。
这种带估值函数的优先队列bfs,就是A*。
接下来我们通过几个例子,讲一讲A*估值函数的设计。
先看一道例题:
集合位置
题意:求
我们已经说过,每个状态第一次出队,就是初始状态到它的最小代价。事实上,每个状态第
并且每个状态的第
证明:
由于每个状态第
k 次出队,就是初始状态到它的第k 小代价(k\in N^+) ,故对于一个已出队k 次的状态i ,由于另一个最小的被出队k 次并能扩展到i 的状态j 扩展到i 的当前代价,必然比任意一出队k+1 次的状态l 扩展到i 的当前代价低(该代价必然比出队k 次的l 扩展到i 的当前代价大),又比任意一出队k-1 次的状态扩展到i 的代价要大,故当前代价就是初始状态到i 的第k+1 小代价,是由出队k 次的j 扩展得到的。综上所述,就是初始状态到每个状态的第
k 小代价,是由一个出队k-1 次的状态扩展得到的。
故控制每个节点出队不超过
一个注意点:每个节点最多扩展一次,入队一次,出队两次。
那么现在我们需要设计一个估值函数。
然后发现,直接令
#include <bits/stdc++.h>
using namespace std;
typedef double db;
db x[205],y[205],f[205];
int vis[205];
struct dat
{
int u;
db w;
bool operator < (const dat &rhs) const {
return w>rhs.w;
}
};
struct ndat //注意:不仅要存储每个节点的当前代价+f(x),还要存储该搜索路径上的每个节点的访问情况。每个节点最多访问一次!
{
int u;
db w;
int vis[205];
ndat (int u,db w):u(u),w(w) {
memset(vis,0,sizeof(vis));
}
bool operator < (const ndat &rhs) const {
return w>rhs.w;
}
};
inline double a(int i,int j)
{
return sqrt(pow(x[i]-x[j],2)+pow(y[i]-y[j],2));
}
vector<int> G[205];
void addedge(int i,int j)
{
G[i].push_back(j);
G[j].push_back(i);
}
int main()
{
int n,m,tot=0;
db ans=-1;
cin>>n>>m;
for (int i=1;i<=n;i++) cin>>x[i]>>y[i];
for (int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
addedge(u,v);
}
memset(f,127,sizeof(f));
f[n]=0;
priority_queue<dat> Q;
priority_queue<ndat> q;
Q.push((dat){n,0});
while (!Q.empty()) //先预处理处每个节点的f(x)
{
dat p=Q.top();Q.pop();
int u=p.u;
if (vis[u]) continue;
vis[u]=1;
for (int i=0;i<G[u].size();i++)
{
int v=G[u][i];
if (f[u]+a(u,v)<f[v])
{
Q.push((dat){v,f[u]+a(u,v)});
f[v]=f[u]+a(u,v);
}
}
}
q.push((ndat){1,f[1]});
while (!q.empty())
{
ndat p=q.top();q.pop();
int u=p.u;
db w=p.w-f[u];
if (u==n) ++tot;
if (tot==2)
{
ans=w;
break;
}
for (int i=0;i<G[u].size();i++)
{
int v=G[u][i];
if (p.vis[v]) continue; //如果当前搜索路径上访问过该点,则不必再次访问
ndat nv=p;
nv.u=v;
nv.w=w+a(u,v)+f[v];
nv.vis[v]=1;
q.push(nv);
}
}
(ans<0)?printf("%d\n",-1):printf("%.2f",ans);
return 0;
}
利用这个思路,我们还可以解决
对于题目中的“总能量”条件,其实和总数量(即
事实上,对于标准的
(顺便说一句,这题不知道怎么回事恶意卡A*,非要用左偏树可并堆来做。在我看来这是一种无聊而可恶的行径,没有什么教育意义。前面的都是屁话,最重要的是,不让我们多A一道题)
这道题并不需要控制每个状态访问的次数。
// luogu-judger-enable-o2
#include <bits/stdc++.h>
using namespace std;
typedef double db;
db dis[5005],f[5005];
int t[5005],vis[5005];
struct dat
{
int u;
db w;
bool operator < (const dat &rhs) const {
return w>rhs.w;
}
};
vector<dat> G[5005],g[5005];
void addedge(int i,int j,db w)
{
G[i].push_back((dat){j,w});
g[j].push_back((dat){i,w});
}
int main()
{
int n,m,ans=0;
db e;
cin>>n>>m>>e;
if (e>1000000)
{
cout<<"2002000"<<endl;
return 0;
}
for (int i=1;i<=m;i++)
{
int u,v;
db w;
cin>>u>>v>>w;
addedge(u,v,w);
}
memset(f,127,sizeof(f));
f[n]=0;
priority_queue<dat> q,Q;
Q.push((dat){n,0});
while (!Q.empty())
{
dat p=Q.top();Q.pop();
int u=p.u;
if (vis[u]) continue;
vis[u]=1;
for (int i=0;i<g[u].size();i++)
{
dat v=g[u][i];
if (f[u]+v.w<f[v.u])
{
Q.push((dat){v.u,f[u]+v.w});
f[v.u]=f[u]+v.w;
}
}
}
q.push((dat){1,f[1]});
while (!q.empty())
{
dat p=q.top();q.pop();
int u=p.u;
db w=p.w-f[u];
if (u==n)
{
e-=w;
if (e>=1e-6) ans++;
else break;
continue;
}
for (int i=0;i<G[u].size();i++)
{
dat v=G[u][i];
q.push((dat){v.u,w+v.w+f[v.u]});
}
}
cout<<ans<<endl;
return 0;
}
A*的另一个应用就是8数码问题
我们发现,无论是多么好的策略,从一个状态到目标状态的代价,都不会低于该状态中每个数不为
并且,不同于
如何判定一个状态是否扩展过呢?这里直接使用翻题解百度。
代码:
#include <bits/stdc++.h>
#define in inline
using namespace std;
// lyd /-\|<|O|
const int end=123804765;
map<int,int> vis;
int d[4]={-3,-1,1,3}; //四个方向
int pow10[]={
1,10,100,1000,10000,100000,1000000,10000000,100000000
};
in int get(int x,int p) //获取x的右数第p位
{
return int(x/pow10[p-1])%10;
}
in int isup(int x) //判断是否在边缘
{
return x<=3;
}
in int isdown(int x)
{
return x>=7;
}
in int isleft(int x)
{
return x%3==1;
}
in int isright(int x)
{
return x%3==0;
}
in int swap(int x,int a,int b)
{
int s=get(x,a),t=get(x,b);
x-=s*pow10[a-1]+t*pow10[b-1];
x+=s*pow10[b-1]+t*pow10[a-1];
return x;
}
in int row(int x)
{
return (x-1)/3;
}
in int col(int x)
{
return (x-1)%3;
}
in int f(int state) //估值函数
{
int s[20],t[20];
memset(s,0,sizeof(s)),memset(t,0,sizeof(t));
int ans=0;
for (int i=1;i<=9;i++) s[get(state,i)]=i,t[get(end,i)]=i;
for (int i=1;i<=8;i++) ans+=abs(row(s[i])-row(t[i]))+abs(col(s[i])-col(t[i]));
return ans;
}
struct data
{
int s,w;
data () {}
data (int s,int w):s(s),w(w+f(s)) {}
bool operator < (const data &rhs) const
{
return w>rhs.w;
}
};
priority_queue<data> q;
int main()
{
int s;
cin>>s;
q.push(data(s,0));
vis.clear();
while (!q.empty())
{
int p;
data u=q.top();q.pop();
if (vis[u.s]) continue;
if (u.s==end)
{
cout<<u.w<<endl;
return 0;
}
vis[u.s]=1;
for (int i=1;i<=9;i++)
{
int x=get(u.s,i);
if (!x)
{
p=i;
break;
}
}
for (int i=0;i<4;i++)
{
if (i==0 && isup(p)) continue; //判断位置是否合法
if (i==1 && isleft(p)) continue;
if (i==2 && isright(p)) continue;
if (i==3 && isdown(p)) continue;
int pp=p+d[i];
int t=swap(u.s,p,pp);
q.push(data(t,u.w-f(u.s)+1));
}
}
}
A*算法是启发式搜索的一种。事实上除了A*算法外还有IDA*(迭代加深启发式搜索)。在考场上,如果有扎实的搜索功底,是可以拿到很多分的。(毕竟,像最短路算法,dp等,都和搜索有关系。)