$0$ 以上 $n$ 未満の整数の集合を $E$ とします。
$n$ 個の整数の要素からなる配列 $\mathrm{f}$ が与えられ、これは $i$ 番目の要素を入力とする関数 $\mathrm{f}$ の答えが $\mathrm{f}[i]$ であることを意味しています。
$E$ の部分集合 $S$ について、
"$S$ の要素を $x$ としたとき、全ての $x$ で $\mathrm{f}[x]$ も $S$ の要素である" ($\forall x \in S$, $\mathrm{f}[x] \in S$)
という条件を満たすような集合 $S$ はいくつありますか。ちなみに $S$ が空集合のときは常にこの条件を満たします。
・$1 \le n \le 50$
・$0 \le \mathrm{f}$ の各要素 $\lt n$
TopCoder の解説は vexorian さんが書いてたときは分かりやすかった。
英語で読むのは面倒なので、かいつまんで日本語にする。
---
グラフで考える。
$\mathrm{f}(x) = y$ のとき、x → y と y → x の二種類の辺の張り方ができる。どちらでもいいが、
今回の問題では y → x という張り方だと、すべてのノードにおいて "ひとつ前のノード" というのは多くてひとつ(入次数が高々一個)になり、利便性が高いことに注目したい。
y → x というリンクがあるとき、
・集合から要素 y を取り除いたときは要素 x も取り除かなければならない
・集合に要素 x を含める場合は要素 y も含めなければならない
ということを意味する。
グラフには閉路が含まれる可能性があるが、強連結成分分解すれば DAG にできる。
この DAG においても入次数が高々一個、という特殊な状況は維持されており、この DAG は森になる。
森(複数の木)なので、再帰的に処理できる。
このとき、葉から根に向かって矢印を張るようにする($\mathrm{parent\_of}$)。
#include <iostream>
#include <algorithm>
using namespace std;
using i64 = long long;
// 強連結成分 (Strongly Connected Components) 分解
// DFS 2回 O(V+E)
class SCC {
int n;
vector<vector<int>> fG, rG;
vector<bool> used;
vector<int> vertices; // 帰りがけ順の並び
void dfs(int u);
void rdfs(int v, int k);
public:
SCC();
SCC(int n); // 頂点数が n のグラフ
void add_edge(int u, int v); // u から v に辺を張る
void solve(); // 強連結成分分解する
vector<vector<int>> restore_sccs(); // solve() の後に呼び出す。強連結成分をトポロジカル順序で返す
int scc_count; // solve() の後に呼び出せる。強連結成分の個数
vector<int> order; // solve() の後に呼び出せる。属する強連結成分のトポロジカル順序
};
// {{{ SCC実装
SCC::SCC() : n(0) {}
SCC::SCC(int n) : n(n) {
fG.assign(n, vector<int>());
rG.assign(n, vector<int>());
}
void SCC::add_edge(int u, int v) {
fG[u].push_back(v);
rG[v].push_back(u);
}
void SCC::dfs(int u) {
used[u] = true;
for(int v : fG[u]) {
if(!used[v]) { dfs(v); }
}
vertices.push_back(u);
}
void SCC::rdfs(int v, int k) {
used[v] = true;
order[v] = k;
for(int u : rG[v]) {
if(!used[u]) { rdfs(u, k); }
}
}
void SCC::solve() {
used.assign(n, false);
order.assign(n, 0);
vertices.clear();
for(int u=0; u<n; ++u) {
if(!used[u]) { dfs(u); }
}
used.assign(n, false);
scc_count = 0;
for(int i=vertices.size()-1; i>=0; --i) {
if(!used[vertices[i]]) { rdfs(vertices[i], scc_count++); }
}
}
vector<vector<int>> SCC::restore_sccs() {
vector<vector<int>> res(scc_count, vector<int>()); // res[scc_count][]
for(int i=0; i<n; ++i) {
res[order[i]].push_back(i);
}
return res;
} // }}}
class InvariantSets { public: i64 countSets(vector<int> f); };
SCC scc;
vector<int> parent_of;
// v番目の強連結成分のパターン数を求める
i64 solve(int v) {
i64 res = 1;
// vを含める: 全ての子の事象は互いに排反なので、それらのパターン数を掛ける
for(int i=0; i<scc.scc_count; ++i) {
if(parent_of[i] == v) { // リンクを逆に最終的に葉まで辿る。
res *= solve(i);
}
}
// vを含めない: 空集合の1個を足す
return ++res;
}
i64 InvariantSets::
countSets(vector<int> f) {
int n = f.size();
::scc = SCC(n);
for(int i=0; i<n; ++i) {
scc.add_edge(f[i], i);
}
scc.solve();
vector<int> &order = scc.order; // 頂点iと頂点jが同じ強連結成分ならorder[i]==order[j]
::parent_of.assign(n, -1);
for(int i=0; i<n; ++i) {
if(order[i] != order[f[i]]) { // f[i] → i が異なる強連結成分への矢印なら
parent_of[order[i]] = order[f[i]]; // 根に向かっていくような今までと逆のリンクを張る
}
}
i64 res = 1LL;
for(int i=0; i<scc.scc_count; ++i) {
if(parent_of[i] == -1) { // リンクの終端(根)から
res *= solve(i); // 葉に向かう
}
}
return res;
}
(ΦωΦ)<
あとがきへ