【bzoj5110】Yazid的新生舞会

这里是 $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\times 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$……

有没有发现这规律跟二进制位很像?

比如说,这样的一个序列 $$1\space 2\space 2\space 3\space 3\space 3\space 3\space 4\space 4\space 4\space 4\space 4\space 4\space 4\space 4\space 5...$$ 它刚好卡着数量要求,使所有数都是众数。

需求量以这样的增长速度,不同的众数显然只有 $log$ 级别个了。

这是最坏情况,即区间内所有的数都是众数。把这个序列任意改数,不同的众数只会少不会多。可以意会一下,具体证明感觉说不清楚。

而该这个序列的数能组成其它任意情况的序列,所以不同的众数的数量最多只有 $log$ 级别。

 

之前说过,每个区间只有一个绝对众数,所以可以独立考虑每个众数是哪些区间的众数。

于是枚举每个众数,然后考虑怎么快速计数。

有了归并排序的经验(其实并没什么关系),很容易写出判定式子。对于一个区间 $[l,r]$,设 $cnt_l,cnt_r$ 分别表示当前众数在区间 $[l,mid]$ 和区间 $[mid+1,r]$ 的出现次数,则当满足 $$r-l+1\lt (cnt_l+cnt_r)\times 2$$ 时,当前枚举的众数是这个区间的众数,即对答案有 $1$ 的贡献。

移项得到 $$r-cnt_r\times 2+1\lt l+cnt_l\times 2$$,方便按位维护。

所以枚举经过中点 $mid$ 的区间的右端点 $r$ 时,其实就是找一个与左端点 $l$ 相关的后缀和(有多少个数比某个值大)。遍历左半区间的左端点 $l$ 后,推一次后缀和就行了。(后缀和……就是反向前缀和)

啥?暴力推后缀和?后缀和的范围最大是多少?

显然, $l$ 和 $cnt$ 都是 $n$ 级别的,所以 $l+cnt\times 2$ 的范围最大是 $3n$ 级别的。

分治的层数复杂度是 $O(log(n))$,每层中枚举众数的复杂度是 $O(log(n))$(其实比这个大,但层数越小越趋近于这个),每层在左半区间遍历左端点 $l$ 加上在右半区间遍历右端点 $r$ 的复杂度是 $O(n)$,总复杂度是 $O(n\times log^2(n))$。

这里说明一下,$l+cnt\times 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\n",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\n",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('\n'); 
47         rep(j,mid+1,r){
48             if(lsh[i]==a[j]) ++sum;
49             //printf("%d:%d\n",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\n",ans);
52         }
53         //printf("faq:%d\n",ans);
54         vis[lsh[i]]=0;
55         rep(j,mn,mx) cnt[j]=0;
56     }
57     //printf("\n%d %d %d\n",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 */
View Code

 

2.$O(n\times log(n))$

考虑优化 $type=3$ 的做法。

本来我们要对每一种众数都开一棵线段树,维护把其看成 $1$、把其它数看成 $-1$ 时的每个前缀和,但如果对序列的数没有限制的话这样显然炸了。

我们考虑一下为什么会炸。

其实就是状态太多了,最多有 $50w\times 50w$ 大小呢。

可序列里最多只有 $50w$ 个数啊!好像每个数都不会出现很多次。

对于一个没出现很多次的数,它所对应的序列 好像会出现很多 $-1$?

然后就会发现,我们把众数看成 $1$,其它数看成 $-1$ 后,序列的 $50w\times 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\times 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('\n');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('\n');
 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 }
View Code

 

想看另一种线段树方法(只记众数的出现次数,不记其它数为 $-1$)的 点 这 里

 

3.$O(n)$

这个吧,我也没明白捏 _^_

转载于:https://www.cnblogs.com/scx2015noip-as-php/p/bzoj5110.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值