再帰関数

再帰関数

次のような問題を考えます。

[問題]

整数 k に対して、レベル k の AB文字列 を次のように定義します。

・レベル 0 の AB 文字列は "B" です。

・レベル k の AB 文字列は "A" + (レベル k - 1 の AB 文字列) + "B" + (レベル k - 1 の AB 文字列) + "A" です。

 例 : レベル 2 の ABC 文字列は "A" + "ABBBA" + "B" + "ABBBA" + "A" = "AABBBABABBBAA" となります。

レベル k の AB 文字列の x 文字目 までに "B" が何個あるかを求めてください。

[条件]

・レベル k は、1以上 50以下の整数

・x(何文字目) は、1以上 レベル kのAB文字列の長さ以下の整数

解法・ソースコード

まず レベル k のAB文字列の長さは 2の k 乗 以上になり k ≦ 50 という条件を考慮すると、単純に文字列を生成する方法では処理が終わりません。

そこで 再帰関数 を使った解法を考えてみます。

問題をよく読むと、レベル k の AB文字列は レベル k - 1 の AB文字列によって定義されていることがわかります。

つまり、AB文字列は 1つ前のAB文字列 によって再帰的に定義されています。

レベル k の AB文字列の長さを len_s[k]、レベル k の AB文字列に含まれる文字列”B”の個数を cnt_b[k]とします。

このとき、レベル k の AB文字列を表す文字列の先頭から x文字目 に含まれる 文字列”B”の数 は次のように考えることができます。

  • x=1のとき
    0個
  • x=2 ~ len_s[k-1]+1のとき
    ひとつ前の文字列レベルの先頭から、x-1文字列の中に含まれる文字列”B”の個数
  • x=len_s[k-1]+2のとき
    ひとつ前の文字列レベルの文字列”B”の個数+1
  • x=len_s[k-1]+3 ~ 2 * len_s[k-1]+2のとき
    ひとつ前の文字列レベルの先頭から、x-len_s[k-1]-2文字の中に含まれる文字列”B”の個数 + cnt_b[k-1] + 1個
  • x=2*len_s[k-1]+3のとき
    cnt_b[k-1] * 2 + 1個

以上により、レベル k の AB文字列レベル k-1 の AB文字列 に関する問題へと帰着することができました。

また、レベル k の AB文字列の長さ(len_s)文字列”B”の個数(cnt_b) は以下のように定義できます。

  • レベル k の AB文字列の長さ
    len_s[k] = len_s[k-1] * 2 + 3   (len_s[0] = 1)
  • 文字列”B”の個数
    cnt_b[k] = cnt_b[k-1] * 2 + 1   (cnt_b[0] = 1)

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#---------- 入力例 1 ---------
k = 2 # 文字列レベル k
x = 7 # 何文字目までの "B" をカウントするか
#---------- 入力例 2 ---------

# 再帰関数
def rec(k, x, len_s, cnt_b):
if k == 0: # 終了条件
return 1
# xに応じて処理分け
if x == 1:
return 0
elif x <= len_s[k - 1] + 1:
return rec(k - 1, x - 1, len_s, cnt_b)
elif x == len_s[k - 1] + 2:
return cnt_b[k - 1] + 1
elif x <= len_s[k - 1] * 2 + 2:
return rec(k - 1, x - len_s[k - 1] - 2, len_s, cnt_b) + cnt_b[k - 1] + 1
else:
return cnt_b[k - 1] * 2 + 1

len_s = [1] * (k + 1) # レベルごとの文字列の長さ
cnt_b = [1] * (k + 1) # レベルごとの文字列"B"の数
for i in range(1, k + 1):
len_s[i] = len_s[i - 1] * 2 + 3 # ひとつ前のレベル文字列を2倍し3を足したものが文字列数
cnt_b[i] = cnt_b[i - 1] * 2 + 1 # ひとつ前のレベル文字列を2倍し1を足したものが文字列"B"の数

# 再帰的に求める
print(rec(k, x, len_s, cnt_b))

[実行結果]

4

文字列レベル2の文字列は “AABBBABABBBAA” であり、この文字列の先頭から7文字の中に 文字列”B”4個 含まれます。

パリティ②

パリティ

操作に関する問題は、決まったパターンを持たず 柔軟な思考 を必要とするものが多くあります。

しかしながら、次のように問題を解くための指針となる考え方があります。

  • 操作をわかりやすく言い換える。
  • 操作によって作れるものを特徴付ける。

[問題]

N個の整数が一列に並んでいます。

この整数列に対して次の操作を好きなだけ行うことができます。

「隣り合う2つの整数を選んで、両方の整数に-1を掛ける」

操作終了後のN個の整数の 総和 として考えられる最大値を求めて下さい。

解法・ソースコード

今回の操作を分類すると、次のいずれかになります。

  • 数列の「プラス」「プラス」が連続している箇所を「マイナス」「マイナス」にする。
  • 数列の「プラス」「マイナス」が連続している箇所を「マイナス」「プラス」にする。
  • 数列の「マイナス」「プラス」が連続している箇所を「プラス」「マイナス」にする。
  • 数列の「マイナス」「マイナス」が連続している箇所を「プラス」「プラス」にする。

上記により「マイナス」の個数は「2つ増える」「変わらない」「2つ減る」 かのいずれかであることが分かります。

つまり「マイナス」の個数の パリティ(偶数/奇数)操作の前後で不変 です。

実装は次のように行います。

  • 数列の中のマイナスの数値が 偶数個 ある場合
    マイナスの数値を全てなくして(絶対値をとって)、総和を算出する。
  • 数列の中のマイナスの数値が 奇数個 ある場合
    マイナスの数値を全てなくして(絶対値をとって)、総和を算出する。
    その総和から、絶対値が最小 である数値を 2回差し引く
    (総和にはすでに最小の数値が含まれているため 2回差し引く 必要があります。)

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#---------- 入力例 1 ---------
lst = [-10, 5, -4]
#---------- 入力例 2 ---------
# lst = [1, 2, -3, 10, 100]
#-----------------------------
# マイナスの個数
cnt_minus = sum(x < 0 for x in lst)

# N個の整数の絶対値の総和
total = sum(map(abs, lst))
# N個の整数の絶対値の最小値
abs_minus = min(map(abs, lst))

# マイナスの個数が[偶数の場合]は、[絶対値の総和]を出力。
# マイナスの個数が[奇数の場合]は、[絶対値の総和] - [絶対値の最小値×2]を出力。
print(total if abs_minus % 2 == 0 else total - abs_minus * 2)

[実行結果 (入力例 1)]

19

マイナスの数値の個数が 偶数 なので、すべての整数の 絶対値の総和 が解となります。

[実行結果 (入力例 2)]

114

マイナスの数値の個数が 奇数 なので、すべての整数の 絶対値の総和 から 絶対値の最小値×2 を引いた値が解となります。

パリティ①

パリティ

パリティ とは、偶数と奇数に関する性質 のことです。

パリティ に注目すると問題が簡単に解けることがあります。

[問題]

2次元平面上で旅をしようとしています。

時刻 0 に地点 (0, 0) を出発し、時刻 t に地点 (x, y) を訪れる予定です。

時刻 t に地点 (x, y) にいるときには、時刻 t + 1 には

地点 (x + 1, y)、(x - 1, y)、(x, y + 1)、(x, y - 1) のうちいずれかに

移動することができます。

その場にとどまることはできません。

入力は、時刻と地点(t, x, y)が複数与えられます。

入力の旅行プランが実行可能であれば "Yes"、不可能であれば "No" を出力してください。

解法・ソースコード

まず、不可能な場合を考えてみます。

例えば、時刻が1 のときは、1つしか移動できない ので地点(5,6)のような移動はできません。

次に時間は足りているのに移動できない状況は考えられるでしょうか。

例えば、時刻が3 のときには 地点(1, 1) への移動はできません。

これをよく考えてみる、パリティ(奇数か偶数か) によって移動できる地点とできない地点が 交互 になっていることに気づくことができます。

つまり、時刻のパリティ移動数のパリティ が同じであれば移動でき、パリティ が異なれば移動できないことになります。

以上を踏まえて、コードを書くと次のようになります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#---------- 入力例 1 ---------
plan = [(3, 1, 2), # 時刻 t、地点 (x, y)
(6, 1, 1)] # 時刻 t、地点 (x, y)
#---------- 入力例 2 ---------
#plan = [(3, 1, 2), # 時刻 t、地点 (x, y)
# (6, 2, 1)] # 時刻 t、地点 (x, y)
#---------- 入力例 3 ---------
#plan = [(3, 1, 2), # 時刻 t、地点 (x, y)
# (2, 1, 1)] # 時刻 t、地点 (x, y)
#-----------------------------
# 前回の時刻と座標
pt, px, py = 0, 0, 0 # 初期状態

for t, x, y in plan:
# 前回状態との差分
dt, dx, dy = t - pt, abs(x - px), abs(y - py)
# 時間が足りない場合は移動不可
if dt < dx + dy:
print('No (時間が足りない)')
break
# パリティが合わないときは移動不可
if dt % 2 != (dx + dy) % 2:
print('No (パリティが合わない)')
break
# 前回状態を更新
pt, px, py = t, x, y
else:
print('Yes (移動可能)')

[実行結果 (入力例 1)]

Yes (移動可能)

入力例 1 では、時間が足りておりパリティもあっている ので移動が可能です。

[実行結果 (入力例 2)]

No (パリティが合わない)

入力例 2 では、時間は足りていますが パリティ があっていないので移動できません。

[実行結果 (入力例 3)]

No (時間が足りない)

入力例 3 では、時間が足りていない ため移動できません。

ダイクストラ法+ヒープキュー+経路表示(最短経路問題)

ダイクストラ法+ヒープキュー+経路表示

前回記事では、ダイクストラ法ヒープキュー を使って最短経路問題の効率を上げて解きました。

今回は、さらに 最短経路 を表示します。

グラフは前回同様のものを使います。

[グラフ]

ソースコード

経路を表示するのはとても簡単です。

前回記事では ヒープキューコスト頂点名 を設定していましたが、さらに 経路 を設定するだけです。

heappush関数 の引数が3つになっています。(22行目、40行目)

(入力データの形式は前回記事と同様です)

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#---------- 入力例 ----------
nodes = { # ノード情報
'Start':{'edge':{'B':4, 'C':3}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':0}, # 頂点Startまでの最小コスト
'B': {'edge':{'C':1, 'D':1, 'E':5}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Bまでの最小コスト
'C': {'edge':{'F':2}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Cまでの最小コスト
'D': {'edge':{'E':3}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Dまでの最小コスト
'E': {'edge':{'Goal':2}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Eまでの最小コスト
'F': {'edge':{'E':1, 'Goal':4}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Fまでの最小コスト
'Goal': {'edge':{}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')} # 頂点Goalまでの最小コスト
}
#----------------------------
import heapq

q = []
heapq.heappush(q, (0, 'Start', 'Start')) # スタート位置である頂点の(コスト,頂点名、経路)をヒープキューに追加

# ダイクストラ法(最もコストの小さい頂点を順番に確認していく方法)
while len(q): # キューがある限り実行
# 最もコストが小さい頂点名をヒープキューから取得
d = heapq.heappop(q) # コスト,頂点名,経路が返ってくる
min_node_name = d[1] # 頂点名
path = d[2] # 経路
if min_node_name == 'Goal': # ゴール位置の頂点だった場合
print('解:最小コストは', nodes['Goal']['cost']) # 最小コストを表示して終了
print('経路:', path)
break
# 最もコストが小さい頂点からの辺を繰り返しチェックする
for dst, cost in nodes[min_node_name]['edge'].items():
# 次の頂点までのコストが更新対象であれば更新
if nodes[dst]['cost'] > nodes[min_node_name]['cost'] + cost:
nodes[dst]['cost'] = nodes[min_node_name]['cost'] + cost
# コストを更新した頂点の(コスト,頂点名,経路)をヒープキューに追加
heapq.heappush(q, (nodes[dst]['cost'], dst, path + ' → ' + dst))

[実行結果]

解:最小コストは 8

経路: Start -> C -> F -> E -> Goal

最小コストの場合の、経路を表示 することができました。

ダイクストラ法+ヒープキュー(最短経路問題)

ダイクストラ法+ヒープキュー

前回記事では、ダイクストラ法 を使って最短経路問題を解きました。

今回は、ヒープキュー を使って処理をより効率的にしていきます。

グラフは前回同様のものを使います。

[グラフ]

ソースコード

ヒープキュー は、格納されているデータの中から 一番小さなもの を効率よく取り出せるキューです。

ダイクストラ法 の処理の中で、一番小さなコストの頂点を探すときに ヒープキュー(heapq) を使います。

(入力データの形式は前回記事と同様です)

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#---------- 入力例 ----------
nodes = { # ノード情報
'Start':{'edge':{'B':4, 'C':3}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':0}, # 頂点Startまでの最小コスト
'B': {'edge':{'C':1, 'D':1, 'E':5}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Bまでの最小コスト
'C': {'edge':{'F':2}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Cまでの最小コスト
'D': {'edge':{'E':3}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Dまでの最小コスト
'E': {'edge':{'Goal':2}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Eまでの最小コスト
'F': {'edge':{'E':1, 'Goal':4}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Fまでの最小コスト
'Goal': {'edge':{}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')} # 頂点Goalまでの最小コスト
}
#----------------------------
import heapq

q = []
heapq.heappush(q, (0, 'Start')) # スタート位置である頂点の(コスト,頂点名)をヒープキューに追加

# ダイクストラ法(最もコストの小さい頂点を順番に確認していく方法)
while len(q): # キューがある限り実行
# 最もコストが小さい頂点名をヒープキューから取得
min_node_name = heapq.heappop(q)[1] # コストと頂点名が返ってくるので、頂点名だけ取得
if min_node_name == 'Goal': # ゴール位置の頂点だった場合
print('解:最小コストは', nodes['Goal']['cost']) # 最小コストを表示して終了
break
# 最もコストが小さい頂点からの辺を繰り返しチェックする
for dst, cost in nodes[min_node_name]['edge'].items():
# 次の頂点までのコストが更新対象であれば更新
if nodes[dst]['cost'] > nodes[min_node_name]['cost'] + cost:
nodes[dst]['cost'] = nodes[min_node_name]['cost'] + cost
# コストを更新した頂点の(コスト,頂点名)をヒープキューに追加
heapq.heappush(q, (nodes[dst]['cost'], dst))

[実行結果]

解:最小コストは 8

前回同様 頂点Start から 頂点Goal までの最小コストは 8 であることを導き出すことができました。

ヒープキュー を使うことにより、ソースコードがすっきりした感じがします。

また、最小コストの頂点取得の 検索効率があがった ので処理も高速化しているはずです。
(今回のグラフはシンプルすぎて効果を実感できないかもしれませんが・・・)

ダイクストラ法(最短経路問題)

ダイクストラ法

ダイクストラ法 は、最短経路問題 を解くアルゴリズムの1つです。

ダイクストラ法 には次のような特徴があります。

  • コストが最小になる頂点 を探しながら最短経路を求める。
  • マイナスの値 の場合は使えないが、ベルマンフォード法よりも高速に解を求めることができる。

次のようなグラフを考えてみます。

[グラフ]

ソースコード

ソースコードでは、グラフの情報を次の入力例のように定義してみます。

  • 頂点は名前(頂点名)をもち、それぞれの頂点は隣の頂点情報(edge)と自分のコスト(cost)情報を持っています。
  • 隣の頂点情報は、頂点名(辞書型データのキー)とそこに移動するためのコスト(辞書型データのバリュー)があり、複数存在することがあります。
  • 頂点自体がもっているコストは、そこに辿りつくためのコストであり、初期値としてはスタート地点が0、それ以外は無限大を設定しておきます。

ダイクストラ法 では、コストが最もコストが小さくなる頂点 を選択することを繰り返して探索する方法です。

一度チェックしたした頂点は チェック済み として、チェックしていない頂点 の中からコストが最も小さいものを順番に確認していきます。

最も小さいコストの頂点が複数ある場合は、そのうちどれか1つを選択してチェックしていきます。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#---------- 入力例 ----------
nodes = { # ノード情報
'Start':{'edge':{'B':4, 'C':3}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':0}, # 頂点Startまでの最小コスト
'B': {'edge':{'C':1, 'D':1, 'E':5}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Bまでの最小コスト
'C': {'edge':{'F':2}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Cまでの最小コスト
'D': {'edge':{'E':3}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Dまでの最小コスト
'E': {'edge':{'Goal':2}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Eまでの最小コスト
'F': {'edge':{'E':1, 'Goal':4}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Fまでの最小コスト
'Goal': {'edge':{}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')} # 頂点Goalまでの最小コスト
}
#----------------------------
# ダイクストラ法(最もコストの小さい頂点を順番に確認していく方法)
while len(nodes): # 未チェックの頂点がある限り実行
# 最もコストが小さい頂点を探す
min_cost = min([node_info['cost'] for src, node_info in nodes.items()])
for name, node_info in nodes.items():
if node_info['cost'] == min_cost:
min_node_name = name # 最もコストが小さい頂点名を取得
break

# 最もコストが小さい頂点からの辺を繰り返しチェックする
for dst, cost in node_info['edge'].items():
# 次の頂点が未チェック かつ 次の頂点までのコストが更新対象であれば更新
if dst in nodes and nodes[dst]['cost'] > nodes[min_node_name]['cost'] + cost:
nodes[dst]['cost'] = nodes[min_node_name]['cost'] + cost

if min_node_name == 'Goal': # チェックした頂点が'Goal'の場合
print('解:最小コストは', nodes['Goal']['cost']) # 最小コストを表示して終了
break

del nodes[min_node_name] # チェックが終わった頂点は削除(チェック済みとする)

[実行結果]

解:最小コストは 8

頂点Start から 頂点Goal までの最小コストは 8 であることを導き出すことができました。

ベルマンフォード法(最短経路問題)

ベルマンフォード法

ベルマンフォード法 は、最短経路問題 を解くアルゴリズムの1つです。

ベルマンフォード法 には次のような特徴があります。

  • 頂点を結んだ 辺の重み(コスト)を更新しながら最短経路を求めます。
  • 辺の重み(コスト)が マイナス の場合でも解くことができます。

次のようなグラフを考えてみます。

[グラフ]

各頂点におけるスタート地点からのコストを求めるにあたり、設定した初期値から辺の重みを使って順に更新する作業を繰り返し、それ以上更新できなくなったら処理を終了します。

ソースコード

ソースコードでは、グラフの情報を次の入力例のように定義してみます。

  • 頂点は名前(頂点名)をもち、それぞれの頂点は隣の頂点情報(edge)と自分のコスト(cost)情報を持っています。
  • 隣の頂点情報は、頂点名(辞書型データのキー)とそこに移動するためのコスト(辞書型データのバリュー)があり、複数存在することがあります。
  • 頂点自体がもっているコストは、そこに辿りつくためのコストであり、初期値としてはスタート地点が0、それ以外は無限大を設定しておきます。

ベルマンフォード法 では、次のような手順で頂点のコスト情報を更新していきます。

  1. 辺を1つ選ぶ。
  2. 選んだ辺のコストを使って、両端の頂点のコストを更新する。
    (コストが小さい頂点に辺のコストを加算した値が、もう一方の頂点のコストよりも小さい場合、更新する)
  3. 全ての頂点に対して、コストが更新されなくなるまで上記の1~2を繰り返す。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#---------- 入力例 ----------
nodes = { # ノード情報
'Start':{'edge':{'B':4, 'C':3}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':0}, # 頂点Startまでの最小コスト
'B': {'edge':{'C':1, 'D':1, 'E':5}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Bまでの最小コスト
'C': {'edge':{'F':2}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Cまでの最小コスト
'D': {'edge':{'E':3}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Dまでの最小コスト
'E': {'edge':{'Goal':2}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Eまでの最小コスト
'F': {'edge':{'E':1, 'Goal':4}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')}, # 頂点Fまでの最小コスト
'Goal': {'edge':{}, # 隣の頂点までのコスト(頂点名:コスト)
'cost':float('inf')} # 頂点Goalまでの最小コスト
}
#----------------------------
# ベルマンフォード法(辺の重みに注目する方法)
is_updated = True # 更新フラグを立てる
while is_updated: # 頂点が更新されなくなるまで実行
is_updated = False # 更新フラグを落とす
for src, node_info in nodes.items(): # 頂点をすべてチェック
for dst, cost in node_info['edge'].items(): # その頂点からの辺をすべてチェック
if nodes[src]['cost'] + cost < nodes[dst]['cost']: # 頂点のコスト+辺のコストが、次の頂点のコストより小さい場合
nodes[dst]['cost'] = nodes[src]['cost'] + cost # 次の頂点のコストを更新
is_updated = True # 更新フラグをたてる
print('解:最小コストは', nodes['Goal']['cost'])

[実行結果]

解:最小コストは 8

頂点Start から 頂点Goal までの最小コストは 8 であることを導き出すことができました。

ベルマンフォード法の注意点

辺のコスト というのは 時間料金距離 などが対応すると考えられます。

これらはいずれもプラスの値ですが、もしマイナスの値であってもベルマンフォード法は有効です。

ただし、マイナスの値でループ しているような 経路(閉路) がある場合は、そのループを回り続けるといくらでもコストが小さくなってしまうため ベルマンフォード法 では解くことができません。

グラフ(辞書型編)

グラフ(辞書型編)

グラフ は、対象物の関係性 を表します。

グラフ を使うと、友人関係・鉄道路線・タスクの依存関係など、さまざまな関係性を表現できます。

関係性を グラフ で表すと、問題を抽象的に考えることができるというメリットがあります。

問題

次のような問題があります。

[問題]

頂点数がN個、辺数がM本であるような無向グラフ(辺の向きがないグラフ)があります。

頂点は隣接する頂点の情報(複数)を持っています。

それぞれの頂点は色の情報をもっており、色の情報は塗り替えることができます。

クエリ―は2種類あり下記のような操作を行います。

 クエリ―1:1 aという形式で情報が与えられ、頂点aに隣接する頂点 の色が 頂点aの色 に塗り変えられます。
 
 クエリ―2:2 a bという形式で情報が与えられ、頂点a の色が 色b に塗り替えられます。

[条件]

・頂点数Nは1以上200以下の整数

・変数MはN(N-1)/2以下の整数

・クエリ―数Qは1以上200以下の整数

解法・ソースコード

頂点と辺、色の情報を辞書型の変数で管理します。

辞書型のキーには 頂点のID を設定し、辞書型の値として 色情報と隣接する頂点 の情報を辞書型データとして設定します。

クエリ―ごとに各頂点の色情報を更新し、最後に各頂点の色情報を表示しています。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#---------- 入力例 ----------
# ノード情報
nodes = {
1:{'color':5, # 色情報(初期)
'edge':[2]}, # 隣接ノード(1→2)
2:{'color':10, # 色情報(初期)
'edge':[1,3]}, # 隣接ノード(2→1、2→3)
3:{'color':15, # 色情報(初期)
'edge':[2]}, # 隣接ノード(3→2)
}
# クエリ―情報
querys = [(1, 2),
(2, 1, 20),
(1, 1)]
#----------------------------
# 各ノードの色情報を表示する
def show_color():
for k,v in nodes.items():
print(' node[{}]: color[{}]'.format(k, v['color']))

print('【初期のカラー情報】')
show_color()
print(' -------')

for query in querys:
print('クエリ―', query)
node = nodes[query[1]] # ノードを選択
if query[0] == 1: # クエリ―がタイプ1の場合
for e in node['edge']: # 選択ノードに隣接する各ノードの色を更新
nodes[e]['color'] = node['color']
else: # クエリ―がタイプ2の場合
node['color'] = query[2] # 選択ノードの色を更新
print(' (カラー情報)')
show_color()
print(' -------')

print('【解:カラー情報】')
show_color()

[実行結果]

【初期のカラー情報】
  node[1]: color[5]
  node[2]: color[10]
  node[3]: color[15]
 -------
クエリ― (1, 2)
 (カラー情報)
  node[1]: color[10]
  node[2]: color[10]
  node[3]: color[10]
 -------
クエリ― (2, 1, 20)
 (カラー情報)
  node[1]: color[20]
  node[2]: color[10]
  node[3]: color[10]
 -------
クエリ― (1, 1)
 (カラー情報)
  node[1]: color[20]
  node[2]: color[20]
  node[3]: color[10]
 -------
【解:カラー情報】
  node[1]: color[20]
  node[2]: color[20]
  node[3]: color[10]

1番目のクエリ―により、頂点2に隣接する全ての頂点の色が10に塗り替えられます。

2番目のクエリ―により、頂点1の色が色20に塗り替えられます。

3番目のクエリ―により、頂点1に隣接する全ての色が20に塗り替えられます。

最終的には、頂点1の色が20、頂点2の色が20、頂点3の色が10になっていることが確認できます。

バケット(辞書型編)

バケット(辞書型編)

前回は、配列 を使ってバケットの手法を実現しましたか、今回は 辞書型 を使ってバケットの手法を実現してみます。

問題は前回と同様のものを使います。

[問題]

N個 の正の整数があります。

この中に含まれる数値の種類の個数を求めて下さい。

[条件]

・各数字は、1以上10以下の整数

解法・ソースコード

前回は配列のインデックスと入力の数字を対応させました。

辞書型では、入力の数字を 辞書型のキー とし、数字の個数を 辞書型のバリュー(値) としてみます。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
#---------- 入力例 ----------
lst = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9]
#----------------------------
bucket = {} # バケットを辞書型として定義
# 入力リストからバケットを作成
for l in lst:
if l in bucket: # バケットにすでにキーがある場合
bucket[l] += 1 # カウントアップ
else: # バケットにまだキーがない場合
bucket[l] = 1 # 1つ目としてバケット(辞書型)に登録

print('バケット:', bucket)
print('解(種類の個数):', len(bucket))

前回配列を使ったときとコード量はあまり変わりませんが、配列では固定のエリア(数字の種類分の配列)を確保する必要があり未使用になる可能性もあったので、辞書型の方が無駄なく実装できているような気がします。

バケット: {3: 2, 1: 2, 4: 1, 5: 3, 9: 2, 2: 1, 6: 1, 8: 1}

解(種類の個数): 8

解は 8 となり、前回記事と同様に入力の数字の種類が 8種類 であることが確認できました。

バケット(配列編)

バケット(配列編)

バケット はあらかじめデータがとりうる値を、すべての容器(バケット)に順番どおりに並べ用意しておくアルゴリズムです。

バケット を使うと次のようなさまざまな問題に簡単に解を出すことができます。

  • 配列にある値が含まれるかどうかを判定できる。
  • 配列に含まれる値が何種類あるかわかる。
  • 配列の要素がすべて異なるかどうかを判定できる。

[問題]

N個の正の整数があります。

この中に含まれる数値の種類の個数を求めて下さい。

[条件]

・各数字は、1以上10以下の整数

解法・ソースコード

数字の範囲は10以下なので、サイズが11の配列を準備します。(配列のインデックスが0から10の整数を表す)

次に入力の数字ごとに配列に1を設定していきます。(その数字が存在したことを表す)

最終的にこの配列の合計値が、数値の種類の個数を表すことになります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
#---------- 入力例 ----------
lst = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9]
#----------------------------
# バケット(全体を0で初初期化)範囲 0~10
bucket = [0] * 11

# 入力リストからバケットを作成
for l in lst:
bucket[l] = 1

print('バケット:', bucket)
print('解(数値の種類):', sum(bucket))

[実行結果]

バケット: [0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0]

解(種類の個数): 8

解は 8 となり、入力の数字の種類が 8種類 であることが確認できました。