Official

F - Googol Swaps Editorial by sheyasutaka


必要十分条件の考察 (置換)

文字の位置を頂点番号とし,各操作を辺 \((A_i, B_i)\) とした \(N\) 頂点 \(M\) 辺の無向グラフ \(G\) をおきます. このとき,各操作は辺の両端点の文字を交換する操作と言い換えられます.

頂点 \(i\) にあった文字を 頂点 \(p_i\) に移動させた状態を置換 \(p = (p_1, \dots, p_N)\) で表すことにします.

また,置換の転倒数 (\(i < j\) かつ \(p_i > p_j\) を満たす \((i,j)\) 組の個数) の偶奇によって,置換を偶置換奇置換に二分します.

このとき,操作を何回か行って得られる置換 \(p\) について以下が成り立ちます.

  • 条件 A: 操作の合計回数が偶数であれば \(p\) は偶置換,奇数であれば \(p\) は奇置換.
  • 条件 B: \(G\) の任意の連結成分における頂点集合 \(\{v_1, \dots, v_k\}\) について,\(\{v_1, \dots, v_k\}\)\(\{p_{v_1}, \dots, p_{v_k}\}\) は集合として等しい.

これらはいずれも,操作の合計回数についての帰納法で示せます(証明略).

逆に,条件 B を満たす任意の置換 \(p\) について,\(N^2\) 回以下の操作で最終状態 \(p\) を達成することが可能です.以下に証明を載せます.

$N^2$ 回以下の操作で達成可能であることの証明 $N$ に対する帰納法で示します.$N = 2$ のときは簡単に確かめられます.

$G$ の連結成分 $C$ を $1$ つ任意にとり,その全域木 $T_C$ を任意にとり,その葉 $v_1$ を任意にとります.この連結成分の頂点集合を $\{v_1, \dots, v_{|C|}\}$ とします.
条件 B より,$p_{v_s} = v_1$ となる頂点 $v_s$ が $C$ 内に存在します.
$v_1, v_s$ をつなぐ $T_C$ 上のパスに沿って操作を行うことで,頂点 $v_s$ にあった文字を頂点 $v_1$ に移動させることを $N-1$ 回以下の操作で達成できます.この状態から最終状態を満たすための置換を $p'$ とします.

ここで,頂点 $v_1$ およびそれにつながる辺を取り除いたグラフ $G'$ をとったとき,$C$ 以外の連結成分に変化は無く,$C - \{v_1\}$ は空または $1$ 個の連結成分になっています($v_1$ は全域木 $T_C$ 上で葉であったため).よって,$G'$ 上で $p'$ は条件 B を満たすので,頂点 $v_1$ を取り除いてからは $(N-1)^2$ 回以下の操作で最終状態を達成できます.

$(N-1) + (N-1)^2 \leq N^2$ より,$N^2$ 回以下の操作で $p$ を達成できることが分かります.

ちょうど \(10^{100}\) 回の操作で置換 \(p\) の状態を得るには,\(p\) が偶置換かつ条件 B を満たすことが必要です.

逆に,条件 B を満たす任意の偶置換 \(p\) について,ちょうど \(10^{100}\) 回の操作で \(p\) の状態を得ることが可能です(\(N^2\) 回以下の偶数回の操作で \(p\) を得たあと,適当な特定の操作を偶数回繰り返せばよい).

よって,達成可能な文字の配置は,条件 B を満たす任意の偶置換 \(p\) で得られるものと言い換えられます.

各連結成分ごとに独立に文字を並べかえる配置を考えます. そのうち偶置換で得られるものの個数は,以下の \(2\) 通りの場合分けによって求まります.

  • ある連結成分に同じ文字が \(2\) 個以上ある場合
    • 連結な \(2\) 頂点 \(v, u\) に同じ文字が書いてあるとする.このとき,条件 B を満たす任意の偶置換 \(p\) に対して,\(p_v, p_u\) を入れ替えた奇置換 \(p'\) が一対一対応し,\(p, p'\) から得られる文字配置は一致する.したがって,すべての配置は偶置換で得られる.
  • どの連結成分にも同じ文字が \(2\) 個以上ない場合
    • 条件 B を満たす任意の置換と,任意の配置が一対一対応する.連結な \(2\) 頂点を任意にとって上と同様の議論をすることで,すべての配置のうちちょうど半分が偶置換でのみ得られるもので,残りの半分が奇置換でのみ得られるものとわかる.

数え上げ

各連結成分ごとに独立に文字を並べかえる配置を数え上げ,上の場合分けによって \(1\) 倍または \(\frac{1}{2}\) 倍すれば答えが得られる.

ある連結成分において,各種類の文字の個数が \(c_1, c_2, \dots, c_{\sigma}\) のとき,その連結成分における文字の順列の個数は \(\displaystyle \frac{(c_1 + c_2 + \dots + c_{\sigma})!}{c_1! \cdot c_2! \cdot \ldots \cdot c_{\sigma}!}\) です.これらをすべての連結成分について求め,その積を取ればよいです.

実装例 (C++)

#include <iostream>
using std::cin;
using std::cout;
using std::cerr;
using std::endl;
#include <vector>
using std::vector;
using std::pair;
#include <map>
using std::map;
#include <string>
using std::string;

using std::max;
using std::min;
using std::swap;

#include <atcoder/modint.hpp>
using mint = atcoder::modint998244353;

#ifdef DEBUG
const int debug = 1;
#else
const int debug = 0;
#endif

using ll = int64_t;
using P = pair<ll, ll>;

const ll FOD = 998244353;

ll n, m;
string s;
vector<ll> a, b;

void output (const ll x) {
	cout << x << "\n";
}
void output (const mint x) {
	cout << x.val() << "\n";
}

vector<mint> frac, invf;
void f_init (const ll n) {
	frac.resize(n+1);
	invf.resize(n+1);

	frac[0] = 1;
	for (ll i = 1; i <= n; i++) {
		frac[i] = frac[i-1] * i;
	}
	invf[n] = frac[n].inv();
	for (ll i = n; i >= 1; i--) {
		invf[i-1] = invf[i] * i;
	}
	assert(invf[0] == 1);
}
mint ncr (const ll n, const ll r) {
	if (!(n >= r && r >= 0)) return 0;
	return frac.at(n) * invf.at(r) * invf.at(n-r);
}

class dsu {
	private:
	const ll n;
	struct DSUnode {
		ll p;
		ll sz;
	};
	vector<DSUnode> dnodes;

	public:
	dsu (const ll n_): n(n_) {
		dnodes.resize(n);
		for (ll i = 0; i < n; i++) {
			dnodes[i] = {p: i, sz: 1};
		}
	}

	ll find (const ll x) {
		if (dnodes.at(x).p == x) return x;
		return dnodes.at(x).p = find(dnodes.at(x).p);
	}

	bool unite (ll l, ll r) {
		l = find(l);
		r = find(r);
		if (l == r) return false;

		if (dnodes[l].sz < dnodes[r].sz) swap(l, r);

		dnodes[l].sz += dnodes[r].sz;
		dnodes[r].p = l;
		return true;
	}

	ll size (const ll x) {
		return dnodes[find(x)].sz;
	}
};

void solve() {
	f_init(n*2+5);
	dsu g(n);

	for (ll i = 0; i < m; i++) {
		g.unite(a[i], b[i]);
	}

	vector<map<char, ll>> cnts(n); // cnts[v][c] := count of i s.t. g.find(i) == v && s[i] == c
	bool hassameswap = false;
	for (ll i = 0; i < n; i++) {
		cnts[g.find(i)][s[i]] += 1;
		if (cnts[g.find(i)][s[i]] >= 2) hassameswap = true;
	}

	bool ishalf = (!hassameswap);
	mint ans = 1;
	for (ll i = 0; i < n; i++) {
		if (cnts[i].empty()) continue;

		ll sz = g.size(i);
		for (const auto &[ch, r] : cnts[i]) {
			ans *= ncr(sz, r);
			sz -= r;
		}
		assert(sz == 0);
	}
	if (ishalf) ans /= 2;

	output(ans);
	return;
}

int main (void) {
	std::cin.tie(nullptr);
	std::ios_base::sync_with_stdio(false);

	cin >> n >> m;
	cin >> s;
	a.resize(m);
	b.resize(m);
	for (ll i = 0; i < m; i++) {
		cin >> a[i] >> b[i];
		--a[i];
		--b[i];
	}
	
	solve();

	return 0;
}

posted:
last update: