SlideShare a Scribd company logo
1
Algorithm
速いアルゴリズムを書くための基
礎
2
このスライドは
発表内容のすべてをカバーしているわ
けではありません。
あらかじめご了承ください。
3
アルゴリズムの評価
計算量、オーダ
基本のソート
バブルソート、選択ソート、挿入ソート
再帰木
再帰を使ったソート
クイックソート、マージソート
アルゴリズムのアイディア
グリーディ法、ナップサック問題
4
アルゴリズムの評価
計算量、オーダ
基本のソート
バブルソート、選択ソート、挿入ソート
再帰木
再帰を使ったソート
クイックソート、マージソート
アルゴリズムのアイディア
グリーディ法、ナップサック問題
5
アルゴリズムの評価
計算量
時間計算量
 最良時間計算量
 最悪時間計算量
 平均時間計算量
領域計算量
6
アルゴリズムの評価
漸近的な時間計算量
入力サイズ n が非常に大きい場合を想定
した計算量
オーダ記法
T(n): 時間計算量
T(n) = O(f(n))
⇔ ∃m > 0, c > 0, ∀n > m, T(n) ≦ c f(n)
7
アルゴリズムの評価
T(n) = 2 n8
+ 3 n + 1
m = 100, c = 3 とすれば
∀ n > m, T(n) < c n8
(= 3 n8
)
T(n) = O(n8
)
8
アルゴリズムの評価
T(n) = 4 n3
+ 5 n + 1
⇒ T(n) = O(n3
)
T(n) = 5 n + 4
⇒ T(n) = O(n)
T(n) = 2 n log n + 5 n + 8
⇒ T(n) = O(n log n)
9
アルゴリズムの評価
$numbers という数の配列の中に
同じ数があればそのインデックスを
出力するプログラム
10
<?php
$n = count($numbers);
for ($i = 0; $i < $n - 1; ++$i) {
for ($j = $i + 1; $j < $n; ++$j) {
if ($numbers[$i] == $numbers[$j]) {
echo "{$i} and {$j}n";
}
}
}
• 外の for : (n – 1)
回
• 内の for : (n – i)
回
• if : O(1)
O(1) × {(n – 1) + (n – 2) + … + 1}
= O(1) × n(n – 1) / 2
= O(n2
)
11
アルゴリズムの評価
オーダ記法の注意点
同じ n でも n と 1000 n では大きく異
なる
1000 n と n2
では
後者のほうがオーダが大きいが
1000 未満 の入力しか扱わない場合は
後者の方が速い
12
アルゴリズムの評価
計算量、オーダ
基本のソート
バブルソート、選択ソート、挿入ソート
再帰木
再帰を使ったソート
クイックソート、マージソート
アルゴリズムのアイディア
グリーディ法、ナップサック問題
13
基本のソート
バブルソート
 隣り合う要素を順に
比較して入れ替える
 最悪計算量 : O(n2
)
2 4 1 3
2 4 1 3
2 1 4 3
2 1 3 4
1 2 3 4
1 2 3 4
14
基本のソート
選択ソート
 最小または最大のも
のから順に
選択して並べるソー
ト
 最悪計算量 : O(n2
)
 交換回数 : n – 1
 比較回数 : n(n + 1) /
2
2 4 1 3
2 3 1 4
2 1 3 4
1 2 3 4
15
$a = new array( ……… ); $n = count($a);
for ($i = $n - 1; $i > 0; --$i) {
$max = $a[0];
$maxIndex = 0;
for ($j = 1; $j <= $i; ++$j) {
if ($a[$j] >= $max) {
$max = $a[$j];
$maxIndex = $j;
}
}
$t = $a[$i];
$a[$i] = $a[$maxIndex];
$a[$maxIndex] = $t;
}
16
基本のソート
挿入ソート
 それぞれの要素が挿
入される場所を探し
て挿入するソート
 最悪計算量 : O(n2
)
 平均計算量 : O(n2
)
 最良計算量 : O(n)
2 4 1 3
2 4 1 3
1 2 4 3
1 2 3 4
17
$a = new array( ……… );
$n = count($a);
for ($i = 0; $i < $n; ++$i) {
$c = $a[$i];
$j = $i;
while (($a[$j - 1] > $c) && ($j > 0)) {
$a[$j] = $a[$j - 1];
--$j;
}
$a[$j] = $c;
}
18
基本のソート
基本的な実装では、
バブルソートと選択ソートが必ず O(n2
)
の計算量を要するのに対し、
挿入ソートでは条件によって O(n) の
計算量になる
その他
ヒープソート
19
アルゴリズムの評価
計算量、オーダ
基本のソート
バブルソート、選択ソート、挿入ソート
再帰木
再帰を使ったソート
クイックソート、マージソート
アルゴリズムのアイディア
グリーディ法、ナップサック問題
20
再帰木
再帰アルゴリズムを可視化して
計算量を求めるための便利な方法
例
配列の要素を加算する
21
a = [9, 3, 4, 5, 7, 8]
def sum(a)
if a.length == 1
return a[0]
else
return a.pop + sum(a)
end
end
T(n) = T(n – 1) + c (n > 1)
T(n) = c (n = 1)
22
c
T(n-1) c
T(n-2)
c
c
T(n-3)
c
c
c
c
c
c
T(n) = T(n – 1) + c T(n - 1) = T(n – 2) + c
T(n - 2) = T(n - 1) + c n 個の c
T(n) = cn
23
再帰木
分岐を含む例
配列内要素を前半分と後ろ半分に分けて
合計したものを合算して、総合計を計算す
る
簡単にするために、 n = 2m
(m N)∈ の場合
を考える
24
a = [9, 3, 4, 5, 7, 8]
def sum(a)
n = a.length
if n == 1
return a[0]
else
formar = a[0..((n / 2) - 1)]
latter = a[(n / 2)..n]
return sum(formar) + sum(latter)
end
end
T(n) = 2T(n / 2) + c (n > 1)
T(n) = c (n = 1)
25
c
T(n / 2)
T(n) = 2T(n / 2) + c T(n / 2) = 2T(n / 4) + c
T(n) = c (1 + 2 + … + 2m
)
T(n) = c(1 + 2 + … + 2m
)
= c(2m + 1
– 1)
= c(2n – 1)
= O(n)
T(n / 2)
c
T(n / 4) T(n / 4)
c c
T(n / 4) T(n / 4)
c
c c
c c
c c
T(n / 2m
) T(n / 2m
)
26
アルゴリズムの評価
計算量、オーダ
基本のソート
バブルソート、選択ソート、挿入ソート
再帰木
再帰を使ったソート
クイックソート、マージソート
アルゴリズムのアイディア
グリーディ法、ナップサック問題
27
再帰を使ったソート
クイックソート
マージソート
28
再帰を使ったソート
クイックソート
1. 要素がひとつなら終了
2. 基準値を選ぶ
3. 各データを基準値と比較して
基準値未満と基準値以上の 2 つのグルー
プに分ける
4. できた 2 つのグループを再帰的にソート
する
5. ソート済みの 2 つのグループを、
基準値未満、基準値、基準値以上の順で
連結する
29
# quicksort(data, 0, n - 1) で全体のソート
def quicksort(data, left, right)
# left から right までの間で基準値のインデックスを選び、
# 基準値の右側に基準値以上の値を
# 左側に基準値未満の値を配置する。
key_index = partition(data, left, right)
quicksort(data, left, key_index - 1)
quicksort(data, key_index + 1, right)
end
def partition(data, left. right)
基準値の決定 ;
基準値未満は左へ ; 基準値以上は右へ
end
30
再帰を使ったソート
クイックソート
 partition の計算量
 def partition(data, left, right)
基準値の決定 ;
基準値未満は左へ ; 基準値以上は右へ
end
 要素数を n とすると、 要素の数だけ入れ替
えが行われます。 そのほかの処理は n に応
じて変化するものではないので 計算量は
O(n) になります。
 この部分について Tsub(n) = cn
31
再帰を使ったソート
クイックソート
 全体の計算量
 関数 partition は cn 。
 T(n) = cn + T(nleft) + T(nright)
32
cn
T(n-1) c(n-1)
T(n-2)
cn
c(n-2)
c(n–1)
cn
T(n) = cn +T(n – 1) T(n - 1) = c(n – 1) + T(n –
2)
高さ :
n
最悪の場合 : 基準値によって分割できない場合
c
2c
3c
T(n) = cn(n – 1) / 2
33
T(n) = cn + 2T(n /
2)
T(n / 2) = cn / 2 + 2T(n / 4)
高さ : log2n
最良の場合 : 基準値によって等しく分割される場合
cn
T(n / 2) T(n / 2)
cn
T(n / 4) T(n / 4)
cn/2 cn/2
T(n / 4) T(n / 4)
cn
cn/2 cn/2
c
2c
c c
2c
c c
2c
c c
2c
c c
2c
c c
2c
c c
2c
c c
2c
c
34
再帰を使ったソート
クイックソート
 最良時間計算量
 再帰木
 高さ : log2n
 各階層の計算量の和 : cn
 T(n) = cn log n = O(n log n)
 平均時間計算量
 基準値を選ぶ場合に、それぞれの要素が基準
値になる確率をすべて等しいとした場合の計
算量
 O(n log n)
35
再帰を使ったソート
クイックソート
 ポイント
 2 分割することで計算量を大幅に削減してい
る
36
再帰を使ったソート
マージソート
 配列を単純に分割して、
組み合わせるときに
順序を考慮して組み合わせる
37
(17, 39, 1, 9, 5, 24, 2, 11, 23, 6)
(17, 39, 1, 9, 5), (24, 2, 11, 23, 6)
(17, 39), (1, 9, 5), (24, 2), (11, 23, 6)
(17), (39), (1), (9, 5), (24), (2), (11), (23, 6)
(17), (39), (1), (9), (5), (24), (2), (11), (23), (6)
(17), (39), (1), (5, 9), (24), (2), (11), (6, 23)
(17, 39), (1, 5, 9), (2, 24), (6, 11, 23)
(1, 5, 9, 17, 39), (2, 6, 11, 23, 24)
(1, 2, 5, 9, 11, 17, 23, 24, 39)
38
def mergesort(data, left, right)
mid = (left + right) / 2 # 中央の index 取得
if left < mid # 左部分を ( 要素があれば ) 再帰ソート
mergesort(data, left, mid); end
if mid + 1 < right # 右部分を ( 要素があれば ) 再帰ソート
mergesort(data, left, mid); end
merge(data, left, mid, right) # 左と中央と右を結合
end
def merge(data, left, mid, right)
temp = Array.new(data.length)
a = left; b = mid + 1 # 左と右のグループのインデックス初期化
for i in 0..(right - left)
if a == mid + 1; temp[i] = data[b]; b += 1
else if b == right + 1; temp[i] = data[a]; a += 1
else if data[a] > data[b]; temp[i] = data[a]; a += 1
else temp[i] = data[b]; b += 1
end
end
for i in left..right
data[i] = temp[i] # 生成した配列をコピー
end
end
39
再帰を使ったソート
マージソート
 merge の計算量
 O(n)
 全体の計算量
 T(n) = 2T(n / 2) + cn
 クイックソートの最良時間計算量と同じ cn
log2 n
 T(n) = O(n log n)
 どのような初期状態でも要素数が同じなら時
間計算量も同じになる。
40
再帰を使ったソート
マージソートの時間計算量はクイック
ソートの時間計算量と同じになるが、
実際の実行時間はクイックソートのほ
うが短くなることが多い。
41
再帰を使ったソート
マージソート
 2 分割でなく 3 分割してマージを実行し
た場合の計算量
 T(n) = cn log3 n
 オーダ : O(n log n)
 n の増加に対する計算量の増加率は
2 分割した場合よりも低くなる。
42
アルゴリズムの評価
計算量、オーダ
基本のソート
バブルソート、選択ソート、挿入ソート
再帰木
再帰を使ったソート
クイックソート、マージソート
アルゴリズムのアイディア
グリーディ法、ナップサック問題
43
アルゴリズムのアイディア
グリーディ法 ( 貪欲法 )
 段階的に計算が進むアルゴリズムで
各段階においてその時点で最も得をする
選択をするアルゴリズム。
 比較的短時間で答えを求めることができ
る。
 得られた解が最適解でないこともある。
44
アルゴリズムのアイディア
グリーディ法 ( 貪欲法 )
 最適解が求められる例
 100 円、 50 円、 10 円、 5 円、 1 円の硬貨を
使って
x 円 を支払うときの、硬貨の合計枚数を最小
にするそれぞれの硬貨の枚数を計算する
 大きい硬貨から順に使うと最適解が得られる。
45
アルゴリズムのアイディア
グリーディ法 ( 貪欲法 )
 最適解が求められない例
 50 円、 40 円、 1 円の硬貨を使って
120 円 を支払うときの、硬貨の合計枚数を最
小にするそれぞれの硬貨の枚数を計算する
 大きい硬貨から順に使っても最適解は得られない
。
 最適解 : 40 円 ×3
46
アルゴリズムのアイディア
ナップサック問題
 重さの制限のある中で価値を最大にする
問題
 例
 1 から n まで番号のついた n 種類の荷物が
あり、 番号が i の荷物の重さ、価値はそれ
ぞれ wi,vi とする。 c までの重さなら貨物列
車に載せられるとき、貨物列車に載せる荷物
の価値が最大になるような荷物の組み合わせ
を求めよ。
47
アルゴリズムのアイディア
ナップサック問題
 分割ナップサック問題
 それぞれの荷物を分割して搭載することが可
能。 k 番目の荷物を 半分載せることができ
る。
 0-1 ナップサック問題
 それぞれの荷物を分割して搭載することは不
可能。
48
アルゴリズムのアイディア
ナップサック問題
 分割ナップサック問題
 グリーディ法で最適解が求められる。
 0-1 ナップサック問題
 理論上、総当たりでチェックをすれば最適解
が求められるが、 O(2n
) となり、 n が大き
い場合は現実的でない。
49
0-1 ナップサック問題
ナップサックに 5kg まで入るとき、
どのような組み合わせで入れると価値を最大にできるか。
種類 価格 重さ
1 個体 A 2000 円 2kg
2 個体 B 3000 円 1kg
3 個体 C 3000 円 4kg
4 個体 D 4000 円 5kg
50
各列は重さ (kg) 、各行は個体の番号を表す。
k 番目の個体まで入れるときに、ある重さ以下まで荷物を入れる場合の
最大の金額とその個体の集合を表にする。
0 1 2 3 4 5
1 A (0, []) (2000, [1])
2 B
3 C
4 D
1 番目の個体 A を
2kg を超えない範囲で入れた場合、
2000 円の金額になる。
51
0 1 2 3 4 5
1 A (0, []) (0, []) (2000, [1]) (2000, [1]) (2000,
[1])
(2000, [1])
2 B
3 C
4 D
k 番目 の固体を追加してその重さの和が w となる解は、次のうち価
値が高い方
• (k 1)− 番目以下の固体のみで重さの和が w 以下 となる入れ方
• (k 1)− 番目以下の固体のみで重さの和が w − wk 以下となる入れ方に
k 番目 の固体を追加する入れ方
52
0 1 2 3 4 5
1 A (0, []) (0, []) (2000, [1]) (2000, [1]) (2000, [1]) (2000, [1])
2 B (0, []) (3000, [2]) (3000, [2]) (5000, [1, 2]) (5000, [1, 2]) (5000, [1, 2])
3 C
4 D
2 番目 の固体を追加してその重さの和が w となる解は、次のうち価値
が高い方
• 1 番目以下の固体のみで重さの和が w 以下 となる入れ方
• 1 番目以下の固体のみで重さの和が w 1− 以下となる入れ方に 2 番
目 の固体を追加する入れ方
53
0 1 2 3 4 5
1 A (0, []) (0, []) (2000, [1]) (2000, [1]) (2000, [1]) (2000, [1])
2 B (0, []) (3000, [2]) (3000, [2]) (5000, [1, 2]) (5000, [1, 2]) (5000, [1, 2])
3 C (0, []) (3000, [2]) (3000, [2]) (5000, [1, 2]) (5000, [1, 2]) (6000, [2, 3])
4 D (0, []) (3000, [2]) (3000, [2]) (5000, [1, 2]) (5000, [1, 2]) (6000, [2, 3])
種類 価格 重さ
1 個体 A 2000 円 2kg
2 個体 B 3000 円 1kg
3 個体 C 3000 円 4kg
4 個体 D 4000 円 5kg
54
コードのイメージ
# w : 重さの配列 ; v : 価値の配列 ; c : 容量
# t : テーブルの値 ( 二次元配列 ) で すべてを [0, {}] で初期化
# t はアルゴリズムの実行のために、 0 行目も作成
for i in 1..n
for j in 1..c
if j > w[i]
if t[i - 1][j - w[i]][0] + w[i] > t[i - 1][j][0]
t[i][j] = [
# i 番目 の荷物を足したもの
t[i - 1][j - w[i]][0] + w[i],
t[i - 1][j - w[i]][1] + v[i],
]
else
t[i][j] = [t[i - 1][j][0], t[i - 1][j][1]]
end
end
end
end
55
コードのイメージ
# w : 重さの配列 ; v : 価値の配列 ; c : 容量
# t : テーブルの値 ( 二次元配列 ) で すべてを [0, {}] で初期化
# t はアルゴリズムの実行のために、 0 行目も作成
for i in 1..n
for j in 1..c
if j > w[i]
if t[i - 1][j - w[i]][0] + w[i] > t[i - 1][j][0]
t[i][j] = [
# i 番目 の荷物を足したもの
t[i - 1][j - w[i]][0] + w[i],
t[i - 1][j - w[i]][1] + v[i],
]
else
t[i][j] = [t[i - 1][j][0], t[i - 1][j][1]]
end
end
end
end
O(n) のオーダになり、
全パターンをチェックするより高速
。
56
コードのイメージ
# w : 重さの配列 ; v : 価値の配列 ; c : 容量
# t : テーブルの値 ( 二次元配列 ) で すべてを [0, {}] で初期化
# t はアルゴリズムの実行のために、 0 行目も作成
for i in 1..n
for j in 1..c
if j > w[i]
if t[i - 1][j - w[i]][0] + w[i] > t[i - 1][j][0]
t[i][j] = [
# i 番目 の荷物を足したもの
t[i - 1][j - w[i]][0] + w[i],
t[i - 1][j - w[i]][1] + v[i],
]
else
t[i][j] = [t[i - 1][j][0], t[i - 1][j][1]]
end
end
end
end
ただし、すべての荷物の最大公約数をステップと
して
現実的なループを実行できることが条件となる。
57
アルゴリズムのアイディア
動的計画法
 0-1 ナップサック問題で見たように
問題をいくつかの部分問題に分割して解
くというアイディアに加えて、
部分問題について求めた解を記録して
同じ部分問題が出たときに
記録しておいた解を再利用することによ
り
再計算の時間を節約する手法。
58
アルゴリズムのアイディア
バックトラック法 + 分枝限定法
 問題の解を解くために木を作り、分岐を
効率的に調べる方法で、すべての分岐を
調べることはせず、ある条件に基づいて
調べる分岐を制限する。
 コンピュータ将棋などで使用されている
。
59
0-1 ナップサック問題
各荷物について、入れる (0) 、入れない (1) のパターンでツリーを作る
。
途中で枝狩りを行って捜査するパターンを減らす。
root
0 0
0 1
1 1
0
0
1 0
1
1 0
0
1 0
1
1 0
0
1 0
1
1 0
0
1 0
1
1
60
枝狩りのルール
• 荷物の総和が容量を超えたら列挙操作を打ち切り
• 残るすべての荷物を入れても容量を超えなければ、全て積載したもの
が解
• 残る荷物について分割ナップサック問題として解いたときの価値の和
が暫定解より小さければ枝狩り
root
0 0
0 1
1 1
0
0
1 0
1
1 0
0
1 0
1
1 0
0
1 0
1
1 0
0
1 0
1
1
61
root
0 0
0 1
1 1
0
1
1
0
0
1
1
0
0
1
1
分割ナップサック問題として解いても
暫定解を超えられないので枝狩り
容量がいっぱい
容量オー
バー
容量オーバー
すべて追加しても OK なの
で
すべて追加したものが
暫定解 (4000 円 )
62
コードのイメージ
# w : 荷物の重さの配列 ; v : 価値の配列 ; n : 荷物の総数
z = 0
def solve(level)
if level > n # 列挙木の葉に相当する処理の場合
aw = 重さの総和 ; av = 価値の総和
if aw <= c && av > z
z = av # 暫定解を更新
end
else
if 枝狩り条件 1 が成立しない
if 枝狩り条件 2 が成立する
av = 残るすべての荷物を積載した場合の価値の総和
if av > z; z = av; end
else if 枝狩り条件 3 が成立する
x[level] = 0; solve(level + 1);
x[level] = 1; solve(level + 1);
end
end
end
解の候補を出力
end
63
アルゴリズムのアイディア
バックトラック法 + 分枝限定法
 最悪の場合の計算量は
n 個の荷物に対して O(2n
) 。
 実際の実行時間は列挙を行う順序や
枝狩りの条件によって大きく変化する。
64
(アルゴリズムとデータ構造 藤原 暁宏)
アルゴリズムとデータ構造 (石畑 清)
プログラミングコンテストチャレンジブック
参考書籍

More Related Content

Algorithm 速いアルゴリズムを書くための基礎