[HDU4747] [2013杭州区域赛] Mex [线段树]

Link
题意:给序列a,求所有[l,r]mex的和
1 <= n <= 200000, 0 <= ai <= 10^9

看到题面有两个印象
一个是线段树,一个是计数。

暴力数据结构做法想必不用多说(

计数?那可能要按行(状态意义上的行)之类的,就得考虑 m e x mex 有没有什么性质。

a b c d a\le b\le c\le d 一定有 m e x ( b , c ) m e x ( a , d ) mex(b,c)\le mex(a,d)

于是如果确定了 m e x ( i , j ( j i ) ) mex(i,∀j_{(j\ge i)}) m e x ( i + 1 , j ( j i + 1 ) ) mex(i+1,∀j_{(j\ge i+1)}) 也就有了一个左边界
并且 m e x ( i , j ) j i + 1 mex(i,j)\le j-i+1 。所以 i + 1 i+1 行的每一个 m e x mex 都有了左右边界
这么讲,可以把 m e x ( 1 , j ) mex(1,j) 推上 m e x ( N , j ) mex(N,j) ,并且最多枚举 N N

如果处理出 m e x ( 1 , j ) mex(1,∀j) 然后每列推 N N 次那就 Θ ( N 2 ) \Theta(N^2)
这可不行。

发现 Θ ( N 2 ) \Theta(N^2) 是走回了单个上推。想想怎么转换为整行上推?
上推的本质是每次删除 i i
于是我们可以考虑删除 i i 对于后面的 m e x mex 的影响。
如果 i i 后面位置 n e x t i next_i 马上就又是一个 a i a_i ,那么从 n e x t i next_i 往后的就跟 i i 没半毛钱关系了。

并且在 i + 1 n e x t i 1 i+1\cdots next_i-1 里面,在 m e x ( i + 1 , j ) = a i mex(i+1,j)=a_i 之前, i i 也不存在影响。
况且固定起点的 m e x mex 是单调不递减序列。
于是从 m e x ( i , j ( j [ i + 1 , n e x t i ] ) ) mex(i,∀j_{(j\in[i+1,next_i])}) 里面翻一个 j = u p p e r _ b o u n d ( a i ) j=upper\_bound(a_i)
j n e x t i 1 j\cdots next_i-1 m e x mex 都修改为 a i a_i

由此,这是一个区间查询+区间修改的问题…
然后就可以用线段树… Θ ( N l o g N ) \Theta(NlogN)
说好的计数呢

这是一棵线段树
代码很简单,大家自己想象,我还没写好(
UPD:写好了
注意pushdown的时间不要弄错
小心越界。

Accepted 499MS
2121B    17044K
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<algorithm>
#include<cctype>
#include<cstring>
using namespace std;
#define min(a,b) a<b?a:b
#define max(a,b) a>b?a:b
int N;
long long ans=0;
int Ai[200005]={};
int mex[200005]={};
int nxt[200005]={};
bool ext[200005]={};
long long seg[800005]={};
long long mxx[800005]={};
long long mark[800005]={};
int recent[200005]={};
void pushdown(int x,int L,int R)
{
	if(mark[x]==-1||L==R)return;
	mxx[x<<1]=mxx[x<<1|1]=mark[x<<1]=mark[x<<1|1]=mark[x];
	int Mid=L+R>>1;
	seg[x<<1]=(Mid-L+1)*mark[x];
	seg[x<<1|1]=(R-Mid)*mark[x];
	mark[x]=-1;
}
void build(int Pos,int L,int R)
{
	mark[Pos]=-1;
	if(L==R)
	{
		seg[Pos]=mxx[Pos]=mex[L];
		return;
	}
	int Mid=L+R>>1;
	build(Pos<<1,L,Mid);
	build(Pos<<1|1,Mid+1,R);
	if(L<R)
	{
		seg[Pos]=seg[Pos<<1]+seg[Pos<<1|1];
		mxx[Pos]=max(mxx[Pos<<1],mxx[Pos<<1|1]);
	}
}
void cover(int Pos,int L,int R,int qL,int qR,int x)
{
	if(L>=qL&&R<=qR)
	{
        seg[Pos]=x*(R-L+1);
        mark[Pos]=mxx[Pos]=x;
		return;
	}
	if(L>qR||R<qL)
	{
		return;
	}
	pushdown(Pos,L,R);
	int Mid=L+R>>1;
	cover(Pos<<1,L,Mid,qL,qR,x);
	cover(Pos<<1|1,Mid+1,R,qL,qR,x);
	if(L<R)
	{
		seg[Pos]=seg[Pos<<1]+seg[Pos<<1|1];
		mxx[Pos]=max(mxx[Pos<<1],mxx[Pos<<1|1]);
	}
}
long long query(int Pos,int L,int R,int x)
{
	if(mxx[Pos]<x)return N+1;
	if(L==R)return L;
	int Mid=L+R>>1;
	pushdown(Pos,L,R);
	if(mxx[Pos<<1]>=x)return query(Pos<<1,L,Mid,x);
	else return query(Pos<<1|1,Mid+1,R,x);
}
int main()
{
	while(~scanf("%d",&N))
	{
		if(!N)return 0;
		memset(ext,0,sizeof(ext));
		recent[0]=N+1;
		ans=0;
		for(int pos=0,i=1;i<=N;++i)
		{
			recent[i]=N+1;
			scanf("%d",&Ai[i]);
			if(Ai[i]<=N)ext[Ai[i]]=1;//*
			while(ext[pos])++pos;
			mex[i]=pos;
		}
		for(int i=N;i;--i)
		{
			if(Ai[i]>N)continue;
			nxt[i]=recent[Ai[i]];
			recent[Ai[i]]=i;
		}
		build(1,1,N);
		ans=seg[1];
		for(int t,i=1;i<N;++i)
		{
			t=query(1,1,N,Ai[i]);
			if(t<nxt[i])cover(1,1,N,t,nxt[i]-1,Ai[i]);
			ans+=seg[1];
		}
		printf("%lld\n",ans);
	}
	return 0;
}

那到底怎么计数?

扫描二维码关注公众号,回复: 3640325 查看本文章

m e x [ i , j ] m e x [ i , j + 1 ] \sum mex[∀i,j]\le \sum mex[∀i,j+1]
f ( j ) = m e x [ i , j ] f(j)=\sum mex[∀i,j]
记一下 f ( j + 1 ) = f ( j ) + Δ f(j+1)=f(j)+\Delta
前面线段树我们由第 i i 行推出来第 i + 1 i+1 行,式子也长差不多这样
线段树做法实际上就是对计算 Δ \Delta 做了一种优化。

不过线段树是删除。计数不好搞删除,过程反了过来,变成加入。
线段树的时候我们考虑了 a i a_i 删除之后的影响。
现在我们也要考虑加入 a j a_j 之后,它对 m e x [ i , j 1 ] \sum mex[∀i,j-1] 的影响—— Δ \Delta

如果加入 a j a_j ,还是和线段树的思路那样,记录一个 p r e [ i ] pre[i] 表示 i i 上一次出现的位置
那么在 p r e [ i ] pre[i] 之前的部分都不会产生贡献。
并且在 p r e [ i ] + 1 j 1 pre[i]+1\cdots j-1 的部分里面,
那些 m e x ( x , j ) &gt; a j mex(x,j)&gt;a_j 的位置 x x 才有可能贡献到 Δ \Delta 上。
m e x ( x , j ) a j mex(x,j)\le a_j 的那些就是根本没被更新到…)

于是可能产生贡献的位置 x x 需要满足两个条件。

  1. p r e [ i ] pre[i] 之后
  2. m e x ( x , j ) &gt; a j mex(x,j)&gt;a_j

不过每次加入 a j a_j 暴力更新然后统计满足条件的位置 x x 不可行
p r e [ i ] + 1 j 1 pre[i]+1\cdots j-1 里面, m e x ( x , j ) mex(x,j) 产生的条件就是小于 m e x mex 值的在 [ x , j ] [x,j] 里面都有
m e x ( x , j ) mex(x,j) 转化为 f ( x , j ) + 1 f(x,j)+1

p r e [ i ] + 1 j 1 pre[i]+1\cdots j-1 里面,把这一段对 Δ \Delta 的贡献如果记成一串数,是单调不递增的。
所以我们记录 p o s [ w ] pos[w] 表示产生 f ( x , j ) = w f(x,j)=w 的最靠近 j j 的位置 x x

每次枚举 w w
先让 p o s [ w ] = m i n ( p r e [ w ] , p o s [ w 1 ] ) pos[w]=min(pre[w],pos[w-1])
(更新成 p r e [ w ] pre[w] 炒鸡显然, p o s [ w 1 ] pos[w-1] 也很显然不过我分析得不太清醒
(定义归一回事, p o s [ w ] pos[w] 下面会参与贡献计算所以得这么搞( p o s [ w 1 ] pos[w-1] 参与更新)。)
w w 产生贡献的条件:

  1. p o s [ w ] &gt; p r e [ j ] pos[w]&gt;pre[j]
  2. w a j w\ge a_j

w w 产生贡献 p o s [ w ] p r e [ j ] pos[w]-pre[j]
(因为贡献单调不递减, w w 产生贡献可以当作把 p r e [ j ] + 1 p o s [ w ] pre[j]+1\cdots pos[w] 这一段都加上 1 1
(加到 b r e a k break 为止,得到的贡献就是 j j 状态行的贡献 Δ \Delta 了。)

复杂度分析:
我跟你讲啊
这个复杂度
Θ ( \Theta( 玄学 ) )
线段树多好啊,好写 优雅 复杂度稳定

枚举 i i 1 1 n n ,然后套一层枚举 a i a_i m i n ( N , e x i s t [ p r e [ i ] . . . i ] ) min(N,exist[pre[i]...i])
考虑两种极端情况
第一种是序列值递减,这个…复杂度是 Θ ( N ) \Theta(N) 也就是最后一行状态
序列值递增…每一行状态只会处理一次, Θ ( N ) \Theta(N)
复杂情况可以看作上面两种组合起来
所以复杂度实际上(大概吧)也许是 Θ ( N ) \Theta(N) (因为 p r e [ i ] \le pre[i] 就跳?)

不过呢,实际上跑起来,会慢那么,一点(比一点.jpg)
ps 不过本机跑起来两份代码的耗时差不多让我怀疑复杂度是不是其实是 N l o g N NlogN

Accepted 1591MS
841B     3716K
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
int N,t;
long long ans=0,cur=0;
int Ai[200005]={};
int Pre[200005]={};
int Pos[200005]={};
int main()
{
    while(~scanf("%d",&N))
    {
        if(!N)return 0;
        for(int i=1;i<=N;++i)scanf("%d",&Ai[i]),Pos[i]=Pre[i]=0;
        ans=cur=Pos[0]=Pre[0]=0;
        for(int i=1;i<=N;++i)
        {
            if(Ai[i]<=N)
            {
                t=Pre[Ai[i]],Pre[Ai[i]]=i;
                for(int j=Ai[i];j<=N;++j)
                {
                    Pos[j]=Pre[j];
                    if(j&&Pos[j]>Pos[j-1])Pos[j]=Pos[j-1];
                    if(Pos[j]>t)cur+=Pos[j]-t;
                    else break;
                }
            }
            ans+=cur;
        }
        printf("%lld\n",ans);
    }
    return 0;
}

数据生成器

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstdlib>
#include<ctime>
#include<cctype>
#include<sstream>
#include<cmath>
#include<cstring>
#include<queue>
using namespace std;
#define ll long long
ll GenRand(const ll Lim1,ll Lim2)
{
	ll ret=rand()<<15|rand();
	while(ret<Lim1)ret+=Lim1+(20+Lim2>>2);
	ret%=Lim2;
	if(ret<Lim1)ret=Lim1+rand()%(Lim2-Lim1);
	return ret;
}
stringstream ss;

int N,t;

int main(int argc,char**argv)
{
	int seed=time(NULL);
	if(argc>1)
	{
		ss.clear();
		ss<<argv[1];
		ss>>seed;
	}
    srand(seed);
    N=200000; printf("%d\n",N);
    while(N--)
    {
    	t=GenRand(0,1000000000);
    	printf("%d ",t);
    }
    printf("\n");
	return 0;
}

猜你喜欢

转载自blog.csdn.net/Estia_/article/details/83088071
Mex