原题链接:P4062 [Code+#1]Yazid 的新生舞会
解法一
分块 O ( n n ) {O(n\sqrt{n})} O(nn)
对于所有 c n t x ≤ n {cnt_x\le \sqrt{n}} cntx≤n的 x {x} x,暴力扫描所有长度不大于 2 n {2\sqrt{n}} 2n的区间,即枚举左端点,然后往右扫描,长度不大于 2 n {2\sqrt{n}} 2n,然后维护众数出现的次数,当众数出现的次数大于区间的一半时,该区间对答案的贡献加一。这里注意不要把 c n t x > n {cnt_x> \sqrt{n}} cntx>n的 x {x} x维护进众数里,后面会单独计算每个 c n t x > n {cnt_x> \sqrt{n}} cntx>n的 x {x} x为众数的所有合法区间。
如果数组只由
0
、
1
{0、1}
0、1组成,计算以
1
{1}
1为众数的合法区间数量。
s
i
s_i
si表示前缀和。那么满足条件的区间需要满足:
s
R
−
s
L
>
R
−
L
+
1
2
⇔
2
∗
s
R
−
R
>
2
∗
s
L
−
L
s_R-s_L>\frac{R-L+1}{2} \Leftrightarrow 2*s_R-R>2*s_L-L
sR−sL>2R−L+1⇔2∗sR−R>2∗sL−L。
记
r
e
s
{res}
res表示右端点取
R
−
1
{R-1}
R−1,左端点合法的数量,考虑到
R
−
1
{R-1}
R−1后移,
2
∗
s
R
−
R
{2*s_R-R}
2∗sR−R等于
2
∗
s
R
−
1
−
(
R
−
1
)
{2*s_{R-1}-(R-1)}
2∗sR−1−(R−1)加一或者减一,那么右端点取
R
{R}
R后左区间和法的数量为
r
e
s
+
c
n
t
[
2
∗
s
R
−
1
−
(
R
−
1
)
]
{res+cnt[2*s_{R-1}-(R-1)]}
res+cnt[2∗sR−1−(R−1)]或
r
e
s
−
c
n
t
[
2
∗
s
R
−
1
−
(
R
−
1
)
−
1
]
{res-cnt[2*s_{R-1}-(R-1)-1]}
res−cnt[2∗sR−1−(R−1)−1]
对于所有 c n t x > n {cnt_x> \sqrt{n}} cntx>n的 x {x} x,这些不同的数字不会超过 n {\sqrt{n}} n个,枚举每种 x {x} x为众数的贡献,用上面的方法。
在洛谷上交18s左右,改一下再交hdu应该会T,最慢的做法
原题的代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=5e5+7,maxm=1e6+7,m=5e5+1;
int a[maxn],f2[maxn],cnt[maxm];
bool vis[maxn];
unordered_set<int>s;
signed main() {
int n,t;
scanf("%d%*d",&n);
t=sqrt(n);
for(int i=1; i<=n; ++i) {
scanf("%d",&a[i]);
s.insert(a[i]);
++cnt[a[i]];
}
ll ans(0);
int c2(0);
for(int num:s) {
if(cnt[num]<t) vis[num]=1;
else f2[++c2]=num;
cnt[num]=0;
}
for(int i=1,r,maxx; i<=n; ++i) {
r=i+2*t-1;
if(r>n) r=n;
maxx=0;
for(int l=i; l<=r; ++l) {
if(vis[a[l]]) ++cnt[a[l]];
maxx=maxx<cnt[a[l]]?cnt[a[l]]:maxx;
if(maxx>(l-i+1)/2) ++ans;
}
for(int l=i; l<=r; ++l) {
if(vis[a[l]]) --cnt[a[l]];
}
}
for(int i=1,sum,res; i<=c2; ++i) {
res=sum=0;
for(int k=0;k<maxm;k++) cnt[k]=0;
++cnt[m];
for(int j=1; j<=n; ++j) {
if(a[j]==f2[i]) res+=cnt[2*sum-(j-1)+m],++sum;
else res-=cnt[2*sum-(j-1)-1+m];
ans+=res;
++cnt[2*sum-j+m];
}
}
printf("%lld\n",ans);
return 0;
}
解法二、三
思路
求数 x {x} x为众数的区间数时,原数组中等于 x {x} x的数看作 1 {1} 1,不等于 x {x} x的数看作 − 1 {-1} −1,答案是把每个位置前缀和后去数前面有几个前缀和比当前缀和小的。
求数 1 {1} 1为众数的区间数,原数组 { 1 1 2 2 2 1 1 } {\{1\ 1\ 2\ 2\ 2\ 1\ 1\}} {1 1 2 2 2 1 1},化为数组 { 0 1 2 1 0 − 1 0 1 } {\{0\ 1\ 2\ 1\ 0\ -1\ 0\ 1\}} {0 1 2 1 0 −1 0 1},每个位置的贡献为 { 1 2 1 0 0 1 4 } {\{1\ 2\ 1\ 0\ 0\ 1\ 4\}} {1 2 1 0 0 1 4}。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ooJbXX1f-1636734750035)(https://uploadfiles.nowcoder.com/images/20210807/137609403_1628307719899/5B814454E3D353962B59FFBBBBEEC0AB “图片标题”)]
树状数组 O ( n l o g n ) {O(nlog n)} O(nlogn)
这个跑得最快了
设 d d d表示处理后的数组,前缀和 T i = ∑ j = 1 i d j T_i=\sum_{j=1}^{i}{d_j} Ti=∑j=1idj,假设当前位置的前缀和为 x {x} x,那么当前位置的贡献为 G x − 1 = ∑ i = 1 x − 1 T i {G_{x-1}=\sum_{i=1}^{x-1}T_i} Gx−1=∑i=1x−1Ti。
求数 x {x} x为众数的区间数时,如果有 k {k} k个 x {x} x,那么可以分成 k + 1 {k+1} k+1个区间(以每个 0 {0} 0开头或 x {x} x开头,到下个 x {x} x之前的数),当 x = 1 {x=1} x=1时,上面的原数组可以分为 { 1 } , { 1 2 2 2 } , { 1 } , { 1 } {\{1\},\{1\ 2\ 2\ 2\},\{1\},\{1\}} {1},{1 2 2 2},{1},{1}。可以发现同一段是不会有任何贡献的,因为同一段的前缀和是递减的。
假设某一段为 [ x , y ] {[x,y]} [x,y],那么我们可以每次询问 G y − 1 − G x − 2 G_{y-1}-G_{x-2} Gy−1−Gx−2计算这一段区间对答案的总贡献,然后然后对 d x + = 1 , d y + 1 − = 1 {d_x+=1,d_{y+1}-=1} dx+=1,dy+1−=1。
找到怎么用数据结构维护 G x {G_x} Gx,假设 c i {c_i} ci表示 d i {d_i} di的差分数组。
G x = ∑ i = 1 x T i {G_{x}=\sum_{i=1}^{x}T_i} Gx=∑i=1xTi
= ∑ i = 1 x ∑ j = 1 i d j {=\sum_{i=1}^{x}\sum_{j=1}^{i}d_j} =∑i=1x∑j=1idj
= ∑ i = 1 x ∑ j = 1 i ∑ k = 1 j c k {=\sum_{i=1}^{x}\sum_{j=1}^{i}\sum_{k=1}^{j}c_k} =∑i=1x∑j=1i∑k=1jck
= ∑ k = 1 x c k ∗ ( 1 + 2 + 3 + . . . + ( x − k + 1 ) ) {=\sum_{k=1}^{x}c_k*(1+2+3+...+(x-k+1))} =∑k=1xck∗(1+2+3+...+(x−k+1))
= ∑ k = 1 x c k ∗ ( x − k + 1 ) ( x − k + 2 ) 2 {=\sum_{k=1}^{x}c_k*\frac{(x-k+1)(x-k+2)}{2}} =∑k=1xck∗2(x−k+1)(x−k+2)
= ∑ k = 1 x c k ∗ ( 1 + 2 + 3 + . . . + ( x − k + 1 ) ) {=\sum_{k=1}^{x}c_k*(1+2+3+...+(x-k+1))} =∑k=1xck∗(1+2+3+...+(x−k+1))
= ∑ k = 1 x ( x + 2 ) ∗ ( x + 1 ) 2 d k + 2 x + 3 2 d k ∗ k + 1 2 d k ∗ k 2 {=\sum_{k=1}^{x}}\frac{(x+2)*(x+1)}{2}d_k+\frac{2x+3}{2}d_k*k+\frac{1}{2}d_k*k^2 =∑k=1x2(x+2)∗(x+1)dk+22x+3dk∗k+21dk∗k2
于是我们只需要用树状数组维护 d i , d i ∗ i , d i ∗ i 2 d_i,d_i*i,d_i*i^2 di,di∗i,di∗i2。用线段树的话,如果直接维护前缀和 T i {T_i} Ti,可能常数会大些,需要的空间会更大。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e6+7,maxm=2e6+7,m=1e6+1;
ll t1[maxm],t2[maxm],t3[maxm];
inline void add(int x,ll d) {
ll k=x;
while(x<maxm) {
t1[x]+=d;
t2[x]+=d*k;
t3[x]+=d*k*k;
x+=x&-x;
}
}
inline ll sum(int x) {
ll res(0),k=x;
while(x) {
res+=t1[x]*(k+2)*(k+1)-t2[x]*(2*k+3)+t3[x];
x-=x&-x;
}
return res/2;
}
int a[maxn];
vector<int>v[maxn];
unordered_set<int>s;
signed main() {
int T,n;
scanf("%d",&T);
while(T--) {
scanf("%d",&n);
s.clear();
for(int i=1; i<=n; ++i) {
scanf("%d",&a[i]);
s.insert(a[i]);
v[a[i]].emplace_back(i);
}
ll ans(0);
int x,y,pre,cnt;
for(int num:s) {
v[num].emplace_back(n+1);
pre=cnt=0;
for(int i:v[num]) {
y=2*cnt-pre+m,x=2*cnt-(i-1)+m;
ans+=sum(y-1)-sum(x-2);
add(x,1);
add(y+1,-1);
++cnt;
pre=i;
}
pre=cnt=0;
for(int i:v[num]) {
y=2*cnt-pre+m,x=2*cnt-(i-1)+m;
add(x,-1);
add(y+1,1);
++cnt;
pre=i;
}
v[num].clear();
}
printf("%lld\n",ans);
}
return 0;
}
O ( n ) {O(n)} O(n)
考虑到前缀和的连续性,取 x {x} x为众数,原数组中等于 x {x} x的数看作 1 {1} 1,不等于 x {x} x的数看作 − 1 {-1} −1。
设 s u m {sum} sum表示处理后的数组前 i − 1 {i-1} i−1项的前缀和, r e s {res} res表示第 i − 1 {i-1} i−1项的贡献, x {x} x表示第 i {i} i项的贡献, y {y} y表示处理后的数组前 i {i} i项的前缀和。
当 a i = = x {a_i==x} ai==x时,明显有 z = r e s + c n t [ s u m ] , y = s u m + 1 {z=res+cnt[sum],y=sum+1} z=res+cnt[sum],y=sum+1;反之,有 z = r e s − c n t [ s u m − 1 ] , y = s u m − 1 {z=res-cnt[sum-1],y=sum-1} z=res−cnt[sum−1],y=sum−1。
如果这样做复杂度是 O ( n 2 ) O(n^2) O(n2)的。假设前面出现的最小的前缀和为 m i n n {minn} minn,当 x < = m i n n x<=minn x<=minn时,可以直接跳到下一个 a i = = x {a_i==x} ai==x的位置,因为被跳过的这一段对答案没有贡献,然后用差分数组标记,在起点加一,在终点减一。
code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e6+7;
int a[maxn];
vector<int>v[maxn];
unordered_set<int>s;
unordered_map<int,int>f1,f2;
signed main() {
int T,n;
scanf("%d",&T);
while(T--) {
scanf("%d",&n);
s.clear();
for(int i=1;i<=n;++i) {
scanf("%d",&a[i]); s.insert(a[i]); v[a[i]].emplace_back(i);
}
ll ans(0),res;
int sum,minn,k;
for(int num:s) {
v[num].emplace_back(n+1);
f1.clear(),f2.clear();
res=k=minn=sum=0;
for(int i=1;i<=n;++i) {
if(a[i]!=num&&sum==minn) {
int len=v[num][k]-i-1;
--f2[sum+1];
++f2[sum-len];
i+=len;
sum-=len+1;
}
else if(a[i]==num) {
f1[sum]+=f2[sum];
f2[sum+1]+=f2[sum];
f2[sum]=0;
++f1[sum];
res+=f1[sum];
++sum;
ans+=res;
++k;
}
else {
++f1[sum];
--sum;
res-=f1[sum];
ans+=res;
}
if(minn>sum) minn=sum;
}
v[num].clear();
}
printf("%lld\n",ans);
}
return 0;
}