这里是 $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$ 的前缀。

这就是线段树维护前缀和的裸题了。

 #include<bits/stdc++.h>
#define N 500005
#define ll long long
#define rep(i,x,y) for(int i=x;i<=y;++i)
#define lc o<<1
#define rc o<<1|1
using namespace std;
inline int read(){
int x=; bool f=; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=;
for(; isdigit(c);c=getchar()) x=(x<<)+(x<<)+(c^'');
if(f) return x;
return -x;
}
int n,type,a[N];
namespace force{
ll ans;
int cnt[][];
void solve(){
rep(i,,n){
int mx=;
for(int j=;i+j<=n;++j){
mx=max(mx,++cnt[i][a[i+j]]);
if(mx*>j+) ++ans;
}
}
cout<<ans<<endl;
}
};
namespace type2{
ll ans;
struct Tree{int siz; bool clr;}tr[N<<];
int v;
inline void pushdown(int o){
if(tr[o].clr)
tr[lc].siz=tr[rc].siz=,
tr[lc].clr=tr[rc].clr=,
tr[o].clr=;
}
int ins(int o,int l,int r){
++tr[o].siz;
if(l==r) return tr[o].siz;
pushdown(o);
int mid=(l+r)>>;
if(v<=mid) return ins(lc,l,mid);
else return ins(rc,mid+,r);
}
void solve(){
rep(i,,n){
tr[].siz=, tr[].clr=;
int mx=;
for(int j=; j< && i+j<=n; ++j){
v=a[i+j];
mx=max(mx,ins(,,n-));
if(mx*>j+) ++ans;
}
}
cout<<ans<<endl;
}
};
namespace type3{
ll ans;
int L=-,R,v;
struct Tree{
int siz[N<<];
void ins(int o,int l,int r){
++siz[o];
if(l==r) return;
int mid=(l+r)>>;
if(v<=mid) ins(lc,l,mid);
else ins(rc,mid+,r);
}
int query(int o,int l,int r){
if(L<=l && r<=R) return siz[o];
int mid=(l+r)>>, ret=;
if(L<=mid) ret+=query(lc,l,mid);
if(R>mid) ret+=query(rc,mid+,r);
return ret;
}
}tr[]; int sum[];
void solve(){
rep(i,,) v=, tr[i].ins(,-,);
rep(i,,n){
rep(j,,) --sum[j];
sum[a[i]]+=;
rep(j,,){
R=sum[j]-;
if(L<=R) ans+=tr[j].query(,-,);
v=sum[j];
tr[j].ins(,-,);
}
}
cout<<ans<<endl;
}
};
int main(){
n=read(),type=read();
rep(i,,n) a[i]=read();
if(type==) force::solve();
else if(type== || type==) type3::solve();
else type2::solve();
return ;
}

三个部分分

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$,但我写的代码直接存在了负数下标位,理论上这样可能会出事(负数下标的指针有可能指向其它数组,然后导致指向的数组被无端改了数),不过我交上去过了就没管了……建议把这个值统一加个常数,存在自然数位,保证不会出事。

 #include<bits/stdc++.h>
#define rep(i,x,y) for(int i=x;i<=y;++i)
#define dwn(i,x,y) for(int i=x;i>=y;--i)
#define ll long long
#define N 500001
#define inf 2147483647
using namespace std;
inline int read(){
int x=; bool f=; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=;
for(; isdigit(c);c=getchar()) x=(x<<)+(x<<)+(c^'');
if(f) return x;
return -x;
}
int n,type,a[N]; ll ans;
int lsh[N],cnt[N*],sum[N];
bool vis[N];
void solve(int l,int r){
if(l==r){++ans; return;}
int mid=(l+r)>>;
solve(l,mid), solve(mid+,r);
int m=;
dwn(i,mid,l){
if((++cnt[a[i]])*>mid-i+)
if(!vis[a[i]]) vis[a[i]]=, lsh[++m]=a[i];
}
dwn(i,mid,l)
cnt[a[i]]=;
rep(i,mid+,r){
if((++cnt[a[i]])*>i-mid)
if(!vis[a[i]]) vis[a[i]]=, lsh[++m]=a[i];
}
rep(i,mid+,r)
cnt[a[i]]=;
rep(i,,m){
//printf("yes:%d\n",lsh[i]);
int sum=,mn=inf,mx=-inf;
dwn(j,mid,l){
if(lsh[i]==a[j]) ++sum;
++cnt[j+sum*];
//printf("%d\n",j+sum*2);
mx=max(mx,j+sum*), mn=min(mn,j+sum*);
}
dwn(j,mx-,mn) cnt[j]+=cnt[j+];
sum=; int x;
//printf("que:"); rep(j,mn,mx) printf("%d ",cnt[j]); putchar('\n');
rep(j,mid+,r){
if(lsh[i]==a[j]) ++sum;
//printf("%d:%d\n",j,j-sum*2+2);
x=j-sum*+; if(x>mx) continue; if(x<mn) x=mn;
ans+=cnt[x]; //printf("%d\n",ans);
}
//printf("faq:%d\n",ans);
vis[lsh[i]]=;
rep(j,mn,mx) cnt[j]=;
}
//printf("\n%d %d %d\n",l,r,ans);
}
int main(){
n=read(),type=read();
rep(i,,n) a[i]=read();
solve(,n);
cout<<ans<<endl;
return ;
}
/*
20 0
163 29 29 135 29 29 50 29 85 44 85 135 241 135 135 135 50 50 50 34
7 0
1 1 1 2 2 2 3
*/

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$ 的代码(我并没补这种)

 #include<algorithm>
#include<cmath>
#include<complex>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<ctime>
#include<iomanip>
#include<iostream>
#include<map>
#include<queue>
#include<set>
#include<stack>
#include<vector>
#define rep(i,x,y) for(register int i=(x);i<=(y);++i)
#define dwn(i,x,y) for(register int i=(x);i>=(y);--i)
#define LL long long
#define maxn 500010
#define ls son[u][0]
#define rs son[u][1]
#define mi ((l+r)>>1)
using namespace std;
int read()
{
int x=,f=;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-,ch=getchar();
while(isdigit(ch))x=(x<<)+(x<<)+ch-'',ch=getchar();
return x*f;
}
void write(LL x)
{
if(x==){putchar(''),putchar('\n');return;}
int f=;char ch[];
if(x<)putchar('-'),x=-x;
while(x)ch[++f]=x%+'',x/=;
while(f)putchar(ch[f--]);
putchar('\n');
return;
}
vector<int>v[maxn];
LL tr[maxn*],ans;
int n,a[maxn],s[maxn],rt,son[maxn*][],mkk[maxn*],mkb[maxn*],nd;
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+)/2ll;}
void pd(int u,int l,int r)
{
if(l<r&&(mkk[u]||mkb[u]))
{
if(!ls)ls=++nd;
mark(ls,l,mi,mkk[u],mkb[u]);
if(!rs)rs=++nd;
mark(rs,mi+,r,mkk[u],mkb[u]+(mi+-l)*mkk[u]);
mkk[u]=mkb[u]=;
}
}
void pu(int u){tr[u]=tr[ls]+tr[rs];}
void add(int u,int l,int r,int x,int y,int k,int b)
{
//cout<<l<<" "<<r<<endl;
if(x<=l&&r<=y){/*cout<<"k:"<<k<<" b:"<<b<<" l:"<<l<<" r:"<<r<<endl;*/return mark(u,l,r,k,b);}
pd(u,l,r);
if(x<=mi)
{
if(!ls)ls=++nd;
add(ls,l,mi,x,y,k,b);
}
if(y>mi)
{
if(!rs)rs=++nd;
if(x<=mi)add(rs,mi+,r,mi+,y,k,b+(mi+-x)*k);
else add(rs,mi+,r,x,y,k,b);
}
return pu(u);
}
LL ask(int u,int l,int r,int x,int y)
{
//cout<<"ask l:"<<l<<" r:"<<r<<" mk:"<<mkk[u]<<" "<<mkb[u]<<endl;
if(x<=l&&r<=y)return tr[u];
pd(u,l,r);
LL res=;
if(x<=mi&&ls)res=ask(ls,l,mi,x,y);
if(y>mi&&rs)res+=ask(rs,mi+,r,x,y);
return res;
}
int main()
{
n=read();read();
rep(i,,n)a[i]=read(),v[a[i]].push_back(i);
rep(i,,n-)
{
v[i].push_back(n+);
int lim=v[i].size(),pre=;rt=nd=;
rep(j,,lim-)
{
s[v[i][j]]=s[pre]-v[i][j]+pre+;
LL tmp=ask(rt,-(n+),n+,s[v[i][j]]-,s[pre]);
ans+=tmp;
add(rt,-(n+),n+,s[pre]+,n+,,v[i][j]-pre);
if(v[i][j]>pre+)add(rt,-(n+),n+,s[v[i][j]],s[pre],,);
pre=v[i][j];
}
rep(j,,nd)mkk[j]=mkb[j]=son[j][]=son[j][]=tr[j]=;
}
/*rep(i,1,n)
{
s[i]=s[lst[a[i]]]-i+lst[a[i]]+2;
LL tmp=
if(nd>=(maxn<<5)){cout<<"ooooh"<<endl;return 0;}
if(lst[a[i]]>i){cout<<"nooo";return 0;}
//cout<<i<<" "<<lst[a[i]]<<" "<<s[i]<<" "<<tmp<<endl;
ans+=tmp;
//cout<<"opl:"<<s[lst[a[i]]]+1<<" opr:"<<n+5<<endl;
rep(j,0,n-1)
{
cout<<"num:"<<j<<endl;
rep(k,-(n+5),n+5)cout<<k<<" "<<ask(rt[j],-(n+5),n+5,k,k)<<endl;
}
}*/
//rep(i,0,n-1)ans+=ask(rt[i],-(n+5),n+5,s[lst[i]]-n+lst[i],s[lst[i]]);
write(ans);
return ;
}

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

3.$O(n)$

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

【bzoj5110】Yazid的新生舞会的更多相关文章

  1. [BZOJ5110]Yazid的新生舞会

    题目大意: 给你一个长度为$n(n\leq 5\times 10^5)$的序列$A_{1\sim n}$.求满足区间众数在区间内出现次数严格大于$\lfloor\frac{r-l+1}{2}\rflo ...

  2. 【BZOJ5110】[CodePlus2017]Yazid 的新生舞会 线段树

    [BZOJ5110][CodePlus2017]Yazid 的新生舞会 Description Yazid有一个长度为n的序列A,下标从1至n.显然地,这个序列共有n(n+1)/2个子区间.对于任意一 ...

  3. bzoj5110: [CodePlus2017]Yazid 的新生舞会

    Description Yazid有一个长度为n的序列A,下标从1至n.显然地,这个序列共有n(n+1)/2个子区间.对于任意一个子区间[l,r] ,如果该子区间内的众数在该子区间的出现次数严格大于( ...

  4. [loj 6253] Yazid的新生舞会

    (很久之前刷的题现在看起来十分陌生a) 题意: 给你一个长度为n的序列A,定义一个区间$[l,r]$是“新生舞会的”当且仅当该区间的众数次数严格大于$\frac{r-l+1}{2}$,求有多少子区间是 ...

  5. 【BZOJ5110】[CodePlus2017]Yazid 的新生舞会

    题解: 没笔的时候我想了一下 发现如果不是出现一半次数而是k次,并不太会做 然后我用前缀和写了一下发现就是维护一个不等式: 于是就可以随便维护了

  6. 【bzoj5110】[CodePlus2017]Yazid 的新生舞会 Treap

    题目描述 求一个序列所有的子区间,满足区间众数的出现次数大于区间长度的一半. 输入 第一行2个用空格隔开的非负整数n,type,表示序列的长度和数据类型.数据类型的作用将在子任务中说明. 第二行n个用 ...

  7. BZOJ5110 CodePlus2017Yazid 的新生舞会(线段树)

    考虑统计每个数字的贡献.设f[i]为前缀i中该数的出现次数,则要统计f[r]-f[l]>(r-l)/2的数对个数,也即2f[r]-r>2f[l]-l. 注意到所有数的f的总变化次数是线性的 ...

  8. BZOJ.5110.[CodePlus2017]Yazid 的新生舞会(线段树/树状数组/分治)

    LOJ BZOJ 洛谷 又来发良心题解啦 \(Description\) 给定一个序列\(A_i\).求有多少个子区间,满足该区间众数出现次数大于区间长度的一半. \(n\leq5\times10^5 ...

  9. 「CodePlus 2017 11 月赛」Yazid 的新生舞会(树状数组/线段树)

    学习了新姿势..(一直看不懂大爷的代码卡了好久T T 首先数字范围那么小可以考虑枚举众数来计算答案,设当前枚举到$x$,$s_i$为前$i$个数中$x$的出现次数,则满足$2*s_r-r > 2 ...

随机推荐

  1. ListView适配器Adapter介绍与优化

    一.ListView与Adapter的关系 ListView是Android开发过程中较为常见的组件之一,它将数据以列表的形式展现出来.一般而言,一个ListView由以下三个元素组成: 1.View ...

  2. IE下contentWindow对象与FF、Chrome下的区别

    在ie中frame(iframe)标签通过name和id获取的对象是不同的. 通过name获取的本身就是contentWindow对象.所以 在ie中不用再找contentWindow了 例: let ...

  3. SC || Chapter 8

    栈:方法调用和局部变量的存储位置,保存基本类型 堆:在一块内存里分为多个小块,每块包含 一个对象,或者未被占用

  4. PAT (Basic Level) Practise (中文)- 1011. A+B和C (15)

    http://www.patest.cn/contests/pat-b-practise/1011 给定区间[-231, 231]内的3个整数A.B和C,请判断A+B是否大于C. 输入格式: 输入第1 ...

  5. ReactiveCocoa概念解释进阶篇

    1.ReactiveCocoa常见操作方法介绍 1.1 ReactiveCocoa操作须知 所有的信号(RACSignal)都可以进行操作处理,因为所有操作方法都定义在RACStream.h中,因此只 ...

  6. 哈希表(Hash Table)/散列表(Key-Value)

    目录 1. 哈希表的基本思想 2. 哈希表的相关基本概念 1.概念: 2.哈希表和哈希函数的标准定义: 1)冲突: 2)安全避免冲突的条件: 3)冲突不可能完全避免 4)影响冲突的因素 3. 哈希表的 ...

  7. NOIP模拟赛 数列

    Problem 2 数列(seq.cpp/c/pas) [题目描述] a[1]=a[2]=a[3]=1 a[x]=a[x-3]+a[x-1]  (x>3) 求a数列的第n项对1000000007 ...

  8. Git - revert详解

    git revert 撤销 某次操作,此次操作之前和之后的commit和history都会保留,并且把这次撤销作为一次最新的提交    * git revert HEAD                ...

  9. Linux中的常见命令

    1.   ls    查看当前目录下的所有文件夹 2.   pwd  查看当前所在的文件夹 3. cd 目录名 切换文件夹 4. touch 文件名 创建文件 5. mkdir 目录名 创建文件夹 6 ...

  10. JAVA基础篇—接口实现动态创建对象

    Scanner在控制台输入内容 package com.Fruit; public interface Fruit {//提供接口 } package com.Fruit; public class ...