题目链接:https://codeforces.com/contest/1491/problem/C
题目大意:有张蹦床从左到右排成一排,每张蹦床有个初始弹性,n张蹦床则有。
小明可以从任意一张蹦床开始跳,若小明跳上第张蹦床,则跳往下一个蹦床的位置为,若则跳到外面去了。当小明跳过这张蹦床后,蹦床弹性减少1,蹦床弹性最少为1不能低于1。也就是说跳过蹦床后弹性变为。
小明每次跳上蹦床后,就会一直跳直到跳出去。求,小明最少多少趟跳上蹦床,可把所有蹦床的弹性变为1。
题解:
个人解法:
由于小明可以任意选择每次跳的起点,首先我们考虑下,该如何选择每次跳的起点,才能让小明跳入蹦床的趟数最少?
答案很显然,由于我们只关心跳的趟数,而每次只能往右跳,所以我们可以每次都贪心的选择最左边的蹦床为起点,这样可以使每趟能经过的蹦床尽量的多。
接下来我们分析下复杂度,每趟遍历的复杂度为,最多有趟,总复杂度为,显然不满足本题的要求。
我们可以观察到,假设蹦床i为第一张非1的蹦床,并且,那么我们可以计算出要跳多少次才能不跳出去,我们可以把优化到,这样复杂度为,依然不满足要求,我们继续优化。
我们可以观察到,每趟跳遍历蹦床的时候,若,这时不会再变化,我们不需要再遍历这个位置,我们可以直接跳到下一个弹性非1的蹦床上。若每次遍历都是有效的遍历非1的位置,那么总复杂度可以优化为有效遍历次数。
当时,如何的求出下一个弹性非1的位置呢?
我们可以借鉴并查集算法的思想(也可以抽象为并查集问题),维护为当时下一个弹性非1蹦床的位置,每次递归查询并且更新的值,查询的均摊复杂度为,查询代码如下。
int getNext(int i)
{
if(i>n)
return i;
if(a[i]!=1)
return i;
return nt[i]=getNext(nt[i]);
}
官方解法可做到,请继续往下看!
代码如下:
#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;
}
官服解法:复杂度
若我们把第i次起跳的位置记为,那么数列的所有排列都可以得到相同的结果。
简单证明:我们只考虑两次起跳,我们交换两次起跳的位置可以得到相同的结果,因此相邻的两次起跳位置可以交换,可以推导出上面的结论。
因此,我们可以只考虑数列非递减的情况。由于每次只能往右跳,那么每次起跳的起点,一定是从左边起第一个非1的蹦床。
暴力模拟复杂度为。其实,我们可以不暴力模拟跳跃的过程,我们可以把当前这个点跳来了多少次先记录下来,记为。
当起跳位置来到时,若,则这个点还需要起跳次。反之则不用起跳。
然后我们用当前点,更新后面位置的值。比如时,会经过1次,会经过3次。
到这里我们可以解决这个问题。
但是我们还可以用数据结构优化,把复杂度优化到。
我们继续观察,可能会增加多次,我们可以更新,对于区间到,一定只增加1次。区间累加操作,我们可以用线段树优化到,但本题我们还有更好的做法。
我们把一个位置通过区间累加获得的部分抽离出来单独计算,我们可以把问题抽象为,有多个区间,我们要依次求出点被多少个区间覆盖。
我们可以点1开始,维护一个值,表示当前有多少个区间覆盖。那么当我们进入区间起点时+1,当我们离开区间终点时-1。
我们可以维护位置i有多少个区间起点为,那么当我们进入位置时.
我们可以维护位置i有多少个区间终点为,那么当我们离开位置时.
这样对于每个位置,我们可以求出这个位置被多少个区间覆盖。
回到本题,我们可以用这个方法求出每个位置通过区间累加获得的结果。
这样复杂度就优化到。
代码如下:
#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;
}