题解 nflsoj464 CF1267K 正睿1225:一个简单的计数技巧

前言

考虑一个简单的模型。有\(n\)个物品,\(m\)个位置,第\(i\)物品只能放在\(1\dots p_i\)中的某个位置上。求有多少种方案,使得每个物品恰好匹配一个位置,每个位置上至多只有一个物品。

标题所说的“一个简单的计数技巧”,指的正是解决这个问题的方法。

我们把所有\(p_i\)从小到大排序。此时每个物品的选择空间依次递增。也就是说,第\(i\)个物品可以选择的范围,一定完全包含了第\(1\dots i-1\)个物品的选择范围。那么显然,答案就是\(\prod_{i=1}^{n}(p_i-(i-1))\)

这个模型虽然简单,却非常常用。需要深刻理解。本文讲的就是三道用到该模型的例题。

例题一 nflsoj464 【六校联合训练 CSP #7】唱跳

题目链接

对于所有\(\leq\lfloor\frac{n}{k}\rfloor\)的数\(x\),要么放在最后一个,要么一定有一个\(\geq kx\)后继。发现较前面的数可选的后继的范围包括了较后面的数的可选范围。因此我们从后往前(从\(\lfloor\frac{n}{k}\rfloor\)\(1\))给每个数选一个后继。

分配好后继以后,相当于有\(n-\lfloor\frac{n}{k}\rfloor\)条自由的链,可以任意排列,所以答案乘上\((n-\lfloor\frac{n}{k}\rfloor)!\)即可。

注意特判\(k=1\)的情况。

时间复杂度\(O(n)\)

参考代码(片段):

const int MOD=1e9+7;
int main() {
	int T=read();while(T--){
		int n=read(),k=read(),ans=1;if(k==1){puts("1");continue;}
		for(int i=n/k,c=0;i>=1;--i,++c)ans=(ll)ans*(n-i*k+1+1-c)%MOD;
		for(int i=1;i<=n-n/k;++i)ans=(ll)ans*i%MOD;
		printf("%d\n",ans);
	}
	return 0;
}

例题二 CF1267K Key Storage

题目链接

根据\(n\)和题目给出的定义,我们可以求出\(n\)对应的multiset。设multiset大小为\(k\)

如果知道了\(k\)个元素的出现顺序,就能够还原出原数。给定两个长度为\(k\)的序列,发现如果这两个序列不同(定义两个序列不同当且仅当存在至少一个位置上的值不同),则还原出的数一定不同。

我们知道,所有可能的序列共有\(\frac{k!}{\prod_{x}(cnt(x)!)}\)个。其中\(cnt(x)\)表示\(x\)这个数在multiset里的出现次数。

但并不是每种可能的序列都是合法的。例如根据样例可以发现:\([2,1,1]\)这个序列就不合法,因为第一个位置上的\(2\)大于等于该位置的模数了。又如\([0,0,0,2,3,3,4,0]\)也不合法,因为它的结尾是\(0\)。于是可以总结出:一个序列合法,当且仅当同时满足如下两个条件:

  1. 每个位置上的值要严格小于这个位置的模数。
  2. 结尾不能为\(0\)

先只考虑条件1。我们从大到小考虑每个值放在哪些位置。发现每个值可以放的位置是一段后缀,且这个后缀的长度是递增的。所以每个值可以放的位置数就是这个后缀的长度减去之前已经放了的值的数量

因为multiset中的值不会超过\(20\),所以时间复杂度\(O(n\cdot20)\)

参考代码:

//problem:
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fst first
#define scd second

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

namespace Fread{
const int MAXN=1<<20;
char buf[MAXN],*S,*T;
inline char getchar(){
	if(S==T){
		T=(S=buf)+fread(buf,1,MAXN,stdin);
		if(S==T)return EOF;
	}
	return *S++;
}
}//namespace Fread
#ifdef ONLINE_JUDGE
	#define getchar Fread::getchar
#endif
inline int read(){
	int f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
inline ll readll(){
	ll f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
/*  ------  by:duyi  ------  */ // myt天下第一
int cnt[30],len;
ll comb(ll n,ll k){
	if(n<k)return 0;
	ll res=1;
	for(ll i=n;i>=n-k+1;--i)res*=i;
	for(ll i=1;i<=k;++i)res/=i;
	return res;
}
ll calc(){
	int j=len+1,ch=0;
	ll res=1;
	for(int i=20;i>=0;--i){
		if(!cnt[i])continue;
		while(j-1>=1&&j>i)--j;
		res*=comb(len-j+1-ch,cnt[i]);
		ch+=cnt[i];
	}
	return res;
}
int main() {
	int T=read();while(T--){
		ll n=readll();
		memset(cnt,0,sizeof(cnt));len=0;
		for(int i=2;;++i){
			cnt[n%i]++;
			len++;
			n/=i;
			if(!n)break;
		}
		//for(int i=1;i<=20;++i)cout<<cnt[i]<<" ";cout<<endl;
		ll ans1=calc(),ans2=0;
		if(cnt[0]){
			cnt[0]--;
			len--;
			ans2=calc();
		}
		printf("%lld\n",ans1-ans2-1);
	}
	return 0;
}

例题三 正睿1225 【20省选十联测day3】选拔赛

题目链接

考虑\(a_i+b_i\)的这个序列。题目要求前\(k\)个的最小值\(\geq\)\(n-k\)个的最大值

\(f(L,R)\)表示前\(k\)个数\(\geq L\),后\(n-k\)个数\(\leq R\)的方案数。我们\(O(n^2)\)\(O(\text{值域})\)枚举所有可能的前\(k\)个数的最小值\(T\),则对答案的贡献就是\(f(T,T)-f(T+1,T)\)

考虑计算\(f(L,R)\)。把\(a,c\)从大到小排序后,\(c\)中每个数,如果要匹配前\(k\)个位置,则能匹配的\(a\)中的数是一个前缀,且长度递减;如果要匹配后\(n-k\)个位置,则能匹配的a中的数是一个后缀,且长度递增

\(dp[i][j]\)表示考虑了\(c\)中前\(i\)个数,有\(j\)个数匹配后\(n-k\)个位置的方案数。

转移时,长度递增的后缀很好处理;对于长度递减的前缀,因为我们知道总共一定会选\(k\)个前缀,所以我们可以计算出在\(c_i\)之后还会选择几个前缀,即有几个匹配的可选范围比\(c_i\)小,就可以转移了。

时间复杂度\(O(n^4)\)\(O(\text{值域}\cdot n^2)\)

参考代码:

//problem:zr1225
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fst first
#define scd second

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;

namespace Fread{
	const int MAXN=1<<20;
	char buffer[MAXN],*S,*T;
	inline char getchar(){
		if(S==T){
			T=(S=buffer)+fread(buffer,1,MAXN,stdin);
			if(S==T) return EOF;
		}
		return *S++;
	}
}
#ifdef ONLINE_JUDGE
	#define getchar Fread::getchar
#endif
inline int read(){
	int f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
inline ll readll(){
	ll f=1,x=0;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}

const int MOD=1e9+7;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int &x,int y){x=mod1(x+y);}
inline void sub(int &x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}

int n,m,a[105],c[105],dp[105][105];
int solve(int L,int R){
	memset(dp,0,sizeof(dp));
	dp[0][0]=1;
	for(int i=0,pl=m,pr=n+1;i<n;++i){
		while(pr-1>m&&a[pr-1]+c[i+1]<=R)--pr;
		while(pl>=1&&a[pl]+c[i+1]<L)--pl;
		for(int j=0;j<=i&&j<=n-m;++j)if(dp[i][j]){
			if(j<n-pr+1)add(dp[i+1][j+1],(ll)dp[i][j]*(n-pr+1-j)%MOD);
			if(pl>m-(i-j)-1)add(dp[i+1][j],(ll)dp[i][j]*(pl-(m-(i-j)-1))%MOD);
		}
	}
	return dp[n][n-m];
}

int main() {
	n=read();m=read();
	for(int i=1;i<=n;++i)a[i]=read();
	for(int i=1;i<=n;++i)c[i]=read();
	sort(a+1,a+n+1),reverse(a+1,a+n+1);
	sort(c+1,c+n+1),reverse(c+1,c+n+1);
	int ans=0;
	map<int,bool>mp;
	for(int i=1;i<=n;++i)for(int j=1;j<=n;++j){
		int T=a[i]+c[j];if(mp.count(T))continue;mp[T]=1;
		add(ans,mod2(solve(T,T)-solve(T+1,T)));
	}
	cout<<ans<<endl;
	return 0;
}
posted @ 2020-03-23 17:06  duyiblue  阅读(542)  评论(1编辑  收藏  举报