题目传送门:https://loj.ac/problem/6253
题目分析:这题是我做CodePlus11月月赛的时候见到的,当时由于TUOJ太卡,一直被无法提交的问题困扰。导致我写完前两题正解后也没有再写T3T4的暴力。不过我还是看了一下题面,赛后研究了挺久,结果发现还是不会做QAQ(虽然80分的暴力并不需要怎么动脑子)。看了题解后才发现这是道数据结构好题。
一种可行的思路是:枚举一个值(如果序列中有这个值的话),考虑让这个值成为合法区间中出现次数超过一半的数。将序列中所有等于这个值的位置标为1,其余位置标为-1。那么如果一段区间的和大于0,它就是合法区间。换句话说,如果我们做出前缀和,那么对于任意一个1<=i<=n, sum[j]<sum[i](0<=j<i) 的个数就可以贡献答案。
虽然上面的计算可以用线段树维护,但这样总时间就达到了 O(cnt∗nlog(n)) ,其中cnt是不同数字的个数。如果我们每一次操作的时间都能和1的个数相关的话,就可以将时间降到稳定的 O(nlog(n)) 。这就意味着我们要一次性处理完一段连续的-1区间对答案的贡献。这也就是解题的关键和难点(我自己想这题的时候就是被卡在这里)。
不妨先画个栗子(虽然这张图其实并没有什么用):
现在我们要一次性处理红色括号内的-1对答案的贡献。也就是区间右端点R在这些-1里的时候,有多少个合法的左端点L。假设到红括号之前为止,前面的数前缀和为sum。那么对于第一个-1,红括号之前有多少个sum[L-1]属于(-oo,sum-2],它就对答案有多少贡献;对于第二个-1,红括号之前有多少个sum[L-1]属于(-oo,sum-3],它就对答案有多少贡献(为什么不算进第一个-1,因为很明显左端点L不可能取这个-1);依此类推……。
假设连续的-1的长度为len,那么现在的贡献就变成了:[-n,sum-len-1]的个数被加了len次(因为前缀和最小也就是-n),[sum-len,sum-2]中的数i被加了sum-i-1次。于是我们将权值线段树开出来,维护一个num[i]与i*num[i]的区间和(num[i]表示值为i的前缀和个数),然后加加减减一下就行。处理完这段-1后,再对[sum-1,sum-len]这段区间的num+1。
按照题解的原话来说就是:
我们考虑取出所有 B 中的极长
−1 子区间,观察这些区间中的所有点作为右端点对答案的贡献。不难发现极长 −1 子区间 [l,r] 中的所有点作为右端点对答案的贡献为 ∑r−l+1i=1∑Sl−1−i−1j=−∞cnt[j] ,其中 cnt[j] 表示在区间 [0,l−1] 之间前缀和为 j 的端点数目;在统计这段区间的答案后,我们还需要对区间[Si−1−(r−l+1),Si−1−1] 中的所有 cnt 均进行 +1 操作。显然地,我们使用一个维护 Bi 和 Ci=i×Bi 的线段树就可以支持这些查询、修改操作。于是我们使用这棵线段树维护相关信息,并从左到右枚举右端点,统计答案即可。
CODE:
#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;
const int maxn=500100;
typedef long long LL;
struct Tnode
{
int add,sum;
LL val,cnt;
bool empty;
void Clear()
{
add=sum=cnt=0;
empty=true;
}
void Add(int v,int len)
{
add+=v;
sum+=(v*len);
cnt+=( (long long)v*val );
}
} tree[maxn<<3];
struct data
{
int num,id;
} a[maxn];
int n,m;
bool Comp(data x,data y)
{
return x.num<y.num || ( x.num==y.num && x.id<y.id );
}
void Build(int root,int L,int R)
{
if (L==R)
{
tree[root].val=L;
return;
}
int Left=root<<1;
int Right=Left|1;
int mid=(L+R)>>1;
Build(Left,L,mid);
Build(Right,mid+1,R);
tree[root].val=tree[Left].val+tree[Right].val;
}
void Down(int root,int L,int R)
{
int Left=root<<1;
int Right=Left|1;
int mid=(L+R)>>1;
if (tree[root].empty)
{
tree[Left].Clear();
tree[Right].Clear();
tree[root].empty=false;
}
if (tree[root].add)
{
int &v=tree[root].add;
tree[Left].Add(v,mid-L+1);
tree[Right].Add(v,R-mid);
v=0;
}
}
void Update(int root,int L,int R,int x,int y,int v)
{
if ( y<L || R<x ) return;
if ( x<=L && R<=y )
{
tree[root].Add(v,R-L+1);
return;
}
Down(root,L,R);
int Left=root<<1;
int Right=Left|1;
int mid=(L+R)>>1;
Update(Left,L,mid,x,y,v);
Update(Right,mid+1,R,x,y,v);
tree[root].sum=tree[Left].sum+tree[Right].sum;
tree[root].cnt=tree[Left].cnt+tree[Right].cnt;
}
LL Query(int root,int L,int R,int x,int y,int v)
{
if ( y<L || R<x ) return 0;
if ( x<=L && R<=y )
if (v==1) return tree[root].sum;
else return tree[root].cnt;
Down(root,L,R);
int Left=root<<1;
int Right=Left|1;
int mid=(L+R)>>1;
LL vl=Query(Left,L,mid,x,y,v);
LL vr=Query(Right,mid+1,R,x,y,v);
return (vl+vr);
}
void Work(int last,int now,int &v,LL &ans)
{
int len=now-last-1,L=v-len-1,R=v-2;
if (n+R>=1) ans+=( Query(1,1,2*n,1,n+R,1)*(long long)len );
if (L+1<=R)
{
ans-=Query(1,1,2*n,n+L+1,n+R,2);
ans+=( Query(1,1,2*n,n+L+1,n+R,1)*(long long)(n+L) );
}
Update(1,1,2*n,n+L+1,n+R+1,1);
v-=len;
}
int main()
{
freopen("singledog.in","r",stdin);
freopen("singledog.out","w",stdout);
scanf("%d%d",&n,&m);
for (int i=1; i<=n; i++) scanf("%d",&a[i].num),a[i].id=i;
sort(a+1,a+n+1,Comp);
Build(1,1,2*n);
int head=1;
LL ans=0;
while (head<=n)
{
int tail=head;
while ( tail<n && a[tail+1].num==a[head].num ) tail++;
int last=0,v=0;
Update(1,1,2*n,n,n,1);
for (int i=head; i<=tail; i++)
{
int now=a[i].id;
if (last+1<now) Work(last,now,v,ans);
v++;
if (n+v-1>=1) ans+=Query(1,1,2*n,1,n+v-1,1);
Update(1,1,2*n,n+v,n+v,1);
last=now;
}
if (last<n) Work(last,n+1,v,ans);
tree[1].Clear();
head=tail+1;
}
printf("%lld\n",ans);
return 0;
}