关于某道题的优化
题意
给定一个长度为 $n$ 的序列 $a_i$,求有多少个区间满足每个数出现次数均为偶数。
$n\leq 3\times 10^4,a_i\leq 10^6$
思路
双指针扫描每个区间,拿个桶,$O(1)$ 更新状态,然后 $O(n^2)$,可以得到 $40$ 的好成绩。
代码一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include<bits/stdc++.h> #define y1 y3647 #define INF 1000000000 #define LL long long #define pii pair<int,int> using namespace std; template<typename _type> inline void read(_type &x){ x=0;int f=1;char ch=getchar(); while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar(); if(ch==45){f=-1,ch=getchar();} while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f; } template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;} template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;} const int N=30005; int i,j,k,n,s,t,m,ans; int a[N],b[N]; unsigned cnt[1024*1024]; signed main() { freopen("gf.in","r",stdin); freopen("gf.out","w",stdout); read(n); for(i=1;i<=n;i++)read(a[i]),b[i]=a[i]; int now=0; for(i=1;i<=n;i++){ memset(cnt,0,sizeof(cnt)); now=0; for(j=i;j<=n;j++){ if(cnt[a[j]])now+=(cnt[a[j]]&1)?1:-1; cnt[a[j]]+=1; ans+=now==0; } } cout<<ans<<endl; return 0; }
|
优化
1.高速缓存原理
计算机有个东西叫高速缓存,可以优化内存访问延迟。
一次独立的内存操作会读取一个 $64Byte$ 的 $cacheline$,从指令发出到从内存收到数据需要约 $200$ 个 $CPU$ 周期,而 $CPU$ 会将一些常用的数据塞进 $cache$,也就是高速缓存中,加速读取,寄存器的访问速度是最快的,只需要不到 $1$ 个时间周期,$L1$ 缓存需要约 $3$ 个时间周期,$L2$ 为 $10$ 个左右,$L3$ 为 $20$ 个周期。
具体时间视计算机本身有差别,但基本的比例是这个。
代码一中,我们只关注瓶颈。
1 2 3 4
| now+=-1+((cnt[a[j]]&1)<<1); now+=!cnt[a[j]]; cnt[a[j]]+=1; ans+=now==0;
|
其中,对于 cnt
的访问和对于 a
的访问,由于高速缓存并不大,所以塞不下 cnt
,因此我们每个循环都需要等一个 200
时间周期的延迟,这是相当致命的,实际上等待的延迟并没有 200
时间周期,因为 $CPU$ 将部分数据还是放进来 cache
,但具体放哪些是由 CPU
决定的。
对于这个致命的延迟,我们可以将数据离散化,然后就可以得到一个 75pts
的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include<bits/stdc++.h> #define y1 y3647 #define INF 1000000000 #define LL long long #define pii pair<int,int> using namespace std; template<typename _type> inline void read(_type &x){ x=0;int f=1;char ch=getchar(); while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar(); if(ch==45){f=-1,ch=getchar();} while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f; } template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;} template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;} const int N=30005; int i,j,k,n,s,t,m,ans; int a[N],b[N]; unsigned cnt[1024*30]; signed main() { freopen("gf.in","r",stdin); freopen("gf.out","w",stdout); read(n); for(i=1;i<=n;i++)read(a[i]),b[i]=a[i]; sort(b+1,b+n+1);m=unique(b+1,b+n+1)-b-1; for(i=1;i<=n;i++)a[i]=lower_bound(b+1,b+m+1,a[i])-b; int now=0; for(i=1;i<=n;i++){ memset(cnt,0,sizeof(cnt)); now=0; for(j=i;j<=n;j++){ if(cnt[a[j]])now+=cnt[a[j]]&1?1:-1; cnt[a[j]]+=1; ans+=now==0; } } cout<<ans<<endl; return 0; }
|
2.流水线模式和延迟隐藏
现代计算机各类硬件的设计都采用了流水线模式,将每条汇编指令的执行都划分为了不同的流水线。
也就是说 CPU 可以不间断的向内存发出访问指令,这些指令经过一定延迟后,内存会不间断的向CPU传数据,这个过程中,我们就只等待了一个内存延迟。打个比方,这玩意就像烧水,水壶很多,灌水和倒水的时间都很短,所以正确的方式是全部烧上等,而不是烧一个等一个。
所以,如果我们能够连续的发出内存访问指令,那么内存延迟可以被有效隐藏,但注意到代码中,访问了内存后执行了一些计算,而内存访问是依赖于这些计算的,这产生了一个依赖,我们必须等待计算完成之后才能进行访问,所以无法有效的隐藏延迟。
向内存写入数据也是需要等待延迟的。
3.流水线和依赖分析
上一部分简单介绍了流水线模式,在我们的 $CPU$ 中,也采用流水线的设计。
一条汇编指令的执行在 $CPU$ 上大致分为五个部分,分别是:取指,访(寄)存,计算,内存操作,写回。
$CPU$ 在这五个部分的设计上也采用了流水线,一条指令开始访存时,另一条指令的取指就开始了。
而如果下一条指令对前一条指令有数据依赖,那么 $CPU$ 会通过转发操作消除这种依赖,但如果下一条指令的内容对上一条指令有依赖(比如说 if),那么 $CPU$ 就不得不停止流水线,向流水线中插入气泡以等待。
这会极大降低 $CPU$ 的利用率,因此,内循环中的 $if$ 是相当不应该的。
4.流水线和分支预测
事实上,硬件的设计者注意到了这种依赖,而 $CPU$ 会对这种依赖做出预测,预测基于程序计数器的的原理和一些其它统计数据,正确率约在 $65\%$ 左右,如果分支预测出现错误,那么会花费两个时间周期去消除这个错误,所以我们需要通过算术方式避免分支。
方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include<bits/stdc++.h> #define y1 y3647 #define INF 1000000000 #define LL long long #define pii pair<int,int> using namespace std; template<typename _type> inline void read(_type &x){ x=0;int f=1;char ch=getchar(); while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar(); if(ch==45){f=-1,ch=getchar();} while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f; } template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;} template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;} const int N=30005; int i,j,k,n,s,t,m,ans; int a[N],b[N]; unsigned short cnt[1024*30]; signed main() { freopen("gf.in","r",stdin); freopen("gf.out","w",stdout); read(n); for(i=1;i<=n;i++)read(a[i]),b[i]=a[i]; sort(b+1,b+n+1);m=unique(b+1,b+n+1)-b-1; for(i=1;i<=n;i++)a[i]=lower_bound(b+1,b+m+1,a[i])-b; int now=0; for(i=1;i<=n;i++){ memset(cnt,0,sizeof(cnt)); now=0; for(j=i;j<=n;j++){ now+=-1+((cnt[a[j]]&1)<<1); now+=!cnt[a[j]]; cnt[a[j]]+=1; ans+=now==0; } } cout<<ans<<endl; return 0; }
|
目前是最快代码。