20联赛集训day1 题解

A

类似后缀数组算本质不同串的思路,考虑 \(S\) 的第 \(i\) 个前缀和第 \(i+1\) 个前缀有多少个都会被计算,发现被算重的后缀等价于 \(T\)\(S[i+1]\) 的出现次数。

举个例子:

abc
abc

第 1 个前缀和第二个前缀都会算到 abc,原因是第一个前缀是 "a"+"bc",第二个是 "ab"+"c",第二个前缀比第一个多的字母正好补上了

然后就可以 \(O(n)\) 完成。

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 1e7 + 5;
char S[MAXN],T[MAXN];
int n,m,cnt[26];

int main(){
	scanf("%s%s",S+1,T+1);
	n = strlen(S+1);m = strlen(T+1);
	FOR(i,1,m) cnt[T[i]-'a']++;
	LL ans = m+1;
	FOR(i,1,n){
		ans += m+1-cnt[S[i]-'a'];
	}
	printf("%lld\n",ans);
	return 0;
}

B

比赛时写的循环节找错了。。

首先我们通过找出循环节能算出 \(cnt_i\) 表示 \(i\) 数字出现的次数。注意这里循环节不一定从开头开始(我因为写错了这个 \(100 \to 30\)

我赛场的做法是直接对着大力背包:直观想法是设 \(f_{i,j}\) 表示考虑了前 \(i\) 种出现次数的所有球,已经选出了 \(j\) 个位置的方案数。每次如果能算出 \(g_{c,i}\) 表示从所有上界为 \(c\) 的球中选出 \(i\) 个的方案书,转移就是 \(f_{i,j} = \sum \binom{j}{k} f_{i-1,j-k}g_{i,k}\)。这一部分复杂度是 \(O(m^3)\) 的。

那么如何求 \(g\) 呢?我比赛时的暴力做法是设 \(h_{i,j}\) 表示用了 \(i\) 种球(注意,这里所有的球都是有上界 \(c\) 的),已经选了 \(j\) 个位置的方案书,不难发现 \(g_{c,i} = \sum_{j \leq i} h_{j,i}\)。转移 \(h\) 只需要枚举最后一次选了多少,这一块复杂度是 \(O(m^4)\) 的。

如果会一点点省选技巧,就可以发现算 \(g_{c,i}\) 可以用 EGF。其实就是 \((\sum_{i=0}^c \frac{z^i}{i!})^{cnt_c}\),可以用多项式快速幂(这里可以用 exp 或者是暴力多项式乘法解决),视实现情况这一部分可以做到 \(O(m^3 \log n)\)\(O(m^2 \log m)\)

上面那一部分其实也可以用 NTT 优化,所以这题实际上是可以做到 \(O(m^2 \log m)\) 的。

#pragma GCC optimize("Ofast")
#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

LL n;int m;
int a1,A,B,P;
const int ha = 998244353;
const int MAXN = 100+5;

inline int qpow(int a,int n=ha-2){
	int res = 1;
	while(n){
		if(n & 1) res = 1ll*res*a%ha;
		a = 1ll*a*a%ha;
		n >>= 1;
	}
	return res;
}

LL cnt[MAXN];
int vis[10000000+5];
int f[2][MAXN],inv[MAXN],fac[MAXN],now;

inline int C(int n,int m){
	if(n < 0 || m < 0 || n < m) return 0;
	if(n < MAXN && m < MAXN){
		return 1ll*fac[n]*inv[m]%ha*inv[n-m]%ha;
	}
}

LL t[MAXN];

inline LL gcd(LL x,LL y){
	return !y ? x : gcd(y,x%y);
}

inline int CC(LL n,int m){
	int res = 1;
	FOR(i,1,m) t[i] = n-i+1;
	FOR(i,1,m){
		int x = i;
		FOR(j,1,m){
			if(x == 1) break;
			LL g = gcd(x,t[j]);
			t[j] /= g;x /= g;
		}
	}
	FOR(i,1,m) res = 1ll*res*(t[i]%ha)%ha;
	return res;
}

int g[MAXN][MAXN];
int cc[MAXN],dd[MAXN];

signed main(){
	scanf("%lld%d",&n,&m);
	fac[0] = 1;FOR(i,1,MAXN-1) fac[i] = 1ll*fac[i-1]*i%ha;
	inv[MAXN-1] = qpow(fac[MAXN-1]);ROF(i,MAXN-2,0) inv[i] = 1ll*inv[i+1]*(i+1)%ha;
	scanf("%d%d%d%d",&a1,&A,&B,&P);A %= P;B %= P;int tt = a1;
	vis[a1] = 1;cnt[std::min(m,a1)]++;int len = 1;
	int ls = 1,rs = 1;
	while(len < n){
		a1 = (1ll*A*a1%P+B)%P+1;
		if(vis[a1]){
			ls = vis[a1];
			rs = len;
			break;
		}
		len++;vis[a1] = len;
		cnt[std::min(m,a1)]++;
	}
	if(len < n){
		int ttt = a1;
		len = rs-ls+1;
		a1 = tt;
		FOR(i,1,ls-1) cnt[std::min(m,a1)]--,a1 = (1ll*A*a1%P+B)%P+1;
		LL d = (n-(ls-1))/len,r = (n-(ls-1))%len;
		FOR(i,1,m) cnt[i] = 1ll*d*cnt[i];
		a1 = tt;
		FOR(i,1,ls-1) cnt[std::min(m,a1)]++,a1 = (1ll*A*a1%P+B)%P+1;
		a1 = ttt;
		FOR(i,1,r){
			cnt[std::min(m,a1)]++;
			a1 = (1ll*A*a1%P+B)%P+1;
		}
	}
	f[now=0][0] = 1;
	FOR(c,1,m){
		CLR(f[now^1],0);
		CLR(g,0);g[0][0] = 1;
		FOR(x,1,m){
			FOR(y,1,m){
				FOR(k,1,std::min(y,c)){
					(g[x][y] += 1ll*g[x-1][y-k]*C(y,y-k)%ha) %= ha;
				}
			}
		}
		FOR(x,1,m) cc[x] = CC(cnt[c],x);
		FOR(j,1,m){
			dd[j] = 0;
			FOR(x,1,j){
				(dd[j] += 1ll*cc[x]*g[x][j]%ha) %= ha;
			}
		}
		f[now^1][0] = 1;
		FOR(i,1,m){// f[j]
			f[now^1][i] = f[now][i];
			FOR(j,1,i){
				int gx = dd[j];
				gx = 1ll*gx*C(i,j)%ha;
				(f[now^1][i] += 1ll*f[now][i-j]*gx%ha)%=ha;
			}
		}
		now ^= 1;
	}
	printf("%d\n",f[now][m]);
	return 0;
}

C

首先回忆一下编辑距离怎么做:设 \(g_{i,j}\) 表示 \(i\) 前缀和 \(j\) 前缀的编辑距离,转移 \(g_{i,j} = \min\{g_{i-1,j},g_{i,j-1},g_{i-1,j-1}+[s_i\neq t_j]\}\)

发现 \(T\) 非常短,于是编辑距离应该大部分都是删除,然后小部分和 \(T\) 结构有关。换言之,答案可能是一个和 \(T\) 有关的量。

我们先考虑整个 \(S\) :设 \(h(i)\) 表示 \(S\) 长度为 \(i\) 的后缀的编辑距离,首先最优情况下只需要删掉多出来的就好了,所以 \(h(i) \geq i-|T|\),最坏情况下需要全删完再加回来,所以 \(h(i) \leq |T|+i\)。我们可以发现 \(|i-h(i)| \leq |T|\)

并且观察发现 随着 \(i\) 的增加,\(i-h(i)\) 是单调不减的。因为每次 \(i\) 增加 \(1\)\(h(i)\) 至多增加 \(1\),所以 \(i-h(i)\) 不会减少。

这样我们就可以得到他是一个分段函数,并且拐点只有 \(O(|T|)\) 个。我们如果能对于每次询问 \([l,r]\),快速求出 \(r\) 前缀对应的 \(i - i-h(i)\) 图像上 \(r-l+1\) 对应的值,就可以通过简单移项得到答案,所以我们考虑去维护这个分段函数的所有拐点。

\(f_{i,j,k}\) 表示考虑 \(S\) 的长度为 \(i\) 的前缀和 \(T\) 的长度为 \(j\) 的前缀,满足 \(x-h(x) \leq k\) 的最大的 \(x\) 是什么。

我们先考虑 \(f_{i,j,k}\) 能转移到哪些状态:

  • 删除掉 \(S_{i+1}\):相当于 \(i\) 增加了 \(1\),所有的 \(h(i)\) 增加了 \(1\),所有的后缀长度都增加了 \(1\),所以 \(k\) 不变,有\(f_{i,j,k} \to f_{i+1,j,k}+1\)
  • 删除掉 \(T_{j+1}\):相当于所有的 \(h(i)\) 增加了 \(1\),所以 \(k\) 减少 \(1\),有 \(f_{i,j,k} \to f_{i,j+1,k-1}\)
  • \(S_{i}\) 修改为 \(T_j\),相当于所有的 \(h(i)\) 增加了 \([S_i\neq T_j]\)\(i\) 增加 \(1\),所有后缀长度都增加了 \(1\),所以 \(k\) 增加了 \([S_i = T_j]\),有 \(f_{i,j,k} \to f_{i+1,j+1,k+[S_{i+1}==T_{j+1}]}+1\)

然后就做完了。最后求答案的时候只需要求出最小的 \(k\) 满足 \(f_{r,|T|,k} \geq r-l+1\),就找到了 \(k\) 在图像上的值,就可以直接算了。

群里有一个神仙写了另一种做法:

我们考虑把 \(T\) 先仅通过删除和替换变成 \(S\) 的一个子序列,然后再单调加入。

对于询问 \([l,r]\)\(f_{i,j}\) 表示用 \(j\) 代价对 \(T[i \ldots i]\) 做删除和替换操作变成 \(S[l \ldots x]\) 的子序列,最小的 \(x\)

操作代价定义:

  • 删除操作:代价为 \(2\),因为在这里删除了之后会插入新的
  • 替换操作:代价为 \(1\)

转移考虑这一位做了什么:

  • 替换:\(f_{i,j}+1 \to f_{i+1,j+1}\)
  • 删除:\(f_{i,j} \to f_{i+1,j+2}\)
  • 不做操作:\(nxt(f_{i,j},T[i+1]) \to f_{i+1,j}\),其中 \(nxt(i,j)\) 表示 \(i\) 后面第一个 \(=j\) 的字符位置

询问的时候对于所有 \(f_{|T|,j} \leq r\) 的都更新一下答案就行了(只用插入的操作更新答案)

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 1e5 + 5;
const int MAXM = 20 + 1;
int f[MAXN][MAXM][2*MAXM+3];
// S的前缀i,匹配T的前缀j,len-f(len)<=k的最大len位置
char S[MAXN],T[MAXN];
int n,m,q;

int main(){
	scanf("%s%s",S+1,T+1);
	n = strlen(S+1);m = strlen(T+1);
	scanf("%d",&q);
	CLR(f,0x3f);
	FOR(i,0,n) FOR(j,0,m) FOR(k,-m-1,-j-1) f[i][j][k+MAXM] = -1;
	FOR(i,0,m) f[0][0][i+MAXM] = 0;
	FOR(i,-m-1,-1) f[0][0][i+MAXM] = -1;
	FOR(i,0,n){
		FOR(j,0,m){
			if(i == 0 && j == 0) continue;
			FOR(k,-m,m){
				f[i][j][k+MAXM] = std::min({f[i][j][k+MAXM],i==0?0x3f3f3f3f:f[i-1][j][k+MAXM]+1,j==0?0x3f3f3f3f:f[i][j-1][k+MAXM+1],j==0?0x3f3f3f3f:(f[i-1][j-1][k+MAXM-(S[i]==T[j])]+1)});
			}
		}
	}
	FOR(i,1,q){
		int l,r;scanf("%d%d",&l,&r);
		int len = r-l+1;
		FOR(k,-m,m){
			if(f[r][m][k+MAXM] >= len){
				printf("%d\n",len-k);
				break;
			}
		}
	}
	return 0;
}

D

这种排列字典序问题都是按位确定,我们先枚举卡到哪个上界,然后枚举这一个地方填一个比要求序列小的数,后面就可以乱填。

相当于我们现在是确定了一些点的权值,要求方案数。

注意这题保证父亲节点的编号小于子节点的编号,所以如果一个点 \(i\) 被钦定了,它到根的所有点都被钦定了。

我们考虑对于每个被钦定的点 \(v\) 求出所有它能限制到的点的数量 \(sz_v\),可以设它控制的点叫做"控制点集"。我们把方案数分两步算:第一步计算将权值分配到每个控制点集的方案书,第二步计算每个控制点集内所有数的放置方法。

先考虑算第一步:

我们考虑一个钦定点 \(v\) 的控制点集,设它被钦定的值是 \(b_v\),我们只需要知道当前还剩下多少个点满足 $ \geq b_v$ 并且没有被使用过,设有 \(s\) 个,方案数就是 \(\binom s {sz_v}\)

注意到限制形如 \(\geq b_v\),所以我们按照 \(b_v\) 从大到小排序,能影响第 \(i\) 个控制点集的选择的点的个数只有前面的点(并且一定全部都影响了),所以不难得出方案是

\[\prod_{i} \binom{n-b_i-sz_1-sz_2-\ldots-sz_{i-1}-(i-1)}{sz_i} \]

最后一个 \(-(i-1)\) 是把钦定点也算上不能选。

再考虑算第二步:

我们先考虑求一个大小为 \(n\) 的树,给所有点分配互不相同点权,保证是小根堆的方案数是多少:首先对于一个点 \(i\) 设它的子树为 \(sz_i\),那么它成为子树中最小值的概率就是 \(\frac{1}{sz_i}\)。于是总方案数就是 \(n! \prod_{i=1}^n \frac{1}{sz_i}\)

对应到这个题,我们只需要对于每个控制点集都独立求出方案乘起来就好了,相当于不用算根是最小值的概率(因为已经钦定了)。

这样得到了一个 \(O(n^3)\) 的做法。

发现每次查询我们实际上是对于一个点,询问它填所有值的方案数,我们如果能把这些询问都一次算完就好了。

我们可以考虑双指针,每次维护当前这个点的点集的排名,每次更改排名的时候改一下乘积贡献就好了。

最后是如何确定方案:我们首先跑到一个位置 \(p\) ,表示 \(1 \ldots p\) 如果都卡上界放方案数是 \(0\) 了。那么我们再倒回去,每次枚举最后一个钦定的点是否能通过填一个比上界小的数来使得有方案,如果有就可以顺着构造过去了。

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 5000+5;
const int ha = 1e9 + 7;

std::vector<int> G[MAXN];
int n;
int a[MAXN],b[MAXN];
bool del[MAXN];
int fac[MAXN],inv[MAXN];

inline int qpow(int a,int n=ha-2){
	int res = 1;
	while(n){
		if(n & 1) res = 1ll*res*a%ha;
		a = 1ll*a*a%ha;
		n >>= 1;
	}
	return res;
}

inline void prework(){
	fac[0] = 1;FOR(i,1,MAXN-1) fac[i] = 1ll*fac[i-1]*i%ha;
	inv[MAXN-1] = qpow(fac[MAXN-1]);ROF(i,MAXN-2,0) inv[i] = 1ll*inv[i+1]*(i+1)%ha;
}

inline int C(int n,int m){
	if(n < 0 || m < 0 || n < m) return 0;
	return 1ll*fac[n]*inv[m]%ha*inv[n-m]%ha;
}

inline int iC(int n,int m){
	if(n < 0 || m < 0 || n < m) return 0;
	return 1ll*inv[n]*fac[m]%ha*fac[n-m]%ha;
}

int ssz[MAXN];
bool flag = 1;
std::vector<P> S;
int coe[MAXN],gx = 1;
// 限制的点个数 限制是啥

inline void ddfs(int v){
	ssz[v] = 1;coe[v] = 1;
	for(auto x:G[v]){
		ddfs(x);
		ssz[v] += ssz[x];
		coe[v] = 1ll*coe[v]*coe[x]%ha;
	}
	coe[v] = 1ll*coe[v]*inv[ssz[v]]%ha*fac[ssz[v]-1]%ha;
}

int sp = 0,ss = 0,sf = 0;

inline void dfs(int v,int fa=0){
	if(fa && b[fa] > b[v]) flag = 0;
	int sm=0;
	for(auto x:G[v]){
		if(!b[x]) sm += ssz[x],gx = 1ll*gx*coe[x]%ha;
	}
	if(v != sp){
		S.pb(MP(b[v],sm));
	}
	else ss = sm,sf = fa;
	gx = 1ll*gx*fac[sm]%ha;
	for(auto x:G[v]){
		if(b[x]) dfs(x,v);
	}
}

int w[MAXN][MAXN],sm[MAXN];

int T[MAXN];

inline void work(int v,int limit){// 算出点v填1..n的方案数
	b[v] = 1e9;sp = v;flag = 1;S.clear();gx = 1;dfs(1);
	FOR(i,1,n) T[i] = -1;
	for(auto x:S) T[x.fi] = x.se;
	S.clear();
	ROF(i,n,1) if(T[i] != -1) S.pb(MP(i,T[i]));
	int ans = gx;
	FOR(i,0,(int)S.size()-1){
		if(i) sm[i] += sm[i-1];
		ans = 1ll*ans*C(n-S[i].fi-sm[i],S[i].se)%ha;
		sm[i+1] += S[i].se+1;
	}
	if(S.size()) sm[S.size()] += sm[(int)S.size()-1];
	int ps = (int)S.size()-1;// 在ps后面
	FOR(i,1,limit){
		while(ps >= 0 && S[ps].fi <= i){
			ans = 1ll*ans*iC(n-S[ps].fi-sm[ps],S[ps].se)%ha;
			ans = 1ll*ans*C(n-S[ps].fi-sm[ps]-ss-1,S[ps].se)%ha;
			--ps;
		}
		if(!flag || (sf && i < b[sf])) w[v][i] = 0;
		else w[v][i] = 1ll*ans*C(n-i-sm[ps+1],ss)%ha;
	}
	FOR(i,0,(int)S.size()+4) sm[i] = 0;
}

int fa[MAXN];

int main(){
	prework();
	scanf("%d",&n);
	FOR(i,2,n){
		int f;scanf("%d",&f);
		G[f].pb(i);fa[i] = f;
	}ddfs(1);
	FOR(i,1,n) scanf("%d",a+i);
	int ans = 0,ps = n+1;
	FOR(i,1,n){
		work(i,a[i]);
		FOR(j,1,a[i]-1){
			if(del[j]) continue;
			b[i] = j;
			(ans += w[i][j]) %= ha;
		}
		b[i] = a[i];
		del[a[i]] = 1;
		int f = w[i][a[i]];
		if(!f){
			ps = i;break;
		}
	}
	if(ps == n+1){
		ans++;ans %= ha;
		FOR(i,1,n) printf("%d ",a[i]);
		printf("\n%d\n",ans);
		exit(0);
	}
	while(ps >= 1){
		del[b[ps]] = 0;
		bool flag = 0;
		ROF(j,a[ps]-1,1){
			if(del[j]) continue;
			b[ps] = j;
			int f = w[ps][j];
			if(f){
				flag = 1;break;
			}
		}
		if(flag){
			del[b[ps]] = 1;
			break;
		}
		b[ps] = 0;
		ps--;
	}
	FOR(i,ps+1,n){
		int t = n,tot = 0;
		while(tot < ssz[i]-1){
			if(!del[t]) tot++;
			--t;
		}
		work(i,t);
		ROF(j,t,1){
			if(del[j]) continue;
			b[i] = j;
			int f = w[i][j];
			if(f) break;
		}
		del[b[i]] = 1;
	}
	FOR(i,1,n) printf("%d ",b[i]);
	FOR(i,1,n) del[i] = 0;
	printf("\n%d\n",ans);
	return 0;
}
posted @ 2020-10-12 21:48  RainAir  阅读(171)  评论(2编辑  收藏  举报