计数题

计数题

P1287 盒子与球

设置 \(f(n,m)\) 表示 \(n\) 个球放到 \(m\) 个盒子中 不允许空盒的方案数

分类讨论:

  • 如果一个球独占 那么方案数为 \(f(n-1,m-1)\)
  • 如果这个球和其他球共用一个盒子 那么我们可以先将其余的所有球放在 \(m\) 个盒子中 再将这个球随机放入一个盒子里 就是 \(m*f(n-1,m)\)

这两种方案加起来递归求解即可

注意 所有盒子互不相同 那么我们还需要乘上一个盒子的全排列

#include<bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define inl inline
#define eb emplace_back
#define endl '\n'
#define ls p<<1
#define rs p<<1|1
#define lson ls,l,mid
#define rson rs,mid+1,r
#define pii pair<int,int>
#define fi first
#define se second
#define mkp make_pair
#define pii pair<int,int>
#define print(x) cerr<<#x<<'='<<x<<endl
const double inf = 2e9 + 5;
const int N = 100 + 5;
// char buf[1<<24] , *p1 , *p2;
// #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<24,stdin),p1==p2)?EOF:*p1++)
#define getchar() cin.get()
int read()
{
	int x = 0 , f = 1;
	char ch = getchar();
	while ( !isdigit(ch) ) { if ( ch == '-' ) f = -1; ch = getchar(); }
	while ( isdigit(ch) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
	return x * f;
}

int n , m , res = 1;

int f ( int n , int m )
{
	if ( m <= 0 || n <= 0 || n < m ) return 0;
	if ( n == m ) return 1;
	return f(n-1,m-1) + m * f(n-1,m);
}

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read();
	for ( int i = 1 ; i <= m ; i ++ ) res *= i;
	cout << f ( n , m ) * res << endl;
	return 0;
}

P5520 [yLOI2019] 青原樱

题意可以转化为:有 \(m\) 个互不相同的花 种在 \(n\) 个位置上 每两朵花之间要有一个空位

显然我们可以拿出来 \(m-1\) 个空位作为隔板 然后将剩下的 \(m\) 朵花任意插在 \(n-(m-1)\) 个空位中

所以就是 \(C_{n-m+1}^m*m!=A_{n-m+1}^m\) (因为花是互不相同的 所以要乘上排列)

#include<bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define inl inline
#define eb emplace_back
#define endl '\n'
#define ls p<<1
#define rs p<<1|1
#define lson ls,l,mid
#define rson rs,mid+1,r
#define pii pair<int,int>
#define fi first
#define se second
#define mkp make_pair
#define pii pair<int,int>
#define print(x) cerr<<#x<<'='<<x<<endl
#define int long long
const double inf = 2e9 + 5;
const int N = 100 + 5;
// char buf[1<<24] , *p1 , *p2;
// #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<24,stdin),p1==p2)?EOF:*p1++)
#define getchar() cin.get()
int read()
{
	int x = 0 , f = 1;
	char ch = getchar();
	while ( !isdigit(ch) ) { if ( ch == '-' ) f = -1; ch = getchar(); }
	while ( isdigit(ch) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
	return x * f;
}

int res = 1;

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	int type = read() , n = read() , m = read() , mod = read();
	for ( int i = n - m + 1 ; i >= n - 2 * m + 2 ; i -- )
		( res *= i ) %= mod;
	cout << res << endl;
	return 0;
}

P3197 [HNOI2008] 越狱

很玄妙的思路() 一开始想组合数 但后来发现并不需要

能越狱的情况就是总情况数减去不能越狱的情况数

总情况数是 \(m^n\) 显然 对于第一个人 可以选 \(m\) 种信仰 对于后一个人 为了和前一个不重复 可以选择 \(m-1\) 种信仰 以此类推

答案就是 \(m^n-m*(m-1)^{n-1}\)

注意负数取模问题

#include<bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define inl inline
#define eb emplace_back
#define endl '\n'
#define ls p<<1
#define rs p<<1|1
#define lson ls,l,mid
#define rson rs,mid+1,r
#define pii pair<int,int>
#define fi first
#define se second
#define mkp make_pair
#define pii pair<int,int>
#define print(x) cerr<<#x<<'='<<x<<endl
#define int long long
const double inf = 2e9 + 5;
const int N = 100 + 5;
const int mod = 100003;
// char buf[1<<24] , *p1 , *p2;
// #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<24,stdin),p1==p2)?EOF:*p1++)
#define getchar() cin.get()
int read()
{
	int x = 0 , f = 1;
	char ch = getchar();
	while ( !isdigit(ch) ) { if ( ch == '-' ) f = -1; ch = getchar(); }
	while ( isdigit(ch) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
	return x * f;
}

int n , m;

int ksm ( int base , int k )
{
	int res = 1;
	for ( ; k ; ( base *= base ) %= mod , k >>= 1 )
		if ( k & 1 ) ( res *= base ) %= mod;
	return res;
}

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	m = read() , n = read();
	cout << ( ( ksm ( m , n ) % mod - m % mod * ksm ( m - 1 , n - 1 ) % mod ) + mod ) % mod << endl;
	return 0;
}

P3223 [HNOI2012] 排队

高精 直接用 \(python\) 水罢

一个真正适合高考的题

答案即为:不考虑老师绑定的情况数量-老师一定绑定的情况数量

显然不考虑老师绑定的情况为 \(A_{n+3}^m*A_{n+2}^{n+2}\) 即为:将男生和老师看成同一类型来排列 共有 \(n+3\) 个空位供女生插入 那么问题转化为将 \(m\) 个女生插入 \(n+3\) 个空位中

同时 男生和老师是不同的 需要排列一下

如果我们考虑老师一定绑定的情况 将两个老师看成一个男生即可 运用上面的公式可得情况为 \(2*A_{n+2}^m*A_{n+1}^{n+1}\) 这里 \(*2\) 的原因是因为老师的位置可以互换

需要特判使得 \(n+3\) 一定大于等于 \(m\)

import sys
import math
ans = 0
n , m = map ( int , input().split() )
def f(x):
	return math.factorial(x)
def A(x,y):
	return f(x) // f(x-y)
if(n+3>=m):
	ans += A ( n + 3 , m ) * A ( n + 2 , n + 2 )
if(n+2>=m):
	ans -= A ( n + 2 , m ) * A ( n + 1 , n + 1 ) * 2
print(ans)

P1595 信封问题

错排问题裸题 理一下思路

其次 对于第一封信 我们不能将它放到本身位置 只能放到其余 \(n-1\) 个位置中 有 \(n-1\) 种情况

我们假定它放到了第 \(k\) 个位置 那么分成两种情况:

  • \(k\) 封信放到了第 \(1\) 个位置上 那么就是剩下 \(n-2\) 个数的错排方案数 \(f_{n-2}\)
  • \(k\) 封信没有放到第 \(1\) 个位置上 那么就是 \(n-1\) 个数的错排方案

最终答案就是 \(f_n=(n-1)*(f_{n-2}+f_{n-1})\)

#include<bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define inl inline
#define eb emplace_back
#define endl '\n'
#define print(x) cerr<<#x<<'='<<x<<endl
#define int long long
const double inf = 2e9 + 5;
const int N = 100 + 5;
const int mod = 100003;
// char buf[1<<24] , *p1 , *p2;
// #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<24,stdin),p1==p2)?EOF:*p1++)
#define getchar() cin.get()
int read()
{
	int x = 0 , f = 1;
	char ch = getchar();
	while ( !isdigit(ch) ) { if ( ch == '-' ) f = -1; ch = getchar(); }
	while ( isdigit(ch) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
	return x * f;
}

int n , f[N];

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read();
	f[1] = 0 , f[2] = 1;
	for ( int i = 3 ; i <= n ; i ++ ) f[i] = ( f[i-1] + f[i-2] ) * ( i - 1 );
	cout << f[n] << endl;
	return 0;
}

P4071 [SDOI2016] 排列计数

我们考虑先在 \(n\) 个位置中选 \(m\) 个位置 答案就是 \(C_n^m\) 让这 \(m\) 个位置正序排列 只有一种情况

然后对于其余元素进行错排 就是 \(D_{n-m}\) 相乘即可

#include<bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define inl inline
#define eb emplace_back
#define endl '\n'
#define print(x) cerr<<#x<<'='<<x<<endl
#define int long long
const int N = 1e6 + 5;
const int maxn = 1e6;
const int mod = 1e9 + 7;
// char buf[1<<24] , *p1 , *p2;
// #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<24,stdin),p1==p2)?EOF:*p1++)
#define getchar() cin.get()
int read()
{
	int x = 0 , f = 1;
	char ch = getchar();
	while ( !isdigit(ch) ) { if ( ch == '-' ) f = -1; ch = getchar(); }
	while ( isdigit(ch) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
	return x * f;
}

int n , f[N] , m;

int fac[N] , inv[N] , ifac[N];
void init()
{
	fac[0] = fac[1] = inv[0] = inv[1] = ifac[0] = ifac[1] = 1;
	for ( int i = 2 ; i <= maxn ; i ++ )
	{
		fac[i] = fac[i-1] * i % mod;
		inv[i] = ( ( mod - mod / i ) * inv[mod%i] % mod + mod ) % mod;
		ifac[i] = ifac[i-1] * inv[i] % mod;
	}
}

int C ( int n , int m ) { return fac[n] * ifac[m] % mod * ifac[n-m] % mod; }

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	int T = read();
	init() , f[1] = 0 , f[2] = 1;
	for ( int i = 3 ; i <= maxn ; i ++ ) f[i] = ( i - 1 ) * ( f[i-1] + f[i-2] ) % mod;
	while ( T -- )
	{
		n = read() , m = read();
		if ( n == m ) cout << 1 << endl;
		else cout << C ( n , m ) * f[n-m] % mod << endl;
	}
	return 0;
}

P3182 [HAOI2016] 放棋子

一个结论:矩阵对答案没有影响

对于所有矩阵 都可以通过行上下变换达到对角线全为1的矩阵

转化后每一行不变 每一列的上下位置变化没有影响 那么显然总方案数就不变

于是我们变成了对于 \(n*n\) 的矩阵 将行看成位置 列看成值 相当于是对于一个 \(n*n\) 的数列的错排问题 递推即可

因为需要高精 所以用python实现

n = int(input())
f = [0 , 0 , 1]
ans = 0
for i in range ( 3 , n + 1 ) :
    f.append ( ( i - 1 ) * ( f[i-1] + f[i-2] ) )
print(f[n])

P2822 [NOIP2016 提高组] 组合数问题

看到数据范围很小 直接预处理组合数 ( \(\bmod\ k\) 意义下)

预处理每一个数是否符合 \(k\) 的要求 做一次前缀和查询即可

#include<bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define inl inline
#define eb emplace_back
#define endl '\n'
#define print(x) cerr<<#x<<'='<<x<<endl
const int N = 2e3 + 5;
const int maxn = 2e3;
const int mod = 1e9 + 7;
// char buf[1<<24] , *p1 , *p2;
// #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<24,stdin),p1==p2)?EOF:*p1++)
#define getchar() cin.get()
int read()
{
	int x = 0 , f = 1;
	char ch = getchar();
	while ( !isdigit(ch) ) { if ( ch == '-' ) f = -1; ch = getchar(); }
	while ( isdigit(ch) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
	return x * f;
}

int T , k , n , m , f[N][N] , sum[N][N];

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	T = read() , k = read();
	for ( int i = 0 ; i <= maxn ; i ++ ) f[i][i] = 1 , f[i][0] = 1;
	for ( int i = 1 ; i <= maxn ; i ++ )
		for ( int j = 1 ; j <= maxn ; j ++ )
			f[i][j] = ( f[i-1][j] + f[i-1][j-1] ) % k;
	for ( int i = 1 ; i <= maxn ; i ++ )
		for ( int j = 1 ; j <= maxn ; j ++ )
		{
			if ( !f[i][j] && i >= j ) sum[i][j] = 1;
			sum[i][j] += sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1];
		}
	while ( T -- )
	{
		n = read() , m = read();
		cout << sum[n][m] << endl;
	}
	return 0;
}

P2606 [ZJOI2010] 排列计数

先补了 \(lucas\) 定理再来的()

\(ps:\)\(lucas\) 定理的板子题中 不能先预处理组合数 因为每次的模数都不一样 如果提前预处理的话模数为 \(0\) 会导致错误

\(lucas\) 定理适用条件:在 \(n>mod\) 的时候 \(n!\) 必然有一个因子为 \(mod\)

那么 \(n!\)\(\bmod\ mod\) 下逆元一定是 \(0\) 那么组合数也为 \(0\) 显然不符合条件 那么必须用 \(lucas\) 来解决

回到本题 我们需要求的就是满足小根堆的排列数量

\(f[i]\) 表示以 \(i\) 为根节点的树的方案个数

那么我们相当于是在 \(sz[i]-1\) 个数中(因为根节点固定为最小值)选出 \(sz[l]\) 个数 使得左子树和右子树都满足小根堆的排列

即为 \(f[i] = lucas ( sz[i] - 1 , sz[i<<1] ) * f[i<<1] * f[i<<1|1]\)

如果给定了 \(n\) 那么树的形态是固定的 我们可直接从 \(n\) 逆序统计答案

#include<bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define inl inline
#define eb emplace_back
#define endl '\n'
#define print(x) cerr<<#x<<'='<<x<<endl
#define getchar() cin.get()
#define int long long
const int N = 2e6 + 5;
const int maxn = 2e6;
int read()
{
	int x = 0 , f = 1;
	char ch = getchar();
	while ( !isdigit(ch) ) { if ( ch == '-' ) f = -1; ch = getchar(); }
	while ( isdigit(ch) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
	return x * f;
}

int n , m , mod , sz[N] , f[N];

int fac[N] , ifac[N] , inv[N];

void init()
{
    fac[0] = fac[1] = inv[0] = inv[1] = ifac[0] = ifac[1] = 1;
	for ( int i = 2 ; i <= maxn ; i ++ )
	{
		fac[i] = fac[i-1] * i % mod;
		inv[i] = ( ( mod - mod / i ) * inv[mod%i] % mod + mod ) % mod;
		ifac[i] = ifac[i-1] * inv[i] % mod;
	}
}

int C ( int n , int m ) { return fac[n] * ifac[m] % mod * ifac[n-m] % mod; }

int lucas ( int n , int m ) { if ( m == 0 ) return 1; return C ( n % mod , m % mod ) * lucas ( n / mod , m / mod ) % mod; }

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
    n = read() , mod = read();
    init();
    for ( int i = 1 ; i <= n ; i ++ ) sz[i] = 1;
    for ( int i = 1 ; i <= (n*2+1) ; i ++ ) f[i] = 1;
    for ( int i = n ; i >= 2 ; i -- ) sz[i>>1] += sz[i];
    for ( int i = n ; i ; i -- )
        f[i] = lucas ( sz[i] - 1 , sz[i<<1] ) * f[i<<1] % mod * f[i<<1|1] % mod;
    cout << f[1] << endl;
	return 0;
}

P5664 [CSP-S2019] Emiya 家今天的饭

非常好的一道题 学到很多()

可以发现从正面计算食材不超过一半的情况比较困难 如果不考虑第三条限制 答案就是 \(\sum_{i=1}^n(1+\sum_{j=1}^m a_{i,j})-1\)

相当于是对于每一种烹饪方式 要么有一道菜 要么一道菜也没有 加和之后减去什么都不做的情况

但在这个柿子的基础之上 还需要减掉主要食材使用超过一半的方案才是答案

朴素 \(dp\) : \(f[i][j][k][l]\) 表示在前 \(i\) 种烹饪方式中 做了 \(k\) 个菜 使得第 \(j\) 种食材用了 \(l\) 次的方案数

易得 \(dp_{i,j,k,l}=dp_{i-1,j,k,l}+a_{i,j}dp_{i-1,j,k-1,l-1}+\sum_{1\le j'\le m,j'\ne j}a_{i,j'}dp_{i-1,j,k-1,l}\)

三项分别为:对于这种方式 什么食材都不选(放弃这种烹饪方式) + 选 \(j\) 食材 + 选 \(j\) 食材以外的食材

其中 \(j\) 一维我们通过枚举方法实现 实际实现中可以省略 这样是 \(84pts\)

\(code:\)

#include<bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define inl inline
#define eb emplace_back
#define endl '\n'
#define print(x) cerr<<#x<<'='<<x<<endl
#define getchar() cin.get()
#define int long long
const int mod = 998244353;
const int N = 1e2 + 5;
const int M = 2e3 + 5;
const int maxn = 1e2;

int read()
{
	int x = 0 , f = 1;
	char ch = getchar();
	while ( !isdigit(ch) ) { if ( ch == '-' ) f = -1; ch = getchar(); }
	while ( isdigit(ch) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
	return x * f;
}

int n , m , a[N][M] , ans = 1 , f[N][N][N] , sum[N];

int id ( int x ) { return x + maxn; }

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read();
	for ( int i = 1 ; i <= n ; i ++ )
		for ( int j = 1 ; j <= m ; j ++ )
			a[i][j] = read() , ( sum[i] += a[i][j] ) %= mod;
	for ( int i = 1 ; i <= n ; i ++ ) ( ans *= sum[i] + 1 ) %= mod; ( ans += mod - 1 ) %= mod;
	for ( int j = 1 ; j <= m ; j ++ ) //枚举超过一半的食材
	{
		memset ( f , 0 , sizeof f );
		for ( int i = 0 ; i <= n ; i ++ ) f[i][0][0] = 1;//什么也不做
		for ( int i = 1 ; i <= n ; i ++ ) //考虑前i种烹饪方法
			for ( int k = 1 ; k <= i ; k ++ ) //做菜个数
				for ( int l = 0 ; l <= k ; l ++ ) //j食材的使用次数
				{
					( f[i][k][l] += f[i-1][k][l] + f[i-1][k-1][l] * ( sum[i] - a[i][j] ) % mod ) %= mod;
					if ( l ) ( f[i][k][l] += f[i-1][k-1][l-1] * a[i][j] % mod ) %= mod;
					if ( i == n && l > k / 2 ) ( ( ans -= f[i][k][l] ) += mod ) %= mod;
				}
	}
	cout << ans << endl;
	return 0;
}

考虑优化 我们对限制三进行转化 即为: \(2x\le k\) \(\rightarrow x-(k-x)\le0\) 发现我们只需要关注不合法的菜和合法的菜的差值即可

记 $ f[i][j][k]$ 表示在前 \(i\) 种烹饪方式中,使得第 \(j\) 种食材的数量减去其他食材的数量和的结果恰为 \(k\) 的方案数 这里我们的 \(j\) 还是通过枚举实现

柿子即为 $dp_{i,j,k}=dp_{i-1,j,k}+a_{i,j}dp_{i-1,j,k-1}+\sum_{1\le j'\le m,j'\ne j}a_{i,j'}dp_{i-1,j,k+1} $

\(k\) 可能为负数 于是我们为所有状态加一个偏移量即可

#include<bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define inl inline
#define eb emplace_back
#define endl '\n'
#define print(x) cerr<<#x<<'='<<x<<endl
#define getchar() cin.get()
#define int long long
const int mod = 998244353;
const int N = 1e2 + 5;
const int M = 2e3 + 5;
const int maxn = 1e2;

int read()
{
	int x = 0 , f = 1;
	char ch = getchar();
	while ( !isdigit(ch) ) { if ( ch == '-' ) f = -1; ch = getchar(); }
	while ( isdigit(ch) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
	return x * f;
}

int n , m , a[N][M] , ans = 1 , f[N][N*2] , sum[N];

int id ( int x ) { return x + maxn; }

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read();
	for ( int i = 1 ; i <= n ; i ++ )
		for ( int j = 1 ; j <= m ; j ++ )
			a[i][j] = read() , ( sum[i] += a[i][j] ) %= mod;
	for ( int i = 1 ; i <= n ; i ++ ) ( ans *= sum[i] + 1 ) %= mod; ( ans += mod - 1 ) %= mod;
	for ( int j = 1 ; j <= m ; j ++ ) //枚举超过一半的食材
	{
		memset ( f , 0 , sizeof f );
		f[0][n] = 1;//没做菜 差值为0
		for ( int i = 1 ; i <= n ; i ++ ) //考虑前i种烹饪方法
			for ( int k = - i + n ; k <= i + n ; k ++ ) //差值
			{
				( f[i][k] += ( f[i-1][k] + f[i-1][k-1] * a[i][j] % mod + f[i-1][k+1] * ( sum[i] - a[i][j] ) % mod ) ) %= mod;
				if ( i == n && k > n ) ( ( ans -= f[i][k] ) += mod ) %= mod;
			}
	}
	cout << ans << endl;
	return 0;
}
posted @ 2023-09-26 09:16  Echo_Long  阅读(14)  评论(0编辑  收藏  举报