#17 CF715E & CF1375H & CF1483F
Complete the Permutations
题目描述
解法
找到快速计算相似度的方式是做这道题的前提,发现相似度就等于 \(n-\)置换环个数。其中置换环的含义是:对于所有 \(i\),连边 \(p_i\rightarrow q_i\) 所形成的环。
我们并不关心排列长什么样子,我们只关心图上的每一条边,可以按照下列方法把边分类:① \(a\rightarrow b\);② \(a\rightarrow 0\);③ \(0\rightarrow b\);④ \(0\rightarrow 0\);
首先考虑简化问题,对于 ① 边,可以把已知的 \(a\rightarrow b,b\rightarrow c\) 拼成 \(a\rightarrow c\),这样场上可能会剩下若干条已经确定的链,和 \(cnt\) 个已经确定的环。对于 ①② 边,可以把 \(a\rightarrow b,b\rightarrow 0\) 合并成 \(a\rightarrow 0\);对于 ①③ 边,可以把 \(0\rightarrow a,a\rightarrow b\) 合并成 \(0\rightarrow b\)
上述的合并都是不需要计算方案数的,接下来考虑更复杂的合并:
- 对于 ② 边,可以自身合并,得到 ② 边;也可以和 ④ 边合并,得到 ④ 边。
- 对于 ③ 边,可以自身合并,得到 ③ 边;也可以和 ④ 边合并,得到 ④ 边。
- 对于 ④ 边,可以自己构成环;也可以和已经存在的 ① 链构成环。
关键的 \(\tt observation\) 是:在合成环之前,④ 边的数量不会改变,所以我们可以考虑下面的计数顺序:
- 对于 ① 链 \(a\rightarrow b\),我们把场上还剩下的 \(a,b\) 给一个新的标号 \(c\),以后的合并都通过 \(c\) 来完成,最后再把 \(c\) 替换成 \(a\rightarrow b\) 链即可,这样可以排除掉 ① 链的影响(这步很重要,但貌似网上的题解没有说清楚)
- ② 边自己构成环,或者是转化为 ④ 边。
- ③ 边自己构成环,或者是转化为 ④ 边。
- ④ 边自己构成环,此时的方案数计算不受前面任何操作的影响。
那么三步计数可以看成独立的,我们用生成函数来标记置换环个数,首先写出 ② 边合并的生成函数,设 \(n_2/n_3\) 分别表示 ②③ 类边的数量,\(m\) 表示 ④ 类边的数量:
解释一下上式的含义:我们考虑构造 \(k\) 个置换环,先选出 \(i\) 个点 \({n_2\choose i}\),然后就是 \(i\) 个点构成 \(k\) 个圆排列的方案数 \({i\brack k}\),剩下的点可以自己合并也可以合并到 ④ 上去,第一个合并的方案数是 \(n_2+m-i-1\),每合并一次合并的选择就减少 \(1\),所以呈现出来是下降幂的形式。类似可以写成 ③ 边合并的生成函数:
现在写出 ④ 边合并的生成函数,我们先构成圆排列,然后分配标号(可分配的标号个数就是 \(m\)):
如果你完全理解了计数顺序,那么就知道这三个生成函数卷起来就是最终的生成函数。设得到的函数时 \(G(x)\),相似度为 \(i\) 的方案数就是 \([x^{n-i-cnt}]G(x)\),暴力实现卷积和第一类斯特林数,时间复杂度 \(O(n^2)\)
#include <cstdio>
const int M = 255;
#define int long long
const int MOD = 998244353;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,cnt,a[M],p1[M],p2[M],fa[M],v1[M],v2[M];
int fac[M],inv[M],f[M],g[M],h[M],s[M][M];
int find(int x)
{
return x==fa[x]?fa[x]:fa[x]=find(fa[x]);
}
void init(int n)
{
fac[0]=inv[0]=inv[1]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
for(int i=2;i<=n;i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
for(int i=2;i<=n;i++) inv[i]=inv[i-1]*inv[i]%MOD;
s[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
s[i][j]=(s[i-1][j-1]+s[i-1][j]*(i-1))%MOD;
}
int C(int n,int m)
{
if(n<m || m<0) return 0;
return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
void work(int *a,int n,int m)
{
if(m==0)
{
for(int k=0;k<=n;k++) a[k]=s[n][k];
return ;
}
for(int k=0;k<=n;k++)
for(int i=k;i<=n;i++)
a[k]=(a[k]+C(n,i)*s[i][k]%MOD
*fac[n+m-i-1]%MOD*inv[m-1])%MOD;
}
signed main()
{
n=read();init(n);
for(int i=1;i<=n;i++) fa[i]=a[p1[i]=read()]=i;
for(int i=1;i<=n;i++) p2[i]=read();
for(int i=1;i<=n;i++) if(p1[i] && p2[i] && !v1[i])
{
int x=i;v1[x]=1;
while(1)
{
if(p2[x] && a[p2[x]]) x=a[p2[x]];
else break;
v1[x]=1;fa[find(p1[x])]=find(p1[i]);
if(x==i) {cnt++;break;}
}
}
for(int i=1;i<=n;i++)
p1[i]=find(p1[i]),p2[i]=find(p2[i]),v1[i]=0;
int n2=0,n3=0,m=0;
for(int i=1;i<=n;i++)
v2[p2[i]]|=!p1[i],v1[p1[i]]|=!p2[i];
for(int i=1;i<=n;i++)
if(v1[i] && v2[i]) m++;
for(int i=1;i<=n;i++)
{
if(p1[i] && !p2[i] && !v2[p1[i]]) n2++;
else if(!p1[i] && p2[i] && !v1[p2[i]]) n3++;
else if(!p1[i] && !p2[i]) m++;
}
work(f,n2,m);work(g,n3,m);
for(int i=0;i<=n2;i++) for(int j=0;j<=n3;j++)
h[i+j]=(h[i+j]+f[i]*g[j])%MOD;
for(int i=0;i<=n;i++) f[i]=0;
for(int i=0;i<=m;i++)
{
int c=s[m][i]*fac[m]%MOD;
for(int j=0;j<=n2+n3;j++)
f[i+j]=(f[i+j]+c*h[j])%MOD;
}
for(int i=0;i<n;i++)
printf("%lld ",n-i-cnt>=0?f[n-i-cnt]:0);
}
Set Merging
题目描述
解法
感觉越来越思考不动了,感觉自己又没有见过什么套路,脑子还不好用 \(...\)
一眼 \(2\cdot 2^{20}=2\cdot 2^{12}\cdot 2^{8}\approx 2.2\cdot 10^6\),鉴定为:值域分块。
考虑把值域划分为 \(\frac{n}{B}\) 个块长为 \(B\) 的块,我们把值域块中的元素对应到原序列中,得到 \(p_1,p_2...p_B\) 。对于一个块,如果我们能够处理出 \(\forall l,r\in\{p_1,p_2...p_B\}\),原序列区间 \([l,r]\) 中所出现元素的集合,那么对于一个询问,我们可以在每个块中取出区间 \([ql,qr]\) 对应的集合,由于块间有天然偏序关系,可以直接合并,询问就只需要划分 \(q\cdot \frac{n}{B}\) 次了。
考虑对于每个块分别处理信息,由于每个块的信息有 \(O(B^2)\) 个,而我们需要的询问次数就是 \(\frac{n}{B}\cdot B^2\),是不允许带上其他东西的。所以我们考虑值域分治,因为值域分治如果配合上平方是不会让复杂度升级的。
具体来说,我们递归获得 \([l,mid],[mid+1,r]\) 的区间信息,然后暴力枚举 \([l,r]\) 的所有区间,可以利用 \([l,mid],[mid+1,r]\) 中的子序列合并得到当前的子序列,初始时我们传入 \([1,B]\),复杂度可以这样分析:
那么处理的复杂度就做到了 \(\frac{n}{B}\cdot B^2=nB\),总询问次数 \(n(B+\frac{q}{B})\),取 \(B=\sqrt q\) 就可以得到 \(2n\sqrt q\) 的询问次数。由于需要二分,时间复杂度是 \(O(n\sqrt q\log n)\)
总结
值域分块带有天然的偏序关系,在偏序关系限制比较严格的时候可以尝试使用。
分治法处理平方信息时,暴力合并上来可以让复杂度不升级(处理 \(n^2\) 的信息只需要 \(n^2\) 的时间)
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int M = 1<<22;
#define V vector<int>
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,q,B,a[M],ans[M],c[M],d[M];
int comb(int x,int y)
{
if(!x || !y) return x+y;
m++;c[m]=x;d[m]=y;return m;
}
struct node
{
V p;vector<V> id;
void init(int x)
{
p.resize(x);id.resize(x);
for(int i=0;i<x;i++) id[i].resize(x-i);
}
node(int x=-1) {if(~x) init(1),p[0]=a[x],id[0][0]=a[x];}
int ask(int l,int r) const
{
if(r<p.front() || l>p.back()) return 0;
l=lower_bound(p.begin(),p.end(),l)-p.begin();
r=upper_bound(p.begin(),p.end(),r)-p.begin()-1;
return l>r?0:id[l][r-l];
}
node upd(const node &x,const node &y)
{
init(x.p.size()+y.p.size());
merge(x.p.begin(),x.p.end(),y.p.begin(),y.p.end(),p.begin());
for(int i=0;i<p.size();i++) for(int j=i;j<p.size();j++)
id[i][j-i]=comb(x.ask(p[i],p[j]),y.ask(p[i],p[j]));
return (*this);
}
}t[1<<9];
node solve(int l,int r)
{
if(l>r) return node(0);
if(l==r) return node(l);
int mid=(l+r)>>1;node tmp;
return tmp.upd(solve(l,mid),solve(mid+1,r));
}
signed main()
{
n=read();m=n;q=read();B=1<<8;
for(int i=1;i<=n;i++) a[read()]=i;
for(int i=0;i<=n/B;i++)
t[i]=solve(i*B+1,min(n,(i+1)*B));
for(int i=1;i<=q;i++)
{
int l=read(),r=read();
for(int j=0;j<=n/B;j++)
ans[i]=comb(ans[i],t[j].ask(l,r));
}
printf("%d\n",m);
for(int i=n+1;i<=m;i++)
printf("%d %d\n",c[i],d[i]);
for(int i=1;i<=q;i++)
printf("%d ",ans[i]);
}
Exam
题目描述
解法
考虑枚举 \(s_i\),然后寻找所有可能的 \(s_j\)
我们从左往右枚举 \(s_i\) 的右端点,找到左端点最靠左,并且是它子串的 \(s_j\),它是可能成为答案的。我们把所有可能的 \(j\) 都记录下来,并且记录下对应的左端点 \(L_i\)
考虑如何处理可能答案之间的包含关系,考虑这样一种特例:aa
、aab
、aaaab
在 \(s_3\) 中,aab
包含了 aa
,但是我们如何判断呢?发现 \(L_4=L_3=3\) 说明了它们具有包含关系,所以我们从右往左扫描,\(L\) 递减的点才是有效的,其它都应该是被淘汰的。
此外我们应该要求 \(s_j\) 在 \(s_i\) 中出现的每一个位置都不被其它串包含,所以 \(s_j\) 在 \(s_i\) 中的出现次数应该等于在有效点集合中的出现次数,不难发现这样的判断是充分的。
考虑 \(s_j\) 在 \(s_i\) 中的出现次数,应该用树状数组加 \(\tt AC\) 自动机来维护,一开始把 \(s_i\) 中的所有前缀在自动机上标记,然后用树状数组统计 \(\tt fail\) 树内有多少被标记点即可,时间复杂度 \(O(|S|\log |S|)\)
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int M = 1000005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,cnt,ans,out[M],ch[M][26],mx[M],id[M];
int dfn[M],fa[M],b[M],L[M],np[M],ln[M],c[M],ed[M];
vector<int> g[M];string s[M];
void add(string &s,int x)
{
int p=0;ln[x]=s.length();
for(int i=0;i<ln[x];i++)
{
int c=s[i]-'a';
if(!ch[p][c]) ch[p][c]=++cnt;
p=ch[p][c];
}
mx[p]=max(mx[p],ln[x]);id[p]=x;ed[x]=p;
}
void dfs(int u)
{
dfn[u]=++m;
for(int v:g[u]) dfs(v);
out[u]=m;
}
void build()
{
queue<int> q;
for(int i=0;i<26;i++)
if(ch[0][i]) q.push(ch[0][i]);
while(!q.empty())
{
int u=q.front();q.pop();
mx[u]=max(mx[u],mx[fa[u]]);
if(!id[u]) id[u]=id[fa[u]];
for(int i=0;i<26;i++)
if(ch[u][i]) fa[ch[u][i]]=ch[fa[u]][i],q.push(ch[u][i]);
else ch[u][i]=ch[fa[u]][i];
}
for(int i=1;i<=cnt;i++)
g[fa[i]].push_back(i);
cnt++;dfs(0);
}
void add(int x,int f)
{
for(int i=x;i<=cnt;i+=i&(-i)) b[i]+=f;
}
int ask(int x)
{
int r=0;
for(int i=x;i>0;i-=i&(-i)) r+=b[i];
return r;
}
int get(int x)
{
return ask(out[ed[x]])-ask(dfn[ed[x]]-1);
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)
cin>>s[i],add(s[i],i);
build();
for(int i=1;i<=n;i++)
{
vector<int> vc;
for(int j=0,p=0;j<ln[i];j++)
{
p=ch[p][s[i][j]-'a'];add(dfn[p],1);
if(j+1==ln[i]) p=fa[p];
L[j]=j-mx[p]+1;np[j]=id[p];
}
L[ln[i]]=1e9;
for(int j=ln[i]-1;j>=0;j--)
{
if(L[j]<=j && L[j]<L[j+1])
vc.push_back(np[j]);
L[j]=min(L[j],L[j+1]);
}
for(int x:vc) c[x]++;
for(int x:vc) if(c[x])
ans+=get(x)==c[x],c[x]=0;
for(int j=0,p=0;j<ln[i];j++)
p=ch[p][s[i][j]-'a'],add(dfn[p],-1);
}
printf("%d\n",ans);
}