Skip to main content
図と実装で理解する
木構造入門
@Pro_ktmr
JOI2019 優秀賞
2
注意
• 内容の正確性には十分注意していますが、間
違った情報が含まれることもあります。何か
お気づきのことがあればご連絡ください。
• このスライドのサンプルコードはC++で実装さ
れています。
JOI2019 春合宿
• JOI2019春合宿の問題の解法/概要を見てみる
• Segment Tree
木構造
貪欲法
Segment Tree
Segment Tree
ダイクストラ法
Segment Tree
木構造
動的計画法
Segment Tree
木構造
分割統治
3
JOI2019 春合宿
• JOI2019春合宿の問題の解法/概要を見てみる
• Segment Tree
木構造
貪欲法
Segment Tree
Segment Tree
ダイクストラ法
Segment Tree
木構造
動的計画法
Segment Tree
木構造
分割統治
• 木構造に関する問題が3問も出題されている!
4
JOI春合宿と木構造
• 2019年に限らず、例年2~3問ほど木構造に関
する問題が出題されています
• グラフ理論に見せかけた木構造の問題もあります
• グラフ理論の問題の部分点解法で木を使うこともあります
• Interactive / Communication / Output Onlyでも出題されています
• 木構造に関する問題を解くには典型的な知識が
必要なことも多く、逆に知識があれば簡単なこ
とも多いです
• なので勉強しましょう
5
木構造入門
1. 木構造とは
i. 木構造
ii. 根つき木
iii. 根つき木の実装
2. DFS木
I. 深さ優先探索とDFS木
II. DFS木の作り方
III. DFS木と部分木
3. Euler Tour
I. Euler TourとLCA
6
木構造入門
1. 木構造とは
i. 木構造
ii. 根つき木
iii. 根つき木の実装
2. DFS木
I. 深さ優先探索とDFS木
II. DFS木の作り方
III. DFS木と部分木
3. Euler Tour
I. Euler TourとLCA
7
木構造とは
• 頂点(ノード)と辺(エッジ)か
ら構成されている
• ノードがN個の時、エッジは
N-1本
• 連結(すべてのノード間で行
き来できる)
• 2つのノード間の最短路が一
意に決まる
• 閉路(サイクル)がない
• グラフの特殊な場合で、うれ
しい性質がたくさんあるイ
メージ
8
木構造とは
• 頂点(ノード)と辺(エッジ)か
ら構成されている
• ノードがN個の時、エッジは
N-1本
• 連結(すべてのノード間で行
き来できる)
• 2つのノード間の最短路が一
意に決まる
• 閉路(サイクル)がない
• 誰がどう考えてもこのルート
が明らかに最短
9
終
始
根つき木
• 木のうれしい性質を最大限使
うために、根(ルート)つき木
を考えることが多い
• 問題で根が指定されていない
場合は適当に決めてしまう
10
根つき木
• 木のうれしい性質を最大限使
うために、根(ルート)つき木
を考えることが多い
• 問題で根が指定されていない
場合は適当に決めてしまう
11
根
根つき木
• 木のうれしい性質を最大限使
うために、根(ルート)つき木
を考えることが多い
• 問題で根が指定されていない
場合は適当に決めてしまう
12
根
根つき木
• 木のうれしい性質を最大限使
うために、根(ルート)つき木
を考えることが多い
• 問題で根が指定されていない
場合は適当に決めてしまう
13
根
根つき木
• 木のうれしい性質を最大限使
うために、根(ルート)つき木
を考えることが多い
• 問題で根が指定されていない
場合は適当に決めてしまう
14
根
家族関係
• 各ノードは0個以上の子ノー
ドを持つ
• 根以外のノードは1個の親
ノードを持つ
• 同じ親を持つノードは兄弟
ノードと呼ばれる
• 子ノードやその子ノードなど
は子孫ノードと呼ばれる
• 親ノードやその親ノードなど
は先祖ノードと呼ばれる
• 辺が1本しか出ていない(次数
が1の)ノードを葉と呼ぶ
15
子
自
親
兄
弟
家族関係
• 各ノードは0個以上の子ノー
ドを持つ
• 根以外のノードは1個の親
ノードを持つ
• 同じ親を持つノードは兄弟
ノードと呼ばれる
• 子ノードやその子ノードなど
は子孫ノードと呼ばれる
• 親ノードやその親ノードなど
は先祖ノードと呼ばれる
• 辺が1本しか出ていない(次数
が1の)ノードを葉と呼ぶ
16
3
根つき木の実装
• 配列parent[N]を用意して親
ノードの番号を入れる
• 必要に応じてvector<int>
children[N]を用意して子ノー
ドの一覧も保持しておいても
よい
17
2
5
4
1
0
6
3
根つき木の実装
• 配列parent[N]を用意して親
ノードの番号を入れる
• 必要に応じてvector<int>
children[N]を用意して子ノー
ドの一覧も保持しておいても
よい
• parentって長いので、parと
略す人が多い(気がする)
• 根の親は-1にしたり、自分自
身(par[4] = 4)にする
18
2
5
4
1
0
6
par[4] = -1
par[5] = 4
par[2] = 5
par[1] = 4
par[0] = 4
par[3] = 0 par[6] = 0
3
根つき木の実装
• 配列parent[N]を用意して親
ノードの番号を入れる
• 必要に応じてvector<int>
children[N]を用意して子ノー
ドの一覧も保持しておいても
よい
• chiって略す人は見たことな
い気が・・・
19
2
5
4
1
0
6
chi[4] = {5,1,0}
chi[5] = {2}
chi[2] = {}
chi[1] = {}
chi[0] = {3,6}
chi[3] = {} chi[6] = {}
木構造入門
1. 木構造とは
i. 木構造
ii. 根つき木
iii. 根つき木の実装
2. DFS木
I. 深さ優先探索とDFS木
II. DFS木の作り方
III. DFS木と部分木
3. Euler Tour
I. Euler TourとLCA
20
3
深さ優先探索(DFS)
• 木上で深さ優先探索(DFS)を
行う
• ノードに子があれば次はその
子ノードを探索する
• 子がなくなるまで深く潜って
いく
21
2
5
4
1
0
6
3
DFS木
• DFSの探索順にノード番号を
振りなおすと、DFS木になる
22
2
5
4
1
0
6
3
DFS木
• DFSの探索順にノード番号を
振りなおすと、DFS木になる
23
2
5
0
1
0
6
3
DFS木
• DFSの探索順にノード番号を
振りなおすと、DFS木になる
24
2
1
0
1
0
6
3
DFS木
• DFSの探索順にノード番号を
振りなおすと、DFS木になる
25
2
1
0
1
0
6
3
DFS木
• DFSの探索順にノード番号を
振りなおすと、DFS木になる
26
2
1
0
3
0
6
3
DFS木
• DFSの探索順にノード番号を
振りなおすと、DFS木になる
27
2
1
0
3
4
6
5
DFS木
• DFSの探索順にノード番号を
振りなおすと、DFS木になる
28
2
1
0
3
4
6
5
DFS木
• DFSの探索順にノード番号を
振りなおすと、DFS木になる
29
2
1
0
3
4
6
5
DFS木
• DFSの探索順にノード番号を
振りなおすと、DFS木になる
• 完成!
30
2
1
0
3
4
6
DFS木を作る実装
• 再帰関数でDFSをする
vector<int> children[N]; //もとの子
int par[N],pos[N]; //DFS木における親、DFS終了時のカウンターの状態
int c = 0; //cはカウンターでDFS木のノード番号になる
void dfs(int v){ //vはもとのノード番号
int dfsOrder = c;
c++;
for(int i=0; i<children[v].size(); i++){
par[c] = dfsOrder;
dfs(children[v][i]);
}
pos[dfsOrder] = c;
} //変数が「もと」か「あと(DFS木)」かどちらを表しているのか混乱しないように注意
31
5
DFS木の性質「部分木」
• あるノードvについて、その
子孫のDFSが終了したタイミ
ングでのカウンターをpos[v]
として保存しておく
• vを根とする部分木は
[v,pos[v])である
• [a,b)は半開区間でa≦i<bみ
たいな
32
2
1
0
3
4
6
pos[0] = 7
pos[1] = 3
pos[5] = 6
pos[4] = 7
pos[3] = 4
pos[2] = 3
pos[6] = 7
5
DFS木の性質「部分木」
• あるノードvについて、その
子孫のDFSが終了したタイミ
ングでのカウンターをpos[v]
として保存しておく
• vを根とする部分木は
[v,pos[v])である
• [a,b)は半開区間でa≦i<bみ
たいな
33
2
1
0
3
4
6
pos[0] = 7
pos[1] = 3
pos[5] = 6
pos[4] = 7
pos[3] = 4
pos[2] = 3
pos[6] = 7
ノード4を根とする部分木は
ノード[4,7) つまり4,5,6からなる
5
部分木の使い方
• RAQやRUQを使って部分木に
対して一気に操作することが
できる
• 例)各辺にコストが設定され
ている。以下のクエリを処理
する。
①頂点vとその親との間の辺
のコストをxに変更する
②頂点vと根との最短距離dを
求める
34
2
1
0
3
4
6
v 0 1 2 3 4 5 6
d 0 5 6 2 4 4 5
5
1
2
4
0 1
5
部分木の使い方
• RAQやRUQを使って部分木に
対して一気に操作することが
できる
• 例)各辺にコストが設定され
ている。以下のクエリを処理
する。
①頂点vとその親との間の辺
のコストをxに変更する
②頂点vと根との最短距離dを
求める
• クエリ:①v=4,x=2
35
2
1
0
3
4
6
v 0 1 2 3 4 5 6
d 0 5 6 2 2 2 3
5
1
2
2
0 1
コスト:4→2なので
部分木全部RAQ:-2
5
部分木の使い方
• RAQやRUQを使って部分木に
対して一気に操作することが
できる
• 例)各辺にコストが設定され
ている。以下のクエリを処理
する。
①頂点vとその親との間の辺
のコストをxに変更する
②頂点vと根との最短距離dを
求める
• クエリ:②v=5
36
2
1
0
3
4
6
v 0 1 2 3 4 5 6
d 0 5 6 2 2 2 3
5
1
2
2
0 1
補足:RAQとRUQとRMQ
• RAQ:Range Add Query
配列の特定の区間に対して値を加算する
• RUQ:Range Update Query
配列の特定の区間に対して値を変更する
• どちらも遅延評価Segment Treeや平方分割で
実装できます
• RMQ:Range Minimum Query
配列の特定の区間の値の最小値を求める
も後で出てきます
37
木構造入門
1. 木構造とは
i. 木構造
ii. 根つき木
iii. 根つき木の実装
2. DFS木
I. 深さ優先探索とDFS木
II. DFS木の作り方
III. DFS木と部分木
3. Euler Tour
I. Euler TourとLCA
38
5
Euler Tour
• 頂点のDFSでの訪問順を
vector等で持つことで、木を
直線にできる
39
2
1
0
3
4
6
5
Euler Tour
• 頂点のDFSでの訪問順を
vector等で持つことで、木を
直線にできる
• 0 → 1 → 2 → 1 → 0 → 3 → 0
→ 4 → 5 → 4 → 6 → 4 → 0
40
2
1
0
3
4
6
5
Euler Tour
• 頂点のDFSでの訪問順を
vector等で持つことで、木を
直線にできる
• 0,1,2,1,0,3,0,4,5,4,6,4,0
• この配列の要素数は頂点数を
Nとして2N-1個になる
41
2
1
0
3
4
6
Euler Tourの実装
vector<int> EulerTour;
int pre[N];
void dfs(int v){
pre[v] = EulerTour.size();
EulerTour.push_back(v);
for(int i=0; i<children[v].size(); i++){
dfs(edge[v][i]);
EulerTour.push_back(v);
}
}
• 関数名がdfsでさっきと被ってるが、その辺りは臨機応変に対
応する
42
2
深さ
• ところで、あるノードの根から
の距離を深さと呼ぶ
• 再帰関数で求められる
int depth(int v){
if(par[v] == -1) return 0;
return depth(par[v])+1;
}
• 木の構造が変わらないならメモ
化するとよい
int d[N] = {};
int depth(int v){
if(par[v] == -1) return 0;
if(d[v] != 0) return d[v];
return d[v] = depth(par[v])+1;
}
43
2
1
0
1
1
2
LCA(最近共通祖先)
• Lowest Common Ancestor
• 2つ以上のノードについて共
通な祖先のうちもっとも深い
もの
44
LCA(最近共通祖先)
• Lowest Common Ancestor
• 2つ以上のノードについて共
通な祖先のうちもっとも深い
もの
45
0
1
LCA(0,1)
2
LCA(最近共通祖先)
• Lowest Common Ancestor
• 2つ以上のノードについて共
通な祖先のうちもっとも深い
もの
46
1
LCA(2,1)
5
LCAとRMQ
• さっきのスライドより右の木
のEuler Tourは
0,1,2,1,0,3,0,4,5,4,6,4,0
• 頂点vを最初に訪問した時の
pushする前のvectorの要素
数をpre[v]とする
47
2
1
0
3
4
6
5
LCAとRMQ
• さっきのスライドより右の木
のEuler Tourは
0,1,2,1,0,3,0,4,5,4,6,4,0
• 頂点vを最初に訪問した時の
pushする前のvectorの要素
数をpre[v]とする
• 2つの頂点i, j (pre[i] < pre[j])
のLCAは
RMQ[pre[i],pre[j])になる
48
2
1
0
3
4
6
pre[0] = 0
pre[1] = 1
pre[2] = 2
pre[3] = 5
pre[4] = 7
pre[5] = 8
pre[6] = 10
LCAとRMQ
•
0,1,2,1,0,3,0,4,5,4,6,4,0
49
pre[0] = 0
pre[1] = 1
pre[2] = 2
pre[3] = 5
pre[4] = 7
pre[5] = 8
pre[6] = 10
LCAとRMQ
50
pre[0] = 0
pre[1] = 1
pre[2] = 2
pre[3] = 5
pre[4] = 7
pre[5] = 8
pre[6] = 10
Euler Tour 0 1 2 3 4 5 6 7 8 9 10 11 12
v 0 1 2 1 0 3 0 4 5 4 6 4 0
LCAとRMQ
51
Euler Tour 0 1 2 3 4 5 6 7 8 9 10 11 12
v 0 1 2 1 0 3 0 4 5 4 6 4 0
v 0 1 2 3 4 5 6
pre[v] 0 1 2 5 7 8 10
LCAとRMQ
• 頂点2と5のLCAは?
52
Euler Tour 0 1 2 3 4 5 6 7 8 9 10 11 12
v 0 1 2 1 0 3 0 4 5 4 6 4 0
v 0 1 2 3 4 5 6
pre[v] 0 1 2 5 7 8 10
LCAとRMQ
• 頂点2と5のLCAは?
Euler TourにおけるRMQ[pre[2]=2,pre[5]=8)
→0
• 数スライド戻ると確かにLCA(2,5)=0
53
Euler Tour 0 1 2 3 4 5 6 7 8 9 10 11 12
v 0 1 2 1 0 3 0 4 5 4 6 4 0
v 0 1 2 3 4 5 6
pre[v] 0 1 2 5 7 8 10
LCAとRMQ
• 木構造を完全に忘れてただのRMQとしてLCAを
求めることができる
• 他にはDoublingやHL分解などでもLCAを求め
られる
• RMQを使うにはSegment Tree(or平方分割)を
実装する必要があるが、個人的にはEuler
Tour→RMQが好き
54
まとめ
「木があるということは数列があるということ」
55
まとめ
• 木構造の問題に対しては、適当な頂点を根とし
てDFS木を作ると良いことが多い
• 木があるということは数列があるということ
vを根とした部分木は[v,pos[v])
• LCA(i,j)はEuler Tourにおける
RMQ[pre[i],pre[j])で求められる
• 他にも辺や頂点をなくす(くっつける)場合の
UnionFindによる縮約もJOI春合宿では頻出
56
練習問題
• AOJ GRL_5_C LCA
https://onlinejudge.u-
aizu.ac.jp/courses/library/5/GRL/5/GRL_5_C
• AOJ GRL_5_D Range Query on a Tree
https://onlinejudge.u-
aizu.ac.jp/courses/library/5/GRL/5/GRL_5_D
• 最初はAOJで基礎を固めるのがおすすめです
57
練習問題
• JOI春合宿2015 Day2-3 Road Development
https://atcoder.jp/contests/joisc2015/tasks/j
oisc2015_g
• このスライドの内容をすべて網羅しているよう
な問題で、難しく重実装ですが、これさえ解け
ればかなり力が付きます
58
図と実装で理解する『木構造入門』