【JZOJ3943】【GDOI2015模拟11.29】【HNOI2014 Day4】环游世界(树+并查集)

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);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值