[WerKeyTom_FTD的模拟赛]刻画在历史舞台上的群星

31 篇文章 0 订阅
9 篇文章 0 订阅

题目背景

受愤怒权能情感伤害链接的影响,昴死亡轮回了三次。在第四次的轮回中,与贝蒂一起使用魔法杀马克屏蔽了愤怒权能,但艾米莉亚不敌愤怒火焰权能,激战中又被强欲司教劫走,于是留下歌姬和普莉希拉单挑愤怒,昴与莱因哈鲁特前去营救艾米。同时,色欲司教的袭击广播,奥托遇到到处吞噬的暴食司教,复活的尸体特雷西亚的出现,水门都市各个战场战斗激化。
王选阵营VS大罪司教,他们,是刻画在历史舞台上的群星。

题目描述

广场站着一排人,在观看着挟持小男孩的愤怒司教的演说,一共有n个,编号为1~n,第i个人的编号为i。
第i个人,有一个恐惧程度值为ai。如果[l,r]的人被愤怒司教使用权能链接,那么对于l<=i<=j<=r,便会产生(a[i]+a[i+1]+……a[j-1]+a[j]) mod p那么多的恐惧总值,其中p是给定的一个常数。
为了更好的应战愤怒司教,现在有q个询问,每个询问给定区间[l,r],请你找出对于任意l<=i<=j<=r,[i,j]产生恐惧总值的最小值。

输入格式

第一行两个正整数表示n,p。
接下来一行,n个整数,表示序列a。
接下来一行一个正整数表示询问个数q。
接下来q行,每行两个正整数l,r表示一次询问。

输出格式

q行每行一个

样例输入

3 3
1 1 2
3
1 2
2 3
3 3

样例输出

1
0
2

数据范围及约定

40%,2<=p<=n<=50,q<=50
60%,2<=p<=n<=100,q<=100
70%,2<=p<=n<=1000,q<=2000
80%,2<=p<=n<=200000,q<=2000
前80%数据共8个测试点,每个测试点10分,时限均为1s。后20%数据共4个测试点,每个测试点5分。
第九个测试点:2<=p<=n<=2000000,q<=2000,时限3s
第十个测试点:2<=p<=n<=6000000,q<=2000,时限3.5s
第十一个测试点:2<=p<=n<=6000000,q<=2000,时限2s
第十二个测试点:2<=p<=n<=6000000,q<=2000,时限1.5s
其中,本题的数据保证随机生成,生成方法如下:
每个测试点有一个对应的常数len,前70%数据的len约等于n的3/4,后30%的len值如下。
第八个测试点:len为2650
第九个测试点:len为8500
后三个测试点:len为14500
对于a序列,每个ai均在[0,p-1]中等概率随机生成。
对于每组询问,其有3/4的概率满足条件一,1/4的概率满足条件2
条件1:被询问区间的区间大小<=len
条件2:被询问区间的区间大小>len
被询问区间的区间大小x在满足条件情况下等概率随机生成,然后等概率随机生成符合条件的右端点,相减得到左端点。

请注意对读入进行优化。
请注意常数带来的影响。
请注意利用题目的条件优化程序。

题目来源

改编自《小奇的数列》

题目背景相关

题目名选自《Re:从零开始的异世界生活》第五章标题“刻画在历史舞台上的群星”,本章讲述了解决圣域问题后的一年后,五大王选阵营因为各种契机集合在水门都市商议,接着水门都市遭到愤怒、强欲、暴食、色欲的大罪司教的进攻,众人一同对敌的故事。

40%算法

暴力枚举统计答案。
复杂度为O(n^3q)。

60%算法

记录前缀和,计算区间和使用前缀和快速得出,其余暴力。
复杂度为O(n^2q)。

70%算法

对于每个询问,我们要找l<=i<=j<=r中,(sum[j]-sum[i-1]) mod p的最小值。
顺序扫描,对于枚举的一个j,如何找到这个i?假如sum[j]是新的最小值,那么这个i便是前面的最大值。否则,我们要找一个不比sum[j]大的最小数。
要支持插入和查询,可以使用线段树维护。
复杂度o(nq log p)

80%算法

优化一

相信这个优化大家一定都会。
在扫描过程中,如果答案已经为0,那么就退出。
我们如何合理分析这个算法的复杂度呢?
首先根据a的随机方法,第i个位置是j的概率P(a[i]为j)=1/p
我们考虑sum。
先给出结论:P(sum[i]为j)=1/p
当i=1时,显然是正确的,显然sum[1]=a[1]嘛。
如果i=n时结论正确,那么i=n+1时结论也正确。
P(sum[i+1]为j)=sigma{P(sum[i]为k)*P(a[i+1]为(j-k)mod p)}
后面部分为1/(p^2),一共p项加起来为1/p。
因此归纳完毕。
然后呢,每次我们期望会在多少步后就结束算法?
假设i步就结算,那么花费了i步,概率是i-1步内未结束,在第i步时结束了。P(i-1步内未结束)=P(前i-1个互不相同)=(p/p)[(p-1)/p][(p-2)/p]……[(p-i)/p]=p!/[(p-i-1)!*p^(i-1)]
P(在第i步恰好结束)=P(i-1步内未结束)P(第i个与前i-1个中某一个相同)=p!/[(p-i-1)!*p^(i-1)](i-1)/p
对期望的贡献为P(在第i步恰好结束)*i
我们计算一下2*10^5时的期望,大约为560。
2*10^6时的期望,大约为1773。
6*10^6时的期望,大约为3070。
记这个期望步数为a。
那么复杂度为aq log p。

优化二

我们都知道抽屉原理,对于本题,如果询问的区间长度比p还大,那么一定会出现两个相同的,因此答案为0。
然而这并没有什么用,因为p和n是同阶的。
我们思考抽屉原理的本质,那就是P(长度为p+1的区间互不相同)=0
假如我们定义一个常数b,那么P(长度为b的区间互不相同)是多少?
可以发现,在80%数据下,b随便一取概率已经是小的不行了。
这个其实叫生日悖论。
因此我们可以考虑一个优化,设置一个常数b,每次询问的区间长度大于b时,就认为答案为0,直接退出。只要我们合理设置b,可以保证程序错误率很小。
怎么计算错误率呢?用优化一的方法可以得到单词错误率,那么程序的正确率为(1-单次错误率)^q,再用1减得到程序的错误率。
我们发现题目条件中有个神秘常数len,事实上,代入题目中给定的len可以发现程序的错误率极小。你设置的b可以直接用len,也可以再小一些。
题目的每组询问有1/4的概率生成长度大于len的区间,因此期望情况下,我们会去执行的区间只有0.75q个。
加了优化后,只有<=b的区间会被执行,那么单次询问最坏复杂度是b log p,总复杂度为0.75bq log p。

将优化一与优化二一起使用,单次询问变成期望复杂度a log p。
总复杂度为0.75aq log p。

85%算法

我们应该如何继续优化每次询问的复杂度呢。
注意到每次做的区间长度都很小,但是因为值域是p很大,线段树又是静态的,那么我们会很慢。
因此考虑到使用复杂度动态的平衡树,使用任意一种应该都能过,直接调用STL中的set也行,set求前驱的方法是lower_bound,如果不想改变set的大小比较,可以考虑把数变成相反数放进去,取出来再取相反数即可。
总复杂度0.75aq log b

90%算法

到这里我们考虑优化常数。STL的set是很大常数的,写splay无疑也是很慢的,因为了解splay势能分析的都知道splay带了一个3的常数。
可以选择跑的很快的treap。
treap的原理大家应该都知道了。每次新插入的新点如果为t,旋转到了k,那么t的fix在k子树内一定最小,概率为1/size[k],旋转一次t的深度减一,旋转的复杂度是O(1),那么这样复杂度就是d[t]-d[k]+1,对期望贡献为(d[t]-d[k]+1)*1/size[k],我们又知道size[k]>=(d[t]-d[k]+1),所以这个东西<=1。t一共有log n个父亲,所以每次的期望复杂度<=log n。

95%算法

计算过程其实并不会很慢,那么慢的地方到底在哪里呢?
n=6000000,读入就超时了啊。
所以我们对读入进行优化,用fread即可。

100%算法

将算法各个地方的常数优化得好一点,就可以过。通常情况下IO优化都加了,不要写太丑,应该都能过了。
如果过不了,你可以尝试一下下面这个优化。

优化三

我们观察前两个优化,显然都在考虑如何快速判断一个询问答案是否为0。
基于这个思想,我们可以预处理出right[i]表示最小的j>i,满足sum[i]=sum[j]。
那么在做每个询问前,可以先扫一遍[l-1,r-1],如果存在一个i满足right[i]<=r,那么这个询问答案一定为0。
我们检验所需的复杂度?首先因为优化2我们只考虑<=b的区间,那么根据随机方法每种长度区间出现概率均为1/b。我们枚举区间长度x,然后枚举在第i步时,那么前i个一定要互不相同,这是不在i步之前停下的概率。后面x-i个全部不和前i个相同,是(p-i)^(x-i),再用1减,这是在i步可以停下的概率,相乘得到恰好在i步停下的概率。
计算出n=6000000时检验的期望复杂度为2655,设这个常数为c。
这个优化关键还在于有多少个答案为0的区间,根据计算<=b的区间有1/5的概率不为0,那么这些区间就被我们略去了,剩余的区间计算复杂度和原来一致。
总复杂度0.75*0.2(c+b log b)q+0.75*0.8cq
你计算一下发现这样子比原来还慢,所以我们考虑不在每次询问前都去检验,而是类似优化一那样,只是退出条件由答案为0改成了找到后面有相同的。
这样子做总复杂度0.75cq log b,比原来快,不过本题q不大,只是常数优化。
注意使用优化三也要配合优化二!

优化三的改进

我们来追求更快的判断一个询问是否答案为0。
注意到[l,r]如果答案为0,那么存在一个在[l-1,r-1]范围中的i满足next[i]<=r,由于满足next[i]<=r的i一定<=r-1,因此只需要满足i>=l-1。
题目没有强制我们在线,那么把[l,r]挂在l-1上,接着我们离线的处理所有询问。扫描线倒着扫,维护next的最小值,每次处理掉挂在左端点的所有询问,因为维护了next的最小值能很轻易的判断一个区间的答案是否为0。
那么总复杂度为0.75*0.2bq log b。

标程

#include<cstdio>
#include<algorithm>
#include<ctime>
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
using namespace std;
const int maxn=6000000+10,maxq=2000+10,len=15000,R=8000000*10;
char buf[R+7],*ptr=buf-1;
int sum[maxn],sta[100],next[maxn],last[maxn];
int father[maxn],key[maxn],tree[maxn][2],fix[maxn];
int ask[maxq][2],num[maxq],h[maxn],go[maxq],n2[maxq];
int i,j,k,l,r,t,n,m,p,q,ans,top,tot,root,mx,mi;
bool czy;
int read(){
    int x=0,c=*++ptr;
    while(c<48)c=*++ptr;
    while(c>47)x=x*10+c-48,c=*++ptr;
    return x;
}
void add(int x,int y){
    go[++tot]=y;
    n2[tot]=h[x];
    h[x]=tot;
}
int pd(int x){
    if (x==tree[father[x]][0]) return 0;else return 1;
}
void rotate(int x){
    int y=father[x],z=pd(x);
    father[x]=father[y];
    if (father[y]) tree[father[y]][pd(y)]=x;
    tree[y][z]=tree[x][1-z];
    if (tree[x][1-z]) father[tree[x][1-z]]=y;
    tree[x][1-z]=y;
    father[y]=x;
    if (root==y) root=x;
}
void insert(int x,int y,int v){
    if (!x){
        x=++tot;
        key[x]=v;
        tree[x][0]=tree[x][1]=0;
        father[x]=y;
        if (y){
            if (v<=key[y]) tree[y][0]=x;else tree[y][1]=x;
        }
        fix[x]=rand();
        return;
    }
    if (v<=key[x]){
        insert(tree[x][0],x,v);
        if (fix[tree[x][0]]<fix[x]) rotate(tree[x][0]);
    }
    else{
        insert(tree[x][1],x,v);
        if (fix[tree[x][1]]<fix[x]) rotate(tree[x][1]);
    }
}
void work(int l,int r,int id){
    int i,t,k;
    tot=root=0;
    insert(root,0,sum[l-1]);
    root=1;
    mx=mi=sum[l-1];
    ans=p;
    fo(i,l,r){
        if (ans==1) break;
        /*if (next[i]<=r){
            ans=0;
            break;
        }*/
        if (sum[i]<mi){
            if (sum[i]-mx+p<ans)
                ans=sum[i]-mx+p;
        }
        else{
            t=root;k=0;
            while (t){
                if (key[t]>sum[i]) t=tree[t][0];
                else{
                    if (key[t]>k) k=key[t];
                    t=tree[t][1];
                }
            }
            if (sum[i]-k<ans) ans=sum[i]-k;
        }
        if (sum[i]<mi) mi=sum[i];
        if (sum[i]>mx) mx=sum[i];
        insert(root,0,sum[i]);
    }
    num[id]=ans;
}
void write(int x){
    if (!x){
        putchar('0');
        putchar('\n');
        return;
    }
    top=0;
    while (x){
        sta[++top]=x%10;
        x/=10;
    }
    while (top){
        putchar('0'+sta[top]);
        top--;
    }
    putchar('\n');
}
int main(){
    freopen("history.in","r",stdin);freopen("history.out","w",stdout);
    fread(buf,1,R,stdin);
    srand(time(0));
    n=read();p=read();
    fo(i,1,n){
        t=read();
        sum[i]=(sum[i-1]+t)%p;
    }
    fo(i,0,p-1) last[i]=n+1;
    fd(i,n,0){
        next[i]=last[sum[i]];
        last[sum[i]]=i;
    } 
    q=read();
    tot=0;
    fo(i,1,q){
        ask[i][0]=read();ask[i][1]=read();
        add(ask[i][0]-1,i);
    }
    r=n+1;
    fd(i,n-1,0){
        if (next[i]<r) r=next[i];
        t=h[i];
        while (t){
            if (r>ask[go[t]][1]) work(ask[go[t]][0],ask[go[t]][1],go[t]);
            t=n2[t];
        }
    }
    fo(i,1,q) write(num[i]);
}

题目情况

80分:1人
70分:3人
60分:5人
0分:1人
平均分:59

想说的话

jason80分好厉害。毕竟两个优化才加我才给80,jason有其他蜜汁方法过了80。
Drin_E也厉害,fread+优化二三+线段树+蜜汁优化艹过了(考试后过的别误解)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值