luogu [PKUSC2018] 星际穿越

题目描述

有 n 个星球,它们的编号是 1 到 n,它们坐落在同一个星系内,这个星系可以抽象为一条数轴,每个星球都是数轴上的一个点,特别地,编号为 i 的星球的坐标是 i。

一开始,由于科技上的原因,这 n 个星球的居民之间无法进行交流,因此他们也不知道彼此的存在。现在,这些星球独立发展出了星际穿越与星际交流的工具。对于第 i 个星球,他通过发射强力信号,成功地与编号在 l_i 的所有星球取得了联系(编号为 1 的星球没有发出任何信号),取得联系的两个星球会建立 **双向** 的传送门,对于建立了传送门的两个星球 u,v,u 上的居民可以花费 1 单位时间传送到 v,v 上的居民也可以花费 1 单位时间传送到 u ,我们用 dist(x,y) 表示从编号为 x 的星球出发,通过一系列星球间的传送门,传送到编号为 y 的星球最少需要花费的时间。

现在有 q 个星际商人,第 i 个商人初始所在的位置是 x_i, 他的目的地是 [l_i,r_i] 中的其中一个星球,保证l_i<r_i<x_i。他会在这些星球中等概率挑选一个星球 y (每个星球都有一样的概率被选中作为目的地),然后通过一系列星球的传送门,**花费最少的时间**到达星球 y 。商人想知道他花费的期望时间是多少?也就是计算$\frac{1}{r_i-l_i+1}{\sum_{y=l_i}^{r_i}{dist(x_i,y)}}$

对于 $100\%$ 的数据,满足 $n,q\leq 3\times 10^5$

思路

接下来让我总结下解题思路,这道题运用了倍增的思路,对我来说很有挑战,看了题解过了题之后我来总结一下。

首先题目给出2到n的范围a[i]代表第i个点一步可以跳到左侧最远的点。题目给出q个查询,每个查询包含一个区间[l,r]和一个点x,问对于一个点x,等可能到达区间任意值的期望值,实际上就是x到区间内所有值总和除以区间长度。

首先从求解角度,题目求的就是[l,x]的步数总和-[r+1,x]的步数总和。首先考虑怎么维护每个点到前面的步数和,根据题目给定的a数组,可以想到先求对于每个点i求出i步能到达的最远的点,线性必超时,考虑倍增。

设dp[i][j]为i位置向前跳2^j步最远的点,很容易看出每个点向前跳1步就是题目给定的a[i],所以有dp[i][0]=a[i](i>1),由于i位置向前面的跳跃步数可能比i后面位置向前跳的步数小,导致不是最优(如i的a[i]大于a[i+1]会导致i跳到小于i的位置会花费更多步数),所以要从最后面n开始递推,所以有dp[n][0]=a[n],dp[i][j]=dp[dp[i][j-1][j-1]。

随后可以由类似求前缀和的方法递推出sum数组,sum[i][j]代表i位置向前跳2^j步的区间步数和,sum[n][0]=1*(n-a[n])(此时步数为1),sum[i][j]=sum[i][j-1]+sum[dp[i][j-1]][j-1]+times*(dp[i][j-1]-dp[i][j]),代表位置i跳2^j的步数和由跳2^(j-1)的区间和加上i跳2^(j-1)步到达的最远位置跳2^(j-1)步的区间和,最后因为对于i位置后者的步数要加上区间差(dp[i][j-1]-dp[i][j])乘上当前步数(1<<(j-1)),到此为止dp数组和sum数组维护完成。

然后进行查询x位置到l位置的区间和,也是简单的倍增处理,ans代表总和,times代表已经跳了几次,x代表当前位置,从最大的指数递减枚举,如果dp[x][i]大于l代表还没走完,就可以更新答案,ans+=times*(x-dp[x][i])+sum[x][i],因为sum[x][i]代表x位置跳2^i次的区间和,times*(x-dp[x][i])则是额外加上sum的偏移,例如假如已经走了一步,不能直接加上sum[x][i],因为sum数组计算是从第一步开始,这时要把当前位置到最前的位置这段区间,也就是sum覆盖的区间,把这部分区间的和乘上步数,才能正确处理总的区间和。最后gcd计算进行通分化到最简输出答案,完毕

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 500;
#define endl '\n'
#define ll long long
const int MOD=1e9+7;
int main()
{
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    ll n;cin>>n;
    vector<ll> a(n+1);
    for(int i=2;i<=n;i++)
        cin>>a[i];
    vector<vector<ll>> dp(n+1,vector<ll>(30));//代表从i开始跳2^j最小的位置
    vector<vector<ll>> sum(n+1,vector<ll>(30));//代表从i开始跳2^j最小的位置的和
    dp[n][0]=a[n],sum[n][0]=n-a[n];
    for(int i=n-1;i>=2;i--)//由于i位置向前面的跳跃步数可能比i后面位置向前跳的步数小,要从后推
    {
        dp[i][0]=min(dp[i+1][0],a[i]);
        sum[i][0]=i-dp[i][0];
    }
    for(int j=1;j<=29;j++)
    {
        for(int i=(1<<j);i<=n;i++)
        {
            dp[i][j]=dp[dp[i][j-1]][j-1];
            sum[i][j]=sum[i][j-1]+sum[dp[i][j-1]][j-1]+(1<<(j-1))*(dp[i][j-1]-dp[i][j]);
            //加上区间差(dp[i][j-1]-dp[i][j])乘上当前步数(1<<(j-1))
        }
    }
    auto query=[&](ll x,ll l)//x到l的最小次数
    {
        if(l>=a[x])return x-l;
        ll times=1,ans=x-a[x];//先跳一次
        x=a[x];
        for(int i=29;i>=0;i--)
        {
            if(dp[x][i]>=l)
            {
                ans+=times*(x-dp[x][i])+sum[x][i];
                //因为sum[x][i]代表x位置跳2^i次的区间和,times*(x-dp[x][i])则是额外加上sum的偏移
                times+=(1<<i);
                x=dp[x][i];
            }
        }
        if(x>l)ans+=(times+1)*(x-l);
        return ans;
    };
    ll q;cin>>q;
    while(q--)
    {
        ll l,r,x;cin>>l>>r>>x;
        ll a=query(x,l)-query(x,r+1);
        ll b=r-l+1;
        ll g=__gcd(a,r-l+1);
        a/=g,b/=g;
        cout<<a<<'/'<<b<<endl;
    }
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值