2021 ICPC EC 网络预选赛第二场 K.Meal
个人选 个物品, 每个人选一个. 每个人对物品有一个喜爱值 . 从第一个人开始, 在没有被选的物品中随机选择一个物品 , 概率为 , 其中 是剩余物品集合. 问每个人选到每个物品的概率, 答案对 取模.
学好概率论, 走遍天下都不怕(雾)
根据全概率公式, 一个很自然的思想是: 设 为事件 “第 个人选到第 个物品”, 那么有:
然后这玩意相当于枚举全排列……
考察 的某些项, 可以发现 和 是一样的, 这是重复计算. 想象更后面的人, 重复项会越来越多, 累计到阶乘的复杂度. 所以, 这是优化算法的一个突破口.
于是尝试重新对事件做一个划分: 设 为随机事件 “前 个人选择的物品集合为 ”. 那么有:
那么计算所有 的总枚举量, 就是 .
然后考虑怎么计算 .
这玩意显然不能直接算, 所以大抵能猜是个 dp, 有了上一步的经验, 来看看如何对事件 进行一个划分, 得到有关联的全概率公式, 然后 dp 求解. 可以发现:
这个公式和前面那个一样, 总枚举量为 .
公式里的 和 其实是已知的, 就是第 个人在 被选的条件下, 选到 的概率, 也就是 .
然后两个方程一起 dp. 基本上就没了.
细节是要先计算一下之前那个式子的分母, 在 的时间内可预处理. 然后因为要算分数取模, 逆元不能每次求都用费马小, 否则复杂度稳定多一个 . 注意到分母最大也就 个 , 而 只有 . 所以可以预处理出 的逆元. 这里费马小或者线性推都可, 复杂度不会叠上去.
int count(int x) {
int cnt = 0;
while (x) {
cnt++;
x ^= lowbit(x);
}
return cnt;
}
int inv[MAXM];
void init_inv(int n) {
inv[0] = inv[1] = 1;
for (int i = 2; i <= n; i++)
inv[i] = mult(P - P/i, inv[P%i]);
}
int n, a[MAXN][MAXN], sum[MAXN][MAXS], dp[MAXS], ans[MAXN][MAXN];
VI S[MAXN];
int main() {
scanf("%d", &n);
init_inv(2005);
for (int i = 1; i <= n; i++)
for (int j = 0; j < n; j++)
scanf("%d", &a[i][j]);
for (int s = 0; s < 1 << n; s++)
S[count(s)].emplace_back(s);
for (int i = 1; i <= n; i++) {
sum[i][0] = 0;
for (int c = 1; c <= n; c++)
for (auto s : S[c]) {
int s0 = s ^ lowbit(s);
sum[i][s] = sum[i][s0] + a[i][__lg(lowbit(s))];
}
}
dp[0] = 1;
for (int i = 1; i <= n; i++)
for (int s : S[i])
for (int j = 0; j < n; j++)
if (s & (1 << j)) {
int s0 = s ^ (1 << j);
int inv_s0 = (~s0) & ((1 << n) - 1);
int cur = mult(dp[s0], mult(a[i][j], inv[sum[i][inv_s0]]));
dp[s] = pls(dp[s], cur);
ans[i][j] = pls(ans[i][j], cur);
for (int i = 1; i <= n; i++)
for (int j = 0; j < n; j++)
printf("%d%c", ans[i][j], " \n"[j == n-1]);
return 0;
}
预览: