P4042 [AHOI2014/JSOI2014]骑士游戏

P4042 [AHOI2014/JSOI2014]骑士游戏

分析

这题的思路可太绝了。是对spfa的绝对深入理解

我们先简单的分析题目。

从题目中我们可以知道,杀死一个魔物有两种方法

  1. 普通攻击,不能彻底解决魔物,还会分裂成其他魔物(可以是其他魔物且同一种魔物可以重复出现)
  2. 魔法攻击,可以彻底解决魔物

来说说我的思路。

魔法攻击

看到的时候,就想到,我们可以建立一个虚拟源点,从虚拟源点向所有点连一条边,边权就是将其彻底消灭的代价。

但实际上,我们并不需要真的建立出来,只需要在初始化代价dist数组的时候,直接将魔杀值直接赋给对应的dist就可以了

但是怎么知道用一个普通攻击解决魔物,最后所需要付出的代价?

这里我们着重说一下

普通攻击

不难想到,i产生的新魔物,我们可以直接从i向产生的边连一条边。

这里,我遇见了一个小问题,我们需要解决所有产生的魔物,才是普攻所得到的最优解,但是有时一个点产生了相同的魔物多个,怎么办?

其实这不是问题,我们用链式向前星的时候,只需要产生一个魔物就建一条边,这样对同一个魔物,我们就用边数,知道了产生该魔物的数量。

那具体怎么知道,我们解决这个魔物的普攻所需要付出的代价呢。可以直接累加。不清楚可以看代码

int res = ra[t];//ra[t]是t的普攻魔法
for(int i=h[t];~i;i=ne[i]) res += dist[e[i]];//dist[e[i]]就是解决e[i]需要付出的最小代价
dist[t] = max(res,dist[t]);

但我们的问题主要是

用普通攻击解决魔物i后,我们需要确定了解决产生的新魔物的最小代价,才能得到对i而言到底怎样才是最小代价

这里,就是精彩的地方了

首先,我们来讲讲spfa

spfa之所以能用来处理Bellman-ford并进行优化,其原因是在Bellman-ford中,每一轮扫描的边有很多都没有可能更新其它点。spfa指出,当一条边有希望更新它的一个端点的最短路值,当且仅当它的另一个端点的最短路值被更新过。spfa使用一个队列保留这些可能被更新的信息,通常,spfa在队列中保存的更新信息的意义是"这个点被更新过,因而有可能通过与它连接的边更新这些边的其它端点"。直到队列为空,此局面表示"没有一个结点可能更新其它的结点"。

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

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

因此,我们再进行更新的时候,当一个节点被更新之后,我们需要将它的前驱节点全部放入队列中,代表这些节点可以再次被更新了

所以我们将,上述代码再进行改变

auto t = q.front();
q.pop();
st[t] = 0;
LL res = ra[t];//ra[t]是t的普攻魔法
for(int i=h[t];~i;i=ne[i]) res += dist[e[i]];//dist[e[i]]就是解决e[i]需要付出的最小代价
if(res<dist[t]) 
{
    dist[t] = res;
    for(int i=hr[t];~i;i=ne[i])//hr[t]就是,记录的t的前驱节点
    if(!st[e[i]])
    {
    q.push(e[i]);
    st[e[i]] = 1;
    }
}

以上就是,一个节点出队列后会进行的操作。

温馨提醒:记得开long long

附上全部代码

AC_code

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10,M = 2e6 + 10;
LL ra[N],dist[N];
int h[N],hr[N],e[M],ne[M],idx;
bool st[N];
int n;

void add(int h[],int a,int b)
{
    e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void spfa()
{
    queue<int> q;
    for(int i=1;i<=n;i++)
        q.push(i),st[i] = 1;
    while(q.size())
    {
        auto t = q.front();
        q.pop();
        st[t] = 0;
        LL res = ra[t];//ra[t]是t的普攻魔法
        for(int i=h[t];~i;i=ne[i]) res += dist[e[i]];//dist[e[i]]就是解决e[i]需要付出的最小代价
        if(res<dist[t]) 
        {
            dist[t] = res;
            for(int i=hr[t];~i;i=ne[i])//hr[t]就是,记录的t的前驱节点
            if(!st[e[i]])
            {
            q.push(e[i]);
            st[e[i]] = 1;
            }
        }
    }
}

int main()
{
    cin>>n;
    memset(h,-1,sizeof h);
    memset(hr,-1,sizeof hr);
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>ra[i]>>dist[i]>>x;
        while(x--)
        {
            int a;cin>>a;
            add(h,i,a),add(hr,a,i);
        }
    }
    spfa();   
    cout<<dist[1]<<endl;
    return 0;
}
posted @ 2022-03-18 21:07  艾特玖  阅读(76)  评论(0)    收藏  举报