Codeforces Global Round 13 C. Pekora and Trampoline(贪心+数据结构)

20 篇文章 0 订阅
1 篇文章 0 订阅

题目链接:https://codeforces.com/contest/1491/problem/C

题目大意:有n(1<=n<=5000)张蹦床从左到右排成一排,每张蹦床有个初始弹性s_{_{i}}(1<=s_{_{i}}<=10^{9}),n张蹦床则有s_{1},s_{2},.....s_{i}...,s_{n}

小明可以从任意一张蹦床开始跳,若小明跳上第i张蹦床,则跳往下一个蹦床的位置为i+s[i],若i+s[i]>n则跳到外面去了。当小明跳过这张蹦床后,蹦床弹性减少1,蹦床弹性最少为1不能低于1。也就是说跳过蹦床后弹性变为s_{i}=max(s_{i}-1,1)

小明每次跳上蹦床后,就会一直跳直到跳出去。求,小明最少多少趟跳上蹦床,可把所有蹦床的弹性变为1。

 

题解:

个人解法:

由于小明可以任意选择每次跳的起点,首先我们考虑下,该如何选择每次跳的起点,才能让小明跳入蹦床的趟数最少?

答案很显然,由于我们只关心跳的趟数,而每次只能往右跳,所以我们可以每次都贪心的选择最左边的蹦床为起点,这样可以使每趟能经过的蹦床尽量的多。

接下来我们分析下复杂度,每趟遍历的复杂度为O(n),最多有10^{9}*n趟,总复杂度为O(10^9*n^2),显然不满足本题的要求。

我们可以观察到,假设蹦床i为第一张非1的蹦床,并且i+s_{i}>n,那么我们可以O(1)计算出要跳多少次才能不跳出去,我们可以把O(10^9)优化到O(n),这样复杂度为O(n^3),依然不满足要求,我们继续优化。

我们可以观察到,每趟跳遍历蹦床的时候,若s_{i}==1,这时s_{_{i}}不会再变化,我们不需要再遍历这个位置,我们可以直接跳到下一个弹性非1的蹦床上。若每次遍历都是有效的遍历非1的位置,那么总复杂度可以优化为有效遍历次数O(n^2)

s_{i}==1时,如何O(1)的求出下一个弹性非1的位置呢?

我们可以借鉴并查集算法的思想(也可以抽象为并查集问题),维护next[i]为当s_{i}==1时下一个弹性非1蹦床的位置,每次递归查询并且更新next[i]的值,查询的均摊复杂度为O(1),查询代码如下。

int getNext(int i)
{
    if(i>n)
        return i;
    if(a[i]!=1)
        return i;
    return nt[i]=getNext(nt[i]);
}

官方解法可做到O(n),请继续往下看!

代码如下:

#include<bits/stdc++.h>

using namespace std;
const int nn =5100;
const int inff = 0x3fffffff;
const double eps = 1e-8;
typedef long long LL;
const double pi = acos(-1.0);
const LL mod = (479 << 21) + 1;
LL n;
LL a[nn];
int nt[nn];
LL ans;
int getNext(int i)
{
    if(i>n)
        return i;
    if(a[i]!=1)
        return i;
    return nt[i]=getNext(nt[i]);
}
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        cin>>n;
        ans=0;
        for(int i=1;i<=n;i++)
        {
            cin>>a[i];
            if(a[i]==1)
                nt[i]=i+1;
        }
        a[n+1]=2;
        while(true)
        {
            int i=1;
            if(a[1]==1)
            {
                i=getNext(1);
                if(i>n)
                    break;
            }
            if(i+a[i]>n)
            {
                if(i==n)
                {
                    ans+=a[i]-1;
                    a[i]=1;
                    nt[i]=i+1;
                } else {
                    ans+=i+a[i]-n;
                    a[i]=n-i;
                    if(a[i]==1)
                        nt[i]=i+1;
                }
                continue;
            }
            for(;i<=n;)
            {
                if(a[i]==1)
                {
                    i=getNext(i);
                } else {
                    int v=a[i];
                    a[i]--;
                    if(a[i]==1)
                    {
                        nt[i]=i+1;
                    }
                    i+=v;
                }
            }
            ans++;
        }
        cout<<ans<<endl;
    }
    return 0;
}

官服解法:复杂度O(n)

若我们把第i次起跳的位置记为p_{i},那么数列p的所有排列都可以得到相同的结果。

简单证明:我们只考虑两次起跳,我们交换两次起跳的位置可以得到相同的结果,因此相邻的两次起跳位置可以交换,可以推导出上面的结论。

因此,我们可以只考虑数列p非递减的情况。由于每次只能往右跳,那么每次起跳的起点,一定是从左边起第一个非1的蹦床。

暴力模拟复杂度为O(n^3)。其实,我们可以不暴力模拟跳跃的过程,我们可以把当前这个点i跳来了多少次先记录下来,记为c[i]

当起跳位置来到i时,若s[i]-1>c[i],则这个点还需要起跳s[i]-1-c[i]次。反之则不用起跳。

然后我们用当前点,更新后面位置的c[i]值。比如s[i]=4,c[i]=6时,i+2,i+3,i+4会经过1次,i+1会经过3次。

到这里我们可以O(n^2)解决这个问题。

但是我们还可以用数据结构优化,把复杂度优化到O(n)

我们继续观察,i+1可能会增加多次,我们可以O(1)更新,对于区间i+2i+s[i],一定只增加1次。区间累加操作,我们可以用线段树优化到O(logn),但本题我们还有更好的做法。

我们把一个位置通过区间累加获得的部分抽离出来单独计算,我们可以把问题抽象为,有多个区间[l_{i},r_{i}],我们要依次求出点1,2,3...n被多少个区间覆盖。

我们可以点1开始,维护一个值t,表示当前有多少个区间覆盖。那么当我们进入区间起点l_{i}t+1,当我们离开区间终点r_{i}t-1。

我们可以维护位置i有多少个区间起点为L[i],那么当我们进入位置it+L[i].

我们可以维护位置i有多少个区间终点为R[i],那么当我们离开位置it-R[i].

这样对于每个位置,我们可以O(1)求出这个位置被多少个区间覆盖。

回到本题,我们可以用这个方法O(1)求出每个位置通过区间累加获得的结果。

这样复杂度就优化到O(n)

代码如下:

#include<bits/stdc++.h>

using namespace std;
const int nn =5100;
const int inff = 0x3fffffff;
const double eps = 1e-8;
typedef long long LL;
const double pi = acos(-1.0);
const LL mod = (479 << 21) + 1;
int n;
int s[nn];
LL c[nn];
int L[nn];
int R[nn];
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            cin>>s[i];
            L[i]=R[i]=0;
            c[i]=0;
        }
        int tem=0;
        LL ans=0;
        for(int i=1;i<=n;i++)
        {
            tem+=L[i];
            c[i]+=tem;
            if(s[i]-1>c[i])
            {
                ans+=s[i]-1-c[i];
                c[i]=s[i]-1;
            }
            int l=i+2;
            int r=min(i+s[i],n);
            if(l<=r)
            {
                L[l]++;
                R[r]++;
            }
            c[i+1]+=c[i]-s[i]+1;
            tem-=R[i];
        }
        cout<<ans<<endl;
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值