2020.11.2
题目描述
给定一个长为 (n (3leq nleq 5 imes 10^6)) 的环,每个点上有一个权值 (a_i)。对于一对点 ((i,j)),会将环分成一段优弧和劣弧。只要这两个弧的其中之一上的所有点的权值都严格不大于 (min{a_i,a_j}),那么我们就说 ((i,j)) 这对点是危险的。求一共有多少对危险的点。注:我们认为 ((i,j)) 和 ((j,i)) 是同一对点。
解法
带一个 (log n) 肯定过不了,想究极线性算法。
首先将这个环断开,从哪里断?有一个非常好的策略是从序列中最大值那里断开,因为在最大值两边的元素一定不会通过经过最大值的那段弧来产生贡献。
当计算蓝色点的时候,只有前面标黑的点会产生贡献,观察发现一定是一个单调递减的序列。
考虑用单调栈维护。每次加入一个元素的时候,将比该元素小的弹掉,同时计算贡献。
while(top&&sta[top]<a[i]) ans++;
之后将该元素入栈。若栈内元素个数大于等于2,那么还要加一的贡献,因为该元素的上一个没有被弹掉的元素一定是刚好比该元素大一点,中间没有其他值,所以一定可以构成一组。
但实际上这个做法是错的,因为有相同的元素。我们考虑将连续的相同元素合并成一个点,如果新加入的元素与栈顶元素相同,则该元素个数加一,并统计贡献(为个数那么多次)。弹栈的时候加一变成加个数。
for(int i=1;i<=n;i++){
while(top&&sta[top]<b[i]) ans+=num[top--];
if(b[i]==sta[top]) ans+=(num[top]++);
else sta[++top]=b[i],num[top]=1;
ans+=(top>1);
}
但这样还是错的,例如 6 6 5 5 这组数据,该程序会输出 4,但实际上答案是 6 。程序结束后,栈内是一个单调递减的序列,如果我们把其放在环上,其实是会产生贡献的。所以当最后如果栈内元素还多余2的时候需要继续弹栈。
if(top==2&&num[1]+num[2]>2) ans+=num[2];
else if(top>2) for(int i=3;i<=top;i++) ans+=num[i];
#include<stdio.h>
#define ll long long
#define N 5000007
inline int read(){
int x=0; bool flag=1; char c=getchar();
while(c<'0'||c>'9'){if(c=='-') flag=0;c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+c-48;c=getchar();}
return flag? x:-x;
}
ll ans=0,num[N];
int n,a[N<<1],b[N],sta[N],top=0;
int main(){
freopen("jolyne.in","r",stdin);
freopen("jolyne.out","w",stdout);
n=read();
int maxn=-1,pos;
for(int i=1;i<=n;i++){
a[i]=a[i+n]=read();
if(a[i]>maxn) maxn=a[i],pos=i;
}
for(int i=1;i<=n;i++) b[i]=a[i+pos-1];
for(int i=1;i<=n;i++){
while(top&&sta[top]<b[i]) ans+=num[top--];
if(b[i]==sta[top]) ans+=(num[top]++);
else sta[++top]=b[i],num[top]=1;
ans+=(top>1);
}
if(top==2&&num[1]+num[2]>2) ans+=num[2];
else if(top>2) for(int i=3;i<=top;i++) ans+=num[i];
printf("%lld",ans);
}