随机棋盘(Easy Version) - 题解
标签与难度
标签: 组合数学, 期望, 容斥原理, Rook多项式, 多项式快速幂, NTT 难度: 2200
题目大意喵~
哈喵~!各位算法大师们,今天我们来挑战一个有趣的棋盘问题,喵~
题目要求我们计算在一个 的棋盘上,放置 个互不攻击的“车”的方案数的期望值。不过呢,这个棋盘不是一个普通的棋盘,上面有一些格子是禁止放车的。
这些禁止格子的生成方式有点特别:
- 设 。
- 我们先随机生成一个 的排列 。
- 对于每个 从 到 ,我们会在棋盘上的三个位置打上禁止标记:
我们需要计算在所有可能的随机排列 (一共有 种)下,合法放置方案数的平均值(也就是期望),结果对 取模。
简单来说,就是:求在随机生成的禁区下,放置 个互不攻击的车的方案数期望。
解题思路分析
这道题看起来好复杂呀,又是随机又是期望的,让人头大,喵~ 不过别怕,让我来带你一步步解开它的神秘面纱!
核心洞察:期望的线性性质与对称性
题目的核心是求期望。根据期望的线性性质,一个随机变量的和的期望等于它们各自期望的和。我们可以把总方案数这个随机变量 分解成很多个小的指示随机变量的和。
这里 代表一种在无限制的 棋盘上的合法布阵方案(也就是一个排列), 是一个指示变量:如果布阵 在当前随机生成的棋盘上是合法的(没有棋子在禁止格上),则 ,否则为 。 就是布阵 合法的概率。
这个思路看起来还是好复杂,因为每个 的合法概率都和 的具体结构有关。
但是,我们可以换个角度思考!我们要求的是所有 种可能的棋盘的方案数之和,再除以 。
这里的 指的是由排列 生成的棋盘所对应的合法方案数。
这时,一个关键的、让问题瞬间变简单的想法出现了:对于任何一个排列 ,它生成的棋盘的合法方案数都是一样的! 也就是说, 是一个与 无关的常数。
为什么呢?喵?
我们可以通过一个巧妙的“重新标记”法来证明。假设我们有两个不同的排列 和 。我们可以找到一种方式重新标记棋盘的行,使得由 生成的禁止格位置,在新标记下,完全等同于由 生成的禁止格位置。
比如说,我们想把由任意排列 生成的棋盘,变成由单位排列 (即 )生成的棋盘。 我们可以定义一个行的置换 。对于任意行组 ,我们把它映射到新的行组 。这样一来,原来排列 中 的禁止关系,在新的行标下就变成了 的关系,这不就和 生成的棋盘一模一样了吗?
既然对于任何排列 ,方案数都相同,那我们只需要计算其中一个最简单情况的方案数就好啦!这个最简单的当然就是 是单位排列, 的情况。
此时,期望值就等于单位排列 所生成的棋盘的方案数。
转化为固定棋盘问题
当 时,禁止的格子变成了: 对于所有 :
我们的问题就变成了:在一个有上述固定禁区的棋盘上,放 个互不攻击的“车”有多少种方案?
这是一个经典的带禁区的排列计数问题,可以用容斥原理解决。而解决这类问题的有力工具,就是Rook多项式 (Rook Polynomials),喵~
Rook多项式大法好!
Rook多项式是专门用来解决这类问题的组合工具。对于一个棋盘上的禁区 ,它的Rook多项式 定义为:
其中 是在禁区 上放置 个互不攻击的“车”的方案数。
根据容斥原理,在 棋盘上,避开禁区 放置 个互不攻击的“车”的方案数为:
现在,我们来分析我们这个问题的禁区 。它是由 个互不相干的小块组成的。 每个小块 (对应 )的禁区都在行 和列 的交集上。具体来说,是 这三格。
由于这些禁区小块 之间在行和列上都没有重叠,整个禁区 的Rook多项式就是所有小块的Rook多项式的乘积!
我们来计算一下一个小块 的Rook多项式 。这个小块的禁区可以看作是一个 棋盘上禁止了 三个位置。
- : 在禁区里放0个车,只有1种方法(什么都不放)。
- : 在禁区里放1个车,有3个格子可以选,所以有3种方法。
- : 在禁区里放2个互不攻击的车,只能放在 和 。但 也被禁止了,所以只能放在 和 。哦,我看看,是 和 吗?不对,是 和 。啊哈,这两个格子在同一列 和 上,所以是可以的。不对, 和 是可以的,但 和 不行。让我重新看看禁区:, , 。 要放2个不互相攻击的车,只能是 和 的组合。但列 只有一个禁区格 ,所以只能是 和 。啊,这两个位置的行不同,列也不同,所以是互不攻击的!所以 。
- for 。
所以,单个小块的Rook多项式是 。
那么,整个禁区的Rook多项式就是:
最后的计算
我们的任务就清晰了:
- 计算多项式 。
- 计算最终答案 。
如何高效地计算多项式的 次方呢?当然是用多项式快速幂啦!每次多项式乘法,我们用 NTT (Number Theoretic Transform) 来加速,它可以在 的时间内完成两个度为 的多项式乘法。 总的时间复杂度就是 ,对于 来说,这完全足够了!
于是,解题步骤就是:
- 确定 。
- 预计算阶乘和阶乘的逆元。
- 构造基础多项式 。
- 使用NTT实现的快速幂,计算出 的系数 。
- 根据公式 计算最终答案。
这样,一个看起来很棘手的问题,就被我们一步步分解成熟悉的模块解决了,是不是很有成就感呢?喵~
代码实现
这是我根据上面的思路,精心重构的一份代码,注释超详细的哦,希望能帮到你理解,喵~
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
using namespace std;
// 定义一个足够大的质数和NTT的原根
const int MOD = 998244353;
const int G = 3; // MOD的原根
// 快速幂函数,用于计算 a^b % MOD
long long power(long long base, long long exp) {
long long res = 1;
base %= MOD;
while (exp > 0) {
if (exp % 2 == 1) res = (res * base) % MOD;
base = (base * base) % MOD;
exp /= 2;
}
return res;
}
// 模逆元函数,用于计算 a^{-1} % MOD
long long modInverse(long long n) {
return power(n, MOD - 2);
}
// NTT核心实现
// is_inverse为false时是正变换(DFT),为true时是逆变换(IDFT)
void ntt(vector<long long>& a, bool is_inverse) {
int n = a.size();
// Cooley-Tukey蝶形变换的位逆序置换
for (int i = 1, j = 0; i < n; i++) {
int bit = n >> 1;
for (; j & bit; bit >>= 1) {
j ^= bit;
}
j ^= bit;
if (i < j) {
swap(a[i], a[j]);
}
}
// 迭代计算DFT
for (int len = 2; len <= n; len <<= 1) {
long long wlen = power(G, (MOD - 1) / len);
if (is_inverse) {
wlen = modInverse(wlen);
}
for (int i = 0; i < n; i += len) {
long long w = 1;
for (int j = 0; j < len / 2; j++) {
long long u = a[i + j];
long long v = (a[i + j + len / 2] * w) % MOD;
a[i + j] = (u + v) % MOD;
a[i + j + len / 2] = (u - v + MOD) % MOD;
w = (w * wlen) % MOD;
}
}
}
// 如果是逆变换,需要乘以 1/n
if (is_inverse) {
long long n_inv = modInverse(n);
for (long long& x : a) {
x = (x * n_inv) % MOD;
}
}
}
// 多项式乘法 (a * b)
vector<long long> multiply(vector<long long> a, vector<long long> b) {
int sz = 1;
while (sz < a.size() + b.size()) sz <<= 1;
a.resize(sz);
b.resize(sz);
ntt(a, false);
ntt(b, false);
vector<long long> c(sz);
for (int i = 0; i < sz; i++) {
c[i] = (a[i] * b[i]) % MOD;
}
ntt(c, true);
return c;
}
// 多项式快速幂 (p^exp)
vector<long long> poly_pow(vector<long long> p, int exp) {
vector<long long> res = {1};
while (exp > 0) {
if (exp % 2 == 1) res = multiply(res, p);
p = multiply(p, p);
exp /= 2;
}
return res;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
int m = n / 2;
if (m == 0) {
cout << 1 << endl; // n=1时,只有一种方案
return 0;
}
// 预计算阶乘
vector<long long> fact(n + 1);
fact[0] = 1;
for (int i = 1; i <= n; i++) {
fact[i] = (fact[i - 1] * i) % MOD;
}
// 基础Rook多项式 P(x) = 1 + 3x + x^2
vector<long long> p = {1, 3, 1};
// 计算 C(x) = P(x)^m
vector<long long> rook_poly_coeffs = poly_pow(p, m);
// 根据容斥原理计算最终答案
long long ans = 0;
for (int k = 0; k < rook_poly_coeffs.size(); k++) {
if (n - k < 0) break;
long long term = (rook_poly_coeffs[k] * fact[n - k]) % MOD;
if (k % 2 == 1) {
ans = (ans - term + MOD) % MOD;
} else {
ans = (ans + term) % MOD;
}
}
cout << ans << endl;
return 0;
}复杂度分析
时间复杂度:
- 预计算阶乘需要 。
- 多项式快速幂需要进行 次多项式乘法。
- 每次多项式乘法,我们需要将多项式扩展到长度为 的2的幂,然后进行NTT。NTT的复杂度是 。
- 所以,计算Rook多项式系数的总时间是 。
- 最后计算总和需要 。
- 整体复杂度由多项式快速幂主导。
空间复杂度:
- 存储阶乘需要 的空间。
- NTT和多项式乘法中,需要存储长度为 的多项式系数向量。所以空间复杂度为 。
知识点总结
这道题是一个非常漂亮的组合数学问题,把多个知识点巧妙地串联在了一起,喵~
- 期望的线性性质: 它是我们简化问题的出发点,让我们能够将对随机过程的期望计算,转化为对单一确定性问题的求解。
- 对称性分析: 发现问题解的数量与具体排列无关,是本题破局的关键!这大大降低了问题的复杂度。
- 容斥原理与Rook多项式: 这是解决“带禁止区域的排列计数”问题的标准武器。理解Rook多项式的定义和它与容斥原理的关系,可以让我们系统地解决这类问题。
- 多项式运算与NTT: 当组合问题涉及到递推或生成函数,且规模较大时,多项式运算(特别是卷积)就派上用场了。NTT是实现多项式乘法的高效算法。
- 多项式快速幂: 这是将普通快速幂思想应用到多项式上,用于高效计算多项式的n次方。
通过这道题,我们可以深刻体会到组合数学的魅力,以及如何运用代数工具(多项式)来解决复杂的计数问题。希望大家都能从中学到新知识,喵~!