問題
個の頂点からなる木構造のグラフがあり、 番目(ただし、 )の辺は頂点 と頂点 を結んでいる。
「葉である頂点 を1つ選び、その頂点と接続されている辺を削除する」という操作を何回でも行えるとき、頂点 を削除するのに必要な操作回数の最小値を求めよ。
ただし、「葉」とは接続されている辺の本数が高々1本である頂点のことを指す。
入力
最初の1行に、整数 が与えられる。
次の 行の 行目には、整数 が与えられる。
条件
- 実行時間制限: 2s
- メモリ制限: 1024MB
- 与えられるグラフは木構造である。
出力
答えを1行で出力すること。
解法
与えられるグラフは木構造なので、頂点 を根として考えます。 このとき、頂点 については以下の図のようになっています。
ここで、頂点 を削除するためには、頂点 を葉にしなければなりません。 そのためには、頂点 に接続されている辺の本数を1本以下にする必要があります。
従って、図の青い部分が 個あったとすると、 個分だけ削除すれば頂点 を削除することのできる状態になります。 このとき、削除する頂点数を最小にするには、 個の箇所のうち、含まれる頂点数が少ないものから順に 個取ることにより達成できます。
すなわち、頂点 を削除した後には、 個の箇所のうち、含まれる頂点数が最大の部分のみが残るということになります。 よって、この部分の頂点数を とすると、 個の頂点が削除されたことになります。
以上を求めるために、頂点 を根としたときに、 を「頂点 以下に含まれる頂点の個数」と定めて、これを求めていきます。
これは頂点 の子を頂点 とすると、 \begin{align} \mathrm{dp}_{u} = \sum_{v} \mathrm{dp}_{v} + 1 \end{align} で求めることができます。 ここで、 を足しているのは、個数に頂点 自身を含めているためです。
これをすべて求めるには、頂点 から再帰的に辿っていくことにより、 の計算量で求めることができます。
そして、頂点 を削除するために必要な操作回数は、頂点 の子を とすると、 \begin{align} n - \max_{v} \mathrm{dp}_{v} \end{align} によって計算することができます。
以上の方法で、この問題を 程度の計算量で解くことができ、実行時間制限にも十分間に合います。
ソースコード
まず、グラフが与えられている状態で、 の値を求める dfs()
関数を以下のように実装しました。
int dp[int(3e5 + 5)]; vector<int> graph[int(3e5 + 5)]; int dfs(int now_node, int before_node = -1) { // now_node は現在の頂点を、 before_node は前に見ていた頂点を表す dp[now_node] = 1; // 頂点数に自身を含める // 頂点 now_node に接続されている頂点について for ループを回す for (int next_node : graph[now_node]) { if (next_node == before_node) { // next と before が一致するとき、next は親に当たるので、無視する continue; } dp[now_node] += dfs(next_node, now_node); // 子となる next の頂点数を再帰的に求め、足していく } return dp[now_node]; }
この dfs()
関数が実装されている状態で、答えを出力する部分を以下のように main()
関数の中に直接実装しました。
int n, dp[int(3e5 + 5)]; vector<int> graph[int(3e5 + 5)]; int main() { // 値の入力 cin >> n; // グラフの作成 for (int i = 1; i <= n - 1; i++) { int u, v; cin >> u >> v; graph[u].push_back(v); graph[v].push_back(u); } int m = 0; for (int i : graph[1]) { // 頂点 1 の子について、dfs を用いて頂点数を求めて m の値を更新する m = max(m, dfs(i, 1)); } cout << n - m << endl; // 答えの出力 return 0; }
感想
木構造での探索で再帰を使うものに関して、以前は この問題 での visited
のような「その頂点を探索したかを表す bool 値」を用意して行っていました。
(この問題は木構造でもなければ有向グラフでもあるので、例として出すのは間違っているかもしれませんが…。)
現在は この問題 のように「前に探索した頂点」を引数に入れることによって、コードを書く量自体は少し減ったのかなと思っています。
正直、どちらの方法でも特に問題は無いと思いますが、現在扱っている方法の場合は「根が決まっている」ことが前提になる部分もあるので、少し実装に気をつけたいですね。