NOI.AC170 数数(计数DP)

文章目录

题目

题目描述
求有多少对 1 ∼ n 1∼n 1n的排列 ( a , b ) (a, b) (a,b)满足 m ≤ ∑ i = 1 n max ⁡ { a i , b i } m \leq \sum\limits_{i = 1}^{n} \max\{a_i,b_i\} mi=1nmax{ai,bi}
两个方案 ( a , b ) (a, b) (a,b) ( a ′ , b ′ ) (a', b') (a,b)不同当且仅当存在 i i i使得 a i ≠ a i ′ a_i≠a'_i ai=ai b i ≠ b i ′ b_i≠b'_i bi=bi

输入
一行两个整数 n , m n, m n,m

输出
一行一个整数表示答案。对 998244353 998244353 998244353取模。

样例输入
3 8

样例输出
18

数据范围
对于 100 % 100\% 100%的数据, 1 ≤ n ≤ 50 1 \leq n \leq 50 1n50 1 ≤ m ≤ 1 0 9 1 \leq m \leq 10^9 1m109

分析

考虑对于排列 a = 1 , 2 , 3 , ⋯   , n a = 1, 2, 3, \cdots, n a=1,2,3,,n,有多少种 b b b满足上述要求,那么再乘 n ! n! n!就是最终答

d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示, 1 ∼ i 1 ∼ i 1i中,有 j j j个还未匹配( a a a b b b中未匹配的数应该是一样的),剩下的已匹配的 i − j i - j ij max ⁡ { a x , b x } \max\{a_x, b_x\} max{ax,bx}之和为 k k k,的方案数。

数字 i i i要产生贡献的话,就只能匹配一个不小于它的数,那么转移方程也可以列了,按照数字 i i i a , b a, b a,b中是否产生贡献分四种情况转移。下面举一个例子:
i i i a a a中产生贡献时, d p [ i ] [ j ] [ k ] = d p [ i ] [ j ] [ k ] + d p [ i − 1 ] [ j ] [ k − i ] × j dp[i][j][k] = dp[i][j][k] + dp[i - 1][j][k - i] \times j dp[i][j][k]=dp[i][j][k]+dp[i1][j][ki]×j,原因是从前 i − 1 i - 1 i1个数中未匹配的 j j j里面随便选一个跟 a a a中的 i i i匹配可以确保 i i i产生贡献,但是由于 b b b中的 i i i未被匹配,因此未匹配的对数还是 j j j

其他情况可以看注释。

最后,尽管 m m m很大,但是粗略估计 m m m最多 n 2 n^2 n2,大于 n 2 n^2 n2无解,枚举一下统计答案即可。

代码

#include <algorithm>
#include <cstdio>
#include <cstring>

const int MAXN = 50;
const int MOD = 998244353;

inline int Add(int x, const int y) {
	x += y; if (x > MOD) x -= MOD; return x;
}

inline int Mul(int x, const int y) {
	x = (long long)x * y % MOD; return x;
}

int N, M;
int Dp[MAXN + 5][MAXN + 5][MAXN * MAXN + 5];

int main() {
//	freopen("sum.in", "r", stdin);
//	freopen("sum.out", "w", stdout);
	scanf("%d%d", &N, &M);
	if (M > N * N)
		return puts("0"), 0;
	Dp[0][0][0] = 1;
	for (int i = 1; i <= N; i++) { // 对于a / b里面所有小于等于 i 的数
		for (int j = 0; j <= i; j++) { // a / b 里面小于等于 i 的数中各有 j 个数还未匹配
			for (int k = 0; k <= N * N; k++) { // ∑ max(a[x], b[x]) = k
				int &cur = Dp[i][j][k];
				if (j >= 1)
					cur = Add(cur, Dp[i - 1][j - 1][k]);
					// 1. a / b 中 i 均不产生贡献 => 多了一个未配对
				if (k >= i && j <= i - 1) {
					cur = Add(cur, Mul(Dp[i - 1][j][k - i], j));	
					// 2. a 中产生贡献 => 未配对数不变, 从小于等于 i - 1 的 j 个未匹配数中随便选一个和 i 匹配
					cur = Add(cur, Mul(Dp[i - 1][j][k - i], j + 1));
					// 3. b 中产生贡献 => 未配对数不变, 与上式同理, 但考虑到 a[x] = b[x] = i 的情况, 只在从此处 + 1 而上面不 + 1
				}
				if (k >= 2 * i)
					cur = Add(cur, Mul(Dp[i - 1][j + 1][k - 2 * i], Mul(j + 1, j + 1)));
					// 4. a / b 中均产生贡献 => 之前的状态要多一个未配对才行, 两个各自匹配故均为 j + 1 种情况
			}
		}
	}
	int Ans = 1, Tot = 0;
	for (int i = 1; i <= N; i++)
		Ans = Mul(Ans, i);
	for (int i = M; i <= N * N; i++)
		Tot = Add(Tot, Dp[N][0][i]);
	printf("%d", Mul(Ans, Tot));
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值