这里是 $THUWC$ 选拔时间
模拟赛的时候犯 $SB$ 了,写了所有的部分分,然后直接跑过了 $4$ 个大样例(一个大样例是一个特殊情况)……
我还以为我非常叼,部分分都写对了,于是就不管了……
离考试结束还有 $10$ 分钟的时候才发现……程序跑暴力的条件写的是 $$if(n<=5000)space force::solve();$$
由于 $4$ 个大样例的 $n$ 都只有几百,我之前测的全是暴力……
然后赶紧改改改,测了三个部分分的程序,都小错不断……
于是最后调不完了,自闭 -_-。当做是一次教训吧。
题解
这道题的部分分给得很足,所以这里都说一下。
另外,我在下文直接把“新生舞会的数”简称为“众数”,虽然两者定义不一样,但我想少打点字。
type=1
只要区间内 $0,1$ 的数量不相等,就一定有一个众数($type=3$ 的部分会提到这叫“绝对众数”)。
所以把 $1$ 看成 $1$,$0$ 看成 $-1$,就变成问有多少对不相等的前缀和。
把所有前缀和桶排后随便乘一乘即可。
type=2
区间的众数不会出现超过 $15$ 次,也就是说满足要求的区间长度不会超过 $29$。
暴力枚举所有长度为 $1$ 到 $29$ 的区间即可。
type=3
此时只有 $8$ 种众数。
由于一个区间内只有一个众数(绝对众数),所以我们可以枚举众数,然后找这个数是哪些区间的众数。
当设众数为 $x$ 时,把 $x$ 记为 $1$,其它数记为 $-1$,那么就是要找所有和 $ge 1$ 的区间。
改成线性推:假设第 $1$ 到 $i$ 位的和为 $y$(也就是第 $i$ 位的前缀和),那就要快速查找前面有多少和 $lt y$ 的前缀。
这就是线段树维护前缀和的裸题了。
1 #include<bits/stdc++.h> 2 #define N 500005 3 #define ll long long 4 #define rep(i,x,y) for(int i=x;i<=y;++i) 5 #define lc o<<1 6 #define rc o<<1|1 7 using namespace std; 8 inline int read(){ 9 int x=0; bool f=1; char c=getchar(); 10 for(;!isdigit(c);c=getchar()) if(c=='-') f=0; 11 for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^'0'); 12 if(f) return x; 13 return 0-x; 14 } 15 int n,type,a[N]; 16 namespace force{ 17 ll ans; 18 int cnt[2001][2001]; 19 void solve(){ 20 rep(i,1,n){ 21 int mx=0; 22 for(int j=0;i+j<=n;++j){ 23 mx=max(mx,++cnt[i][a[i+j]]); 24 if(mx*2>j+1) ++ans; 25 } 26 } 27 cout<<ans<<endl; 28 } 29 }; 30 namespace type2{ 31 ll ans; 32 struct Tree{int siz; bool clr;}tr[N<<2]; 33 int v; 34 inline void pushdown(int o){ 35 if(tr[o].clr) 36 tr[lc].siz=tr[rc].siz=0, 37 tr[lc].clr=tr[rc].clr=1, 38 tr[o].clr=0; 39 } 40 int ins(int o,int l,int r){ 41 ++tr[o].siz; 42 if(l==r) return tr[o].siz; 43 pushdown(o); 44 int mid=(l+r)>>1; 45 if(v<=mid) return ins(lc,l,mid); 46 else return ins(rc,mid+1,r); 47 } 48 void solve(){ 49 rep(i,1,n){ 50 tr[1].siz=0, tr[1].clr=1; 51 int mx=0; 52 for(int j=0; j<29 && i+j<=n; ++j){ 53 v=a[i+j]; 54 mx=max(mx,ins(1,0,n-1)); 55 if(mx*2>j+1) ++ans; 56 } 57 } 58 cout<<ans<<endl; 59 } 60 }; 61 namespace type3{ 62 ll ans; 63 int L=-500000,R,v; 64 struct Tree{ 65 int siz[N<<2]; 66 void ins(int o,int l,int r){ 67 ++siz[o]; 68 if(l==r) return; 69 int mid=(l+r)>>1; 70 if(v<=mid) ins(lc,l,mid); 71 else ins(rc,mid+1,r); 72 } 73 int query(int o,int l,int r){ 74 if(L<=l && r<=R) return siz[o]; 75 int mid=(l+r)>>1, ret=0; 76 if(L<=mid) ret+=query(lc,l,mid); 77 if(R>mid) ret+=query(rc,mid+1,r); 78 return ret; 79 } 80 }tr[8]; 81 82 int sum[8]; 83 void solve(){ 84 rep(i,0,7) v=0, tr[i].ins(1,-500000,500000); 85 rep(i,1,n){ 86 rep(j,0,7) --sum[j]; 87 sum[a[i]]+=2; 88 rep(j,0,7){ 89 R=sum[j]-1; 90 if(L<=R) ans+=tr[j].query(1,-500000,500000); 91 v=sum[j]; 92 tr[j].ins(1,-500000,500000); 93 } 94 } 95 cout<<ans<<endl; 96 } 97 }; 98 int main(){ 99 n=read(),type=read(); 100 rep(i,1,n) a[i]=read(); 101 if(type==0) force::solve(); 102 else if(type==1 || type==3) type3::solve(); 103 else type2::solve(); 104 return 0; 105 }
100pts
1.$O(n imes log^{2}n)$
看到这种统计区间数的问题,考虑不沿用部分分的方法,而分治处理。
然后对于一个区间 $[l,r]$,我们只需要统计经过其中点 $mid$ 的满足条件的区间数,未经过中点的区间都在两边,可以分治下去解决。
那枚举哪些数为众数呢?
不难发现,如果一个数是某个经过中点 $mid$ 的区间的众数的话,把该区间沿中点拆成两半,这个数至少也是一半区间的众数。
所以我们可以预处理出从中点出发、往左和往右延伸区间,找到所有可能的众数(当然它们要不同)。
然后有人可能认为:这样不是暴力取众数吗?不同的众数的数量级别不可能是 $O(n)$ 的吗?
实际上这样找的话,假设区间长度是 $len$,不同的众数最多有 $O(log(len))$ 个。也就是说量级是 $log$ 级别的。
$why???$
我们考虑最坏情况,也就是说往其中一边延伸的区间中,所有数都是众数。
由于这里“众数”的特殊定义,区间越长,众数所需要出现的次数就越多。
那需要出现多少次呢?
根据题意推理,区间延伸的长度为 $1$ 时,众数的出现次数要大于 $0$;延伸的长度为 $2$ 时,众数的出现次数要大于 $1$;延伸的长度为 $4$ 时,众数的出现次数要大于 $2$;延伸的长度为 $8$ 时,众数的出现次数要大于 $4$……
把所有要求都 $-1$(可以证明这依然满足众数过半的条件),就变成了:区间延伸的长度为 $0$ 时,众数的出现次数要要至少为 $0$;延伸的长度为 $1$ 时,众数的出现次数要至少为 $1$;延伸的长度为 $3$ 时,众数的出现次数要至少为 $2$;延伸的长度为 $7$ 时,众数的出现次数要至少为 $4$……
有没有发现这规律跟二进制位很像?
比如说,这样的一个序列 $$1space 2space 2space 3space 3space 3space 3space 4space 4space 4space 4space 4space 4space 4space 4space 5...$$ 它刚好卡着数量要求,使所有数都是众数。
需求量以这样的增长速度,不同的众数显然只有 $log$ 级别个了。
这是最坏情况,即区间内所有的数都是众数。把这个序列任意改数,不同的众数只会少不会多。可以意会一下,具体证明感觉说不清楚。
而该这个序列的数能组成其它任意情况的序列,所以不同的众数的数量最多只有 $log$ 级别。
之前说过,每个区间只有一个绝对众数,所以可以独立考虑每个众数是哪些区间的众数。
于是枚举每个众数,然后考虑怎么快速计数。
有了归并排序的经验(其实并没什么关系),很容易写出判定式子。对于一个区间 $[l,r]$,设 $cnt_l,cnt_r$ 分别表示当前众数在区间 $[l,mid]$ 和区间 $[mid+1,r]$ 的出现次数,则当满足 $$r-l+1lt (cnt_l+cnt_r) imes 2$$ 时,当前枚举的众数是这个区间的众数,即对答案有 $1$ 的贡献。
移项得到 $$r-cnt_r imes 2+1lt l+cnt_l imes 2$$,方便按位维护。
所以枚举经过中点 $mid$ 的区间的右端点 $r$ 时,其实就是找一个与左端点 $l$ 相关的后缀和(有多少个数比某个值大)。遍历左半区间的左端点 $l$ 后,推一次后缀和就行了。(后缀和……就是反向前缀和)
啥?暴力推后缀和?后缀和的范围最大是多少?
显然, $l$ 和 $cnt$ 都是 $n$ 级别的,所以 $l+cnt imes 2$ 的范围最大是 $3n$ 级别的。
分治的层数复杂度是 $O(log(n))$,每层中枚举众数的复杂度是 $O(log(n))$(其实比这个大,但层数越小越趋近于这个),每层在左半区间遍历左端点 $l$ 加上在右半区间遍历右端点 $r$ 的复杂度是 $O(n)$,总复杂度是 $O(n imes log^2(n))$。
这里说明一下,$l+cnt imes 2$ 有可能小于 $0$,但我写的代码直接存在了负数下标位,理论上这样可能会出事(负数下标的指针有可能指向其它数组,然后导致指向的数组被无端改了数),不过我交上去过了就没管了……建议把这个值统一加个常数,存在自然数位,保证不会出事。
1 #include<bits/stdc++.h> 2 #define rep(i,x,y) for(int i=x;i<=y;++i) 3 #define dwn(i,x,y) for(int i=x;i>=y;--i) 4 #define ll long long 5 #define N 500001 6 #define inf 2147483647 7 using namespace std; 8 inline int read(){ 9 int x=0; bool f=1; char c=getchar(); 10 for(;!isdigit(c);c=getchar()) if(c=='-') f=0; 11 for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^'0'); 12 if(f) return x; 13 return 0-x; 14 } 15 int n,type,a[N]; ll ans; 16 int lsh[N],cnt[N*3],sum[N]; 17 bool vis[N]; 18 void solve(int l,int r){ 19 if(l==r){++ans; return;} 20 int mid=(l+r)>>1; 21 solve(l,mid), solve(mid+1,r); 22 int m=0; 23 dwn(i,mid,l){ 24 if((++cnt[a[i]])*2>mid-i+1) 25 if(!vis[a[i]]) vis[a[i]]=1, lsh[++m]=a[i]; 26 } 27 dwn(i,mid,l) 28 cnt[a[i]]=0; 29 rep(i,mid+1,r){ 30 if((++cnt[a[i]])*2>i-mid) 31 if(!vis[a[i]]) vis[a[i]]=1, lsh[++m]=a[i]; 32 } 33 rep(i,mid+1,r) 34 cnt[a[i]]=0; 35 rep(i,1,m){ 36 //printf("yes:%d ",lsh[i]); 37 int sum=0,mn=inf,mx=-inf; 38 dwn(j,mid,l){ 39 if(lsh[i]==a[j]) ++sum; 40 ++cnt[j+sum*2]; 41 //printf("%d ",j+sum*2); 42 mx=max(mx,j+sum*2), mn=min(mn,j+sum*2); 43 } 44 dwn(j,mx-1,mn) cnt[j]+=cnt[j+1]; 45 sum=0; int x; 46 //printf("que:"); rep(j,mn,mx) printf("%d ",cnt[j]); putchar(' '); 47 rep(j,mid+1,r){ 48 if(lsh[i]==a[j]) ++sum; 49 //printf("%d:%d ",j,j-sum*2+2); 50 x=j-sum*2+2; if(x>mx) continue; if(x<mn) x=mn; 51 ans+=cnt[x]; //printf("%d ",ans); 52 } 53 //printf("faq:%d ",ans); 54 vis[lsh[i]]=0; 55 rep(j,mn,mx) cnt[j]=0; 56 } 57 //printf(" %d %d %d ",l,r,ans); 58 } 59 int main(){ 60 n=read(),type=read(); 61 rep(i,1,n) a[i]=read(); 62 solve(1,n); 63 cout<<ans<<endl; 64 return 0; 65 } 66 /* 67 20 0 68 163 29 29 135 29 29 50 29 85 44 85 135 241 135 135 135 50 50 50 34 69 7 0 70 1 1 1 2 2 2 3 71 */
2.$O(n imes log(n))$
考虑优化 $type=3$ 的做法。
本来我们要对每一种众数都开一棵线段树,维护把其看成 $1$、把其它数看成 $-1$ 时的每个前缀和,但如果对序列的数没有限制的话这样显然炸了。
我们考虑一下为什么会炸。
其实就是状态太多了,最多有 $50w imes 50w$ 大小呢。
可序列里最多只有 $50w$ 个数啊!好像每个数都不会出现很多次。
对于一个没出现很多次的数,它所对应的序列 好像会出现很多 $-1$?
然后就会发现,我们把众数看成 $1$,其它数看成 $-1$ 后,序列的 $50w imes 50w$ 个数中,只有 $50w$ 个是 $1$,其它一大堆都是 $-1$。
也就是说前缀和连续下降的频率很高。
画个图意会一下。这张图反映了 设一个数为众数时,所有前缀和的变化趋势。横坐标是位置,纵坐标是这位的前缀和。
我们是否可以直接用一次函数维护这些斜率为 $-1$ 的前缀和变化线?
肯定可以,因为 $n$ 卡满时,$50w$ 个线段树总共 $50w$ 个位置的斜率是 $1$,所以我们可以暴力枚举线段树中的这些位置,然后把它与当前线段树中上一个 斜率为 $1$ 的位置连一条斜率为 $-1$ 的一次函数。
还有,暴力枚举那些斜率为 $1$ 的小段时,还可以顺便把那上面的点的贡献都算了。
贡献就是这个点左边有多少个点在它下边($type=3$ 的部分说过,要找所有和 $ge 1$ 的区间,也就是说对于一个前缀,要找它前面有多少个比它小的前缀)。
从左往右扫这些斜率为 $1$ 的小段时,维护一个 $y$ 轴的至于线段树就行了。
现在就剩下这样的问题:
1. 怎么插入一次函数;
2. 斜率为 $-1$ 的极大线段的贡献怎么算(刚才只说了斜率为 $1$ 的部分)
插入一次函数比较简单,设要插入的斜率为 $-1$ 的极大线段的上下端 $y$ 坐标分别为 $l$ 和 $r$,把值域线段树的 $[l,r]$ 区间加 $1$ 即可。
然后斜率为 $-1$ 的段的贡献嘛……
首先,我们要找的是关于位置和前缀和的同序对,所以对于斜率为 $-1$ 的线段中的任意一点,这段线段没有点可以对它造成贡献。
所以我们只考虑插入这条线段之前的线段树。
然后会发现,这条线段的贡献(统计量) 是插入这条线段之前的线段树的一个一次函数。
什么意思?画个图。
图中的“几次”表示其对应的 $y$ 轴的所有点被统计了几次。
这样统计的一次函数,纵坐标 $y$ 每 $+1$,该纵坐标上的点的被统计次数就 $-1$,显然是个一次函数。这个还比较好弄,原来我们的值域线段树只记每个纵坐标 $y$ 上有多少个点(假设用 $cnt$ 维护),现在再维护一个 $cnt imes i$ 就行了($i$ 表示纵坐标),做点减法即可把系数 $i$ 变成 $-i$,就是我们所需要的斜率了。
这是 $SYF$ 的代码(我并没补这种)
1 #include<algorithm> 2 #include<cmath> 3 #include<complex> 4 #include<cstdio> 5 #include<cstdlib> 6 #include<cstring> 7 #include<ctime> 8 #include<iomanip> 9 #include<iostream> 10 #include<map> 11 #include<queue> 12 #include<set> 13 #include<stack> 14 #include<vector> 15 #define rep(i,x,y) for(register int i=(x);i<=(y);++i) 16 #define dwn(i,x,y) for(register int i=(x);i>=(y);--i) 17 #define LL long long 18 #define maxn 500010 19 #define ls son[u][0] 20 #define rs son[u][1] 21 #define mi ((l+r)>>1) 22 using namespace std; 23 int read() 24 { 25 int x=0,f=1;char ch=getchar(); 26 while(!isdigit(ch)&&ch!='-')ch=getchar(); 27 if(ch=='-')f=-1,ch=getchar(); 28 while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar(); 29 return x*f; 30 } 31 void write(LL x) 32 { 33 if(x==0){putchar('0'),putchar(' ');return;} 34 int f=0;char ch[20]; 35 if(x<0)putchar('-'),x=-x; 36 while(x)ch[++f]=x%10+'0',x/=10; 37 while(f)putchar(ch[f--]); 38 putchar(' '); 39 return; 40 } 41 vector<int>v[maxn]; 42 LL tr[maxn*21],ans; 43 int n,a[maxn],s[maxn],rt,son[maxn*21][2],mkk[maxn*21],mkb[maxn*21],nd; 44 void mark(int u,int l,int r,int k,int b){mkk[u]+=k,mkb[u]+=b,tr[u]+=(LL)(b+(LL)(r-l)*(LL)k+b)*(LL)(r-l+1)/2ll;} 45 void pd(int u,int l,int r) 46 { 47 if(l<r&&(mkk[u]||mkb[u])) 48 { 49 if(!ls)ls=++nd; 50 mark(ls,l,mi,mkk[u],mkb[u]); 51 if(!rs)rs=++nd; 52 mark(rs,mi+1,r,mkk[u],mkb[u]+(mi+1-l)*mkk[u]); 53 mkk[u]=mkb[u]=0; 54 } 55 } 56 void pu(int u){tr[u]=tr[ls]+tr[rs];} 57 void add(int u,int l,int r,int x,int y,int k,int b) 58 { 59 //cout<<l<<" "<<r<<endl; 60 if(x<=l&&r<=y){/*cout<<"k:"<<k<<" b:"<<b<<" l:"<<l<<" r:"<<r<<endl;*/return mark(u,l,r,k,b);} 61 pd(u,l,r); 62 if(x<=mi) 63 { 64 if(!ls)ls=++nd; 65 add(ls,l,mi,x,y,k,b); 66 } 67 if(y>mi) 68 { 69 if(!rs)rs=++nd; 70 if(x<=mi)add(rs,mi+1,r,mi+1,y,k,b+(mi+1-x)*k); 71 else add(rs,mi+1,r,x,y,k,b); 72 } 73 return pu(u); 74 } 75 LL ask(int u,int l,int r,int x,int y) 76 { 77 //cout<<"ask l:"<<l<<" r:"<<r<<" mk:"<<mkk[u]<<" "<<mkb[u]<<endl; 78 if(x<=l&&r<=y)return tr[u]; 79 pd(u,l,r); 80 LL res=0; 81 if(x<=mi&&ls)res=ask(ls,l,mi,x,y); 82 if(y>mi&&rs)res+=ask(rs,mi+1,r,x,y); 83 return res; 84 } 85 int main() 86 { 87 n=read();read(); 88 rep(i,1,n)a[i]=read(),v[a[i]].push_back(i); 89 rep(i,0,n-1) 90 { 91 v[i].push_back(n+1); 92 int lim=v[i].size(),pre=0;rt=nd=1; 93 rep(j,0,lim-1) 94 { 95 s[v[i][j]]=s[pre]-v[i][j]+pre+2; 96 LL tmp=ask(rt,-(n+5),n+5,s[v[i][j]]-1,s[pre]); 97 ans+=tmp; 98 add(rt,-(n+5),n+5,s[pre]+1,n+5,0,v[i][j]-pre); 99 if(v[i][j]>pre+1)add(rt,-(n+5),n+5,s[v[i][j]],s[pre],1,1); 100 pre=v[i][j]; 101 } 102 rep(j,1,nd)mkk[j]=mkb[j]=son[j][0]=son[j][1]=tr[j]=0; 103 } 104 /*rep(i,1,n) 105 { 106 s[i]=s[lst[a[i]]]-i+lst[a[i]]+2; 107 LL tmp= 108 if(nd>=(maxn<<5)){cout<<"ooooh"<<endl;return 0;} 109 if(lst[a[i]]>i){cout<<"nooo";return 0;} 110 //cout<<i<<" "<<lst[a[i]]<<" "<<s[i]<<" "<<tmp<<endl; 111 ans+=tmp; 112 //cout<<"opl:"<<s[lst[a[i]]]+1<<" opr:"<<n+5<<endl; 113 rep(j,0,n-1) 114 { 115 cout<<"num:"<<j<<endl; 116 rep(k,-(n+5),n+5)cout<<k<<" "<<ask(rt[j],-(n+5),n+5,k,k)<<endl; 117 } 118 }*/ 119 //rep(i,0,n-1)ans+=ask(rt[i],-(n+5),n+5,s[lst[i]]-n+lst[i],s[lst[i]]); 120 write(ans); 121 return 0; 122 }
想看另一种线段树方法(只记众数的出现次数,不记其它数为 $-1$)的 点 这 里
3.$O(n)$
看这个吧,我也没明白捏 _^_