2018.12.5【WC2017】【LOJ2286】【洛谷P4604】挑战(卡常)

版权声明:转载请声明出处,谢谢配合。 https://blog.csdn.net/zxyoi_dreamer/article/details/84828291

洛谷传送门

LOJ传送门


解析:

目前LOJ速度rank1。但是洛谷上面有两个40msAC的在我前面(空间还小的出奇,估计连排序的数组都存不下)。。。估计是面向数据编程。。

说明:博主是一个高一OIER,没有认真学过大学计算机科学课程,如果题解中有任何地方不严谨,请在评论区告知博主。


Subtask1:

首先这个排序用 O ( n log n ) O(n\log n) 的快排做是妥妥的超时,而且不便于常数优化。

桶排也算了,空间开不下。

所以我们会用到与桶排类似的一种算法,基数排序。

不知道什么是基数排序的自行百度。

它的复杂度是 O ( n m a x ( a i ) / b a s e ) O(n\cdot max(a_i)/base) ,其中 b a s e base 是选取的基数大小,一般来说在不超过内存限制的情况下,选择的基数越大排序越快。

注意是一般情况下,不包括需要卡常数的时候。

然后考虑选择什么样的基数,由于目标大小是 2 32 1 2^{32}-1 以内,所以我们可以选择 2 16 2^{16} 或者 2 8 2^8 作为基数进行排序。( 2 4 2^4 太小,排序次数有点多了,不考虑)。

那么问题来了,选择哪个?

你可能觉得选择 2 16 2^{16} 作为基数只需要排两次就能出结果,应该比 2 8 2^8 排四次要快吧?

大错特错!常数要是算的这么简单,为什么大学还要学计算机组成原理?

要明白这个,首先引入一个东西,高速缓存器。


高速缓存器(Cache):

一下内容摘自百度百科

高速缓冲存储器(Cache)其原始意义是指存取速度比一般随机存取记忆体(RAM)来得快的一种RAM,一般而言它不像系统主记忆体那样使用DRAM技术,而使用昂贵但较快速的SRAM技术,也有快取记忆体的名称。
高速缓冲存储器是存在于主存与CPU之间的一级存储器, 由静态存储芯片(SRAM)组成,容量比较小但速度比主存高得多, 接近于CPU的速度。在计算机存储系统的层次结构中,是介于中央处理器和主存储器之间的高速小容量存储器。它和主存储器一起构成一级的存储器。高速缓冲存储器和主存储器之间信息的调度和传送是由硬件自动进行的。
高速缓冲存储器最重要的技术指标是它的命中率。

注意最后一句话

高速缓冲存储器最重要的技术指标是它的命中率。

那么关于命中率的相关概念也请自己去百度百科里面了解,或者感性理解如下:

高速缓存器里面存了一段内存的东西,可以通过高速缓存器快速访问这段内存里面的任何一个元素。

一旦访问内存外的元素,Cache就会看情况选择是否清空并重新装填内存,而在重新装填次数尽可能少的同时尽可能多的访问Cache里面的东西,就能手动提高它的命中率。


那么我们的目的就是使得访问(准确说是查询,不考虑修改)的内存尽可能的相邻,如果你选择的桶的大小是 2 16 = 65536 2^{16}=65536 ,就卡不进一级缓存,不能尽可能利用Cache带来的优势。

所以选择桶的大小为 2 8 = 256 2^8=256 ,Cache肯定能够存下的大小,随便过吧。

实际测试中 2 16 2^{16} 的桶根本过不去。

具体实现请看文章末尾代码中的namespace Sorting


Subtask2:

首先不要考虑用数据结构维护了吧。。。刻意造的数据你根本维护不了任何有用的东西。

这个显然是 O ( n q ) O(nq) 的复杂度,那么我们继续考虑如何优化。

首先考虑一个东西:循环展开


循环展开:

比如说我们要求一个数列的和,一般的写法是这样:

long long sum=0;
for(int i=1;i<=n;++i)sum+=a[i];
return sum;

但是循环展开后是这样写的:

long long sum1=0,sum2=0,sum3=0,sum4=0,sum5=0,sum6=0,sum7=0,sum8=0;

for(int i=0;i+8<=n;i+=8){
	sum1+=a[i+1];
	sum2+=a[i+2];
	sum3+=a[i+3];
	sum4+=a[i+4];
	sum5+=a[i+5];
	sum6+=a[i+6];
	sum7+=a[i+7];
	sum8+=a[i+8];
}
switch(n&7){
	case 7:sum7+=a[n-6];
	case 6:sum6+=a[n-5];
	case 5:sum5+=a[n-4];
	case 4:sum4+=a[n-3];
	case 3:sum3+=a[n-2];
	case 2:sum2+=a[n-1];
	case 1:sum1+=a[n]; 
}

return sum1+sum2+sum3+sum4+sum5+sum6+sum7+sum8;

这样写有什么好处呢?也就是说,为什么这样写要快那么多呢?

那么我们回到计算机执行程序的本质:存储,查询和计算。

其中存储没有什么可以在时间上产生太多优化的做法,卡空间常数并不会对时间产生过多影响。

查询上的优化主要就是 S u b t a s k 1 Subtask1 中用到的卡高速缓冲器和常用的卡register寄存器。

那么优化的主要目的就到了计算上面。
听说过一种做法似乎可以把整型和实型的四则运算常数优化10倍,没学过,而且听说码量略大,不予考虑。

那么我们就用到了循环展开。

考虑我们计算的步骤(需要循环的算法)

1.初始化
2.进行一次循环中的操作
3.进入下一次循环

一下的讨论假设循环变量为 i i
我们发现,每一次计算下一个循环中的东西时候,需要修改 i i
所以说,下一次的 i i 是与这一次的修改相关的

那么要调用下一个循环中的 i i 实际上需要这一次的修改。

不要为难编译器,它也无法预测下一次的 i i 会不会变成什么奇怪的东西,所以它只能一个步骤一个步骤的执行。

那么我们可以明确告诉它接下来的几个操作中所要用到的 i i 与现在的 i i 有什么关系。从而让它能够知道接下来该干什么,让CPU以一定概率同时执行这些操作中的好几个,这就是CPU并发,也是循环展开的终极目的。

至于为什么能够让CPU做到这样,读者可以自行了解,详细的叙述已经偏离了本文的目的。这里只稍微提一下,一般来说CPU中是有多个运算器的,也就是多核心,让这么多运算器睡大觉真是一种资源的浪费啊。

Deltail:

一般来说,循环展开只需要展开6~8层就已经够了,多了的话可能造成寄存器溢出从而反使程序的运行速度变慢。至于为什么是寄存器,请读者自己了解,这里不再过多展开。

再来看两种不够优秀的写法,但是也有优化作用:

1.只用一个sum,不能充分刺激CPU并发。

long long sum=0;

for(int i=0;i+8<=n;i+=8){
	sum+=a[i+1];
	sum+=a[i+2];
	sum+=a[i+3];
	sum+=a[i+4];
	sum+=a[i+5];
	sum+=a[i+6];
	sum+=a[i+7];
	sum+=a[i+8];
}
switch(n&7){
	case 7:sum+=a[n-6];
	case 6:sum+=a[n-5];
	case 5:sum+=a[n-4];
	case 4:sum+=a[n-3];
	case 3:sum+=a[n-2];
	case 2:sum+=a[n-1];
	case 1:sum+=a[n]; 
}

return sum;

2.展开的时候用了++i,也是不能充分刺激CPU并发。

long long sum1=0,sum2=0,sum3=0,sum4=0,sum5=0,sum6=0,sum7=0,sum8=0;

for(int i=0;i+8<=n;){
	sum1+=a[++i];
	sum2+=a[++i];
	sum3+=a[++i];
	sum4+=a[++i];
	sum5+=a[++i];
	sum6+=a[++i];
	sum7+=a[++i];
	sum8+=a[++i];
}
switch(n&7){
	case 7:sum7+=a[n-6];
	case 6:sum6+=a[n-5];
	case 5:sum5+=a[n-4];
	case 4:sum4+=a[n-3];
	case 3:sum3+=a[n-2];
	case 2:sum2+=a[n-1];
	case 1:sum1+=a[n]; 
}

return sum1+sum2+sum3+sum4+sum5+sum6+sum7+sum8;

TIPS:关于上述代码中的switch

懂上面为什么这样写的可以跳过这一段不看。

switch内部有两种可以用的关键字:case和default,其中case后面还需要跟一个常量表达式。每次switch进入大括号的时候,直接根据选择分支跳到相应的位置。

然后按照顺序一直执行到switch的末尾,除非遇到break。

所以上面的循环展开就写成了那个样子。(并且减少了多次 i f if 判断)


但是光是循环展开是不能把卡常数做到极致的,对于这种连续区间型 b o o l bool 计数(博主瞎yy的一种叫法,好记又好理解),我们可以用压位。


压位:

由于是 b o o l bool 型计数,所以我们可以压位乱搞。

s 1   s 2 s1\text{ }s2 分别设置两个数组 f 1   f 2 f1\text{ }f2 ,每一个字符串在数组位置上占有3个位置,分别表示它出石头,剪刀或布,查询的时候两个部分的拿出来一起搞就行了,直接用与运算判断是否有东西,然后统计位数就行了。

统计位数平时可以直接用 O ( 1 ) O(1) 的__builtin_popcountll(C++STL里面的函数),或者自己预处理256以内的数的位数个数,然后每个数分四段用位移运算取得每一段统计一下就行了。


具体实现请看文章末尾的namespace Game
听说有 O ( n 1.5 log n ) O(n^{1.5}\log n) (在洛谷讨论区看见的)FFT的做法,没有这种优化过的 O ( n q ) O(nq) 快(话说FFT本身就不利于常数优化),不讲了。(可以自己去LOJ看xumingkuan大佬的代码


Subtask3:

一般的做法就是直接在DP数组上面用一个指针扫动,遇到 ? ? 就把后面所有的全部更新一遍,其实就是考虑上一个位置为‘)’和‘(’的情况就行了。

但是这个还能优化:并非所有下标都可以达到。

所以我们只需要在遇到‘(’和‘)’的时候移动指针判断一下奇偶性就行了(不想搞奇偶性可以直接朴素写法+奇偶性卡常,区别不大)。

具体实现参考文章末尾的namespace Parentheses


一个非常实用并且常用的卡常技巧:指针优化寻址

一般来说,如下两种方式都声明了一个大小为100的int数组,没有任何区别

int x[100];
int *const y=new int[100];

所以说,数组名实际上是数组的头指针。

同理,对于指针,我们也可以用[]运算符来寻址,这样就能解释为什么负数下标是允许的了。

一下两种方式都是表示在数组 x x 中的第 i i 个对象

x[i];
*(x+i);

那么,在访问一个数组的时候,我们可以考虑用一个指针扫一遍。

这个卡常主要就是卡一个加法的常数,因为f[0]的访问是比*f访问指针指向的第一个元素快的。


代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define re register
#define gc getchar
#define pc putchar
#define cs const

#define u32 unsigned int
#define u64 unsigned ll

inline u32 getint(){
    re u32 num;
    re char c;
    while(!isdigit(c=gc()));num=c^48;
    while(isdigit(c=gc()))num=(num+(num<<2)<<1)+(c^48);
    return num;
}

inline void skip(){while(isspace(cin.peek()))gc();}

#define nxt_integer(x) (x^=x<<13,x^=x>>17,x^=x<<5)

inline void output_arr(u32 * a,u32 size){
    re u32 ret=size;--a;
    for(u32 re x=23333333,*to=a+(size>>2)+1;++a<to;)
    ret^=*a+x,nxt_integer(x);
    printf("%u\n",ret);
}

namespace Sorting{
    u32 a[200000000],b[200000000];
    int cnt0[1<<8],cnt8[1<<8],cnt16[1<<8],cnt24[1<<8];
    int n;u32 seed;
    
    inline void init_data(){
        for(u32 re *to=a+n,*now=a;now<to;++now){
            *now=nxt_integer(seed);
            ++cnt0[seed&255];
            ++cnt8[seed>>8&255];
            ++cnt16[seed>>16&255];
            ++cnt24[seed>>24&255];
        }
    }
    
    inline void sort(){
        for(int re i=1;i<256;++i){
            cnt0[i]+=cnt0[i-1];
            cnt8[i]+=cnt8[i-1];
            cnt16[i]+=cnt16[i-1];
            cnt24[i]+=cnt24[i-1];
        }
        
        re int rep=n>>3,tim=rep;
        re u32 *now=a+n-1;
        while(tim--){
            b[--cnt0[now[0]&255]]=now[0];
            b[--cnt0[now[-1]&255]]=now[-1];
            b[--cnt0[now[-2]&255]]=now[-2];
            b[--cnt0[now[-3]&255]]=now[-3];
            b[--cnt0[now[-4]&255]]=now[-4];
            b[--cnt0[now[-5]&255]]=now[-5];
            b[--cnt0[now[-6]&255]]=now[-6];
            b[--cnt0[now[-7]&255]]=now[-7];now-=8;
        }
        switch(n&7){
            case 7:{b[--cnt0[*now&255]]=*now;--now;}
            case 6:{b[--cnt0[*now&255]]=*now;--now;}
            case 5:{b[--cnt0[*now&255]]=*now;--now;}
            case 4:{b[--cnt0[*now&255]]=*now;--now;}
            case 3:{b[--cnt0[*now&255]]=*now;--now;}
            case 2:{b[--cnt0[*now&255]]=*now;--now;}
            case 1:{b[--cnt0[*now&255]]=*now;--now;}
        }
        tim=rep;
        now=b+n-1;
        while(tim--){
            a[--cnt8[now[0]>>8&255]]=now[0];
            a[--cnt8[now[-1]>>8&255]]=now[-1];
            a[--cnt8[now[-2]>>8&255]]=now[-2];
            a[--cnt8[now[-3]>>8&255]]=now[-3];
            a[--cnt8[now[-4]>>8&255]]=now[-4];
            a[--cnt8[now[-5]>>8&255]]=now[-5];
            a[--cnt8[now[-6]>>8&255]]=now[-6];
            a[--cnt8[now[-7]>>8&255]]=now[-7];now-=8;
        }
        switch(n&7){
            case 7:{a[--cnt8[*now>>8&255]]=*now;--now;}
            case 6:{a[--cnt8[*now>>8&255]]=*now;--now;}
            case 5:{a[--cnt8[*now>>8&255]]=*now;--now;}
            case 4:{a[--cnt8[*now>>8&255]]=*now;--now;}
            case 3:{a[--cnt8[*now>>8&255]]=*now;--now;}
            case 2:{a[--cnt8[*now>>8&255]]=*now;--now;}
            case 1:{a[--cnt8[*now>>8&255]]=*now;--now;}
        }
        tim=rep;
        now=a+n-1;
        while(tim--){
            b[--cnt16[now[0]>>16&255]]=now[0];
            b[--cnt16[now[-1]>>16&255]]=now[-1];
            b[--cnt16[now[-2]>>16&255]]=now[-2];
            b[--cnt16[now[-3]>>16&255]]=now[-3];
            b[--cnt16[now[-4]>>16&255]]=now[-4];
            b[--cnt16[now[-5]>>16&255]]=now[-5];
            b[--cnt16[now[-6]>>16&255]]=now[-6];
            b[--cnt16[now[-7]>>16&255]]=now[-7];now-=8;
        }
        switch(n&7){
            case 7:{b[--cnt16[*now>>16&255]]=*now;--now;}
            case 6:{b[--cnt16[*now>>16&255]]=*now;--now;}
            case 5:{b[--cnt16[*now>>16&255]]=*now;--now;}
            case 4:{b[--cnt16[*now>>16&255]]=*now;--now;}
            case 3:{b[--cnt16[*now>>16&255]]=*now;--now;}
            case 2:{b[--cnt16[*now>>16&255]]=*now;--now;}
            case 1:{b[--cnt16[*now>>16&255]]=*now;--now;}
        }
        tim=rep;
        now=b+n-1;
        while(tim--){
            a[--cnt24[now[0]>>24]]=now[0];
            a[--cnt24[now[-1]>>24]]=now[-1];
            a[--cnt24[now[-2]>>24]]=now[-2];
            a[--cnt24[now[-3]>>24]]=now[-3];
            a[--cnt24[now[-4]>>24]]=now[-4];
            a[--cnt24[now[-5]>>24]]=now[-5];
            a[--cnt24[now[-6]>>24]]=now[-6];
            a[--cnt24[now[-7]>>24]]=now[-7];now-=8;
        }
        switch(n&7){
            case 7:{a[--cnt24[*now>>24]]=*now;--now;}
            case 6:{a[--cnt24[*now>>24]]=*now;--now;}
            case 5:{a[--cnt24[*now>>24]]=*now;--now;}
            case 4:{a[--cnt24[*now>>24]]=*now;--now;}
            case 3:{a[--cnt24[*now>>24]]=*now;--now;}
            case 2:{a[--cnt24[*now>>24]]=*now;--now;}
            case 1:{a[--cnt24[*now>>24]]=*now;--now;}
        }
    }
    
    inline void main(){
        n=getint();
        seed=getint();
        init_data();
        sort();
        output_arr(a,n<<2);
    }
}

namespace Game{
    
    ll f1[64][14063],f2[64][14063];
    char s1[300000],s2[300000];
    u32 x[300000],y[300000],len[300000],ans[300000];
    int n,q;
    
    inline void set(ll f[][14063],int idx){
        if(idx<64)for(int re i=0;i<=idx;++i)*f[i]|=1ll<<(idx-i);
        else for(int re i=0;i<64;++i){
            re int j=idx-i;
            f[i][j>>6]|=1ll<<(j&63);
        }
    }
    
    inline void solve(){
        for(int re i=0;i<n;++i)
        switch(s1[i]){
            case '0':set(f1,i*3);break;
            case '1':set(f1,i*3+1);break;
            case '2':set(f1,i*3+2);
        }
        for(int re i=0;i<n;++i)
        switch(s2[i]){
            case '0':set(f2,i*3+2);break;
            case '1':set(f2,i*3);break;
            case '2':set(f2,i*3+1);break;
        }
        for(int re i=0;i<q;++i){
            x[i]*=3;y[i]*=3;len[i]*=3;
            re int l=len[i]>>6,tim=l>>3;
            re ll *p1=f1[x[i]&63]+(x[i]>>6),*p2=f2[y[i]&63]+(y[i]>>6);
            re u32 ans0=0,ans1=0,ans2=0,ans3=0,ans4=0,ans5=0,ans6=0,ans7=0;
            while(tim--){
                ans0+=__builtin_popcountll(p1[0]&p2[0]);
                ans1+=__builtin_popcountll(p1[1]&p2[1]);
                ans2+=__builtin_popcountll(p1[2]&p2[2]);
                ans3+=__builtin_popcountll(p1[3]&p2[3]);
                ans4+=__builtin_popcountll(p1[4]&p2[4]);
                ans5+=__builtin_popcountll(p1[5]&p2[5]);
                ans6+=__builtin_popcountll(p1[6]&p2[6]);
                ans7+=__builtin_popcountll(p1[7]&p2[7]);
                p1=p1+8;p2=p2+8;
            }
            switch(l&7){
                case 7:ans7+=__builtin_popcountll(p1[6]&p2[6]);
                case 6:ans6+=__builtin_popcountll(p1[5]&p2[5]);
                case 5:ans5+=__builtin_popcountll(p1[4]&p2[4]);
                case 4:ans4+=__builtin_popcountll(p1[3]&p2[3]);
                case 3:ans3+=__builtin_popcountll(p1[2]&p2[2]);
                case 2:ans2+=__builtin_popcountll(p1[1]&p2[1]);
                case 1:ans1+=__builtin_popcountll(p1[0]&p2[0]);
                p1+=l&7,p2+=l&7;
            }
            ans[i]=ans0+ans1+ans2+ans3+ans4+ans5+ans6+ans7+__builtin_popcountll(*p1&*p2&(1ll<<(len[i]&63))-1);
        }
    }
    
    inline void main(){
        n=getint(),q=getint();
        skip();
        fread(s1,1,n,stdin);
        skip();
        fread(s2,1,n,stdin);
        for(int re i=0;i<q;++i)x[i]=getint(),y[i]=getint(),len[i]=getint();
        solve();
        output_arr(ans,q<<2);
    }
}

namespace Parentheses{
    
    u32 pool[266666<<1|1],*p=pool+266666;
    char s[266666];
    int n;
    
    inline u32 solve(){
        *p=1;
        for(int re i=0;i<n;++i)
        switch(s[i]){
            case '(':if(i&1)*--p=0;break;
            case ')':p+=i&1^1;break;
            case '?':{
                int m=(min(i,n-i)>>1)+1;
                if(i&1)*--p=0,++m;
                re int tim=m>>3;
                re u32 *f0=p;
                while(tim--){
                    f0[0]+=f0[1];++f0;
                    f0[0]+=f0[1];++f0;
                    f0[0]+=f0[1];++f0;
                    f0[0]+=f0[1];++f0;
                    f0[0]+=f0[1];++f0;
                    f0[0]+=f0[1];++f0;
                    f0[0]+=f0[1];++f0;
                    f0[0]+=f0[1];++f0;
                }
                switch(m&7){
                    case 7:f0[0]+=f0[1];++f0;
                    case 6:f0[0]+=f0[1];++f0;
                    case 5:f0[0]+=f0[1];++f0;
                    case 4:f0[0]+=f0[1];++f0;
                    case 3:f0[0]+=f0[1];++f0;
                    case 2:f0[0]+=f0[1];++f0;
                    case 1:f0[0]+=f0[1];++f0;
                }
            }
        }
        return *p;
    }
    
    inline void main(){
        n=getint();
        skip();
        fread(s,1,n,stdin);
        printf("%u",solve());
    }
}

int op;
signed main(){
    op=getint();
    switch(op){
        case 1:Sorting::main();break;
        case 2:Game::main();break;
        case 3:Parentheses::main();break;
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/zxyoi_dreamer/article/details/84828291