【AtCoder】【模拟】【模型转化】Camel and Oases(AGC012)
题意:
有一个骆驼,n个绿洲遍布在数轴上,第i个绿洲的坐标为x[i],保证x[i]单增。骆驼的驼峰有体积初始值V。当驼峰的体积变为v的时候,驼峰中至多只能够存储v L的水。骆驼希望走完所有的绿洲,并且可以向下面这样来走:
1.走距离d,消耗驼峰中d L的水,但是驼峰的体积不会减少。任意时候驼峰中的水的体积均不能够为负数;
2.跳跃到任意一个位置,消耗完所有的水,并且让驼峰的体积变为v/2。该操作在v=0的时候是不能够进行的。
骆驼能够在绿洲将水补满至v。且一个绿洲可以多次访问并进行补给。最后要求你输出从每个位置出发,能否走完所有的绿洲。
数据范围:
N,V<=2*1e5 ,-1e9<=x[i]<=1e9 ,且x是单增的。
思路:
首先看完题目,我们可以发现v/2这个操作是十分玄学的。这意味着只会减少log(V)+1次,就不能够再进行任何的移动了。也说明了v的取值只会有log(V)+2种,这个数量级是很小的。
那么对于某一个v而言,骆驼能够在一些连续的绿洲之间任意的穿梭,也就形成了一些线段。具体而言就是假如说x[i+1]-x[i]<=v,那么这两个绿洲就是联通的,就可以让i和i+1处于一条线段中。我们可以发现,当v从V变换到0的时候,每一条线段的长度是在逐渐变短的,而线段的数量在逐渐增多。也就是说,v越小,骆驼的移动能力越差。现在这个问题就变成了强迫你选择了第一层(也就是v=V的那一层)中的某一条线段,在剩下的每一层当中,选出至多一条线段,能否存在一种方式使得最后选出来的所有的线段能够覆盖完所有的绿洲。
对于这个问题而言,瞬间就简化了不少。可以定义f1[state],表示在选择state(第i位为0表示第i层没有选择线段,第i位为1表示第i层选择了一条线段)的时候,(线段从最左边开始选)能够覆盖完全的最靠右的位置;定义f2[state],表示在选择state的时候,(线段从最右边开始选)能够覆盖完全的最靠左的位置。最后再扫一遍第一层中的所有的线段,表示强行选择其中的某一条线段,然后找是否有一个state,满足第0位不为1(第一层已经被确定了),且f1[state],f2[全集-state-1],加上这条线段,使得能够覆盖完所有的绿洲。
这样子看上去似乎问题已经解决了,但是当V奇小的时候,第一层最坏可能有n条线段(一个点一条),然后就变成了\(O(n2^{log_n})=O(n^2)\)的状态。仔细分析之后,我们可以发现,假如第一层的线段数量大于了logV+2,那么由于接下来的线段只会越来越短并且越来越多,就算是选择第一层的所有线段也需要>logV+2条线段,而一共才能够选择log(V)+2条线段线段,那就显然不可能有解了。全部输出Impossible即可。
这样子就变为了O(logV * n)的时间复杂度了。
代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
#define MAXN 1000000
#define INF 2000000000
using namespace std;
int n,V,x[MAXN+5];
int cnt[25];//cnt用于计算每一层所有的线段数
int l[25][MAXN+5],r[25][MAXN+5];//l表示线段的左端点,r表示的则是右端点
int f1[MAXN*5+5],f2[MAXN*5+5];//如思路中的定义
bool ans[MAXN+5];//ans记录的是对于某一条线段的答案,不是某个绿洲的答案
void Init()
{
for(int i=0;i<=MAXN*5+3;i++)
f1[i]=0,f2[i]=INF;
}
int UpFind(int id,int pos)//找l在pos+1的左边的线段,也就是能够向右扩展的最靠右的线段
{
pos++;
int p=upper_bound(l[id]+1,l[id]+cnt[id]+1,pos)-l[id];
p--;
if(p<=0)
return pos;
return max(r[id][p],pos-1);
}
int LowFind(int id,int pos)//找r在pos-1的右边的线段,也就是能够向左扩展的最靠左的线段
{
pos--;
int p=lower_bound(r[id]+1,r[id]+cnt[id]+1,pos)-r[id];
if(p>=cnt[id]+1)
return pos;
return min(l[id][p],pos+1);
}
int main()
{
Init();
scanf("%d %d",&n,&V);
int logV=0;
for(logV=0;(1<<logV)<=V;logV++);//求出来的实际上是logV+1
for(int i=1;i<=n;i++)
scanf("%d",&x[i]);
x[n+1]=INF;
x[0]=-INF;//便于操作
for(int LG=0;LG<=logV;LG++)
{
int d=V/(1<<LG);
cnt[LG]=1;
l[LG][1]=1;
//求线段
for(int i=1;i<=n;i++)
{
r[LG][cnt[LG]]=i;
if(x[i+1]-x[i]>d)
{
cnt[LG]++;
l[LG][cnt[LG]]=i+1;
}
}
cnt[LG]--;
}
if(cnt[0]>logV+1)//特判
{
for(int i=1;i<=n;i++)
printf("Impossible\n");
return 0;
}
int all=(1<<(logV+1));
f1[0]=0,f2[0]=n+1;//预处理两个f数组
for(int s=0;s<all;s+=2)
for(int i=0;i<=logV;i++)
{
if(!(s&(1<<i)))
continue;
f1[s]=max(f1[s],UpFind(i,f1[s-(1<<i)]));
f2[s]=min(f2[s],LowFind(i,f2[s-(1<<i)]));
}
for(int i=1;i<=cnt[0];i++)//枚举第一层的每一条线段
{
int ln=l[0][i],rn=r[0][i];
for(int s1=0;s1<all;s1+=2)
{
int s2=all-1-s1-1;
int lpos=f1[s1];
int rpos=f2[s2];
if(lpos>=ln-1&&rpos<=rn+1)//看能否覆盖所有的绿洲
{
ans[i]=true;//存的是线段的答案
break;
}
}
}
int pos=1;
for(int i=1;i<=n;i++)
{
if(ans[pos]==true)
printf("Possible\n");
else
printf("Impossible\n");
if(x[i+1]-x[i]>V)
pos++;
}
return 0;
}