けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

yukicoder No.763 Noelちゃんと木遊び

いわゆる「木上の最大安定集合問題」です! 超典型問題です。

問題概要

頂点数  N の木が与えられます。木からいくつかの頂点を選んで削除します。その結果、木はいくつかの連結成分に分かれます。

分かれた連結成分の個数の最大値を求めてください。

制約

  •  1 \le N \le 10^{5}

最大安定集合問題

さて、今回の問題は、実は「木上の最大安定集合問題」に一致します。

安定集合とは、グラフの頂点集合の部分集合であって「どの 2 頂点も隣接しない」という条件を満たすものをいいます。最大安定集合問題は、最大サイズの安定集合を求める問題です。

f:id:drken1215:20210806104342p:plain

最大安定集合問題は一般には NP 困難であり、解決は困難です。しかしたとえば二部グラフであれば二部マッチングに帰着して解けることが知られています。

木も二部グラフですから、木上の最大安定集合問題ももちろん二部マッチングで解くこともできます。しかし木であれば、より簡潔な Greedy 解法をとることができます。

葉から考える Greedy

木に関する問題では、葉から考えていく考察によってうまくいくことが多々あります。

drken1215.hatenablog.com

実は木の葉を貪欲に選んでいく方法で解くことができます。

木の葉頂点  v を一つとり、「頂点  v を含まない安定集合  S があったとき、そのサイズを減らすことなく、頂点  v を含む安定集合へと変形できること」を示しましょう。葉頂点  v に隣接する頂点を  u とします。

  •  S u を含まないならば、 S v を追加したものも安定集合となっている
  •  S u を含むならば、 S から  u を除去して代わりに  v を追加したものも安定集合となっている

というようになります。いずれにしても、安定集合のサイズを減らすことなく、葉頂点  v を含むように変形できることがわかりました。

以上より、木の葉を貪欲に選び、その葉と葉に隣接する頂点を除去していく方法によって最大安定集合を求めることができることがわかりました。

コード

実装上は、木 DP の要領で、木の根を 1 つ決めて各部分木について考えていくのが有効です。計算量は  O(N) となります。

#include <iostream>
#include <vector>
using namespace std;
using Graph = vector<vector<int>>;

vector<bool> used;
void rec(const Graph &G, int v, int p = -1) {
    // 子頂点の中に採用済みの頂点があるかどうか
    bool exist = false;
    for (auto ch: G[v]) {
        if (ch == p) continue;

        // 再帰的探索
        rec(G, ch, v);
        if (used[ch]) exist = true;
    }

    // 子頂点の中に採用済みの頂点がなければ採用する
    if (!exist) used[v] = true;
}

int main() {
    // 入力
    int N;
    cin >> N;
    Graph G(N);
    for (int i = 0; i < N - 1; ++i) {
        int u, v;
        cin >> u >> v;
        --u, --v;
        G[u].push_back(v);
        G[v].push_back(u);
    }

    // 頂点 0 を根として探索
    used.assign(N, false);
    rec(G, 0);

    // 数える
    int res = 0;
    for (int v = 0; v < N; ++v) {
        if (used[v]) ++res;
    }
    cout << res << endl;
}