排列题选做

发现自己对于排列的信息完全不会刻画,于是找了一些排列题。

诶诶怎么差点变成经典 trick 集合了啊,没事反正我都不会。

[JOI Open 2016] 摩天大楼 / Skyscraper

给定长度为 \(n\) 的整数序列 \(A\)。求 \(1\) 到 \(n\) 的排列 \(P\) 数量,满足 \(\sum\limits_{i=1}^{n-1} |a_{p_i}-a_{p_{i+1}}| \leq L\)。答案对 \(10^9+7\) 取模。

\(1 \leq n \leq 100\),\(1 \leq a_i,L \leq 1000\)。

本题中对排列的刻画方式:将所有数按一定顺序加入排列,同时维护序列信息。

在这里,我们需要维护的是连续段。话说这个貌似有个专门的名字,叫连续段 dp?

在本题中,我们先将 \(A\) 从大到小排序,接下来考虑对每个 \(a_i-a_{i+1}\) 求出其贡献。容易发现,只有以下两种情况会产生贡献:

  • \(a_{p_i} \leq a_{i+1}\) 且 \(a_{p_{i+1}} \geq a_i\)

  • \(a_{p_i} \geq a_i\) 且 \(a_{p_{i+1}} \leq a_{i+1}\)

在本题中,我们按照 \(i\) 从小到大的顺序加入 \(a_i\)。此时在加入 \(a_i\) 后,两种情况可以表为:

  • \(a_{p_i}\) 未加入且 \(a_{p_{i+1}}\) 已加入

  • \(a_{p_{i+1}}\) 未加入且 \(a_{p_i}\) 已加入

我们考虑维护已加入的数构成的连续段。注意,这里不必考虑每个连续段在序列里的具体位置,只需要保证加入所有数后,序列只构成一个连续段即可。容易证明,这么考虑的方案数不会改变。

于是我们令 \(dp_{i,j,k}\) 表示加入了前 \(i\) 个数,构成 \(j\) 个连续段,总贡献为 \(k\) 的方案数。然而这么做会导致我们无法处理开头和结尾的情况。我们只需要多加两个 \(01\) 变量 \(f\) 和 \(g\),分别表示开头与结尾是否被连续段覆盖。

我们考虑加入一个数只有三种情况:新建一个段,加入一个段与合并两个段,大力 \(dp\) 即可,细节见代码。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const long long mod=1000000007;
int a[110],dp[110][1010][2][2],lst_dp[110][1010][2][2];
bool cmp(const int &lhs,const int &rhs){
	return lhs>rhs;
}
int main(){
	int n,L;
	scanf("%d %d",&n,&L);
	if(n==1){
		printf("1");
		return 0;
	}
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	sort(a+1,a+1+n,cmp);
	dp[1][0][0][1]=dp[1][0][1][0]=1;
	if(n>2){
		dp[1][0][0][0]=1;
	}
	for(int i=1;i<n;i++){
		for(int j=0;j<=n;j++){
			for(int k=0;k<=L;k++){
				for(int f=0;f<=1;f++){
					for(int g=0;g<=1;g++){
						lst_dp[j][k][f][g]=dp[j][k][f][g];
						dp[j][k][f][g]=0;
					}
				}
			}
		}
		int pre_num=a[i]-a[i+1];
		for(int j=0;j<=n;j++){
			for(int k=0;k<=L;k++){
				for(int f=0;f<=1;f++){
					for(int g=0;g<=1;g++){
						int new_k=k+(j*2-f-g)*pre_num;
						if(new_k<0  ||  new_k>L){
							continue;
						}
						long long pre_dp=lst_dp[j][k][f][g];
						if(j+1<=n){
							if(j>=2){
								dp[j+1][new_k][f][g]+=pre_dp*(j-1)%mod;
								dp[j+1][new_k][f][g]%=mod;
							}
							if(f==0){
								dp[j+1][new_k][0][g]+=pre_dp;
								dp[j+1][new_k][0][g]%=mod;
								dp[j+1][new_k][1][g]+=pre_dp;
								dp[j+1][new_k][1][g]%=mod;
							}
							if(g==0){
								dp[j+1][new_k][f][0]+=pre_dp;
								dp[j+1][new_k][f][0]%=mod;
								dp[j+1][new_k][f][1]+=pre_dp;
								dp[j+1][new_k][f][1]%=mod;
							}
						}
						if(j>=2){
							dp[j][new_k][f][g]+=pre_dp*2*(j-1)%mod;
							dp[j][new_k][f][g]%=mod;
						}
						if(f==0){
							dp[j][new_k][0][g]+=pre_dp;
							dp[j][new_k][0][g]%=mod;
							dp[j][new_k][1][g]+=pre_dp;
							dp[j][new_k][1][g]%=mod;
						}
						if(g==0){
							dp[j][new_k][f][0]+=pre_dp;
							dp[j][new_k][f][0]%=mod;
							dp[j][new_k][f][1]+=pre_dp;
							dp[j][new_k][f][1]%=mod;
						}
						if(j-1>=0){
							if(j>=2){
								dp[j-1][new_k][f][g]+=pre_dp*(j-1)%mod;
								dp[j-1][new_k][f][g]%=mod;
							}
						}
					}
				}
			}
		}
	}
	long long ans=0;
	for(int i=0;i<=L;i++){
		ans+=dp[1][i][1][1];
		ans%=mod;
	}
	printf("%lld",ans);
	return 0;
}

『JROI-4』沈阳大街 2

给定长度为 \(n\) 的序列 \(A\) 和 \(B\),保证对于 \(i \in [1,n)\) 有 \(a_i \geq a_{i+1}\) 且 \(a_n \geq \min\limits_{i=1}^{n} b_i\)。对于一个 \(1\) 到 \(n\) 的排列 \(P\),定义 \(f(P)=\prod\limits_{i=1}^{n} \min(a_i,b_{p_i})\)。求 \(f(P)\) 的期望,对 \(998244353\) 取模。

\(1 \leq n \leq 5000\),\(1 \leq a_i,b_i \leq 10^9\)。

本题中对排列的刻画方式:对于两个排列之间有关联的情况,考虑将两个排列中的元素加入一个序列,并对新序列进行匹配。

具体地,在本题中我们考虑将 \(A\) 和 \(B\) 中的元素加入新的序列 \(C\),并将 \(C\) 从小到大排序。问题转化为对 \(C\) 中的元素进行两两匹配,满足每组匹配的两个元素分别属于 \(A\) 和 \(B\),产生的贡献即为靠前的元素。

套路性的,我们让期望转为 \(f(P)\) 之和。不妨记录 \(dp_{i,j,k}\) 表示当前考虑了 \(1\) 到 \(i\) 中的元素,有 \(j\) 个 \(A\) 序列中的元素和 \(k\) 个 \(B\) 序列中的元素未匹配的答案。此时我们容易做到 \(O(n^3)\),考虑优化。

注意到我们可以预处理出每个前缀中属于 \(A\) 的元素个数和属于 \(B\) 的元素个数,此时我们就可以通过 \(i\) 和 \(j\) 算出 \(k\)。这样我们就得到了 \(O(n^2)\) 的做法。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const long long mod=998244353;
struct Node{
	int belong;
	long long num;
}a[10010];
bool operator <(const Node &lhs,const Node &rhs){
	return lhs.num<rhs.num;
}
int cnt_from_1[10010];
long long dp[5010],lst_dp[5010];
long long inv(long long num){
	long long pre=mod-2,ans=1;
	while(pre){
		if(pre&1){
			ans*=num;
			ans%=mod;
		}
		num*=num;
		num%=mod;
		pre>>=1;
	}
	return ans;
}
int main(){
	int n;
	scanf("%d",&n);
	long long fact=1;
	for(int i=1;i<=n;i++){
		fact*=i;
		fact%=mod;
		long long num;
		scanf("%lld",&num);
		a[i]=(Node){1,num};
	}
	for(int i=1;i<=n;i++){
		long long num;
		scanf("%lld",&num);
		a[i+n]=(Node){2,num};
	}
	sort(a+1,a+1+2*n);
	dp[0]=1;
	for(int i=1;i<=n*2;i++){
		cnt_from_1[i]=cnt_from_1[i-1];
		if(a[i].belong==1){
			cnt_from_1[i]++;
		}
		for(int j=0;j<=n;j++){
			lst_dp[j]=dp[j];
			dp[j]=0;
		}
		for(int j=0;j<=n;j++){
			int cnt_1=j,cnt_2=(i-cnt_from_1[i])-(cnt_from_1[i]-j);
			if(cnt_1>=0  &&  cnt_1<=cnt_from_1[i]  &&  cnt_2>=0  &&  cnt_2<=i-cnt_from_1[i]){
				if(cnt_1>0  &&  a[i].belong==1){
					dp[j]+=lst_dp[j-1]*a[i].num%mod;
					dp[j]%=mod;
				}
				if(cnt_2>0  &&  a[i].belong==2){
					dp[j]+=lst_dp[j]*a[i].num%mod;
					dp[j]%=mod;
				}
				if(cnt_1<cnt_from_1[i]  &&  a[i].belong==2){
					dp[j]+=lst_dp[j+1]*(cnt_1+1)%mod;
					dp[j]%=mod;
				}
				if(cnt_2<i-cnt_from_1[i]  &&  a[i].belong==1){
					dp[j]+=lst_dp[j]*(cnt_2+1)%mod;
					dp[j]%=mod;
				}
			}
		}
	}
	printf("%lld",dp[0]*inv(fact)%mod);
	return 0;
}

[ARC207A] Affinity for Artifacts

给定长度为 \(n\) 的整数序列 \(A\) 和整数 \(X\)。对于一个 \(1\) 到 \(n\) 的排列 \(P\),若满足下面的条件则称其合法:

  • 进行 \(n\) 次操作,第 \(i\) 次操作首先产生 \(a_{p_i}\) 的代价,然后对于所有 \(i \in [1,n]\),将 \(a_i\) 变为 \(\max(0,a_i-1)\)。排列 \(P\) 合法当且仅当每次操作的代价之和不超过 \(X\)。

求合法的排列 \(P\) 总数,对 \(998244353\) 取模。

\(1 \leq n \leq 100\),\(0 \leq a_i,X \leq 10^9\)。

本题的刻画方式和上一题相似。

考虑不对 \(a_i\) 进行显式的修改,不妨令初始的代价为 \(\sum\limits_{i=1}^{n} a_i\),接下来的操作类似于省钱,可以写出 \(\sum\limits_{i=1}^{n} a_i - \sum\limits_{i=1}^n \min(a_{p_i},i-1) \leq X\),变形可得 \(\sum\limits_{i=1}^n \min(a_{p_i},i-1) \geq \sum\limits_{i=1}^{n} a_i-X\)。

令 \(Q=\sum\limits_{i=1}^{n} a_i-X\),则 \(\sum\limits_{i=1}^n \min(a_{p_i},i-1) \geq Q\)。容易发现左式是 \(O(n^2)\) 级别的,考虑计算出对于每个 \(T\) 满足 \(\sum\limits_{i=1}^n \min(a_{p_i},i-1)=T\) 的排列 \(P\) 总数。

我们套用上一题的方法,将所有的 \(a_i\) 与 \(i-1\) 加入同一个序列并从小到大排序,此时可以将原问题看作匹配,记 \(dp_{i,j,k}\) 表示对于新序列的前 \(i\) 个数,有 \(j\) 个 \(A\) 中的元素没有匹配,当前贡献和为 \(k\) 的方案数,我们就得到了一个 \(O(n^4)\) 的做法,可以通过。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const long long mod=998244353;
struct Node{
	int belong;
	long long num;
}a[210];
bool operator <(const Node &lhs,const Node &rhs){
	return lhs.num<rhs.num;
}
int num[110],cnt_from_1[210];
long long dp[110][10010],lst_dp[110][10010];
int main(){
	int n,V=0;
	long long X,T=0;
	scanf("%d %lld",&n,&X);
	for(int i=1;i<=n;i++){
		scanf("%d",&num[i]);
		a[i]=(Node){1,num[i]};
		a[i+n]=(Node){2,i-1};
		V+=i-1;
		T+=num[i];
	}
	T-=X;
	sort(a+1,a+1+2*n);
	dp[0][0]=1;
	for(int i=1;i<=n*2;i++){
		cnt_from_1[i]=cnt_from_1[i-1];
		if(a[i].belong==1){
			cnt_from_1[i]++;
		}
		for(int j=0;j<=n;j++){
			for(int k=0;k<=V;k++){
				lst_dp[j][k]=dp[j][k];
				dp[j][k]=0;
			}
		}
		for(int j=0;j<=n;j++){
			for(int k=0;k<=V;k++){
				int cnt_1=j,cnt_2=(i-cnt_from_1[i])-(cnt_from_1[i]-j);
				if(cnt_1>=0  &&  cnt_1<=cnt_from_1[i]  &&  cnt_2>=0  &&  cnt_2<=i-cnt_from_1[i]){
					if(cnt_1>0  &&  a[i].belong==1  &&  k>=a[i].num){
						dp[j][k]+=lst_dp[j-1][k-a[i].num];
						dp[j][k]%=mod;
					}
					if(cnt_2>0  &&  a[i].belong==2  &&  k>=a[i].num){
						dp[j][k]+=lst_dp[j][k-a[i].num];
						dp[j][k]%=mod;
					}
					if(cnt_1<cnt_from_1[i]  &&  a[i].belong==2){
						dp[j][k]+=lst_dp[j+1][k]*(cnt_1+1)%mod;
						dp[j][k]%=mod;
					}
					if(cnt_2<i-cnt_from_1[i]  &&  a[i].belong==1){
						dp[j][k]+=lst_dp[j][k]*(cnt_2+1)%mod;
						dp[j][k]%=mod;
					}
				}
			}
		}
	}
	long long ans=0;
	for(int i=0;i<=V;i++){
		if(i>=T){
			ans+=dp[0][i];
			ans%=mod;
		}
	}
	printf("%lld",ans);
	return 0;
}
posted @ 2025-12-15 16:39  Oken喵~  阅读(9)  评论(1)    收藏  举报