我果然一点组合数学都不会。
这里记录两个小 trick,因为短,所以就不分开了。
CF1525E
\(n\) 个城市,\(m\) 个点,每秒等概率随机点亮一个未点亮的城市,被点亮的城市在此后第 \(j\) 秒能照亮距离 \(j+1\) 以内的点,求最后照亮点的期望。
首先转换成每个点的概率和是老套路了,然后就是在排列中限制了一些点要在某个之前出现。
但是直接计算会有重复,因为可能有多个城市照亮了这个点。
那么就正难则反,考虑所有点都不照亮,但这个限制似乎会互相影响。
实际上只要按照能填的端点从后往前排个序再乘法原理就可以了,因为排了序之后前面填上的就一定会让后面填上的能填的位置数量减少 \(1\),这应该是一个很常见的技巧了。
#include <algorithm>
#include <iostream>
#include <vector>
#define int long long
const int M = 50005, P = 998244353;
int n, m, fac = 1, inv, ans, x;
std::vector<int> d[M];
int Pow(int a, int b) {
int an = 1;
for ( ; b; b >>= 1, a = a * a % P)
if (b & 1) an = an * a % P;
return an;
}
signed main() {
std::cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
std::cin >> x, d[j].push_back(x-1);
for (int i = 1; i <= m; i++) std::sort(d[i].begin(), d[i].end());
for (int i = 1; i <= n; i++) fac = fac * i % P;
inv = Pow(fac, P-2);
for (int i = 1; i <= m; i++) {
int an = 1;
for (int j = 0; j < (int)d[i].size(); j++)
an = an * (d[i][j]-j) % P;
ans = (ans + (1 - an*inv % P + P) % P) % P;
}
std::cout << ans;
}
CF1511E
一个 \(n \times m\) 的网格,一些点不能涂色,其它点染成黑色或白色,然后放 \(1 \times 2\) 的骨牌,黑色只能放横着,白色只能放竖着,求每种方案最多骨牌的个数之和。
容易发现黑和白是不影响的,横和竖也是不影响的,所以分开来做,对于每一个骨牌,确定其在多少中方案里出现就可以了。
当我要直接上 \(2\) 的幂次的时候发现了一个问题,如果连续 \(3\) 个在那里,那么三个全染成同样颜色的只能放 \(1\) 个,而这个一个会被计算两次。事实上,所有连续奇数个的都会出现这个问题。
那么就要上这小 trick 了,我们令 \(f_i\) 为强制规定最后两个放上的方案数,强制规定了最后俩放上有什么好处呢?我们等于是规定了所有单独剩下来的都放到一边去了。
这样一来,讨论这两个的前一个是不是同色,若不同色,好,方案数就是 \(2^{i-3}\),前面随便填嘛。但若同色,方案数就不能是这样了,得是 \(f_{i-2}\),因为规定了把单独剩下的扔到了一边去,所以同色就强制它放上,因为我们 dp 初始值 \(f_1 = 0\),所以只有最边上一个这个答案是 \(0\),相当于把剩下一个放到了最前面,规定了剩下的放在最前面,就不会重复计数了。