Problem
ZTY有s(≤100)架飞机,第i架飞机一次能飞di,要飞过n(≤1000000)个点构成的环,两点间均有距离,求不定起点时,至少要飞多少次才能从起点飞回起点。
Solution
这题我刚看完后觉得就是道纯模拟水题,结果被别人提醒不定起点,于是感觉这种题看上去及其简单,用各种数据结构都能维护比这麻烦很多的东西,但却不能维护这个纯模拟。于是我就打了暴力。
那么正解的话,设城市间的距离存储在a数组中,首先copy一遍整个a数组,然后设一个单调上升的指针j,表示从城市i出发,最多能飞到哪里。用前缀和线性求出每个i对应的j,然后从i向j连边,j为i的父亲。这样就会连出一个树形结构。这个可以用并查集维护。
然后我们可以从每个点j出发,计算出它向根须走的最少步数,使得它走到的祖先≥n+j,则此步数即为以此点为起点的答案。
首先肯定不能暴力算,否则复杂度会退化到骇人听闻的程度。那么暴力建树,在树上乱搞,显然很麻烦。
于是考虑并查集。但是我们不能傻逼逼地一格一格地往上跳,又不能用按秩合并(因为它哪个点会成为哪个点的爸爸已经固定了),所以我们考虑路径压缩。
但是这个路径压缩非同一般。我们知道,一般的路径压缩都是将某个点到根的路径上的所有点的父亲赋为根,但是我们不能跑到根。在本题中,我们如果没有跑到根,却跑到了某个点x,满足x≥n+j,那么我们也要弹出来。而且这个路径压缩也比较麻烦。我们为了维护每个点到根的距离,可以设dis[x]表示点x到其父亲节点的距离,则每次路径压缩时,设x原来的父亲为f,则dis[x]+=dis[f]。对于之前连边时,若x的父亲不为x,则dis[x]=1,否则为0。
而且这题的n也坑爹的大,我一开始打了递归的getfather,MLE了四个点;于是花了十几分钟改成了非递归。
但是故事并没有就此结束——我又T掉了。
当时的我百思不得其解,加了个O3怒气腾腾地切掉了。但现在我大致清楚了。这应该是因为我做的这个路径压缩并不完全,所以它并不能助我将这个算法优化到
O(snα(n))
O
(
s
n
α
(
n
)
)
。
我现在加了个优化——那就是在计算以j为起点的答案时,如果从点1到点j的距离>我们所能飞行的距离d,则直接break掉,因为这样的话,如果有解,我们肯定会在1到j-1中的某一个点上降落。那么此点肯定小于j,所以我们肯定已经计算过了以此点为起点的答案,那再计算后面的就没有意义了。于是这次我关掉了O3,跑了将近7s。
时间复杂度:
O(snα(n))
O
(
s
n
α
(
n
)
)
左右。
Code
#include <cstdio>
#include <algorithm>
using namespace std;
#define N 1000002
#define a(x) a[(x-1)%n+1]
#define fo(i,a,b) for(i=a;i<=b;i++)
int i,j,k,n,s,a[N],d,sum,fat[2*N],dis[2*N],ans,stack[N],top;
int gef(int x)
{
int f=x,root;
stack[top=1]=f;
while(fat[f]!=f&&f<n+j)stack[++top]=f=fat[f];
dis[root=f]=0;
while(--top)
{
f=stack[top+1];
x=stack[top];
if(!dis[x])dis[x]=1;
dis[x]+=dis[f];
fat[x]=root;
}
return root;
}
int main()
{
scanf("%d%d",&n,&s);
fo(i,1,n)scanf("%d",&a[i]);
fo(i,1,s)
{
scanf("%d",&d);
k=1;
sum=0;
fo(j,1,2*n)
{
while(k<2*n&&sum+a(k)<=d)sum+=a(k),k++;
fat[j]=k;
if(j!=k)dis[j]=1;
sum-=a(j);
}
ans=3*n;
sum=0;
fo(j,1,n)
if(gef(j)>=n+j)
{
ans=min(ans,dis[j]);
sum+=a(j);
if(sum>d)break;
}
if(ans==3*n)
printf("NO\n");
else printf("%d\n",ans);
}
}