POJ 2449 Remmarguts' Date [A*算法 K短路]

题目链接: POJ 2449 Remmarguts’ Date
题目大意:

给一个图,求第k短的路。

输入格式:

第一行两个数 N(1<= N <= 1000), M(0 <= M <= 100000)。其中N表示顶点数, M表示边数。
接下来M行,每行三个数,a, b, t, 表示从a到b的边,权为k。
最后一行三个数, S, T, K, 表示让你求从S到T的第K短路径的长度

输出格式:

一个数, 第K短路径的长度

样例输入:

2 2
1 2 5
2 1 4
1 2 2

样例输出:

14

题解:

借着这题学一下A*算法。之前做过一题,求次短路(POJ 3255-Roadblocks [次短路 Dijkstra]),当时得知了A*可以求解K短路,不过没有深究。现在再返回来研究一下K短路的问题。

看了好多别人的总结,找到一系列非常棒的文章《关于寻路算法的一些思考》这是我看到的讲的最清晰的文章了。
这里还有一个A*算法可视化的Demo,也是在上面的文章里找到的。

先来看下Wikipedia对A*算法的定义:

A*算法

A*搜索算法,俗称A星算法。这是一种在图形平面上,有多个节点的路径,求出最低通过成本的算法。常用于游戏中的NPC的移动计算,或在线游戏的BOT的移动计算上。
该算法综合了Best-First Search和Dijkstra算法的优点:在进行启发式搜索提高算法效率的同时,可以保证找到一条最优路径(基于评估函数)。
在此算法中,如果以 g(n)表示从起点到任意顶点n的实际距离,h(n)表示任意顶点n到目标顶点的估算距离(根据所采用的评估函数的不同而变化),那么 A*算法的估算函数为:
f(n)=g(n)+h(n)
这个公式遵循以下特性:
如果g(n)为0,即只计算任意顶点n到目标的评估函数h(n),而不计算起点到顶点n的距离,则算法转化为使用贪心策略的Best-First Search,速度最快,但可能得不出最优解;
如果h(n)不为0,则一定可以求出最优解,而且h(n)越小,需要计算的节点越多,算法效率越低,常见的评估函数有——欧几里得距离、曼哈顿距离、切比雪夫距离;
如果h(n)为0,即只需求出起点到任意顶点n的最短路径g(n),而不计算任何评估函数h(n),则转化为单源最短路径问题,即Dijkstra算法,此时需要计算最多的定点;

定义看的不是很理解,可以看下《关于寻路算法的一些思考》,比很多讲得一知半解的博客强得多。


伪代码:

function A*(start,goal)
     closedset := the empty set                 //已经被估算的节点集合    
     openset := set containing the initial node //将要被估算的节点集合
     came_from := empty map
     g_score[start] := 0                        //g(n)
     h_score[start] := heuristic_estimate_of_distance(start, goal)    //h(n)
     f_score[start] := h_score[start]            //f(n)=h(n)+g(n),由于g(n)=0,所以……
     while openset is not empty                 //当将被估算的节点存在时,执行
         x := the node in openset having the lowest f_score[] value   //取x为将被估算的节点中f(x)最小的
         if x = goal            //若x为终点,执行
             return reconstruct_path(came_from,goal)   //返回到x的最佳路径
         remove x from openset      //将x节点从将被估算的节点中删除
         add x to closedset      //将x节点插入已经被估算的节点
         foreach y in neighbor_nodes(x)  //对于节点x附近的任意节点y,执行
             if y in closedset           //若y已被估值,跳过
                 continue
             tentative_g_score := g_score[x] + dist_between(x,y)    //从起点到节点y的距离

             if y not in openset          //若y不是将被估算的节点
                 add y to openset         //将y插入将被估算的节点中
                 tentative_is_better := true     
             elseif tentative_g_score < g_score[y]         //如果y的估值小于y的实际距离
                 tentative_is_better := true         //暂时判断为更好
             else
                 tentative_is_better := false           //否则判断为更差
             if tentative_is_better = true            //如果判断为更好
                 came_from[y] := x                  //将y设为x的子节点
                 g_score[y] := tentative_g_score
                 h_score[y] := heuristic_estimate_of_distance(y, goal)
                 f_score[y] := g_score[y] + h_score[y]
     return failure

 function reconstruct_path(came_from,current_node)
     if came_from[current_node] is set
         p = reconstruct_path(came_from,came_from[current_node])
         return (p + current_node)
     else
         return current_node 




K短路

A*算法可以找到从原点S到终点T的一条最短路,然后结束算法。但A*算法同样也可以用来寻找K短路。
如果我们不设置closeset,A*算法的搜索空间里会有很多冗余的重复状态,但我们每次都只搜索距离终点最近的节点,所以如果终点可达,最终我们第一次搜索到终点T时,一定就是找到了一条最短路。当我们下一次又搜索到终点T时,一定有是当前的最优的路径,所以就是次短路……这样我们就可以得到我们要求的K短路了。
这题的难点也就是评估函数h(n)的设置。当评估函数h(n)恰好等于实际距离h*(n)时,A*算法是非常高效的。我们可以在给定图G的反向图G’中从T点执行一次Dijkstra,求得G’中T到所有其他节点的最短距离,就是图G中所有其他节点到T的最短距离,这恰好就是我们需要的h*(n)。
g(n)是从S到点n的最短距离,可以在A*算法的执行过程中逐步求得。
然后,我们就可以使用 f(n)=g(n)+h(n) 来给openset设置优先级,每次取出权最小的节点进行搜索,直到搜索到T点K次,算法结束。
如果T点是从S点不可达的,则不加限制的话算法将永远无法结束。我们需要找K短路,那么每个节点都不可能出现大于K次。所以当某个节点出现了大于K次时,就不要再该节点上继续扩展搜索空间了。

代码:

#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
#include <queue>
#define MAXN 10000
#define INF 0x3f3f3f3f
using namespace std;

typedef pair<int, int> P;
int N, M, S, T, K;
int dist[MAXN];
int tdist[MAXN];
int cnt[MAXN];
bool f[MAXN];
vector<P> Adj[MAXN];
vector<P> Rev[MAXN];
struct Edge {
    int to, len;
    Edge(){}
    Edge(int t, int l):to(t), len(l){}
};
priority_queue<Edge> q;

bool operator<(const Edge &a, const Edge &b) {
    return (a.len + dist[a.to]) > (b.len + dist[b.to]);
}

void dijkstra() {
    memset(dist, 0, sizeof(dist));
    fill(tdist, tdist+MAXN, INF);
    tdist[T] = 0;
    while(!q.empty()) q.pop();
    q.push(Edge(T, 0));
    while (!q.empty()) {
        int x = q.top().to;
        int d = q.top().len;
        q.pop();
        if (tdist[x] < d) continue;
        for (int i = 0; i < Rev[x].size(); i++) {
            int y = Rev[x][i].first;
            int len = Rev[x][i].second;
            if (d+ len < tdist[y]) {
                tdist[y] = d + len;
                q.push(Edge(y, tdist[y]));
            }
        }
    }
    for (int i = 1; i <= N; i++){
        dist[i] = tdist[i];
    }
}


int aStar() {
    if (dist[S] == INF) return -1;
    while (!q.empty()) q.pop();
    q.push(Edge(S, 0));
    memset(cnt, 0, sizeof(cnt));
    while (!q.empty()) {
        int x = q.top().to;
        int d = q.top().len;
        q.pop();
        cnt[x]++;
        if (cnt[T] == K) return d;
        if (cnt[x] > K) continue;
        for (int i = 0; i < Adj[x].size(); i++) {
            int y = Adj[x][i].first;
            int len = Adj[x][i].second;
            q.push(Edge(y, d+len));
        }
    }

    return -1;
}

int main() {
    std::ios::sync_with_stdio(false);
    int a, b, t;
    cin >> N >> M;
    for (int i = 0; i < M; i++) {
        cin >> a >> b >> t;
        Adj[a].push_back(make_pair(b, t));
        Rev[b].push_back(make_pair(a, t));
    }
    cin >> S >> T >> K;
    //本题需要特别注意的地方,S==K时,也得走出去转一圈。。。- -!
    if (S == T) K++; 

    dijkstra();
    cout << aStar() << endl;
    return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这是一道比较经典的计数问题。题目描述如下: 给定一个 $n \times n$ 的网格图,其中一些格子被标记为障碍。一个连通块是指一些被标记为障碍的格子的集合,满足这些格子在网格图中连通。一个格子是连通的当且仅当它与另一个被标记为障碍的格子在网格图中有公共边。 现在,你需要计算在这个网格图中,有多少个不同的连通块,满足这个连通块的大小(即包含的格子数)恰好为 $k$。 这是一道比较经典的计数问题,一般可以通过计算生成函数的方法来解决。具体来说,我们可以定义一个生成函数 $F(x)$,其中 $[x^k]F(x)$ 表示大小为 $k$ 的连通块的个数。那么,我们可以考虑如何计算这个生成函数。 对于一个大小为 $k$ 的连通块,我们可以考虑它的形状。具体来说,我们可以考虑以该连通块的最左边、最上边的格子为起点,从上到下、从左到右遍历该连通块,把每个格子在该连通块中的相对位置记录下来。由于该连通块的大小为 $k$,因此这些相对位置一定是 $(x,y) \in [0,n-1]^2$ 中的 $k$ 个不同点。 现在,我们需要考虑如何计算这些点对应的连通块是否合法。具体来说,我们可以考虑从左到右、从上到下依次处理这些点,对于每个点 $(x,y)$,我们需要考虑它是否能够与左边的点和上边的点连通。具体来说,如果 $(x-1,y)$ 和 $(x,y)$ 都在该连通块中且它们在网格图中有公共边,那么它们就是连通的;同样,如果 $(x,y-1)$ 和 $(x,y)$ 都在该连通块中且它们在网格图中有公共边,那么它们也是连通的。如果 $(x,y)$ 与左边和上边的点都不连通,那么说明这个点不属于该连通块。 考虑到每个点最多只有两个方向需要检查,因此时间复杂度为 $O(n^2 k)$。不过,我们可以使用类似于矩阵乘法的思想,将这个过程优化到 $O(k^3)$ 的时间复杂度。 具体来说,我们可以设 $f_{i,j,k}$ 表示状态 $(i,j)$ 所代表的点在连通块中,且连通块的大小为 $k$ 的方案数。显然,对于一个合法的 $(i,j,k)$,我们可以考虑 $(i-1,j,k-1)$ 和 $(i,j-1,k-1)$ 这两个状态,然后把点 $(i,j)$ 加入到它们所代表的连通块中。因此,我们可以设计一个 $O(k^3)$ 的 DP 状态转移,计算 $f_{i,j,k}$。 具体来说,我们可以考虑枚举连通块所包含的最右边和最下边的格子的坐标 $(x,y)$,然后计算 $f_{x,y,k}$。对于一个合法的 $(x,y,k)$,我们可以考虑将 $(x,y)$ 所代表的点加入到 $(x-1,y,k-1)$ 和 $(x,y-1,k-1)$ 所代表的连通块中。不过,这里需要注意一个细节:如果 $(x-1,y)$ 和 $(x,y)$ 在网格图中没有相邻边,那么它们不能算作连通的。因此,我们需要特判这个情况。 最终,$f_{n,n,k}$ 就是大小为 $k$ 的连通块的个数,时间复杂度为 $O(n^2 k + k^3)$。 参考代码:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值