赛后——2.8 寒假模拟4


\(\text{T1}\)

题意

给定一个 \(0 \sim n-1\) 的排列 \(p\)

一个 \(0\sim n-2\) 的排列 \(q\) 被认为是完美的,当且仅当满足下列条件:

对排列 \(s=\{0,1,2\cdots n-1\}\) 进行 \(n-1\) 次交换(下标从 \(0\) 开始),第 \(i\) 次交换时,交换 \(s[q_{i-1}],s[q_{i-1}+1]\),最后能使排列 \(s=p\)

问有多少个优美的排列,答案对 \({10}^9+7\) 取模。

思路

\(s\) 得到 \(p\) 与由 \(p\) 得到 \(s\) 是等价的,于是考虑把 \(p\) 弄成一个有序的排列 \(s\)

发现在一次交换 \(p_k,p_k+1\) 后,后一部分的任意一个数都应大于前一部分的任意一个数(因为不会在对这两个区间跨区间交换),于是考虑区间 \(dp\)

只要找到一个能保证满足上述左右性质的下标,交换后转移即可。

对于区间 \([L,R]\) 中交换 \(k\),区间内可以交换的数对为 \(R-L\) 个,除去当前一个,并在其中选择 \(k-L\) 个(我们假设是按序交换的,乘上排列数后就释放了这一限制),得到排列数 \(\mathrm{C}_ {R-L+1}^{k-L}\)

代码

int n;
int p[55];
ll dp[55][55],C[55][55];
inline ll dfs(int l,int r){
	if(dp[l][r]!=-1) return dp[l][r];
	if(l==r) return dp[l][r]=1;
	dp[l][r]=0;
	int maxx=0;
	for(int i=l;i

\(\text{T2}\)

题意

\(S\) 在一个 \(n\times n\) 的棋盘上玩游戏。

他首先在方格上随机地填入 \(1\)\(m\) 之间的正整数(每个方格填的数互不相同),然后随机地选出 \(k\) 个数字(可能不在棋盘上),把它们出现在棋盘上的方格涂黑。

设有 \(R\) 行被整行涂黑,有 \(C\) 列被整列涂黑,便可以得到 \(2^{R+C}\) 分。

求它的期望得分。

若结果大于 \(10^{99}\) 输出 \(10^{99}\)

思路

每个 \(R\)\(C\) 列都有一个 \(2^{R+C}\) 的贡献,发现恰好这个 \(R\)\(C\) 列的方格有 \(2^{R+C}\) 个子集(行和列在本质上是一样的),于是每个子集对答案的贡献为 \(1\)

接着枚举 \(i\in[0,n],j\in[1,n]\) 表示一个 \(i\)\(j\) 列的子集,保证他已经被填满,显然这个子集的情况数是 \(\mathrm{C}_ {n}^i\times \mathrm{C}_ {n}^j\)。填满需要的数字为 \(x=(i+j)\times n-i\times j\),于是剩余的填入正整数个数为 \(m-x\),可染色的个数为 \(k-x\),于是\(i\)\(j\) 列以外情况数为 \(\mathrm{C}_ {m-x}^{k-x}\),总情况数是 \(\mathrm{C}_ {m}^{k}\)

于是答案:

\[\operatorname{ans}=\sum_{i=0}^n\sum_{j=0}^n \frac{\mathrm{C}_ {n}^i\times \mathrm{C}_ {n}^j\times \mathrm{C}_ {m-x}^{k-x}}{\mathrm{C}_ {m}^{k}} \]

考虑怎样快速求这个式子,显然任何一个 \(\mathrm{C}_ n^i\) 都能递推出来,递推式为:

\[\mathrm{C}_ n^i=\frac{n!}{i!(n-i)!}=\frac{n!}{(i-1)!(n-i+1)!}\times \frac{n-i+1}{i}=\mathrm{C}_ n^{i-1}\times \frac{n-i+1}{i} \]

而同样的,剩余的分式,也可以递推得出:

\[\frac{\mathrm{C}_ {m-i}^{k-i}}{\mathrm{C}_ m^k}=\frac{\mathrm{C}_ {m-i+1}^{k-i+1}}{\mathrm{C}_ m^k}\times \frac{k-i+1}{m-i+1} \]

代码

int n,m,k;
db c[305],f[maxn];
db ans;
int main(){
	n=read(),m=read(),k=read();
	c[0]=1.0,f[0]=1.0;
	for(int i=1;i<=n;i++){
		c[i]=c[i-1]*(n-i+1)/i;
	}
	for(int i=1;i<=k;i++){
		f[i]=f[i-1]*(k-i+1)/(m-i+1);
	}
	for(int i=0;i<=n;i++){
		for(int j=0;j<=n;j++){
			int siz=(i+j)*n-i*j;
			if(siz<=k){
				ans+=c[i]*c[j]*f[siz];
			}
		}
	}
	if(ans>1e99) printf("1e99\n");
	else printf("%.7lf\n",ans);
	return 0;
}

\(\text{T3}\)

题意

\(S\) 有一个 \(n\) 个节点的二叉树。每个节点上有一个权值。节点从 \(1\) 开始编号。

现在,小 \(S\) 打算把这个二叉树改造成一个二叉排序树,二叉排序树的定义是,它的权值要比左子树的权值要大,但是比右子树要小。

他要修改某些节点的权值,使得它成为一个二叉排序树,求最小的修改次数。

思路

把树上问题转移到线性,发现要求的是这棵树的中序遍历修改成单调递增序列的最小次数。

如果直接求最长上升子序列的长再用 \(n\) 去减显然是错的,因为有可能一个区间的定义域大于值域,直接把这个区间修改时显然错的。

于是设一个 \(f_i\) 表示前 \(i\) 个数无需修改的个数,最后答案是 \(n-f_n\),发现这个的转移是 \(\max_{j}^{i-1}\{f_j+1\}\ (val_i-val_j\ge i-j)\),换句话说,转移次数就是最终的结果,那么为了让 \(n-f_n\) 最小,要让转移次数最多,也就是尽量长的一个 \(val_i-val_j\ge i-j\),移项得到 \(val_i-i\ge val_j-j\),于是设 \(a_i=val_i-i\),显然是要求 \(a\) 的最长不下降子序列长度,也就是 \(f_n\) 了。

这里不能用朴素的 \(\operatorname{LIS}\) 算法,需要二分优化。

代码

int n,len;
int val[maxn],a[maxn],tot;
int tr[maxn][2];
inline void dfs(int u){
	if(tr[u][0]) dfs(tr[u][0]);
	a[++tot]=val[u];
	if(tr[u][1]) dfs(tr[u][1]);
}
int f[maxn],g[maxn];
inline int get(int x){
	int l=1,r=len;
	int res=0;
	while(l<=r){
		int mid=(l+r)>>1;
		if(g[mid]<=x){
			res=mid;
			l=mid+1;
		}
		else{
			r=mid-1;
		}
	}
	return res;
}
int main(){
	n=read();
	for(int i=1;i<=n;i++){
		val[i]=read();
	}
	for(int i=2;i<=n;i++){
		int fa=read(),ch=read();
		tr[fa][ch]=i;
	}
	dfs(1);
	for(int i=1;i<=n;i++){
		a[i]-=i;
	}
	for(int i=1;i<=n;i++){
		f[i]=get(a[i])+1;
		g[f[i]]=a[i];
		len=max(len,f[i]);
	}
	printf("%d\n",n-len);
	return 0;
}

\(\text{T4}\) 铁路

题意

\(S\) 有一个长度为 \(n\) 的序列。

一个区间 \([L,R]\) 是好的,当且仅当存在 \(k\in [L,R]\),使得 \(\forall\ i\in [L,R],\ a_k\mid a_i\)

现在,小 \(S\) 想要知道,最长的好的区间是多少,并且这些区间是什么。

思路

首先这个 \(a_k\) 显然只能是 \([L,R]\) 的最小值,赛时用线段树去维护这东西(不要问我为什么不带修用线段树),预处理出一堆前缀后缀和之类的,复杂度是 \(O(n^2\log n)\) 的。

然而正解是 \(O(n\log n)\) 的,用 \(\operatorname{ST}\) 表去维护静态的区间最小值和区间 \(\gcd\),二分答案 \(len\) 去判断每一个这样的区间最小值与 \(\gcd\) 是否相等,然后大力判断输出答案即可。

代码

int n;
int a[maxn];
int stmin[maxn][21],stgcd[maxn][21];
inline int gcd(int a,int b){
	if(!b) return a;
	return gcd(b,a%b);
}
inline bool check(int len){
	if(!len) return true;
	for(int i=1;i+len<=n;i++){
		for(int k=20;k>=0;k--){
			if((1<=0;k--){
			if((1<>1;
		if(check(mid)){
			len=mid;
			l=mid+1;
		}
		else{
			r=mid-1;
		}
	}
	print(len);
	return 0;
}