题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=4747
题目大意:
给一个含有n个数的序列 ns[1~n]
定义函数 mex(l,r)为区间 [l,r] 中未出现的最小的非负整数
求序列ns所有区间的 mex 和
可以表示为:
for( l=1 ; l<=n ; l++)
for( r=l ; r<=n ; r++)
sum += mex( l , r );
解题思路:
这道题除了暴力之外,我的其他思路都是直接参考其他博客大神的
有两种思路:
一种是线段树思路,来源https://blog.youkuaiyun.com/acm_cxlove/article/details/11749383
一种是递推计数思路,来源https://blog.youkuaiyun.com/cc_again/article/details/11856847
首先是比较简单一点的线段树思路:
先构建一个数组 mex_i[i] = mex(1,i),
之后每删除最左边的节点(ns[i]),更新mex_i区间 [i,j] 中比 ns[i] 大的值为 ns[i]
其中 i 表示现在遍历到(删除)的节点, j 表示为数 ns[i] 下次出现的位置
用线段树维护 值修改 与 区间求和 的步骤
具体代码可见上面的链接
之后是递推计数的思路,这个思路比较懵,我实在不知道那些大神是怎么想出来那么凶残的方法的,之后详解:
首先是有两个数组,pos 与 full
其中 pos[i] 表示值 i 前一次出现的位置,full[j] 表示区间[x,i] 已经覆盖(出现)了0~j这些数,中x的最大值(最右)
而 tt(i) 表示为对目前的 i,mex(1 , i)~mex(i , i) 的和,注意是 mex(1 , i)~mex(i , i) ,再累加进 ans 中
需要操作的区间为(last,i ],因为 ns[i] 的出现对last之前的 区域无影响
而 full[j] = min( full[j-1] , pos[j] ) 可解释为更新的 full[i]的值 为 full[i-1] 与 pos[j] 的较小值(靠左端)
保证了区间 [full[j] , i] 满足已经覆盖(出现)了0~j 这几个数,而 mex[last+1 , i]~ mex[full[j] , i] 的保底值为j+1
所以在这个区间累加上去,而对任意mex(j , i),保证有mex(j-1 , i) >= mex(j , i),由此可以逐步累加上去
而在同一个阶段,也可以保证 full[j] <= full[j-1],由此可以将它 break 掉
之后该思路带注释的代码
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cstring>
#define ll long long
using namespace std;
int ns[200010]; // 序列
int pos[200010]; // pos[i] 表示现阶段 i 在序列 ns 中出现的最后一个位置
int full[200010]; // 现在遍历到了 i ,full[j] 表示已经覆盖0~j的区间[x,i] 中 x 的最大值
int main()
{
int n;
while(~scanf("%d",&n) && n)
{
int i,j;
for(i=1;i<=n;i++)
scanf("%d",&ns[i]);
memset(pos,0,sizeof(pos));
memset(full,0,sizeof(full));
int last;
ll tt=0,ans=0;
for(i=1;i<=n;i++)
{
if(ns[i] < n) //对长度为n的序列无影响
{
last = pos[ns[i]]; //数 ns[i] 前一次出现的最后位置
pos[ns[i]] = i; //更新 ns[i] 最后的位置
for(j=ns[i];j<n;j++) //遍历会产生影响的数字
{
if(j) full[j] = min(full[j-1],pos[j]); //更新full[j] 为 full[j-1] 与 pos[i] 中较小(左)的位置
else full[j] = i; //如果是 0 ,则更新 full[0] 为现在这个 ns[i]=0 的位置i
if(full[j] > last)
tt += full[j]-last; //累加对last之前的位置无影响
else
break;
}
}
ans += tt;
}
printf("%lld\n",ans);
}
return 0;
}