Loading

To argue, or not to argue [from vjudge] 题解

一.题意:

一个\(n\times m\)的矩阵,表示可用的座位,有\(k\)对人不能坐在一起(每个人均不同),同时有些座位被其他客人占了,这\(k\)对人不能选这类位置坐,问合法的方案数\((n\times m\leq144)\)

二.思路分析:

这个\(144\)给的太特别了,很好注意到他是\(12\)的平方,也就等价告诉你,\(min\{n,m\}\leq12\),那其实这样的话第一反应肯定会倾向于往状压靠,但你发现其实还是没那么容易定义状态,所以我们先试着分析其他性质。

再次分析题目,发现不能坐在一起这个条件实在是太难处理了,正难则反,我们考虑能坐在一起的方案,最终所求即为\(0\)对人坐在一起,额,这个很难不往容斥的思路走,我们定义\(G(i)\)表示钦定至少\(i\)对人放在一起的方案数,但是要注意的是,此处的\(i\)对人是不做区分的,也就是并不是题目中所说的每个人互不相同,所以说对于每次的贡献我们理应有:

\[\dbinom{k}{i}\cdot i!\cdot 2^i\cdot p(n\cdot m-cnt-2\cdot i,2\cdot(k-i)) \]

上面那个冗长一段的函数\(p\)是排列数,而\(cnt\)是被普通客人占了的位置。

现在给出组合解释:因为我们的方案数是互不区分的,但是实际上我们有\(k\)对不同的人,所以我们要先从其中选\(i\)对不同的出来。其次,因为我们有区分,所以这\(i\)对人的顺序是可以进行排列的,每一次排列都是不同的方案,注意,这里是组与组之间进行排列。现在考虑每对内部,发现每对内部的位置可以互相交换且对于全局独立,所以有\(2^i\)种方案,最后,剩下的数可以在所有的剩余合法位置任意排列,就有了我们冗长的排列数。

现在我们令刚才的贡献为\(now_i\)
所以最终的答案就为:

\[ans=\sum_{i=0}^k(-1)^i\cdot G(i)\cdot now_i \]

现在的问题就在于如何求每一个\(G\),考虑dp,其实已经有很多部分在引导我们往正确的状态上定义了,首先是\(min\{n,m\}\leq12\)这一条件中的最小值这部分,就提醒我们这可能不是一般的状态压缩dp。其次,你发现\(G\)的定义中有\(i\)对人相邻且每对不做区分这一条件就很像经典多米诺骨牌摆放问题,所以大胆猜想可以使用轮廓线dp解决这个问题,定义:\(f(i,S,k)\)表示考虑到第\(i(1\leq i\leq n\times m)\)号点时,轮廓线状态为\(S\),且放置了\(k\)个骨牌的方案数。

那就有如下转移:

1. 当前点已经被覆盖,那么我们什么都做不了,所以:

\[f(i,S-2^j,k)\leftarrow f(i-1,S,k) \]

2. 当前点未被覆盖:

\((1)\):我们横着放,此时,需要考虑右边格子的状态,若为空才能放:

\[f(i,S+2^{j+1},k+1)\leftarrow f(i-1,S,k) \]

\((2)\):竖放,与上述分析同理:

\[f(i,S+2^j,k+1)\leftarrow f(i-1,S,k) \]

\((3)\):需要注意的是,这个问题于经典的骨牌放置是有区别的,我们不需要把矩阵放满,所以此时可以选择不放:

\[f(i,S,k)\leftarrow f(i-1,S,k) \]

上述便是所有的转移了。

在最终实现时,需要注意:轮廓线dp虽然支持复杂度降到\(O(nmk\cdot 2^{min\{n,m\}})\)
但是在这个问题中,取最小值是有条件的,如果最开始你发现\(m>n\)那么就需要将矩阵的行和列调转再进行操作。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define int long long
const int mod = 1e9 + 7,NM = 150,S = 4096 + 5,K = 75,MAXF = 1000 + 5;

int n,m,k,f[NM][S][K],ans,sum_get,id; 
char s[NM][NM],ts[NM][NM];
int inv[MAXF],fac[MAXF],pow_2[MAXF];

int qpow(int a,int b){
	int res = 1;
	while(b){
		if(b & 1) res = (res * a) % mod;
		a = (a * a) % mod;
		b >>= 1;
	} 
	return res;
}

int C(int a,int b){
	if(b > a || b < 0 || a < 0) return 0;
	return fac[a] * inv[b] % mod * inv[a - b] % mod;;	
}

int A(int a,int b){
	return C(a,b) * fac[b] % mod;
}

void init(){
	pow_2[0] = fac[0] = 1;
	for(int i = 1;i <= 200;++i) pow_2[i] = pow_2[i - 1] * 2 % mod;
	for(int i = 1;i <= 200;++i) fac[i] = fac[i - 1] * i % mod;
	inv[200] = qpow(fac[200],mod - 2);
	for(int i = 200 - 1;i >= 0;--i)
	{
		inv[i] = inv[i + 1] * (i + 1) % mod;
	}
}

void calc_dp(){
	f[0][0][0] = 1;
	id = 0;
	for(int i = 0;i < n;++i){
		for(int j = 0;j < m;++j){
			++id;
			for(int p = k;p >= 0;--p){
				for(int Set = 0;Set < (1 << m);++Set){
					if(f[id - 1][Set][p]){
						if(Set >> j & 1){//这个点已经被覆盖 
							f[id][Set - (1 << j)][p] += f[id - 1][Set][p];
							f[id][Set - (1 << j)][p] %= mod; 	
						}else{//未被覆盖 
							if(p < k && j < m - 1 && ((Set >> (j + 1)) & 1) == 0 && s[i + 1][j + 1] == '.' && s[i + 1][j + 2] == '.'){//横放的条件 
								f[id][Set + (1 << (j + 1))][p + 1] += f[id - 1][Set][p];
								f[id][Set + (1 << (j + 1))][p + 1] %= mod;
							}
							if(p < k && i < n - 1 && s[i + 1][j + 1] == '.' && s[i + 2][j + 1] == '.'){//竖放的条件 
								f[id][Set + (1 << j)][p + 1] += f[id - 1][Set][p];
								f[id][Set + (1 << j)][p + 1] %= mod; 
							}
							//这里可以不放,因为题目不要求填满整个网格图 
							f[id][Set][p] += f[id - 1][Set][p];
							f[id][Set][p] %= mod;
						}
					}
				}
			}
		}
	}
}

void calc_ans(){
	for(int i = 0;i <= k;++i){
		if(i % 2 == 1){
			ans = (ans - (f[id][0][i] * C(k,i) % mod * fac[i] % mod * pow_2[i] % mod * A(n * m - sum_get - 2 * i,2 * (k - i)) % mod) + mod) % mod;
		}else{
			ans = (ans + f[id][0][i] * C(k,i) % mod * fac[i] % mod * pow_2[i] % mod * A(n * m - sum_get - 2 * i,2 * (k - i)) % mod) % mod;
		}
	}
}

void clear(){
	for(int i = 0;i <= n * m;++i)
		for(int _s = 0;_s < (1 << m);++_s)
			for(int p = 0;p <= k;++p)
				f[i][_s][p] = 0;
	ans = 0;		
	sum_get = 0;//没有重置这个浪费了好多时间去找问题 
}

signed main(){
	int T;
	scanf("%lld",&T);
	init();
	while(T--){
		scanf("%lld%lld%lld",&n,&m,&k);
		for(int i = 1;i <= n;++i) scanf("%s",s[i] + 1);
		if(m > n){
			for(int i = 1;i <= m;++i){
				for(int j = 1;j <= n;++j){
					ts[i][j] = s[j][i];
				}
			}
			std::swap(n,m);
			for(int i = 1;i <= n;++i){
				for(int j = 1;j <= m;++j){
					s[i][j] = ts[i][j];
				}
			}
		}  
		for(int i = 1;i <= n;++i){
			for(int j = 1;j <= m;++j){
				sum_get += (s[i][j] != '.');
			}
		}
		//上面的代码是初始化与预处理
		calc_dp();
		calc_ans();
		printf("%lld\n",ans);
		//重置变量
		clear(); 
	} 
	return 0;
} 
posted @ 2025-03-05 08:24  AxB_Thomas  阅读(23)  评论(0)    收藏  举报
Title