C++で木構造における距離の総和を効率的に求める方法
N個のノードからなる無向かつ連結な木を考えます。ノードには0〜N-1のラベルが付いており、N-1本の辺が与えられています。i番目の辺は、ノードedges[i][0]とedges[i][1]を結んでいます。このとき、ans[i]が「ノードiとそれ以外のすべてのノードとの間の距離の合計」を表すようなリストansを求めるのがこの記事の目的です。
たとえば、入力がN = 6、edges = [(0,1),(0,2),(2,3),(2,4),(2,5)]である場合、出力は[8,12,6,10,10,10]となります。
解法のアプローチ
この問題を効率的に解くには、「再根付け(rerooting)」と呼ばれるテクニックを使います。まず最初のDFSで部分木ごとの情報(ノード数と距離の合計)を集め、次のDFSでその情報を流用することで、全ノードの答えを線形時間で求められます。
ステップ1:dfs1()の定義
引数としてnodeとparentを受け取る関数dfs1()を定義します。
- i := 0から始めて、i < graph[node]のサイズである限り、iを1ずつ増やしながら以下を繰り返します。
- child := graph[node][i]
- childがparentと異なる場合:
- dfs1(child, node)を呼び出す
- cnt[node] := cnt[node] + cnt[child]
- ans[node] := ans[node] + cnt[child] + ans[child]
ステップ2:dfs2()の定義
同様に、nodeとparentを受け取る関数dfs2()を定義します。
- graph[node]の各要素childに対して、childがparentと異なる場合:
- ans[child] := ans[node] - cnt[child] + N - cnt[child]
- dfs2(child, node)を呼び出す
その他の準備
- 結果を格納する配列ansを定義する
- 部分木のノード数を格納する配列cntを定義する
- グラフの隣接リスト用に、10005行分の配列graphを定義する
メインメソッドでの処理の流れ
- メンバ変数Nに入力のNを格納する
- サイズNの配列ansを定義する
- サイズNの配列cntを定義し、すべての要素を1で初期化する
- n := edgesのサイズとする
- i := 0からn未満の間、以下を繰り返す:
- u := edges[i][0]、v := edges[i][1]
- graph[u]の末尾にvを挿入し、graph[v]の末尾にuを挿入する
- dfs1(0, -1)とdfs2(0, -1)を実行する
- ansを返す
アルゴリズムの仕組み
dfs1では、各ノードを根とする部分木に含まれるノード数(cnt)と、部分木内部だけでの距離の総和(ans)をボトムアップに計算します。続くdfs2では、親ノードですでに求めた答えを利用して子ノードの答えを導きます。根を隣接ノードへ移動させるとき、移動先の子部分木側に属するcnt[child]個のノードとの距離はそれぞれ1ずつ減り、それ以外のN - cnt[child]個のノードとの距離はそれぞれ1ずつ増えるため、ans[child] = ans[node] - cnt[child] + (N - cnt[child])という式で答えを更新できるのです。
C++実装例
#include <bits/stdc++.h>
using namespace std;
void print_vector(vector<auto> v){
cout << "[";
for(int i = 0;
i<v.size(); i++){
cout << v[i] << ", ";
}
cout << "]"<<endl;
}
class Solution {
public:
void dfs1(int node, int parent) {
for (int i = 0; i < graph[node].size(); i++) {
int child = graph[node][i];
if (child != parent) {
dfs1(child, node);
cnt[node] += cnt[child];
ans[node] += cnt[child] + ans[child];
}
}
}
void dfs2(int node, int parent) {
for (int i = 0; i < graph[node].size(); i++) {
int child = graph[node][i];
if (child != parent) {
ans[child] = ans[node] - cnt[child] + N - cnt[child];
dfs2(child, node);
}
}
}
vector<int> ans;
vector<int> cnt;
vector<int> graph[10005];
int N;
vector<int> sumOfDistancesInTree(int N, vector<vector<int> >& edges) {
this->N = N;
ans = vector<int>(N);
cnt = vector<int>(N, 1);
int n = edges.size();
for (int i = 0; i < n; i++) {
int u = edges[i][0];
int v = edges[i][1];
graph[u].push_back(v);
graph[v].push_back(u);
}
dfs1(0, -1);
dfs2(0, -1);
return ans;
}
};
main(){
Solution ob;
vector<vector<int>> v = {{0,1},{0,2},{2,3},{2,4},{2,5}}; print_vector(ob.sumOfDistancesInTree(6, v));
}入力
{{0,1},{0,2},{2,3},{2,4},{2,5}}出力
[8, 12, 6, 10, 10, 10]
-
C++で木の直径を求めるアルゴリズムを解説
木の直径とは無向木(undirected tree)が与えられたとき、その直径を求めることを考えます。木の直径とは、木の中で最も長い経路に含まれる辺の数のことです。ここでは、木は辺のリストとして与えられます。edges[i] = [u, v] は、ノードuとノードvをつなぐ双方向の辺を表します。また、各ノードには {0, 1, ..., edges.length} の集合からラベルが割り当てられています。例として、次のような木を考えてみましょう。この場合、最も長い経路の長さは4となるため、出力は4になります。解法のアプローチ木の直径を効率的に求めるには、DFS(深さ優先探索)を2回実行するとい
-
C++で二分木の最大垂直和を求める方法
はじめに二分木が与えられたとき、垂直順序走査における各垂直列のノード値の合計を計算し、その中から最大値を求めて出力するのが本記事の課題です。例として、以下のような二分木を考えてみましょう。この二分木を垂直順序走査すると、各列の合計は次のようになります。4 2 1 + 5 + 6 = 12 3 + 8 = 11 7 9各列の合計の中で最大となるのは 12 です。アルゴリズムの考え方アプローチはシンプルです。幅優先探索(BFS)を用いて垂直順序走査を行い、各ノードに水平距離を割り当てます。ルートの水平距離を 0 とし、左に移動するごとに -1、右に移動するごとに +1 とします。同じ水平距離を持つ