计数题
计数题
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;
}