全区间MEX问题
全区间MEX问题
补 CF2084E 的时候,产生了这样的思考,为什么能想到这么做?所以顺便写下了这篇文章。
这里先从简单的问题引入。
P1
给定一个 \([0,n-1]\) 的排列,询问所有子区间的 \(\text{MEX}\) 的和。
形式化的,求 \(\sum_{L=1}^n\sum_{R=L}^n\text{MEX}_{[l,r]}\) 。
暴力是 \(O(n^3)\) 的,稍微差一点的实现是 \(O(n^3\log n)\),不太可接受,想一下这种全区间问题,十有八九是都去拆贡献做,然而单个元素的贡献可拆,这里的 \(\text{MEX}\) 是由区间里面的数共同决定的,怎么去拆?这就是难点。
暴力
结合暴力计算 \([l,r]\) 的 \(\text{MEX}\) 的方法如下:
inline int calc(int l,int r,vector<int> &a)
{
vector<int> tmp;
for(int i=l;i<=r;++i)tmp.push_back(a[i]);
sort(tmp.begin(),tmp.end());
int now=0;
for(int v:tmp)
{
if(v!=now)return now;
now++;
}
return now;
}
Trick
抽象化一下这个计算过程,发现对于一个区间 \([l,r]\),我们循环 \(now\) 从 \(0\) 到 \(n\) 依次检查 \(now\) 是否在 \([l,r]\) 之间出现,如果出现就自加,否则就直接 \(\text{return}\)。这里就有一个非常重要的等价转化,也就是 :
意味着我们得到了一个可以拆散了计算,但是最后得到的结果相同的方法,这就可以拆贡献了。
直接枚举 \(k\) ,然后把 \([0,k-1]\) 的所有数添加到当前的子区间里面,用双指针记录扩展到的 \([l,r]\) ,那么到当前扩展完毕,对答案的贡献就是 : \(l\times (n-r+1)\) 。这可以保证所有包含了 \([0,k-1]\) 所有数的所有子区间在当前情况下的贡献 \(1\) 都被不重不漏地加到了总答案里面。
Code
#include<bits/stdc++.h>
using namespace std;
//计算全区间的MEX和 给定一个0!~n-1排列的情况下
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int n;
cin>>n;
vector<int> a(n+1);
for(int i=1;i<=n;++i)cin>>a[i];
long long ans=0;
vector<int> pos(n+1);
for(int i=1;i<=n;++i)pos[a[i]]=i;
int l=n+1,r=0;
for(int k=0;k<n;++k)
{
while(l>pos[k])l--;
while(r<pos[k])r++;
ans+=1ll*l*(n-r+1);
// cout<<l<<' '<<r<<'\n';
}
cout<<ans;
return 0;
}
P2
CF2084E
同样给定一个 \([0,n-1]\) 的排列,但是其中有些数被挖掉了,用 \(-1\) 表示。
求的东西大差不差,也就是:在所有能够使得整个序列最终是一个排列的填数方案中,求出其全区间 \(\text{MEX}\) 和。
最后的答案就是所有可能情况的 \(\text{MEX}\) 和之和。
分析
不妨沿用上面的思考,仍在最外层枚举一个这样的 \(k\) ,在添加数字并且扩展区间的时候,会发生有一些数字并没有出现而是 \(-1\) 的情况,这不要紧,这种情况我们不扩展即可。
也就是说,当前维护的区间是 所有在总序列中出现了的,被当前的k所要求的数,但这时,贡献不再是简单的 \(l\times (n-r+1)\) ,而是和组合数有关,并且和需要填的缺失的数字的个数有关。
假设随手记录下来的需要填的缺失数字个数是 \(fil\) ,那么能产生贡献的区间需要满足两个条件:
- 区间 \([L,R]\) 满足其完全包含 \([l,r]\)。
- 记区间 \([L,R]\) 中有 \(cnt\) 个 \(-1\) ,那么必须有 \(cnt\ge fil\)。
如果以上两个条件都满足,那么一个区间所能产生的所有情况下的贡献就是 (记整个区间的 \(-1\) 个数是 \(all\))
我们无法接受在枚举了 \(k\) 的情况下再去 \(O(n^2)\) 地枚举区间,实际上,我们关心的就只是:当前合法的区间中 各包含了 \(i\in[fil,n]\) 个 \(-1\) 的区间的个数。
我们一开始 \(O(n^2)\) 预处理出所有 \(i\in[0,n]\) 个 \(-1\) 所对应的区间个数,用 \(cnt[i]\) 表示。
在区间扩展的过程中,我们动态地删除不满足条件的区间,最后再枚举 \(cnt\) 来统计答案即可。
这里删除区间的操作其实很有学问,可以用数学归纳法来证明正确性,建议细品。
#include<bits/stdc++.h>
using namespace std;
const int N=5e3+10;
typedef long long ll;
const ll MOD=1e9+7;
inline ll power(ll a,ll b)
{
ll ans=1;
while(b)
{
if(b&1)ans=ans*a%MOD;
a=a*a%MOD;
b>>=1;
}
return ans%MOD;
}
ll jc[N],ijc[N];
inline ll A(ll n,ll m){if(n<m)return 0;return jc[n]*ijc[n-m]%MOD;}
inline void solve()
{
int n;
cin>>n;
vector<int> a(n+5),s(n+5,0),pos(n+5,0);
vector<ll> cnt(n+5,0);
ll all=0;
for(int i=1;i<=n;++i)cin>>a[i];
for(int i=1;i<=n;++i)
{
s[i]=s[i-1]+(a[i]==-1),all+=a[i]==-1;
if(a[i]!=-1)pos[a[i]]=i;
}
for(int i=1;i<=n;++i)
for(int j=i;j<=n;++j)
cnt[s[j]-s[i-1]]++,assert(s[j]-s[i-1]>=0);
ll ans=0,fil=0;
int r=0,l=n+1;
for(int k=0;k<n;++k)
{
if(pos[k])
{
while(l>pos[k])
{
if(l<=n)for(int R=max(l,r);R<=n;++R)cnt[s[R]-s[l-1]]--,assert(l-1>=0);
l--;
}
while(r<pos[k])
{
if(r>=1)for(int L=min(l,r);L>=1;--L)cnt[s[r]-s[L-1]]--,assert(L-1>=0);
r++;
}//***1
}
else fil++;
for(int hole=fil;hole<=n;++hole)
ans+=cnt[hole]*A(hole,fil)%MOD*jc[all-fil]%MOD,ans%=MOD,assert(all>=fil);//**2
}
cout<<ans<<'\n';
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
jc[0]=1;
const int lim=5000;
for(int i=1;i<=lim;++i)jc[i]=1ll*jc[i-1]*i%MOD;
ijc[lim]=power(jc[lim],MOD-2);
for(int i=lim-1;i>=0;--i)ijc[i]=1ll*ijc[i+1]*(i+1)%MOD;
int T;cin>>T;
while(T--)solve();
return 0;
}
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/18818018

浙公网安备 33010602011771号