[2015 Regional Online]长春 C Aggregated Counting [打表找规律+分块打表]

╮(╯▽╰)╭看到标题,想吐槽的各位大虾请手下留情,因为我的专长也就是直觉瞎搞了——说实话我都不知道怎么来的这么好运,居然猜出来了╮(╯▽╰)╭
满满的打表,233333

慢慢说吧。
定义两个符号:
d[i] 表示原序列的第i项
last(n) 表示(题目里定义的),在原始序列中最后一次出现n的位置的下标(显然题目要求变成,求 last(last(n))
然后,草稿纸上随便写写,显然有:
last(n)=i=1nd[i]

本来计划的是,写一个能够快速计算n很大的时候的 last(n) ,于是我选择打表看看:

#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <math.h>
#include <algorithm>
using namespace std;
typedef long long ll;

int d[500005];

int main(){
    freopen("03table.txt","w",stdout);
    d[1]=1;
    d[2]=2;
    d[3]=2;
    int lastp=3;
    for(int i=3;lastp<=500000;i++){
        for(int j=0;j<d[i]&&lastp<=500000;j++){
            d[++lastp]=i;
        }
    }
    ll sum=0;
    for(int i=1;i<=500000;i++){
        sum+=d[i];
        sum%=1000000007;
        printf("%d %d %I64d\n",i,d[i],sum);
    }
    return 0;
}

然后我也回想不起当时想的是什么了(好像是分段存下d[i]为2、3、4…..的值的位置能产生的数的最后一个位置?反正是个冷静下来很没逻辑的行为……),于是,我把

sum+=d[i];

改写成:

sum+=d[i]*i;

然后打开表一看,好像我求出 last(last(n)) 了?
去洗手间冷静了一下,回来验算了,好像是真的!

于是我们现在有个全新目标,把 last(last(n)) 通过打表的方法,完成计算。
首先要解决一个问题:我们要知道 d[i] ,i 的范围是 10亿,这样的int数组爆内存了,开不下啊……
幸好我们有: last(n)=i=1nd[i] 。于是,借助d[i]的前缀和,我们可以对每个n,用二分查找定位出其 d[n] ,之后往上找也不需要每次重新二分,直接做个推断就行。
然后, d[n] 要预先算出多少才够呢?继续打表,找到 last(n)109 的n值,很高兴的发现, n=500000 的时候就足够了。
当然, n109 的情况下,全部打表很不现实,经典打表方案:分块打表——每隔d个,输出一个答案,之后每次查询期望复杂度 d/2
兴高采烈的写出如下打表程序:

#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <math.h>
#include <algorithm>
using namespace std;
typedef long long ll;

const int MAXN=500000;

int d[MAXN+5];
int sum[MAXN+5];

int main(){
    freopen("03tablecode.txt","w",stdout);
    printf("0,");
    d[1]=1;
    d[2]=2;
    d[3]=2;
    int lastp=3;
    for(int i=3;lastp<=MAXN;i++){
        for(int j=0;j<d[i]&&lastp<=MAXN;j++){
            d[++lastp]=i;
        }
    }
    for(int i=1;i<=MAXN;i++)
        sum[i]+=sum[i-1]+d[i];
    int p=1;
    ll presum=0;
    for(int i=1;i<=1000000000;i++){
        while(sum[p]<i)p++;
        presum+=(ll)p*(ll)i;
        presum%=1000000007;
        if(i%200000==0)printf("%I64d,",presum);
    }
    return 0;
}

注意,这里每隔10万项打一个值太多了,100KB的表不能提交;每隔20万项打一个值,表小了一半,可以顺利提交了。
然后把表往和上面的打表程序类似的代码里一贴,感觉期望复杂度可能运气好能过?一提交就TLE了……

然后仔细观察发现,其实我们并没有充分利用 d[i] 的前缀和 sum[i] ,已知 n 和其对应的 d[n] ,对于所有满足 sum[d[n]1]isum[d[n]] i ,显然这些 i 在最后结果中所乘的 d[i] 都是一样的,于是利用等差数列求和,合并起来,期望复杂度再/2000 ,结果,华丽的以50KB的代码,在 15ms 和 4MB 内存下解决了……

以下是代码:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <algorithm>
using namespace std;
typedef long long ll;

const int MAXDN=500000;

int d[MAXDN+5];
int table[5005]={};//表太大了,这里略去
void init(){
    d[1]=1;
    d[2]=2;
    d[3]=2;
    int lastp=3;
    for(int i=3;lastp<=MAXDN;i++){
        for(int j=0;j<d[i]&&lastp<=MAXDN;j++){
            d[++lastp]=i;
        }
    }
    for(int i=2;i<=MAXDN;i++)
        d[i]+=d[i-1];
}

int main(){
    init();
    int T,n;
    for(scanf("%d",&T);T--;){
        scanf("%d",&n);
        int goal=n/200000*200000;
        int p=lower_bound(d+1,d+MAXDN+1,goal)-d;

        int presum=table[n/200000];
        for(int i=goal+1;i<=n;i++){
            while(d[p]<i)p++;

            //等差数列求和
            int obs=(min(d[p],n)-i+1);
            int fe=i+min(d[p],n);
            ll tmp=(ll)fe*obs/2;
            tmp%=1000000007;
            //乘相应的d[i]并取模,跳到下一段
            presum+=(1LL*p*tmp)%1000000007;
            if(presum>1000000007)presum-=1000000007;
            i=d[p]; p++;
        }
        printf("%d\n",presum);
    }
    return 0;
}

你可能感兴趣的:(网络赛,regional,打表,ACMICPC,2015长春)