https://www.luogu.com.cn/problem/CF1773G
模擬賽中的題是求排列數,不過改為求排列數是純詐騙,需要從求機率的角度來看這題。
先來考慮 \(O(3^m)\) 的做法:
這一部分去看其他題解可能更好- 首先注意到一個題目第二次考不會影響當前集合。
- \(dp_S\) 表示集合 \(S\) 中的人都活著(且其他人死了)的機率。
- 統計可以將 \(S\) 改變為 \(T\) 的題目數量 \(c\)。(滿足 \(T \ne S\) 且 \(T \ne \varnothing\))
- 然後依次轉移,轉移到 \(T\) 的機率就是 \(dp_S\) 乘可以轉移到 \(T\) 的題目數,再除以 \(c\)。
- 這樣正推求機率 dp 是沒有問題的,而且 \(T \ne S\) 且 \(T \ne \varnothing\) 所以是有單調性的,且所有都會到達最終狀態。
- 因為要列舉子集,所以複雜度 \(O(3^m)\)。
考慮 \(O(m^2 2^m)\) 的做法。
- 改寫一下轉移,設 \(g_T\) 表示 \(T\) 在 \(n\) 個集合中的出現次數少。
- \(\frac{dp_S}{c_S} \times g_T \to dp_{S \cap T}\)(要求 \(0 < |S \cap T| < |S|\))。集合 \(T\) 不再是目標狀態了,是轉移。
- 發現實際上 \(0 < |S \cap T|\) 這個限制實際是不需要的,因為 \(dp_0\) 並不會對答案產生任何貢獻。
- 所以實際轉移是:\(\frac{dp_S}{c_S} \times g_T \to dp_{S \cap T} (|S \cap T| < |S|)\)。
- 求 \(c_S\)。
- 首先一個普通容斥,變為求包含 \(S\) 的 \(T\),和完全不包含 \(S\) 的 \(T\)。
- 普通高維字首和。
- 由於 \(|S \cap T| < |S|\) 這個限制,我們考慮列舉 popcount 從 \(m\) 到 \(0\),由 popcount \(> i\) 的狀態轉移到 popcount \(= i\) 的狀態。這樣那個限制就沒有了。
- 最佳化轉移。
- 考慮一個普通容斥,\(dp_S\) 可以先加上每對 \(S \in S',T\) 的貢獻,然後減去那些 \(S' \cap T \ne S\) 且 \(S \in S' \cap T\) 的總和。
- 第一步可以高維字首和,第二步需要高維差分。
Code:
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAXM = (1ll << 20) + 3;
int n, m, pc[MAXM];
double c[MAXM], c1[MAXM], c2[MAXM]; // c 陣列
double g[MAXM]; // g 陣列
double f[MAXM], tmp[MAXM], _f[MAXM];
void ADD(double &x, double w){ x += w; }
void hdi_suf(double *A){
for(int i = 0; i < m; i++){
for(int s = 0; s < (1ll << m); s++) if((s >> i) & 1) ADD(A[s ^ (1ll << i)], A[s]);
}
}
void _hdi_suf(double *A){ // 高維字尾差分
for(int i = 0; i < m; i++){
for(int s = 0; s < (1ll << m); s++) if((s >> i) & 1) ADD(A[s ^ (1ll << i)], - A[s]);
}
}
void hdi_pre(double *A){
for(int i = 0; i < m; i++){
for(int s = 0; s < (1ll << m); s++) if((s >> i) % 2 == 0) ADD(A[s ^ (1ll << i)], A[s]);
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
int R = (1ll << m) - 1;
for(int s = 0; s < (1ll << m); s++){
for(int i = 0; i < m; i++) pc[s] += (s >> i) & 1;
}
for(int i = 1; i <= n; i++){
int S = 0;
for(int j = 0; j < m; j++){
char ch; cin >> ch, S += (ch == '1') * (1ll << j);
}
g[S]++, c1[S]++, c2[S]++;
}
hdi_pre(c1), hdi_suf(c2);
for(int s = 0; s < (1ll << m); s++){
c[s] = (s == 0 ? 0 : n - c1[s ^ R] - c2[s]);
if(c[s] < 0) return 1;
}
f[R] = 1;
hdi_suf(g); // 先對 g 做一遍字尾和
for(int i = m - 1; i >= 1; i--){
for(int s = 0; s < (1ll << m); s++) tmp[s] = f[s], f[s] = (c[s] == 0 ? 0 : f[s] / c[s]);
hdi_suf(f);
for(int s = 0; s < (1ll << m); s++) _f[s] = f[s] * g[s];
_hdi_suf(_f);
for(int s = 0; s < (1ll << m); s++){
if(pc[s] > i) f[s] = tmp[s];
if(pc[s] == i) f[s] = _f[s];
if(pc[s] < i) f[s] = 0;
}
}
vector<double> ans(m, 0);
for(int s = 1; s < (1ll << m); s++){
if(c[s] != 0) continue; // 只統計最終狀態的機率
for(int i = 0; i < m; i++) if((s >> i) & 1) ADD(ans[i], f[s]);
}
cout << fixed << setprecision(10) << ans[0] << "\n";
return 0;
}