C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++で木構造の祖先・子孫関係を判定するクエリ処理の実装方法


問題概要

頂点数 N の木と、Q 個のクエリが与えられます。各クエリは 2 つの整数 i と j から構成され、木の中でノード i がノード j の祖先にあたるかどうかを判定するのが課題です。

具体例で動きを確認してみましょう。

入力

C++で木構造の祖先・子孫関係を判定するクエリ処理の実装方法

Q = 2, query[][] = {{3, 5}, {1, 6}}

出力

No Yes

解説

i = 3, j = 5 : ノード 3 はノード 5 の祖先ではない → No
i = 1, j = 6 : ノード 1 はノード 6 の祖先である → Yes

解法のアプローチ

最も単純な方法は、DFS(深さ優先探索)によってノード i からすべての子孫をたどり、その中にノード j が含まれているかを確認することです。ただし、この方法ではクエリごとに最大 O(N) の計算が必要になるため、クエリ数が多いケースでは非効率になります。

そこで有効なのが、DFS の探索順序を利用したタイムスタンプ法(オイラーツアー)です。各ノードについて、探索で入った時刻(entTime)抜けた時刻(exitTime)を記録しておくと、次の重要な性質が成り立ちます。

ノード i がノード j の祖先 ⟺ entTime[i] ≤ entTime[j] かつ exitTime[j] ≤ exitTime[i]

これは「子孫の区間は必ず祖先の区間に完全に含まれる」というオイラーツアーの性質に基づいています。前処理に O(N)、各クエリの判定は O(1) で完了するため、N 頂点・Q クエリ全体を O(N + Q) で処理できます。

C++ 実装例

#include <bits/stdc++.h>
using namespace std;

// DFS で各頂点の進入時刻と退出時刻を記録する
void depthFirstSearch(vector<int> g[], int u, int parent, int entTime[], int exitTime[], int& cnt){
    entTime[u] = cnt++;
    for (int i = 0; i < g[u].size(); i++) {
        int v = g[u][i];
        if (v != parent) depthFirstSearch(g, v, u, entTime, exitTime, cnt);
    }
    exitTime[u] = cnt++;
}

// 辺リストから隣接リストを構築し、根(0)から DFS を開始する
void calcTimeInAndOut(int edges[][2], int V, int entTime[], int exitTime[]){
    vector<int> g[V];
    for (int i = 0; i < V - 1; i++) {
        int u = edges[i][0];
        int v = edges[i][1];
        g[u].push_back(v);
        g[v].push_back(u);
    }
    int cnt = 0;
    depthFirstSearch(g, 0, -1, entTime, exitTime, cnt);
}

int main(){
    // 木を構成する辺の定義
    int edges[][2] = { { 0, 1 }, { 0, 2 }, { 1, 3 }, { 1, 4 }, { 4, 5 }, { 5, 6 }, { 5, 7 }};
    int E = sizeof(edges) / sizeof(edges[0]);
    int V = E + 1;
    int Q = 2;
    int query[Q][2] = {{3, 5}, {1, 6}};
    int entTime[V], exitTime[V];

    calcTimeInAndOut(edges, V, entTime, exitTime);

    for(int i = 0; i < Q; i++){
        cout << "For query " << (i+1) << " : ";
        // i が j の祖先 ⇔ 区間 [entTime[j], exitTime[j]] が [entTime[i], exitTime[i]] に含まれる
        if (entTime[query[i][0]] <= entTime[query[i][1]] && exitTime[query[i][1]] <= exitTime[query[i][0]])
            cout << "is Ancestor\n";
        else
            cout << "is not Ancestor\n";
    }
    return 0;
}

出力

For query 1 : is not Ancestor
For query 2 : is Ancestor

クエリ 1(ノード 3 とノード 5)では 3 は 5 の祖先ではないため "is not Ancestor"、クエリ 2(ノード 1 とノード 6)では 1 は 6 の祖先であるため "is Ancestor" と正しく判定されています。

この手法を使えば、大量のクエリが発生する場面でも高速に祖先・子孫関係を判定でき、LCA(最小共通祖先)を求める問題などへの応用も可能です。

  1. 配列の全要素を乗算するC++プログラムの解説

    整数型の要素を持つ配列が与えられたとき、配列内のすべての要素を掛け合わせ、その積を表示することを考えます。本記事では、この問題をC++(C言語スタイルのコード)で解く方法を、アプローチ、アルゴリズム、サンプルコード、実行結果まで順を追って解説します。 例 入力: arr[]={1,2,3,4,5,6,7} 出力: 1 x 2 x 3 x 4 x 5 x 6 x 7 = 5040 入力: arr[]={3, 4, 6, 2, 7, 8, 4} 出力: 3 x 4 x 6 x 2 x 7 x 8 x 4 = 32256 解き方のアプローチ この問題は、累積用の一時変数を用意し、配列の要素を先頭

  2. C++で8進数を10進数に変換するプログラムの書き方

    8進数が入力として与えられたとき、それを10進数に変換するのが本記事のテーマです。 コンピュータ上の10進数は基数10で表現されます。一方、8進数は基数8で表現され、使用できる数字は0〜7に限られます。これに対して10進数では、0〜9までの任意の数字を使用することができます。 8進数から10進数への変換手順 右から左へ向かって剰余演算により各桁を取り出し、0から始まるべき乗を掛けます。指数は「桁数 − 1」に達するまで1ずつ増加させます。 8進数を変換するため、べき乗の基数は8となります(8進数の基数が8であるため)。 入力された数値の各桁に基数とべき乗を掛け、その結果を記録します。 すべて