題解:CF1773G Game of Questions

hhhqx發表於2024-12-03

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;
}

相關文章