集合被覆問題(LSCP)入門¶

1. はじめに¶

消防署や救急ステーションのような施設を配置するとき、「すべての住民が一定時間内にサービスを受けられること」を条件としたうえで、施設の数をできるだけ少なくしたい、という状況がよくある。施設が需要点から一定の距離(または時間)内にあるとき、その施設は需要点を「カバーする」という。すべての需要点がいずれかの施設にカバーされるという条件の下で、配置する施設の数を最小にする問題を集合被覆問題(Location Set Covering Problem、LSCP)と呼ぶ。

p-median 問題やp-center 問題では施設の数p をあらかじめ決めて配置の質を最適化したのに対し、LSCP では品質の基準(カバーの条件)を先に決めて、それを満たす最小の施設数を求める。つまり施設数が「入力」ではなく「出力」になる点が大きな違いである。

2. 前提知識と学習目標¶

本資料を読む前に知っておくとよいことは次のとおりである。

  • 数学(高校1〜2年程度): 距離の計算、場合分けで考える方法。
  • プログラミング: if 文による条件分岐、for 文による繰り返し、list によるデータの管理。
  • p-center 問題入門を先に読んでいることが望ましい(本資料は同じ地域データを使う)。

学習目標¶

  1. LSCP が「カバーの条件を満たす最小の施設数」を求めるモデルであることを説明できる。
  2. 具体例から数理モデルを組み立てられる。
  3. PuLP でモデルを作り、答えを得て、図で示せる。

3. 問題設定と定式化¶

3.1 問題の具体例¶

p-center 問題入門と同じ地域を使う。

  • 候補点: 施設を建設できそうな場所4か所。
  • 需要点: 町や丁などの中心地点8か所。
  • 被覆距離: 施設は直線距離で8.5 km 以内の需要点をカバーできるものとする。

このとき、すべての需要点をカバーするには最低で何か所の施設が必要だろうか。またそれはどの候補点だろうか。これがLSCP の問いである。

3.2 数学的な定式化¶

3.2.1 定数および決定変数の定義¶

名前 説明
$I$ 候補点の集合(この例では4か所)
$J$ 需要点の集合(この例では8か所)
$d_{ij}$ 候補点 $i$ から需要点 $j$ までの距離
$D$ 被覆距離(この例では8.5 km)
$N_j$ 需要点 $j$ をカバーできる候補点の集合、すなわち $N_j = \{i \in I : d_{ij} \leq D\}$
$x_i$ 候補点 $i$ に施設を建てるなら1、そうでなければ0

3.2.2 数理モデル¶

$$ \begin{align} \min \ \ & z = \sum_{i \in I} x_i \\ \text{s.t. } \ \ & \sum_{i \in N_j} x_i \geq 1, \quad \forall j \in J, \\ & x_i \in \{0, 1\}, \quad \forall i \in I. \end{align} $$

3.2.3 目的関数の意味¶

目的関数は配置する施設の数の合計であり、これを最小化する。施設1か所あたりの建設費が同じなら、施設数の最小化は費用の最小化と同じ意味になる。なお、候補点ごとに建設費 $w_i$ が異なる場合は、目的関数を $\sum_{i \in I} w_i x_i$ に置き換えればよい。

3.2.4 制約条件の意味¶

1本目の制約条件は「需要点 $j$ をカバーできる候補点のうち、少なくとも1か所には施設が建っていること」を表す。これがすべての需要点 $j$ について課されるので、どの需要点も必ずどこかの施設にカバーされる。2本目は $x_i$ が0か1のみを取ることを表す。

なお、被覆距離 $D$ を小さくしすぎると、どの候補点からもカバーできない需要点が生じ、問題は実行不能(解なし)になる。この点はあとで実際に確かめる。

4. Python + PuLP による実装¶

4.1 準備¶

以下にLSCP の実装例を示す。問題は候補点4点、需要点8点と小規模なので、頑張れば手でも解ける。ぜひ試してみてほしい。

なおGoogle Colab で実行する際には、必ず最初に以下のプログラム2行目(# !pip install pulp matplotlib)のコメントを外して実行すること。

In [1]:
# Google Colab の場合は2行目のコメントを外して実行: ライブラリインストール(1回だけ)
# !pip install pulp matplotlib
In [2]:
# ライブラリ読み込み
import math
import pulp                     # モデル作成
import matplotlib.pyplot as plt # 可視化

# ダミーデータ作成(p-center 問題入門と同じ地域)
coords_fac = [(0, 0), (10, 0), (0, 10), (10, 10)]  # 候補点座標
coords_dem = [(2, 1), (3, 7), (6, 2), (7, 8), (1, 9), (9, 1), (5, 5), (8, 4)]  # 需要点座標

I = range(len(coords_fac))  # 候補点集合
J = range(len(coords_dem))  # 需要点集合
D = 8.5  # 被覆距離

print(f"候補点数: {len(I)}, 需要点数: {len(J)}, 被覆距離: {D}")
候補点数: 4, 需要点数: 8, 被覆距離: 8.5
In [3]:
# 距離行列と被覆関係の計算
dist = [[math.dist(coords_fac[i], coords_dem[j]) for j in J] for i in I]

# N_j: 需要点j をカバーできる候補点の集合
N = {j: [i for i in I if dist[i][j] <= D] for j in J}
for j in J:
    print(f"需要点{j} をカバーできる候補点: {N[j]}")
需要点0 をカバーできる候補点: [0, 1]
需要点1 をカバーできる候補点: [0, 2, 3]
需要点2 をカバーできる候補点: [0, 1]
需要点3 をカバーできる候補点: [2, 3]
需要点4 をカバーできる候補点: [2]
需要点5 をカバーできる候補点: [1]
需要点6 をカバーできる候補点: [0, 1, 2, 3]
需要点7 をカバーできる候補点: [1, 3]

表示された被覆関係を眺めると、需要点ごとにカバーできる候補点の数は1〜4個とばらつきがあることがわかる。特に需要点4と5 は1つの候補点からしかカバーできないため、その候補点には必ず施設を置く必要がある。この関係だけを使って施設の組を選ぶのがLSCP である(距離の値そのものは、カバーできるかどうかの判定にしか使わないことに注意)。

もし「カバーできる候補点」が空になっている需要点があれば、その時点で問題は実行不能である。

4.2 モデル構築¶

In [4]:
# モデル宣言
prob = pulp.LpProblem("LSCP", pulp.LpMinimize)

# 変数
x = pulp.LpVariable.dicts('x', I, cat='Binary')

# 目的関数: 施設数の最小化
prob += pulp.lpSum(x[i] for i in I)

# 制約: すべての需要点が少なくとも1つの施設にカバーされる
for j in J:
    prob += pulp.lpSum(x[i] for i in N[j]) >= 1

print(prob)
LSCP:
MINIMIZE
1*x_0 + 1*x_1 + 1*x_2 + 1*x_3 + 0.0
SUBJECT TO
_C1: x_0 + x_1 >= 1

_C2: x_0 + x_2 + x_3 >= 1

_C3: x_0 + x_1 >= 1

_C4: x_2 + x_3 >= 1

_C5: x_2 >= 1

_C6: x_1 >= 1

_C7: x_0 + x_1 + x_2 + x_3 >= 1

_C8: x_1 + x_3 >= 1

VARIABLES
0 <= x_0 <= 1 Integer
0 <= x_1 <= 1 Integer
0 <= x_2 <= 1 Integer
0 <= x_3 <= 1 Integer

表示されているのはpulp によって生成された問題の中身である。3.2.2 の数理モデルと1行ずつ見比べてみてほしい。

4.3 求解および結果の解釈¶

In [5]:
# CBC ソルバーで解く
prob.solve(pulp.PULP_CBC_CMD(msg=1))
print("求解結果:", pulp.LpStatus[prob.status])
print(f"最小施設数 z* = {int(pulp.value(prob.objective))}")

# 開設した施設
open_sites = [i for i in I if pulp.value(x[i]) > 0.5]
print("開設候補点番号:", open_sites)
Welcome to the CBC MILP Solver 
Version: 2.10.10 
Build Date: Sep 26 2023 

command line - cbc /tmp/2393882d361d414d9abc85475c92d16d-pulp.mps -timeMode elapsed -branch -printingOptions all -solution /tmp/2393882d361d414d9abc85475c92d16d-pulp.sol (default strategy 1)
At line 2 NAME          MODEL
At line 3 ROWS
At line 13 COLUMNS
At line 43 RHS
At line 52 BOUNDS
At line 57 ENDATA
Problem MODEL has 8 rows, 4 columns and 17 elements
Coin0008I MODEL read with 0 errors
Option for timeMode changed from cpu to elapsed
Continuous objective value is 2 - 0.00 seconds
Cgl0004I processed model has 0 rows, 0 columns (0 integer (0 of which binary)) and 0 elements
Cbc3007W No integer variables - nothing to do
Cuts at root node changed objective from 2 to -1.79769e+308
Probing was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Gomory was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Knapsack was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Clique was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
MixedIntegerRounding2 was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
FlowCover was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
TwoMirCuts was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
ZeroHalf was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)

Result - Optimal solution found

Objective value:                2.00000000
Enumerated nodes:               0
Total iterations:               0
Time (CPU seconds):             0.00
Time (Wallclock seconds):       0.01

Option for printingOptions changed from normal to all
Total time (CPU seconds):       0.01   (Wallclock seconds):       0.02

求解結果: Optimal
最小施設数 z* = 2
開設候補点番号: [1, 2]

「求解結果: Optimal」と表示されているので最適解が見つかっている。最小施設数は2、開設される候補点は1と2 である。すなわち、被覆距離8.5 km ですべての需要点をカバーするには施設が2か所必要であり、1か所ではどう置いてもカバーしきれない(実際、各候補点の被覆関係を見ると、1つの候補点で8つの需要点すべてをカバーできるものは存在しない)。

ここでp-center 問題入門の結果を思い出してほしい。同じ地域で施設を2か所選んだときの最良のサービス距離は $R^* = 8.06$ km であった。被覆距離 $D=8.5$ km は8.06 km より大きいから、施設2か所でカバーできることはp-center の結果とも整合している。逆に $D$ を8.06 km 未満まで縮めると、施設2か所ではカバーできなくなる。LSCP とp-center 問題は「施設数を固定して距離を最小化する」か「距離を固定して施設数を最小化する」かという、表裏の関係にある。

4.4 可視化¶

In [6]:
# 結果の可視化: 開設施設を赤、被覆範囲を円で示す
plt.figure(figsize=(8, 8))

# 被覆範囲の円
ax = plt.gca()
for i in open_sites:
    circle = plt.Circle(coords_fac[i], D, fill=True, alpha=0.1, color='red')
    ax.add_patch(circle)
    circle = plt.Circle(coords_fac[i], D, fill=False, color='red', linestyle='--')
    ax.add_patch(circle)

# 候補点(灰)、開設施設(赤)、需要点(青)
for i in I:
    c = 'red' if i in open_sites else 'gray'
    plt.scatter(*coords_fac[i], c=c, s=120, marker='s', zorder=3)
    plt.text(coords_fac[i][0] + 0.2, coords_fac[i][1] + 0.2, f'cand{i}', fontsize=9)
for j in J:
    plt.scatter(*coords_dem[j], c='tab:blue', s=60, zorder=3)
    plt.text(coords_dem[j][0] + 0.2, coords_dem[j][1] + 0.2, f'dem{j}', fontsize=9)

plt.axis('equal')
plt.grid(True, alpha=0.3)
plt.title(f'LSCP solution (D = {D})')
plt.show()
No description has been provided for this image

赤い四角が開設された施設、破線の円がその被覆範囲(半径 $D$)である。すべての需要点(青)が少なくとも1つの円に含まれていることを確認してほしい。

最後に、被覆距離を小さくしすぎた場合を確かめる。$D = 7$ として同じ問題を解いてみる。

In [7]:
# 被覆距離を小さくした場合(実行不能の例)
D2 = 7.0
N2 = {j: [i for i in I if dist[i][j] <= D2] for j in J}
print("カバーできる候補点が存在しない需要点:", [j for j in J if not N2[j]])

prob2 = pulp.LpProblem("LSCP_small_D", pulp.LpMinimize)
x2 = pulp.LpVariable.dicts('x', I, cat='Binary')
prob2 += pulp.lpSum(x2[i] for i in I)
for j in J:
    prob2 += pulp.lpSum(x2[i] for i in N2[j]) >= 1
prob2.solve(pulp.PULP_CBC_CMD(msg=0))
print("求解結果:", pulp.LpStatus[prob2.status])
カバーできる候補点が存在しない需要点: [6]
求解結果: Infeasible

「求解結果: Infeasible」と表示される。$D=7$ ではどの候補点からもカバーできない需要点(需要点6)が存在するため、制約を満たす解が存在しない。現実の計画でも、サービス水準(被覆距離)を厳しくするほど必要な施設数は増え、行き過ぎれば候補点を増やさない限り実現不能になる。このトレードオフを定量的に示せることがLSCP の実用上の価値である。

5. 発展: 道路ネットワーク上の休憩施設配置(RALP)¶

ここまでのLSCP は、人口のように「地域に固定された需要」を対象としていた。この節では、本研究室で取り組んでいるLSCP の拡張である、道路ネットワーク上の休憩施設配置問題(Rest-Area Location Problem、RALP)を紹介する。

5.1 動く需要への拡張¶

長距離を走る貨物車両の運行計画では、遅延のリスクに備えて余裕時間(バッファ時間)が組み込まれる。遅延が起きなかった場合、車両はこの余裕時間をどこかで消化しなければ目的地にジャストインタイムで到着できず、路上駐車などの問題を引き起こす。そこで「すべての車両が経路上のどこかの休憩施設で余裕時間を消化できる」ことを条件に、配置する休憩施設の数を最小化することを考える。

このとき、需要は地域に固定された人口ではなく、「道路ネットワーク上を移動する車両の余裕時間」である。需要の定義を見直すだけで、問題の構造はLSCP と同じ形になる。

5.2 RALP の定式化¶

車両 $j$ の通過する経路上のノード集合を $I_j$、車両 $j$ が消化すべき余裕時間を $b_j$、ノード $i$ の施設で消化できる時間を $u_i$ とすると、RALP は次のように定式化できる。

$$ \begin{align} \min \ \ & z = \sum_{i \in I} x_i \\ \text{s.t. } \ \ & \sum_{i \in I_j} u_i x_i \geq b_j, \quad \forall j \in J, \\ & x_i \in \{0, 1\}, \quad \forall i \in I. \end{align} $$

LSCP との違いは制約条件だけである。$u_i = 1$、$b_j = 1$ とおけば、この制約は $\sum_{i \in I_j} x_i \geq 1$ となり、LSCP の被覆制約そのものに一致する。つまりRALP はLSCP を「重み付きの被覆」に一般化したモデルであり、需要の集合を「地域上の点」から「ネットワーク上の経路」に読み替えたものといえる。

本リポジトリのresearch/ralp フォルダには、このモデルの実装(ralp.py)とデータ(Ij.csv: 各車両の経路上ノード、bj.csv: 余裕時間、u_i.csv: 各ノードで消化できる時間)が置かれている。

5.3 さらに先へ: 時間軸の導入(RALMTD)¶

RALP では「どの車両がいつ施設を使うか」までは扱えないため、同じ時間帯に多くの車両が1つの施設に集中する可能性がある。そこで時刻を離散化し、車両の移動と施設の容量を時刻ごとに追跡することで、施設配置と各車両の運行スケジュールを同時に決定するモデル(RALMTD)が提案されている。詳しくは次の文献を参照してほしい。

  • Y. Kakimoto and Y. Shimakawa, Rest-area location model for time-driven demands to the expend buffer time of freight vehicles, International Journal of Innovative Computing, Information and Control, vol.18, no.1, pp.15-28, 2022.

6. 課題¶

ある物資を7つのエリアに配達したい。配達員の候補者は10名おり、各候補者が配達可能なエリアと、その候補者にかかる配達コストは次の表のとおりである。最小のコストですべてのエリアに物資を届けるには、どの候補者を採用すべきか。

エリアには1〜7のIDを付ける(名称との対応表も示す)。

エリアID 1 2 3 4 5 6 7
エリア名 A B C D E F G
候補者ID 配達可能エリア(ID) 配達コスト
1 1, 2, 3 200
2 1, 4, 6 280
3 2, 5 175
4 3, 4, 5, 6, 7 560
5 1, 6 205
6 2, 4, 6 245
7 4 80
8 3, 7 195
9 3, 6, 7 265
10 2, 5, 7 190
  1. この問題を第3節の集合被覆モデルに当てはめよ。エリアが「需要」、候補者が「施設サイト」に対応する。目的関数は施設数の合計ではなく、採用する候補者のコストの合計(重み付き)になる点に注意すること。
  2. PuLP で実装して解き、採用すべき候補者と最小コストを求めよ。
  3. 結果を同フォルダの lscp_課題_学籍番号_氏名.docx に整理して提出すること。

7. 注意事項¶

7.1 自分で問題を作る場合には¶

4.1 準備に記載のプログラムのうち、coords_fac、coords_dem、D を変更すると自分でダミー問題を作ることができる。候補点や需要点を増やしたら、被覆距離 $D$ もいろいろ変えて、最小施設数がどう変わるか観察してみるとよい。$D$ を大きくすると施設数は減り、小さくすると施設数が増え、やがて実行不能になるはずである。

7.2 定式化が理解できないときは¶

数理最適化問題の定式化に慣れないうちは、数式だけで考えようとせず、実際の問題をイメージしながら定式化を読み解くことが重要である。小さな問題を手作業で解いてみることが理解への近道である。LSCP であれば、候補点3点、需要点4点程度の問題を紙に書き、被覆関係の表を作って、施設の組合せを全通り試してみるとよい。その結果とプログラムの結果が一致することを確認すれば、定式化とプログラムの両方を確かめたことになる。

これらの手続きを踏んでも理解できない場合には、指導教員まで相談すること。

8. 卒業研究概要への手引き¶

  • このテーマを卒業研究のテーマにしたい場合は、ここの対応をすること(必ず事前に相談すること)。
  • 以下、すべて含めて、A4用紙2枚にまとめること。
  • 厳密に守る必要はないが、文章量の比率を目安にすること(15%とは、A4用紙2枚分の15%という意味)。

1章 はじめに(文章量の比率: 15%)¶

  • 施設配置問題とは何か、集合被覆問題以外にどのような施設配置問題があるのかまとめること。
  • 集合被覆問題やその拡張(RALPなど)に関する先行研究を紹介すること。
  • 施設配置問題を解く際に使われる整数計画法とは何か、まとめること。

2章 集合被覆問題(文章量の比率: 30%)¶

  • 集合被覆問題の数理モデルについて、説明すること。

3章 実験(文章量の比率: 40%)¶

3.1節 概要¶

  • 第6節の課題を解いた過程を説明すること。
  • さらに、需要とサイトの構成を自分で変えた問題を作成して解き、結果を図または表で示すこと。

3.2節 結果と考察¶

  • 最小の施設数(またはコストの合計)がいくつになったか示すこと。
  • その値が何を意味しているか説明すること。
  • 結果から、どのようなことが言えるか、考え説明すること。

4章 おわりに(文章量の比率: 15%)¶

  • 今回の実験に対する感想を記載すること。
  • 例えば、直感で施設を選ぶのと数理モデルにより施設を選ぶのでは、どちらがよさそうか、また良い理由を記載すること。

参考文献¶

参考にした資料を、2〜3件記載すること。以下、書き方の例である。

  • [1] 柿本ほか, XXXに関する分析, XXX学会論文誌, 2020.
  • [2] XXXに関する情報, http://xxx.ddd.ttt.com, 2020年4月20日閲覧

本文中で引用する場合は「柿本らはXXXを実施している [1]。また、〜」のように、どこで引用したのか明白にすること。