P4042-骑士游戏

这篇博客介绍了如何利用spfa算法优化处理魔头击杀问题,涉及spfa原理的深入理解,特别是如何通过队列保存“可能被子魔头组更新的魔头”,以高效判断和更新最短路径。文章还强调了spfa在多对一关系中的应用,并提供了详细的步骤和代码示例。
摘要由CSDN通过智能技术生成

链接:https://www.luogu.com.cn/problem/P4042

题解:(转自洛谷 寒鸽儿 仅作为笔记用)
这道题是一道spfa变形题,具体考察对spfa的深入理解

spfa本身是对Bellman-ford算法的优化。在Bellman-Ford算法中,n轮扫描每一条边看一看各个点有没有希望被松弛。

spfa之所以能用来处理Bellman-ford并进行优化,其原因是在Bellman-ford中,每一轮扫描的边有很多都没有可能更新其它点。

spfa指出,当一条边有希望更新它的一个端点的最短路值,当且仅当它的另一个端点的最短路值被更新过。spfa使用一个队列保留这些可能被更新的信息,通常,spfa在队列中保存的更新信息的意义是"这个点被更新过,因而有可能通过与它连接的边更新这些边的其它端点"。直到队列为空,此局面表示"没有一个结点可能更新其它的结点"。

这样保存更新信息的原因是因为,在通常情况下,一个点可以通过与它相连的边更新其它点,每一个结点的更新则是可能由多个通过边连向它的结点决定,即一对多的更新。即因而我们保存的信息是"可能更新其他多个点的点"而不是"可能被其他点更新的点"因为这样做更方便。(想一想,用前者则出队一个结点扫描更新可以将所有由此结点被更新造成的可能被更新的结点的改变情况全部解决,而对于后者,一个结点的更新将所有连向它的结点入队,则当这些结点出队的时候,需要扫描它的所有前驱来决定结果,后者的复杂度显然比前者高很多,相当于n^2和n的区别)。

为了方便,约定若一个魔头物理攻击打出若干魔头,则前者成为后者的父魔头,后者成为前者的子魔头。约定某个点(或魔头)的取值是杀死该点对应魔头所需花费的目前最优代价。约定某魔头的魔杀值为用魔法攻击杀死该魔头的魔法攻击的消耗。

在本题中,一个点(父魔头)的取值(即杀死最优解)取决于多个点(子魔头)的取值,也就是唯一一组确定的子魔头(和其父魔头的魔杀一起)决定了其父魔头的击杀取值,即多对一的关系。因而要保留更新信息,用保留"有希望被它对应的子魔头组更新的魔头"(即第三段中说的第二种保留方式)的方式显然更方便。

具体地,我们对于每一个可能被更新的点扫描其所有子魔头的取值相加再加上其物理攻击看看能否更新,若能更新,则将其所有的父魔头入队表示它们也有希望被更新。 我们可以看到,本题考查对spfa本质的理解,即用"可能被更新的信息"优化处理。而设计可能被更新的信息的保存方式是本题设计的重点。

本题中还有一些细节需要注意,例如,在开始spfa时我们将全部的结点赋值为它们的魔杀值。你可以将之理解为,在开始局面下,所有的结点均有希望被更新。(只是有希望。在这里提到的有无希望的判定就像是A*算法中的估价函数那样,在开局状态下我们没有更多的信息,因而将所有结点"估"成有希望,为了保证正确性)。也可以理解为,在开始的那一瞬间,它们原来的取值是inf,然后均被更新为其魔杀值,因而这些(即所有点)的后继均有可能被更新,假设图联通,那么所有点的后继构成的集合应当包含所有点(不连通则包含部分点,依然对正确性没有什么干扰)。

温馨提示:十年OI一场空,不开long long见祖宗。

最后附上代码

#include<bits/stdc++.h>
using namespace std;

long long pa[200010], d[200010];
int head[200100], ver[1000010], nex[1000010], hf[200010], vf[1000010], nf[1000010], inq[200010], tot, tt, n;
queue<int> q;

inline void add(int u, int v) {
    ver[tot] = v; nex[tot] = head[u]; head[u] = tot++;
}

inline void addf(int u, int v) {
    vf[tt] = v; nf[tt] = hf[u]; hf[u] = tt++;
}

void spfa() {
    for(int i = 1; i <= n; i++) q.push(i);
    memset(inq, 1, sizeof(inq));
    while(!q.empty()) {
        int cur = q.front(); q.pop(); inq[cur] = 0;
        long long rs = pa[cur];
        for(int i = head[cur]; i != -1; i = nex[i]) rs += d[ver[i]];
        //rs=平a当前的怪+彻底杀死其子怪
        //因为当运行一段之后,子怪都被更新成了正确的的d[i]
        //也就是d[i]就是子怪i被彻底杀死的最小花费
        if(d[cur] > rs) {
            d[cur] = rs;
            for(int i = hf[cur]; i != -1; i = nf[i])
                if(!inq[vf[i]]) inq[vf[i]] = 1, q.push(vf[i]);
                //因为不能保证此时就是最优解,所以父怪可能可更新,全部进队列
                //直到没有怪再进队列,此时一定是最优解
        }
    }
}
int main() {
    int r, mos;
    long long s, k;
    scanf("%d", &n);
    memset(head, -1, sizeof(head)); memset(hf, -1, sizeof(hf));
    for(int i = 1; i <= n; i++) {
        scanf("%lld %lld %d", &s, &k, &r);
        d[i] = k; pa[i] = s;
        //给完全杀死子怪的最小花费d[i]赋初值,全部设置为魔法攻击
        while(r--) {
            scanf("%d", &mos);
            add(i, mos); addf(mos, i);
        }
    }
    spfa();
    printf("%lld\n", d[1]);
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值