人員スケジューリング問題 PuLP

人員スケジューリング問題

人員スケジューリング最適化の例題として、従業員のシフトスケジュールを最適化する問題を考えてみましょう。

次のような条件を設定します。

🔹従業員は、1日あたり8時間を超えて働くことはできません。
🔹従業員が、1週間あたりに働くことができる最大時間数はあらかじめ決まっています。
🔹従業員は、週に1日は必ず休む必要があります。

解法・ソースコード

この問題をPuLPを使って解いていきます。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from pulp import *

# ①従業員のリスト
employees = ['Alice', 'Bob', 'Charlie', 'David', 'Tom', 'Nancy']

# ②日付のリスト
days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']

# ③必要な従業員数
shift_requirements = {
'Monday': 2,
'Tuesday': 2,
'Wednesday': 3,
'Thursday': 2,
'Friday': 3,
'Saturday': 3,
'Sunday': 1
}

# ④各従業員が働く最大時間数
work_hours = {
'Alice': 40,
'Bob': 40,
'Charlie': 40,
'David': 40,
'Tom': 40,
'Nancy': 40
}

# ⑤問題を定義する
problem = LpProblem('Shift_Scheduling', LpMinimize)

# ⑥変数を定義する
shifts = LpVariable.dicts('shifts', (employees, days), 0, 1, LpInteger)

# ⑦目的関数を定義する
problem += lpSum([shifts[e][d] for e in employees for d in days])

# ⑧制約条件を追加する
# 各従業員は週に1日は必ず休む
for e in employees:
problem += lpSum([shifts[e][d] for d in days]) >= 1

# 各日のシフトには、必要な人数が割り当てられる必要がある
for d in days:
problem += lpSum([shifts[e][d] for e in employees]) == shift_requirements[d]

# 各従業員は1日に8時間までしか勤務できない
for e in employees:
problem += lpSum([shifts[e][d] for d in days]) <= 8

# 各従業員が1週間に働くことができる時間数
for e in employees:
problem += lpSum([shifts[e][d] for d in days]) * 8 <= work_hours[e]

# ⑨問題を解く
status = problem.solve()

# ⑩解を表示する
print(f'Solution status: {LpStatus[status]}')

for d in days:
print(f'\n{d}')
for e in employees:
if value(shifts[e][d]) == 1:
print(f' {e} is working.')
else:
print(f' {e} is off.')

①従業員のリストを作成しています。

②日付のリストを作成しています。

③各日に必要な従業員数を格納する辞書を作成しています。

④各従業員が働くことができる最大時間数を格納する辞書を作成しています。

⑤問題を定義するために、PuLPLpProblemオブジェクトを作成しています。
 第一引数には問題の名前を指定し、第二引数には最小化するのか、最大化するのかを指定します。

⑥変数を定義しています。
 shiftsという名前のLpVariableオブジェクトを作成します。
 このオブジェクトはemployeesとdaysの2つのインデックスを持ちます。
 変数は、0から1の整数値を取ります。

⑦目的関数を定義しています。
 目的関数は、すべての従業員と日に対して、その従業員がその日に働く場合に1、そうでない場合に0を取るshifts変数の総和です。
 この目的関数は、スケジュールの総コストを最小限に抑えるように最適化されます。

⑧制約条件を追加しています。
 まず、各従業員が週に1日は必ず休むようにします。
 次に、各日のシフトには、必要な人数が割り当てられる必要があるようにします。
 各従業員は1日に8時間までしか勤務できないようにし、各従業員が1週間に働くことができる時間数を制限します。

⑨問題を解くために、PuLPsolveメソッドを呼び出します。
 解決策が見つかった場合は、status変数にSolvedという文字列が格納されます。
 見つからなかった場合は、Infeasibleという文字列が格納されます。

⑩解を表示するために、各日について、各従業員が働いているかどうかを示す出力を行っています。
 これは、shifts変数の値が1の場合にその従業員が働いていることを示します。
 それ以外の場合は、その従業員が休みであることを示します。


実行結果は以下のようになります。

[実行結果]
Solution status: Optimal

Monday
  Alice is working.
  Bob is off.
  Charlie is off.
  David is off.
  Tom is working.
  Nancy is off.

Tuesday
  Alice is off.
  Bob is working.
  Charlie is off.
  David is off.
  Tom is off.
  Nancy is working.

Wednesday
  Alice is working.
  Bob is working.
  Charlie is working.
  David is off.
  Tom is off.
  Nancy is off.

Thursday
  Alice is off.
  Bob is off.
  Charlie is off.
  David is working.
  Tom is working.
  Nancy is off.

Friday
  Alice is off.
  Bob is working.
  Charlie is working.
  David is working.
  Tom is off.
  Nancy is off.

Saturday
  Alice is off.
  Bob is off.
  Charlie is working.
  David is working.
  Tom is off.
  Nancy is working.

Sunday
  Alice is off.
  Bob is off.
  Charlie is off.
  David is off.
  Tom is working.
  Nancy is off.

各曜日に誰が働けばよいのかシフトスケジュールを組むことができました。

プロセス最適化 PuLP

プロセス最適化問題

【問題】
ある化学工場では、原料 A, B, C, D を混合して製品を作成するプロセスがあります。

原料の価格は以下の通りです。

🔹A: 30 円/kg
🔹B: 20 円/kg
🔹C: 10 円/kg
🔹D: 5 円/kg

製品の販売価格は 50 円/単位です。また、原料の供給量は以下の通りです。

🔹A: 200 kg
🔹B: 150 kg
🔹C: 100 kg
🔹D: 50 kg

各原料を最大限に使用することで得られる利益を求めてください。

解法

この問題は線形計画法を用いて解くことができます。以下は、pulpを用いた解法です。

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
from pulp import *

# ①問題の定義
prob = LpProblem('Process Optimization', LpMaximize)

# ②変数の定義
A = LpVariable('A', lowBound=0, upBound=200, cat='Continuous')
B = LpVariable('B', lowBound=0, upBound=150, cat='Continuous')
C = LpVariable('C', lowBound=0, upBound=100, cat='Continuous')
D = LpVariable('D', lowBound=0, upBound=50, cat='Continuous')

# ③目的関数の定義
prob += 50 * (A + B + C + D) - 30 * A - 20 * B - 10 * C - 5 * D

# ④制約条件の定義
prob += A + B + C + D <= 450
prob += A <= 200
prob += B <= 150
prob += C <= 100
prob += D <= 50

# ⑤問題の解決
prob.solve()

# ⑥結果の出力
print('最大利益:', value(prob.objective))
print('原料 A:', value(A))
print('原料 B:', value(B))
print('原料 C:', value(C))
print('原料 D:', value(D))

①問題の定義

PuLP を使う場合、まず LpProblem オブジェクトを作成します。

第一引数に問題の名前、第二引数に最適化の種類を指定します。

この場合は最大化問題なので LpMaximize を指定しています。

②変数の定義

最適化する変数を定義します。

ここでは、原料 A, B, C, D の使用量をそれぞれ LpVariable オブジェクトとして定義しています。

第一引数に変数名を指定し、lowBound、upBound 引数で変数の下限値、上限値を指定します。

cat 引数では変数の種類を指定します。ここでは連続変数として定義しています。

③目的関数の定義

最大化したい目的関数を定義します。

ここでは、総原価が最小となるようにするため、原料 A, B, C, D の使用量をそれぞれ掛け合わせ、総原価を計算しています。

④制約条件の定義

最適化の制約条件を定義します。

ここでは、使用する原料の量がそれぞれの上限値を超えないようにするため、制約条件を設けています。

⑤問題の解決

PuLP を使って問題を解くには、solve() メソッドを呼び出します。

⑥結果の出力

最後に、value() 関数を使って、各変数の最適解を取得し、出力しています


このコードを実行すると、以下のような結果が得られます。

[実行結果]
最大利益: 13750.0
原料 A: 150.0
原料 B: 150.0
原料 C: 100.0
原料 D: 50.0

各原料を最大限に使用することで、最大利益 13,750 円を得られるということが確認できました。

広告最適化問題 PuLP

広告最適化問題

広告最適化問題を解決するために、PuLPを使用して線形計画問題にモデル化することができます。


あるWebサイトの広告枠に表示する広告を複数の広告主から選ぶ場合を考えます。

各広告主は、広告をクリックするユーザー数に応じて支払う広告費が異なります。

広告費の合計を最小化しながら、クリック率を最大化するように広告を選択することが目的です。


以下は、この問題をPuLPで解く例です。

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
import pulp

# 問題設定
ads = ["A", "B", "C", "D"]
click_rates = {"A": 0.02, "B": 0.03, "C": 0.01, "D": 0.04}
costs = {"A": 1000, "B": 1200, "C": 800, "D": 1500}
max_budget = 5000

# モデルの作成
prob = pulp.LpProblem("Ad Optimization", pulp.LpMaximize)

# 変数の定義
x = pulp.LpVariable.dicts("ads", ads, lowBound=0, cat="Integer")

# 目的関数の定義
prob += pulp.lpSum([click_rates[i] * x[i] for i in ads])

# 制約条件の定義
prob += pulp.lpSum([costs[i] * x[i] for i in ads]) <= max_budget
prob += pulp.lpSum([x[i] for i in ads]) == 1

# 最適化の実行
prob.solve()

# 結果の出力
for i in ads:
print(f"Ad {i}: {pulp.value(x[i])}")

この例では、pulp.LpProblem()で問題を設定し、pulp.LpVariable.dicts()で広告選択の変数を定義しています。

目的関数は、クリック率の合計を最大化するように設定し、制約条件は、広告費の合計が最大予算を超えないように設定しています。

prob.solve()で最適化を実行し、結果を出力しています。


このコードを実行すると、各広告の選択率が出力されます。

[実行結果]
Ad A: 0.0
Ad B: 0.0
Ad C: 0.0
Ad D: 1.0

この結果から、広告Dを選択することが最適であることがわかりました。

ナンプレ問題 PuLP

ナンプレ問題

下記のナンプレ問題を、PuLPを使って解いて下さい。

5 3 0 0 7 0 0 0 0 
6 0 0 1 9 5 0 0 0 
0 9 8 0 0 0 0 6 0 
8 0 0 0 6 0 0 0 3 
4 0 0 8 0 3 0 0 1 
7 0 0 0 2 0 0 0 6 
0 6 0 0 0 0 2 8 0 
0 0 0 4 1 9 0 0 5 
0 0 0 0 8 0 0 7 9

解法・ソースコード

ソースコードの構造と各部分の役割について説明します。

①問題の定義

まず、問題を定義するために pulp.LpProblem() 関数を使用しています。

この関数には、問題の名前と目的関数の種類を指定します。

ここでは、ナンプレ問題を解くために、”Sudoku Problem”という名前の問題を定義し、目的関数として pulp.LpMinimize を使用しています。

ただし、ナンプレ問題には目的関数が存在しないため、この値は問題の解法に影響しません。

②変数の定義

問題で扱う変数を定義します。

pulp.LpVariable.dicts() 関数を使用して、choices という名前の辞書を作成しています。

この辞書には、各マスに対応する9つの変数が含まれています。

つまり、choices[i][j][k] は、マス(i, j)に数字kが入っているかどうかを表す0または1の変数になります。

ここでは、cat=’Binary’という引数を指定することで、変数が0または1の整数値しかとらないようにしています。

③制約条件の定義

問題の制約条件を定義します。

各マスには1つの数字しか入らないことを表す制約条件を設定しています。

具体的には、各マスに入る数字の変数の和が1であるという条件を表しています。

④各行には1から9までの数字が1回ずつ現れる

各行には1から9までの数字が1回ずつ現れることを表す制約条件を設定しています。

⑤各列には1から9までの数字が1回ずつ現れる

④と同様に、各列には1から9までの数字が1回ずつ現れることを表す制約条件も設定しています。

⑥各3x3ブロックには1から9までの数字が1回ずつ現れる

各ブロックには1から9までの数字が1回ずつ現れることを表す制約条件も設定しています。

具体的には、各数字がブロック内のある1つのマスに現れるという条件を表しています。

各ブロックの左上のマスを(row, col)として、3x3のブロック内の各マスに対して、そのマスにnumが入っているかどうかを表す変数の和が1であるという制約条件を表しています。

⑦初期状態を設定

初期状態が与えられている場合は、その状態に合わせた制約条件も設定しています。

初期状態に数字が入っているマスに対して、そのマスに対応する変数を1に固定するという制約条件を表しています。

⑧解を求める

problem.solve() で定義した問題を解きます。

⑨解の表示

各変数がとる値を choices[row][col][num].value() で取得し、それを元に解を作成しています。

最終的には、解を二次元配列として solution に保存し、それを行ごとに表示しています。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import pulp

# ①問題の定義
problem = pulp.LpProblem("Sudoku Problem", pulp.LpMinimize)

# ②変数の定義
choices = pulp.LpVariable.dicts("Choice", (range(1, 10), range(1, 10), range(1, 10)), cat='Binary')

# ③制約条件の定義
# 各マスは1つの数字に限定される
for row in range(1, 10):
for col in range(1, 10):
problem += pulp.lpSum([choices[row][col][num] for num in range(1, 10)]) == 1

# ④各行には1から9までの数字が1回ずつ現れる
for row in range(1, 10):
for num in range(1, 10):
problem += pulp.lpSum([choices[row][col][num] for col in range(1, 10)]) == 1

# ⑤各列には1から9までの数字が1回ずつ現れる
for col in range(1, 10):
for num in range(1, 10):
problem += pulp.lpSum([choices[row][col][num] for row in range(1, 10)]) == 1

# ⑥各3x3ブロックには1から9までの数字が1回ずつ現れる
for row in range(1, 10, 3):
for col in range(1, 10, 3):
for num in range(1, 10):
problem += pulp.lpSum([choices[i][j][num] for i in range(row, row + 3) for j in range(col, col + 3)]) == 1

# ⑦初期状態を設定
# ナンプレ問題の初期状態には、空きマスを0として表現します。
initial_state = [
[5, 3, 0, 0, 7, 0, 0, 0, 0],
[6, 0, 0, 1, 9, 5, 0, 0, 0],
[0, 9, 8, 0, 0, 0, 0, 6, 0],
[8, 0, 0, 0, 6, 0, 0, 0, 3],
[4, 0, 0, 8, 0, 3, 0, 0, 1],
[7, 0, 0, 0, 2, 0, 0, 0, 6],
[0, 6, 0, 0, 0, 0, 2, 8, 0],
[0, 0, 0, 4, 1, 9, 0, 0, 5],
[0, 0, 0, 0, 8, 0, 0, 7, 9]
]

for row in range(9):
for col in range(9):
if initial_state[row][col] != 0:
problem += choices[row+1][col+1][initial_state[row][col]] == 1

# ⑧解を求める
problem.solve()

# ⑨解の表示
solution = [[0]*9 for _ in range(9)]
for row in range(1, 10):
for col in range(1, 10):
for num in range(1, 10):
if choices[row][col][num].value() == 1:
solution[row-1][col-1] = num

for row in solution:
print(row)

このコードを実行すると、下記の結果を得ることができます。

[実行結果]
[5, 3, 4, 6, 7, 8, 9, 1, 2]
[6, 7, 2, 1, 9, 5, 3, 4, 8]
[1, 9, 8, 3, 4, 2, 5, 6, 7]
[8, 5, 9, 7, 6, 1, 4, 2, 3]
[4, 2, 6, 8, 5, 3, 7, 9, 1]
[7, 1, 3, 9, 2, 4, 8, 5, 6]
[9, 6, 1, 5, 3, 7, 2, 8, 4]
[2, 8, 7, 4, 1, 9, 6, 3, 5]
[3, 4, 5, 2, 8, 6, 1, 7, 9]

PuLPを使ってナンプレ問題を解くことができました。

巡回セールスマン問題(NetworkX)

巡回セールスマン問題

巡回セールスマン問題(Traveling Salesman Problem)は、すべての都市を一度だけ訪れ、最短距離で戻ってくる経路を求める問題です。

以下は、NetworkXを使用して巡回セールスマン問題を解く例です。

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
import networkx as nx
import matplotlib.pyplot as plt

# 5つの都市の座標を設定
positions = {1: (0, 0), 2: (1, 1), 3: (2, 0), 4: (3, 1), 5: (4, 0)}

# 距離を計算する関数を定義
def distance(u, v):
x1, y1 = positions[u]
x2, y2 = positions[v]
return ((x1-x2)**2 + (y1-y2)**2)**0.5

# グラフを作成
G = nx.Graph()
G.add_nodes_from(positions.keys())
for u in positions:
for v in positions:
if u < v:
G.add_edge(u, v, weight=distance(u, v))

# 巡回セールスマン問題を解く
tsp = list(nx.algorithms.approximation.traveling_salesman_problem(G))

# 解を表示
print("最短距離の順序:", tsp)
print("最短距離:", sum(distance(u, v) for u, v in zip(tsp, tsp[1:]+tsp[:1])))

# グラフを可視化
pos = positions
nx.draw(G, pos=pos, with_labels=True)
nx.draw_networkx_edges(G, pos=pos, edgelist=[(u, v) for u, v in zip(tsp, tsp[1:]+tsp[:1])], edge_color='r')
plt.show()

このコードでは、5つの都市の座標を定義し、距離を計算する関数を定義しています。

その後、座標をノードとし、距離をエッジとしたグラフを作成します。

nx.algorithms.approximation.traveling_salesman_problem関数を使用して、巡回セールスマン問題を解きます。

解は、最短距離の順序として返されます。

最後に、解を表示しグラフを可視化しています。

[実行結果]

全点対最短経路問題(NetworkX)

全点対最短経路問題

全点対最短経路問題(All-Pairs Shortest Path Problem)は、グラフ理論の基本的な問題の一つで、ある重みつきグラフにおいて、すべての頂点の間の最短経路を求める問題です。


具体的には、グラフの任意の2つの頂点u, vに対して、uからvへの最短経路の重みを求めます。

この問題は、ワーシャル-フロイド法ダイクストラ法などのアルゴリズムを使用して解決できます。


全点対最短経路問題は、多くの実世界の問題に適用されます。

例えば、都市間の移動にかかる最小コストを求める問題や、ネットワークの最小距離を求める問題などがあります。

また、グラフが静的である場合には、問題の解決に多項式時間がかかりますが、グラフが動的である場合には、解決が困難であることが知られています。

例題

すべての頂点のペアの最短経路を求めてみます。

NetworkXは、ワーシャル-フロイド法を実装した関数 shortest_pathを提供しています。

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
import networkx as nx

# 無向グラフの定義
G = nx.Graph()

# 頂点の追加
G.add_nodes_from([1, 2, 3, 4, 5])

# 辺の追加
G.add_edge(1, 2, weight=5)
G.add_edge(1, 3, weight=2)
G.add_edge(1, 4, weight=6)
G.add_edge(2, 3, weight=3)
G.add_edge(2, 4, weight=2)
G.add_edge(3, 4, weight=1)
G.add_edge(3, 5, weight=4)
G.add_edge(4, 5, weight=6)

# 全点対最短経路を求める
all_shortest_paths = dict(nx.shortest_path(G))

# 結果を表示する
for source in all_shortest_paths:
for target in all_shortest_paths[source]:
if source != target:
print(f"{source} から {target} までの最短経路: {all_shortest_paths[source][target]}")

まず、グラフを定義します。

nx.Graph() によって空の無向グラフを定義し、add_nodes_from メソッドを使用して頂点を追加し、add_edge メソッドを使用して辺を追加しています。

また、各辺には weight 属性を設定して、重みつきグラフとして定義しています。


次に、shortest_path 関数を使用して、すべての頂点のペアの最短経路を求めます。

ただし、この方法は全点対最短経路を求めるために必要な計算量が大きくなってしまい、大きなグラフには適用しづらい場合があります。

[実行結果]
1 から 2 までの最短経路: [1, 2]
1 から 3 までの最短経路: [1, 3]
1 から 4 までの最短経路: [1, 4]
1 から 5 までの最短経路: [1, 3, 5]
2 から 1 までの最短経路: [2, 1]
2 から 3 までの最短経路: [2, 3]
2 から 4 までの最短経路: [2, 4]
2 から 5 までの最短経路: [2, 3, 5]
3 から 1 までの最短経路: [3, 1]
3 から 2 までの最短経路: [3, 2]
3 から 4 までの最短経路: [3, 4]
3 から 5 までの最短経路: [3, 5]
4 から 1 までの最短経路: [4, 1]
4 から 2 までの最短経路: [4, 2]
4 から 3 までの最短経路: [4, 3]
4 から 5 までの最短経路: [4, 5]
5 から 3 までの最短経路: [5, 3]
5 から 4 までの最短経路: [5, 4]
5 から 1 までの最短経路: [5, 3, 1]
5 から 2 までの最短経路: [5, 3, 2]

すべての頂点のペアの最短経路を求めることができました。

マッチング(NetworkX)

問題

ある男女10人のグループがあり、男女間には好意があると仮定します。

このグループを、男女のペアを作ることができるようにマッチングさせることを考えます。

ただし、それぞれの男性・女性が同時に複数の異性とのペアを持てないものとします。


以下の男女間の好意度が与えられています。

この好意度を最大化するように、マッチングを決定してください。

男1男2男3男4男5
女1709080 8070
女2601080 7060
女370701007060
女4808080 5060
女5909090 9030

解法

NetworkXライブラリを使ってグラフを作成します。

このグラフのノードは男女それぞれの番号で表されます。

辺には好意度に対応する重みが付いています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import networkx as nx

# グラフを定義する
G = nx.Graph()
G.add_nodes_from(["F1", "F2", "F3", "F4", "F5"], bipartite=0)
G.add_nodes_from(["M1", "M2", "M3", "M4", "M5"], bipartite=1)
G.add_weighted_edges_from([
("F1", "M1", 70), ("F1", "M2", 90), ("F1", "M3", 80), ("F1", "M4", 80), ("F1", "M5", 70),
("F2", "M1", 60), ("F2", "M2", 10), ("F2", "M3", 80), ("F2", "M4", 70), ("F2", "M5", 60),
("F3", "M1", 70), ("F3", "M2", 70), ("F3", "M3", 100), ("F3", "M4", 70), ("F3", "M5", 60),
("F4", "M1", 80), ("F4", "M2", 80), ("F4", "M3", 80), ("F4", "M4", 50), ("F4", "M5", 60),
("F5", "M1", 90), ("F5", "M2", 90), ("F5", "M3", 90), ("F5", "M4", 90), ("F5", "M5", 30)
])

# 最大重みマッチングを求める
max_matching = nx.max_weight_matching(G)
print(max_matching)

NetworkXライブラリを用いて、与えられたグラフの最大重みマッチングを求めています。


まず、nx.Graph()を使って空のグラフを定義し、Gという変数に格納しています。

次に、add_nodes_fromを使って、グラフのノードを追加しています。

ここでは、[“F1”, “F2”, “F3”, “F4”, “F5”]と[“M1”, “M2”, “M3”, “M4”, “M5”]という2つの集合を作り、それぞれに対してbipartite=0bipartite=1という属性を付与しています。

これによって、グラフが二部グラフであることを示しています。


その後、add_weighted_edges_fromを使って、グラフの辺を追加しています。

ここでは、各辺に対して、始点、終点、重みを設定しています。

最後に、nx.max_weight_matchingを使って、最大重みマッチングを求め、結果をmax_matchingに格納しています。

[実行結果]
{('F4', 'M1'), ('F1', 'M2'), ('M4', 'F5'), ('M3', 'F3'), ('F2', 'M5')}

最大重みマッチングによって、以上のような男女のペアを作ることができました。

SNS分析(NetworkX)

SNS分析

SNS上で、あるユーザーが自分の友達の友達の中で最も人気がある人を見つけるために、ネットワーク分析を行いたいと考えています。

この問題を解決するために、与えられたSNSグラフ内で、指定されたユーザーの友達の友達の中で最も人気がある人を見つける関数を実装してください。

解法

以下は、この問題に対するNetworkXによる実装例です。

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
import networkx as nx

def find_most_popular_friend_of_friends(G, user):
"""
指定されたユーザーの友達の友達の中で最も人気がある人を見つける関数

Parameters
----------
G : NetworkXのグラフオブジェクト
SNSグラフ
user : ノード
検索対象のユーザー

Returns
-------
ノード
最も人気があるユーザー
"""
# userの友達を取得
friends = set(nx.neighbors(G, user))

# userの友達の友達を取得
friends_of_friends = set()
for friend in friends:
friends_of_friends.update(nx.neighbors(G, friend))

# userとその友達の友達を除く
friends_of_friends.discard(user)
friends_of_friends.difference_update(friends)

# 各ノードの次数を計算
degrees = G.degree(friends_of_friends)

# 次数の降順にソート
sorted_degrees = sorted(degrees, key=lambda x: x[1], reverse=True)

# 最も人気のあるユーザーを返す
return sorted_degrees[0][0]

この関数は、与えられたSNSグラフ内で指定されたユーザーの友達の友達の中で最も人気がある人を見つけるために使用できます。

与えられたユーザーの友達の友達を取得し、各ノードの次数を計算して、次数が最大のノードを返します。

動作確認

テスト用のデータを作成して、上記の関数を使用して解決してみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import networkx as nx

# SNSグラフを作成
G = nx.Graph()
G.add_edges_from([
('Alice', 'Bob'),
('Alice', 'Charlie'),
('Bob', 'David'),
('Bob', 'Eve'),
('Charlie', 'David'),
('Charlie', 'Frank'),
('David', 'Eve'),
('Eve', 'Frank'),
('Eve', 'Grace'),
('Frank', 'Grace')
])

# 'Alice'の友達の友達で最も人気がある人を見つける
most_popular_friend_of_friends = find_most_popular_friend_of_friends(G, 'Alice')

print(f"Most popular friend of friends of Alice: {most_popular_friend_of_friends}")

このコードでは、GというSNSグラフを作成し、find_most_popular_friend_of_friends関数を使用して、’Alice’の友達の友達で最も人気がある人を見つけています。

出力結果は以下の通りになります。

[実行結果]
Most popular friend of friends of Alice: Grace

与えられたSNSグラフ内で指定されたユーザーの友達の友達の中で最も人気がある人を見つけることができました。

強連結グラフ(NetworkX)

強連結グラフ

強連結グラフとは、グラフ上の任意の2つの頂点間に有向パスが存在するグラフのことです。

強連結グラフの判定には、コサラージュのアルゴリズムが使用されます。

  1. 与えられたグラフGに対して、グラフGの転置グラフGTを作成する。
  2. Gの任意の頂点vからDFSを行い、訪れた頂点をスタックに追加する。
  3. スタックの頂点を逆順にした順序で、GTに対してDFSを行い、訪れた頂点の集合を得る。
  4. スタックに残っている頂点がある場合、その頂点からDFSを再開し、訪問順を追加していく。
  5. 2-4を繰り返し、スタックが空になった場合、グラフGが強連結グラフであることが示される。

以下は、上記のアルゴリズムをPythonコードで実装した例です。

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
41
42
43
import networkx as nx

# グラフの作成
G = nx.DiGraph()
G.add_nodes_from([1, 2, 3, 4])
G.add_edges_from([(1, 2), (2, 3), (3, 1), (3, 4)])

# 転置グラフの作成
GT = G.reverse(copy=True)

# DFSを行い、スタックに追加
stack = []
visited = set()
for v in G.nodes:
if v not in visited:
visited.add(v)
stack.append(v)
while stack:
node = stack[-1]
neighbors = list(G.neighbors(node))
unvisited = [n for n in neighbors if n not in visited]
if unvisited:
n = unvisited[0]
visited.add(n)
stack.append(n)
else:
stack.pop()

# スタックから頂点を逆順にして、転置グラフに対してDFSを行う
visited = set()
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
t_neighbors = list(GT.neighbors(node))
for n in t_neighbors:
if n not in visited:
stack.append(n)

if len(visited) == len(G.nodes):
print("グラフは強連結グラフです")
else:
print("グラフは強連結グラフではありません")

上記のコードは、与えられたグラフが強連結グラフである場合、”グラフは強連結グラフです”と出力します。

強連結グラフが強連結グラフでない場合、”グラフは強連結グラフではありません”と出力されます。


このアルゴリズムは、与えられたグラフGに対して、転置グラフGTを作成し、GとGTのDFSを行うことで、強連結グラフであるかどうかを判断します。

GとGTのDFSを行うことで、G上の任意の2つの頂点間に有向パスが存在することを確認することができます。

そして、Gが強連結グラフであるためには、GとGTのDFSによって訪れた頂点が全て同一である必要があります。

このアルゴリズムは、時間計算量がO(V+E)であり、高速に強連結グラフかどうかを判断できます。


グラフ理論では、強連結グラフは重要な役割を持っており、多くのアルゴリズムに応用されます。

強連結グラフの判定アルゴリズムを理解することで、グラフ理論の基礎的な概念を深く理解することができます。

最大流問題(NetworkX)

最大流問題

最大流問題は、与えられたグラフにおいて、ある2つの頂点を指定して、それらの間に最大量の流れを流す問題です。

以下は、NetworkXを使用して最大流問題を解く例です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import networkx as nx

# グラフを作成
G = nx.DiGraph()

G.add_nodes_from(['S', 'A', 'B', 'C', 'D', 'T'])

G.add_edges_from([('S', 'A', {'capacity': 10}),
('S', 'C', {'capacity': 5}),
('A', 'B', {'capacity': 9}),
('A', 'D', {'capacity': 4}),
('B', 'T', {'capacity': 8}),
('C', 'D', {'capacity': 3}),
('C', 'T', {'capacity': 7}),
('D', 'B', {'capacity': 6}),
('D', 'T', {'capacity': 9})])

# 最大流を求める
max_flow_value, flow_dict = nx.maximum_flow(G, 'S', 'T', capacity='capacity')

# 結果を表示する
print('Maximum Flow Value:', max_flow_value)
print('Flow Dictionary:', flow_dict)

上記の例では、6つの頂点を持つグラフが与えられています。

SとTはそれぞれ始点と終点を表し、他の頂点は中間点として機能します。

各辺は容量を持ち、それが最大流を制限するものです。


maximum_flow関数は、グラフ、始点、終点、容量属性を受け取り、最大流量フローの辞書を返します。

capacity引数は、各辺の容量を指定する属性名を表します。

実行結果は以下のようになります。

[実行結果]
Maximum Flow Value: 15
Flow Dictionary: {'S': {'A': 10, 'C': 5}, 'A': {'B': 8, 'D': 2}, 'B': {'T': 8}, 'C': {'D': 0, 'T': 5}, 'D': {'B': 0, 'T': 2}, 'T': {}}

この結果により、グラフの始点から終点に15の最大流量が流れることがわかります。

また、各辺のフローも辞書として表示されます。