p - 茨城大学

アルゴリズムとデータ構造
講義スライド 4
木
 グラフ
茨城大学 工学部 知能システム工学科 井上 康介
E2棟801号室
木とは
 リスト構造では枝分かれは起こらなかった.枝分かれしな
がらデータが伸びていくデータ構造を木 (tree) と呼ぶ.
 木はノード (node) とそれらを結ぶ枝 (branch) から構成
される.データが記録されているのはノードであり,枝は
データとデータの間の親子関係を示す.
A
ノード
B
D
G
p.276-277
枝
C
E
F
H
2
木とは
 あるノードから下に分岐した先のノードを子,分岐元の
ノードを親という.
 木の一番最初のノードを特に,根 (root) と呼び,子を持
たないノードを葉 (leaf) と呼ぶ.
根
A
親
子
D
B
親
子
C
E
F
子
G
H
葉
3
木とは
 あるノードから下につながった木構造の全体を部分木
(subtree) という.
 根からあるノードに到達するまでに通る枝の数をそのノー
ドの深さ (depth) といい,根から最も深いノードまでの
深さを木の高さ (height) という.
A
部分木
B
D
深さ 3
G
深さ 0
C
E
H
深さ 1
F
深さ 2
この木の高さ= 3
4
木とは
 ノードからの分岐が2以下の木を2分木 (binary tree) と呼
ぶ.
 親と子の関係が,ある規則 (大きい,小さいなど) に従っ
て並べられている木を2分探索木 (binary search tree) と
いう.
48
A
32
B
26
D
11
G
59
C
51
E
29
H
2分木
67
F
I
左の子 ≦ 親 ≦ 右の子
5
木とは
 木構造はきわめて重要で,多くの実用的対象が存在する.
 組織データやファイルシステムなどの階層構造の表現
 AI における論理的関係性の表現
 探索を容易にする情報表現 (2分探索木など)
 ヒープ・ソート
 講義では,2分探索木およびヒープに重点を置いて解説す
る.
6
2分探索木の配列表現
 ノードに格納するデータが人名であるとき,名前を辞書順
に並べる2分探索木が作れる.
 全ての親と子の間に,左の子≦親≦右の子という関係が成
立する.
右側の子のノード番号
No.0
1
Machilda
No.1
3
2
No.2
Candy
No.3
4
No.4
6
Ann
No.6
5
Rolla
No.5
Emy
7
Nancy
No.7
Eluza
Lisa
このようにノード番号を管理すれば,配列に格納できる.
p.278-282
7
2分探索木の配列表現
 各ノードに格納する情報は,人名,左の子へのポインタ
(ノード番号),右の子へのポインタ (ノード番号) の3つで
ある.
 これを構造体にまとめ,配列にする.
struct tnode {
int left;
char name[64];
int right;
} a[100];
// 左部分木へのポインタ(番号)
// データ(人名)
// 右部分木へのポインタ(番号)
8
2分探索木の配列表現
 この配列を使って,2分探索木を表現できる.
 ただし,nil はポインタが指し示すものがないという意味
で,具体的には -1 などを使う.
a[p].left a[p].name a[p].right
0
1
2
3
No.0
1
Machilda
2
No.1
3
No.2
Candy
No.3
4
5
No.4
Ann
6
No.6
No.5
Emy
7
Nancy
No.7
Eluza
Lisa
Rolla
4
5
6
7
1 Machilda 2
3
Candy
4
5
Rolla
nil
nil
Ann
nil
6
Emy
7
nil
Nancy
nil
nil
Eluza
nil
nil
Lisa
nil
二分探索木の配列表現
9
2分探索木の配列表現
 2分探索木から key という目的データを探すには,root
から初めて,key がノードのデータより小さいときは左側
の木へ, 大きいときは右側の木へ探索すればよい.
実質的に二分探索 O(log n) の計算量
a[p].left a[p].name a[p].right
0
1
key が Eluza の場合
2
3
No.0
1
Machilda
2
No.1
3
No.2
Candy
No.3
4
5
No.4
Ann
6
No.6
No.5
Emy
7
Nancy
No.7
Eluza
Lisa
Rolla
4
5
6
7
1 Machilda 2
3
Candy
4
5
Rolla
nil
nil
Ann
nil
Emy
7
6
nil
Nancy
nil
nil
Eluza
nil
nil
Lisa
nil
二分探索木の配列表現
10
二分探索木の配列表現 (ソースコード)
 p.279
 このコードでは,2分探索木を初期化において与えてい
る.
 nil は -1 と定義 (プリプロセッサ).
 scanf() で key を読み込み,p を 0 (根ノード) で初期
化.
 p が nil にならない限り続けるループに入る.
 strcmp() を使って,今見ているノード p の名前欄と
key を照合し,合致していたら発見したことを表示して
ループを抜ける.
 もし key の方が小さいときは,p を a[p].left に.
 逆の場合は p を a[p].right にする.
11
2分探索木へのデータの追加
 先程の例では,最初から 2分探索木ができていたが,これ
を作るアルゴリズムが必要
 すでに存在する 2分探索木に対して,新しいノードを追加
する手順は,以下の 2段階を踏む
 新ノードの親となるノードを探索
 新ノードを親ノードに接続
 現在調べているノードの番号を視点 p とし,この視点に一
つ遅れで追従する視点 old も用意する
 p を進めていき,p が nil になったとき,old が指し示し
ているノードが新ノードの親
12
2分探索木へのデータの追加
 p=0;
while (p!=nil){
old=p;

if (strcmp(key,a[p].name)<=0)
p=a[p].left;

else
p=a[p].right;

}





a[sp].left=a[sp].right=nil;
strcpy(a[sp].name,key);
if (strcmp(key,a[old].name)<=0)
a[old].left=sp;
else
a[old].right=sp;
sp++;
sp: 3
4
5
key: “Nancy”
“Patie”
p: -1
p old
0
1
Machilda
1
-1
2
p old
2
Candy
-1
-1
3
-1
p old
3
-1
Rolla
Nancy
-1
4
4
-1
Patie
-1
13
2分探索木の動的表現
 リストの場合と同様,ツリーもノードが動的に追加される
データ構造である.
 従って,自己参照構造体としてノードを表現し,必要に応
じてメモリを動的に確保するのが適している.
struct tnode {
struct tnode *left; /* 左部分木へのポインタ */
char name[12];
/* 名前 */
struct tnode *right; /* 右部分木へのポインタ */
};
struct tnode *talloc(void)
{
return (struct tnode *)malloc(sizeof(struct tnode));
};
p.283-284
14
新規ノード作成手順 (コードを見つつ)
 配列表現の時と同様,(1) 新規ノードの親となるノードの
探索,(2) 新規ノードの生成と親ノードからの接続を行う.
Candy を追加
Emyを追加
 視点 p を root で初期化.
p:NULL
 p は新規データ dat の大小にした
p=p->left
がって木をたどり,old は一つ遅れ
p old
Machilda
で追従:old=p としてから p を
old->left=p
適切な方向へ進める
p old
p=p->right
Candy
 p が NULL なら old が親ノード
old->right=p
p
 p=talloc()としてデータ記入
Emy
 左右の子はいない:NULL に
 親ノードの名前と新ノードの名前の大小関係により,
親ノードの左右どちらかのポインタを p に向ける
15
2分探索木の動的表現 (ソースコード)
 まずはルートノード作成
root=talloc();
scanf("%s",root->name);
root->left=root->right=NULL;
root
Machilda
16
2分探索木の動的表現 (ソースコード)
 while (scanf("%s",dat)!=EOF){
 p=root;
 while (p!=NULL){
 old=p;
 if (strcmp(dat,p->name)<=0)
 p=p->left; ここから下,p は
else
「視点」ではなく,
 p=p->right; 「新規ノード位置」
}
を示す.
 p=talloc();
 strcpy(p->name,dat);
 p->left=p->right=NULL;
 if (strcmp(dat,old->name)<=0)
 old->left=p;
else
 old->right=p;
}
 第2ノード以降作成
(さきほどの手順)
root
dat: Rolla
Nancy
p: NULL
p old
Machilda
p old
Rolla
p
Nancy
17
2分探索木の再帰的表現
(集中して聞くこと)
 木構造は再帰的処理に向いている.例えば探索の場合…
 「rootノード (Machilda) 以下から Emy を探す」と
いう問題は,「root->left (Candy) 以下の部分木から
Emy を探す」という副問題に 分解 できる.
root
この中からの探索
この中か
らの探索
再帰の脱出条件:
発見 or
行き先が NULL (不在)
(例:Eluza)
Machilda
Rolla
Candy
Ann
p.285-288 (途中まで)
Emy
18
ノード追加の再帰的方法
(集中して聞くこと)
 既存の 2分探索木に新しいデータ w を含むノードを追加す
る処理を再帰的に記述することを考える.
 再帰関数のプロトタイプを以下の通りとする:
struct tnode *gentree(struct tnode *p, char w[])
 p:起点ノード (このノード以下が作業対象)
 w:追加するデータ (名前)
 その動作は条件により異なる:
 p が NULL ではないとき: ノード p の適切な方の子
ノードを起点として gentree()を再帰呼び出しし,
受け取ったポインタ p をそのまま返す (オウム返し)
 p が NULL のとき: 新規ノードを作り,w を格納,
作った新規ノードへのポインタを返す
19
ノード追加の再帰的方法
※ ここでは,データ Nancy を含むノードへの
ポインタを [Nancy] と表記する.
root
main()
root = gentree(root,”Emy”)
呼出
Nancy
[Nancy]
第 1引数が NULL 以外:
・適切な子を起点とする
再帰呼び出し
・第一引数をオウム返し
第 1引数が NULL :
・新規ノードを作成
・新規ノードへのポイ
ンタを返す
gentree([Nancy],”Emy”) 同じポインタを上書き
 元の構造を壊さない
[Nancy]->left =
gentree([Nancy]->left,”Emy”);
呼出
[Candy]
gentree([Candy],”Emy”) NULL
Candy
[Candy]->right =
gentree([Candy]->right,”Emy”);
呼出
Emy
[Emy]
gentree(NULL,”Emy”)
ノード作成
戻り値の利用方法
に注目
20
ノード追加の再帰的方法
同じ方法で,第一ノード (root ノード) 作成も
できるだろうか?
root
main()
root = gentree(root,”Nancy”)
呼出
[Nancy]
第 1引数が NULL 以外:
・適切な子を起点とする
再帰呼び出し
・第一引数をオウム返し
第 1引数が NULL :
・新規ノードを作成
・新規ノードへのポイ
ンタを返す
gentree(NULL,”Nancy”)
Nancy
・ノード作成
・作ったノードへのポインタを返す
第一ノード作成もそれ以降も
root = gentree(略) でOK!
 二分探索木の生成は,
root を NULL で初期化し,
root = gentree(略); を
繰り返せば全部できる!
void main(void)
{
char dat[12];
struct tnode *root;
}
root = NULL;
while (scanf("%s",dat)!= EOF){
root = gentree(root,dat);
}
21
ノード追加の再帰的方法 (ソース)
struct tnode *gentree(struct tnode *p, char *w)
{
if (p == NULL){
p = talloc();
新規ノード作成
strcpy(p->name, w);
p->left = p->right = NULL;
 p:新規ノード
}
else if (strcmp(w, p->name) < 0)  適切な子の選択
p->left = gentree(p->left, w);
再帰
else 子ポインタへの代入
再帰呼出
呼出
p->right = gentree(p->right, w);
return p;
 p:第一引数そのまま
}
ポインタを返す
(新ノードを作ったときはそれへのポインタ,そうでないときは
受け取った第一引数 p を (いじらずそのまま) オウム返し)
22
2分探索木のサーチ (再帰版)
while (printf("Search name -->"),
scanf("%s",key)!=EOF){
if ((p=search(root,key))!=NULL)
p=search(root,key))
printf(“%s
printf("%s が見つかりました\n”,p->name);
が見つかりました\n",p->name);
else
printf("見つかりません\n");
printf(“見つかりません\n”);
}
key: “Nancy”
“Rolla”
p
struct tnode *search(struct tnode *p,char *key)
struct tnode *search(struct tnode *p,char *key)
*p,char
*key)
{ struct tnode *search(struct tnode (p:
マチルダ)
{
(p:
ローラ)
(p: NULL)
{if (p==NULL || strcmp(key,p->name)==0)
if (p==NULL || strcmp(key,p->name)==0)
if
(p==NULL
return
p; || strcmp(key,p->name)==0)
return p;
return p;
if (strcmp(key,p->name)<0)
if (strcmp(key,p->name)<0)
if
(strcmp(key,p->name)<0)
return
search(p->left,key);
return search(p->left,key);
elsereturn search(p->left,key);
else
else
return search(p->right,key);
return search(p->right,key);
return search(p->right,key);
}
}
}
Machilda
p
Rolla
p: NULL
27
2分探索木のトラバーサル (深さ優先)
 トラバーサル (traversal):木の全てのノードを訪れるこ
と (木の走査) (例:大学組織データ内からの教員捜し)
 深さ優先探索:「左向きにとにかく進んで,端に来たら 1
つ前の親に戻って右に進む」という行動を繰り返す.
 再帰的に記述すると,関数 treewalk(p)は,以下のよう
な動作をすれば良い (p:起点ノードへのポインタ)
void treewalk(struct tnode *p)
{
 p が NULL なら戻る (終了条件)
if (p!=NULL){
 p の左の子を起点として再帰呼出
treewalk(p->left);
printf("%s\n",p->name);  p での作業 (ノード内容表示)
treewalk(p->right);
 p の右の子を起点として再帰呼出
}
}
28
2分探索木のトラバーサル (深さ優先)
 ノード内容表示の順序次第で,表示順序が変わる
 行きがけ順
void treewalk(struct tnode *p)
{
if (p!=NULL){
printf("%s\n",p->name);
treewalk(p->left);
treewalk(p->right);
}
}
p: NULL
p
Machilda
p
p
Emy
p
Rolla
p
Candy
画面表示
Lisa
Machilda
Emy
Candy
Lisa
Rolla
29
2分探索木のトラバーサル (深さ優先)
 ノード内容表示の順序次第で,表示順序が変わる
 通りがけ順
void treewalk(struct tnode *p)
{
if (p!=NULL){
treewalk(p->left);
printf("%s\n",p->name);
treewalk(p->right);
}
}
p: NULL
p
Machilda
p
p
Emy
p
Rolla
p
Candy
画面表示
アルファベット順 
Lisa
Candy
Emy
Lisa
Machilda
Rolla
30
レベルごとのトラバーサル (試験範囲外)
 木のレベル (深さ) ごとに,さらに同一レベルでは左から
右にノードをトラバーサルするには?
 配列 (TODOリスト) で実現 配列 q: G
BH
A
D
CF
E
copy
BH
D
CF
E
配列 w: G
レベル終了時に配列 w が空なら終了
A
レベル 1
B
C
E
D
G
レベル 2
F
H
※ 教科書でこの配列を「スタック」と呼んでいるのは間違い
レベル 3
レベル 4
31
ヒープ
 ヒープ (heap:山,堆積) とは,すべての親が必ず2つの
子を持つ (最後の要素は左の子だけでも良い) 完全2分木で,
どの親と子をとっても,親≦子になっている木を言う.
1
2
4
配列表現:
10
ノードを深さごとに
左から順に配列に格
納する
3
25
15
5
30
6
26
7
20
29
※ ヒープにおいては添え字は
0 でなく 1 から始める.
35
40
27
28
22
こうすると,子 s の親 p は,
p=s/2 で求まる.
1
2
3
4
5
6
7
8
9
10 11 12
10 25 15 30 26 20 29 35 40 27 28 22
例)11/2=5 (整数演算)
8
p.303-307
9 10
11
12
32
ヒープへのデータ追加 (上方移動)








既に存在するヒープへ新規データを追加することを考える.
その手順は以下の通りである:
(1) 新しいデータをヒープの最後の要素として格納する.
(2) その要素を子とする親のデータと比較し,親の方が大
きければ子と親を交換する.
(3) 次に,その親を子とする親 (おじいちゃん) との間で
比較を行い,必要があれば交換.これを,子の位置が根ま
で上がるか,親≦子になるまで繰り返す.
教科書 p.304 の図6.13を参照せよ.
この手順を上方移動 (shift up) という.
ソースコードは p.305.
33
ヒープへのデータ追加 (上方移動)
 (1) 新しいデータをヒープの最後の要素として格納する.
 (2) その要素を子とする親のデータと比較し,親の方が大きければ子と親
を交換する.
 (3) 次に,その親を子とする親 (おじいちゃん) との間で比較を行い,必
要があれば交換.これを,子の位置が根まで上がるか,親≦子になる
まで繰り返す.
1
10
25
12
4
30
12
25
35
9
30
12
15
5
26
8
3
2
6
20
7
29
s= 2
4
9
p= 1
4
2
34
ヒープの作成 (下方移動)
 上方移動では既存のヒープにデータを一つずつ追加することに
よりヒープを作成したが,既存の全データをそのまま2分木に
割り当ててから,ヒープとして整えるという方法がある.その
手順は以下の通り (教科書は分かりにくい):
(1) 子を持つ最後の親 (データ数が n のとき,n/2) i から開始し,
全ての親に対して逆順に (n/2, n/2-1, n/2-2, …, 1),以下の
(2)~(4) の手順を繰り返す.
(2) 親の番号を p とし,2つの子 (2p, 2p+1) のうちで小さい方を s
とする.(一番最後の親は,左側にしか子を持たない場合がある.
この場合は自動的に s = 2p とする.)
(3) 子 s の値が,親 p 以上なら,(1) のループで次へ.
(4) p と s の値を交換し,交換された子の方を親として (p = s, s
= 2p),(2) へ戻る.
35
ヒープの作成 (下方移動)
 図的に理解しよう.  (1) 子を持つ最後の親から開始してループ
(最初は 9/2 = 4 から)
 (2) 親の番号を p とし,2p と 2p+1 で小
さい方の子を s とする.
 (3) s が p より大きいときは (1) ループで
次へ.
1
2
3
i= 4
p= 6
2
7
4
8
1
3
s= 12
7
14
4
8
16
6
3
7
1
2
4
2
4
3
2
3
4
8
4
9
1
 (4) p と s を交換し,p=s, s=2p とする.
1
7
6
8
2
5
9
5
7
6
6
s>n なら (1) ループで次へ.
s<=n なら (2) へ.
3
8
1
7
(2) から (4) の手順を
下方移動 (shift down)
と呼ぶ
ソースコード:p.307
36
ヒープ・ソート
 ヒープを用いたソーティングアルゴリズムがヒープ・ソー
トである.手順は大まかに以下の通り:
(1)与えられた数列からヒープを作成する.
(2)根ノードには最小値が来ているはずなので, これを取り外
す.その代わり,最後のノード (データ数 n なら n番の要
素) を切り離して根ノードに持ってくる.
(3)これにより残ったデータ (n-1個) のヒープ条件が狂った
ので,下方移動を使ってヒープの修正を行う. この場合,
根ノードからの1回の下方移動を行うだけでよい.
(4)(2) に戻る.
 教科書 p.308 の図6.15 を参照せよ.
p.308-311
37
ヒープ・ソート (ソースコード)
 p.309.
 関数 swap() は2つの配列要素の交換.
 main関数内,最初の whileループは初期ヒープの作成.
データを入れるごとに,入れたデータを最後尾に格納した
上で,上方移動を使ってヒープを整える作業を繰り返して
いる.
 2つめの whileループは,データを取り出すごとのループ.
 最初にルート (1番) と最後尾 (n番) を交換して,最後尾
をヒープから除外している (n--).
 次のループは,視点 p を根に初期化して下方移動を1回
行っている部分.
 p.311 では,初期ヒープ作成を下方移動で行っている.
38
第7章:グラフ
 リスト構造は,ノード群を鎖状につないだもので,分岐は
なかった.一方木構造は分岐はするが,分岐した枝同士は
分かれたまま.
 これらをより一般化したものがグラフ (graph) である.
グラフでは,ノード間の連結は任意であり,ループを含む
こともある.
6
2
5
4
1
8
7
3
p.331-333
39
グラフとは
 円で描いた部分はノード (node 節点,vertex 頂点ともい
う) であり,それを結んでいるのは エッジ (edge 辺,
branch 枝) である.ノード1と2はつながっているが,1
と3はつながっていない.
エッジ
ノード
6
2
5
4
1
8
7
3
40
グラフとは
 グラフをデータとして表現するときに使われるのは隣接行
列 (adjacency matrix) である.ノード i と j の間に接続
がある場合は 1,ない場合は 0 としておく.
2
4
1
3
5
6
教科書ではノード番号は1からとしている.
j
8
7
対角成分は,同じノードへの接続をあると
するかどうかに関係する (この場合はなし
としている).
1 2 3
4 5 6 7
8
1
0
1
0
0
0
0
0
0
2
1
0
1
1
0
0
0
0
3
i 4
5
0
1
0
0
0
0
1
0
0
1
0
0
1
0
0
0
0
0
0
1
0
1
0
0
6
0
0
0
0
1
0
1
1
7
8
0
0
1
0
0
1
0
1
0
0
0
0
0
1
1
0
41
グラフとは
 ここまでの例では,グラフのエッジには向きはなかった.
このようなものを無向グラフ (undirected graph) という.
 これに対して,「進める方向」のような向きを含んだグラ
フを有向グラフ (directed graph) という.この場合,隣
接行列は,ノード i から j へのエッジに対して,要素 (i,
j) は 1,要素 (j, i) は 0 としておく (p.333 図7.4).
6
2
5
4
1
8
7
3
42
深さ優先探索
 グラフのノードにデータが記載されている場合,この中か
ら特定のデータを探すとき,グラフの全てのノードを巡る
というアルゴリズムが必要となる.
 その一つは 深さ優先探索 (depth first search, 縦型探索
とも呼ばれる) である.
(1)始点を出発し,ノード番号の若い順に進む位置を調べ,い
けるところ (エッジで連結されていてまだ訪問していない
ノード) まで進む.
(2)行き場所がなくなったら,まだいける方向の残っている分
岐点まで戻り,再びいけるところまで進む.
(3)行き場所が全てなくなったら終了 (来た道を戻る).
p.334-338
43
深さ優先探索
 どのように探索が進むかを図で見てみよう.
(1) 始点を出発し,ノード番号の若い順に進む位置を調べ,いけるところ
(エッジで連結されていてまだ訪問していないノード) まで進む.
(2) 行き場所がなくなったら,まだいける方向の残っている分岐点まで戻
り,再びいけるところまで進む.
(3) 行き場所が全てなくなったら終了 (来た道を戻る).
2
4
1
3
5
6
8
7
44
深さ優先探索の再帰的解法
 深さ優先探索を再帰
的に行う関数は右の
通り.
 未探索の行き先を順
番に探して,行ける
ときは再帰呼び出し
でそこに進む.
 visit(1)を呼ぶと…
void visit(int i)
{
int j;
}
v[i]=1;
for (j=1;j<=N;j++){
if (a[i][j]==1 && v[j]==0){
printf("%d->%d ",i,j);
visit(j);
}
}
2
1
j= 2
4
j= 3
5
j= 4
6
j= 8
5
8
3
7
j= 7
j= 6
45
深さ優先探索 (ソースコード)
 2次元配列 a[][] によって隣接行列を記述 (初期化).これ
がグラフの実体である.
 配列 v[N+1] は「訪問済みフラグ」を意味する.これが 0
となっているノードは未訪問,1 なら訪問済み.
 main関数では訪問フラグの初期化の後,visit(1) を呼び
出しているだけ.
 関数 visit() は再帰的関数であり,引数 i は探索を開始
するノードの番号.要するに「ノード i から先を探索せ
よ」という関数.
 visit() では,まず起点ノード i の訪問済みフラグを立て
る (1にする).次に他の全てのノード j を調べ,ノード i
とつながっており (a[i][j]==1),未訪問 (v[j]==0) な
らば, ノード j を起点として探索 (再帰呼出 visit(j)).
46
深さ優先探索の応用
 グラフ上の各ノードを起点として深さ優先探索を繰り返す
場合は,各探索の前に訪問済みフラグを下ろして置かなく
てはならない.
 p.337 図7.6 のように,ノード群が分かれているグラフを
非連結グラフと呼ぶ.これがどのように分かれているかを
調べるには,訪問済みフラグのリセットをせずに,各ノー
ドを起点とする探索を繰り返せばよい.
 探索の起点となるノードが訪問済みの場合は次のノードへ
進む.訪問済みでないときは,カウントを1つ進める.
 これにより,カウント数=分かれている集団の数となる.
47
幅優先探索
 幅優先探索 (breadth first search,横型探索とも言う) で
は,「これから進まなくてはならないノードのメモ」を
キューの中に保存し,それを参照しながら探索を進める.
(1)始点をキューに入れる.
(2)キューからノードを取り出し,そのノードに連結している
未訪問のノードを全てキューに入れておく.
(3)キューが空なら終了,そうでないときは (2) へ.
 これにより,始点から近いノードから順序よく探索が進め
られる.
p.339-342
48
幅優先探索
(1)始点をキューに入れる.
(2)キューからノードを取り出し, 訪問済みとする. そのノー
ドに連結している未訪問ノードを全てキューに入れておく.
(3)キューが空なら終了,そうでないときは (2) へ.
ソースコード:p.340-341
6
2
5
4
1
8
7
3
キュー:
1
2
3
4
5
7
6
8
49
トポロジカル・ソート
 グラフを見ると,2から仕事を始めたとき,4や7ができる
ためには,1, 3 が出来ていなければならないのと同時
に,右側では 5, 6, 8 が出来ていなければならない.
教科書に誤植
 つまり,2, 1, 3, 4, 7 という系列と 2, 5, 4, 6, 8, 7 とい
う系列がある.従って,2の次に行いうる仕事,1と5では
どちらを先に行っても良い.このように必ずしも順序を比
較できない場合がある順序関係を半順序関係 (partial
order) という.
6
2
5
1
3
p.343-345
8
4
7
50
トポロジカル・ソート
 半順序関係が与えられたデータに対して,最初に行う仕事
から最後に行う仕事を1列に並べることをトポロジカル・
ソート (topological sort) という.
 どちらを先にやっても良い部分が含まれているので,解は
1つとは限らず,どれかが得られればよい.
2
5
6
8
4
1
3
7
51
トポロジカル・ソート
 有向グラフに対する深さ優先探索を繰り返し実行し,行き
着いたところから来た道を戻ろうとするときに,そのノー
ドを記録していけばよい.
 記録は「最後に行き着く場所」から順に行われるので逆
順.
2
6
5
8
4
1
7
3
2
5
6
8
1
3
7
4
52
Euler の一筆書き
 辺を消しながら深さ優先探索を行い,出発点に戻った
時に全ての経路が消えていれば,そのとき通った経路
が求める経路である
スタック v[n]
1
1
2
3
p.348-350
4
1
2
4
3
2
1
4
ソースコード:p.350
ネットワーク
 各辺に重みが定義されているグラフを 重み付きグラフ
あるいは ネットワーク (network) と呼ぶ.
 重みの意味は,例えば道路網については道路の長さであっ
たり,水道網なら流量だったりである (p.359 図7.22).
1 2 3 4 5 6 7 8
 データの実体は,重み行列 で
1 0 1 7 2 M M M M
記述する.
e
2
b
1
7
a
1
4
f
2
6
d
2
3
c
5
2
g
h
2
1
0
M
M
2
4
M
M
3
7
M
0
M
M
2
3
M
4
2
M
M
0
M
M
5
M
5
M
2
M
M
0
1
M
M
6
M
4
2
M
1
0
M
6
7
M
M
3
5
M
M
0
2
8
M
M
M
M
M
6
2
0
(M:無限大)
54
最短路問題 (Dijkstra のアルゴリズム)
解きたい問題:ノード a から h までの最短距離を求める
全てのノードの確定フラグを下ろし, 距離値を無限大で初期化
スタートノードの距離値を 0 とする.(3), (4) を N 回繰り返す
距離値最小の未確定ノードの番号を p とし,確定フラグを立てる
p につながる全ノードについて,p から進んだときの距離値が現在の
距離値より小さくなるとき,そのノードの距離値を更新する
 あるノードの距離値を更新するとき,
M
3
距離値だけでなく「この距離値に
M 2
1
e
なる場合,直前のノードはこれ」と
1
b
4
いう「手前ノード情報」も記録する
M
5
4
1
f
 距離だけでなく経路も求まる
2

(1)
(2)
(3)
(4)
a
M 7
0
2
c
p.353-359
6
d 7
M
6
M
2
3
5
2
g
10
M
9
h
M
7
55
最短路問題の例
 障害物環境下での移動ロボットのナビゲーション
 コンフィギュレーション障害物の頂点をサブゴールとし,
可視グラフ (visibility graph) を構築,最短路を探索
sub-goal
obstacle
start
移動ロボットのナビゲーション
を主にやってるとこ:城間研
obstacle
goal
configuration obstacle
56
講義のおわりに
 本講義では,知能システム工学科の趣旨に則って,メカと
情報の融合分野を扱う上でのアルゴリズム・データ構造を
実践的に (ソース・コードをベースとして) 解説した.
 情報工学科における同名の講義とは異なり,理論的な議論
はほとんど行っておらず,情報に興味のある学生にとって
は不十分である.
 一方,組み込みシステムなど,ハードウェアに近いプログ
ラミングでは,マイコンや電子回路など,多くのハード
ウェアに関する知識が必要となる.
 それらについては,各自が必要に応じてより深く勉強する
こと.本講義は基礎の基礎しか扱っていない.
 講義終了後もアルゴリズム・データ構造に関する質問は随
時受け付ける.
57