除原理! 学びの多い問題だった。Subset Convolution の方はちょっと頑張ってこの後復習する。
問題概要
頂点番号が であるグラフ がある。最初、辺は 1 本もない。
以上 以下の値からなるサイズ の 2 つの数列 と がある。
今、これら 2 つの数列間をマッチング ( 通りある) を一様ランダムに選び、グラフ 上で対応する辺を張る。このとき、グラフ は頂点数 、辺数 の無向グラフとなる。
このグラフ の連結成分の個数の期待値を求めよ。
制約
考えたこと
僕は初手で失敗していた。「辺を 1 本追加するときに、それが連結成分数を増やす確率は......?」などという方向で考えてしまった。
正解方針は、グラフ の頂点集合 の各部分集合 について、それが 1 つの連結成分になる確率を求めて、その総和をとることだった。勉強になる!
しかし「 が連結」という条件は扱いづらいので、包除原理や除原理 (この記事を参照) が活躍することになる。包除原理も考えたが上手く行かなかったので除原理で考えることにした。数列 の要素のうち、頂点集合 に含まれるものの個数を として、次のようになる。
- = 頂点集合 のみを考える。 の場合は 0 とする。等しい場合は、それらの間のみでマッチングを組む方法の個数 ( がいくつかの連結成分に分かれるのは問わない)
- = 頂点集合 のみを考える。 の場合は 0 とする。等しい場合は、それらの間でマッチングを組む方法のうち、 全体がただ 1 つの連結成分になる方法の個数
を使って を表すことを考える。集合 に含まれる要素のうち、最小の番号を とする。 を から引いて求める際に、 を含む連結成分 で場合分けすると、
と表せる。 は容易に求められるので、これで の計算量の bitDP で解けた。
コード
#include <bits/stdc++.h> using namespace std; /*///////////////////////////////////////////////////////*/ // useful /*///////////////////////////////////////////////////////*/ template<class T> inline bool chmax(T& a, T b) { if (a < b) { a = b; return 1; } return 0; } template<class T> inline bool chmin(T& a, T b) { if (a > b) { a = b; return 1; } return 0; } using pint = pair<int, int>; using pll = pair<long long, long long>; /*///////////////////////////////////////////////////////*/ // debug /*///////////////////////////////////////////////////////*/ #define COUT(x) cout << #x << " = " << (x) << " (L" << __LINE__ << ")" << endl template<class T1, class T2> ostream& operator << (ostream &s, pair<T1,T2> P) { return s << '<' << P.first << ", " << P.second << '>'; } template<class T> ostream& operator << (ostream &s, vector<T> P) { for (int i = 0; i < P.size(); ++i) { if (i > 0) { s << " "; } s << P[i]; } return s; } template<class T> ostream& operator << (ostream &s, deque<T> P) { for (int i = 0; i < P.size(); ++i) { if (i > 0) { s << " "; } s << P[i]; } return s; } template<class T> ostream& operator << (ostream &s, vector<vector<T> > P) { for (int i = 0; i < P.size(); ++i) { s << endl << P[i]; } return s << endl; } template<class T> ostream& operator << (ostream &s, set<T> P) { for(auto it : P) { s << "<" << it << "> "; } return s; } template<class T> ostream& operator << (ostream &s, multiset<T> P) { for(auto it : P) { s << "<" << it << "> "; } return s; } template<class T1, class T2> ostream& operator << (ostream &s, map<T1,T2> P) { for(auto it : P) { s << "<" << it.first << "->" << it.second << "> "; } return s; } /*///////////////////////////////////////////////////////*/ // solver /*///////////////////////////////////////////////////////*/ // modint template<int MOD> struct Fp { // inner value long long val; // constructor constexpr Fp() : val(0) { } constexpr Fp(long long v) : val(v % MOD) { if (val < 0) val += MOD; } constexpr long long get() const { return val; } constexpr int get_mod() const { return MOD; } // arithmetic operators constexpr Fp operator + () const { return Fp(*this); } constexpr Fp operator - () const { return Fp(0) - Fp(*this); } constexpr Fp operator + (const Fp &r) const { return Fp(*this) += r; } constexpr Fp operator - (const Fp &r) const { return Fp(*this) -= r; } constexpr Fp operator * (const Fp &r) const { return Fp(*this) *= r; } constexpr Fp operator / (const Fp &r) const { return Fp(*this) /= r; } constexpr Fp& operator += (const Fp &r) { val += r.val; if (val >= MOD) val -= MOD; return *this; } constexpr Fp& operator -= (const Fp &r) { val -= r.val; if (val < 0) val += MOD; return *this; } constexpr Fp& operator *= (const Fp &r) { val = val * r.val % MOD; return *this; } constexpr Fp& operator /= (const Fp &r) { long long a = r.val, b = MOD, u = 1, v = 0; while (b) { long long t = a / b; a -= t * b, swap(a, b); u -= t * v, swap(u, v); } val = val * u % MOD; if (val < 0) val += MOD; return *this; } constexpr Fp pow(long long n) const { Fp res(1), mul(*this); while (n > 0) { if (n & 1) res *= mul; mul *= mul; n >>= 1; } return res; } constexpr Fp inv() const { Fp res(1), div(*this); return res / div; } // other operators constexpr bool operator == (const Fp &r) const { return this->val == r.val; } constexpr bool operator != (const Fp &r) const { return this->val != r.val; } constexpr Fp& operator ++ () { ++val; if (val >= MOD) val -= MOD; return *this; } constexpr Fp& operator -- () { if (val == 0) val += MOD; --val; return *this; } constexpr Fp operator ++ (int) const { Fp res = *this; ++*this; return res; } constexpr Fp operator -- (int) const { Fp res = *this; --*this; return res; } friend constexpr istream& operator >> (istream &is, Fp<MOD> &x) { is >> x.val; x.val %= MOD; if (x.val < 0) x.val += MOD; return is; } friend constexpr ostream& operator << (ostream &os, const Fp<MOD> &x) { return os << x.val; } friend constexpr Fp<MOD> pow(const Fp<MOD> &r, long long n) { return r.pow(n); } friend constexpr Fp<MOD> inv(const Fp<MOD> &r) { return r.inv(); } }; // Binomial coefficient template<class mint> struct BiCoef { vector<mint> fact_, inv_, finv_; constexpr BiCoef() {} constexpr BiCoef(int n) : fact_(n, 1), inv_(n, 1), finv_(n, 1) { init(n); } constexpr void init(int n) { fact_.assign(n, 1), inv_.assign(n, 1), finv_.assign(n, 1); int MOD = fact_[0].get_mod(); for(int i = 2; i < n; i++){ fact_[i] = fact_[i-1] * i; inv_[i] = -inv_[MOD%i] * (MOD/i); finv_[i] = finv_[i-1] * inv_[i]; } } constexpr mint com(int n, int k) const { if (n < k || n < 0 || k < 0) return 0; return fact_[n] * finv_[k] * finv_[n-k]; } constexpr mint fact(int n) const { if (n < 0) return 0; return fact_[n]; } constexpr mint inv(int n) const { if (n < 0) return 0; return inv_[n]; } constexpr mint finv(int n) const { if (n < 0) return 0; return finv_[n]; } }; const int MOD = 998244353; using mint = Fp<MOD>; int main() { // 入力 int N, M; cin >> N >> M; vector<int> rnum(N, 0), bnum(N, 0); // R, B それぞれの端子頂点の個数 for (int i = 0; i < M; ++i) { int v; cin >> v; --v; rnum[v]++; } for (int i = 0; i < M; ++i) { int v; cin >> v; --v; bnum[v]++; } // 二項係数ライブラリをセット BiCoef<mint> bc(max(N, M) + 1); // 全体集合を V とする // R(S) := R 端子のうち、頂点が S に含まれるものの個数、B(S) も同様 vector<int> R(1<<N, 0), B(1<<N, 0); for (int S = 0; S < (1<<N); ++S) { for (int v = 0; v < N; ++v) { if (S & (1<<v)) { R[S] += rnum[v]; B[S] += bnum[v]; } } } // f(S) := R 端子の頂点が S に含まれるもののみ考えて、 // それを接続させたときに S 外の頂点と結びつかないものの個数 // ただし、余った B 端子の頂点が S に含まれてもいけないものとする vector<mint> f(1<<N, 0); for (int S = 0; S < (1<<N); ++S) { if (R[S] == B[S]) { f[S] = bc.fact(R[S]); } } // g(S) := S が 1 つの連結成分とするような方法の個数 // S の中で最小の頂点 mv を含む連結成分 T で場合分けして、除原理 vector<mint> g(1<<N, 0); for (int S = 0; S < (1<<N); ++S) { int mv = 0; for (; mv < N; ++mv) if (S & (1<<mv)) break; g[S] = f[S]; for (int T = (S-1) & S; T > 0; T = (T-1) & S) { if (!(T & (1<<mv))) continue; g[S] -= g[T] * f[S - T]; } } // 集計 mint res = 0; for (int S = 1; S < (1<<N); ++S) { res += g[S] * bc.fact(M - R[S]) * bc.finv(M); } cout << res << endl; }