容量制約付きの最小木に対する定式化とソルバーによる求解

準備

import networkx as nx
import matplotlib.pyplot as plt
from gurobipy import Model, quicksum, GRB
#from mypulp import Model, quicksum, GRB
from collections import OrderedDict, defaultdict

容量制約付き有向最小木問題

最小木問題において、枝に向きがある場合には、最小有向木問題 (minimum arborescence problem) になる。

この問題は多項式時間で解くことができるが、これに枝の容量制約を付加したものが、容量制約付き有向最小木問題である。 ベンチマーク問題例は、

http://people.brunel.ac.uk/~mastjjb/jeb/orlib/capmstinfo.html

から入手できる。

有向グラフ $D =(N,A)$ と特定の点(根と呼ぶ; $0$ と仮定)が与えられている。他の点は1単位の需要をもち、それは根から選択された枝を経由して運ばなければならない。枝を通過可能なフロー量の上限(容量) $Q$ が与えられているとき、枝上の総費用を最小にする有向木を求めよ。

定式化

  • $D =(N,A)$: 有向グラフ
  • $n$ : 点の数
  • $0$ : グラフの根を表す点
  • $c_{ij}$: 枝 $(i,j)$ の費用
  • $Q$: 枝の容量
  • $x_{ij}$: 枝を使用するとき1、それ以外のとき0
  • $y_{ij}$: 枝上を流れるフロー量
$$ \begin{array}{l l l } minimize & \sum_{i,j} c_{ij} x_{ij} & \\ s.t. & \sum_{i,j} x_{ij} = n-1 & \\ & \sum_{j} x_{ji} = 1 & \forall i \in N \setminus \{0\} \\ & \sum_{j} y_{ji} -\sum_{j} y_{ij} = 1 & \forall i \in N \setminus \{0\} \\ & y_{ij} \leq (Q-1) x_{ij} & \forall (i,j) \in A, i \neq 0 \\ & y_{ij} \leq Q x_{ij} & \forall (i,j) \in A, i = 0 \\ & x_{ij} \in \{0,1\} & \forall (i,j) \in A , j \neq 0 \\ & y_{ij} \geq 0 & \forall (i,j) \in A, j \neq 0 \end{array} $$

データの読み込み

まず,ダウンロードしたデータ capmst1.txt を読み込み,枝のリストのデータを保管しておく. データの保管場所は適宜変更されたい.

with open("../data/capmst/capmst1.txt") as f:
    lines = f.readlines()
n_line = 0
for iter_ in range(20):
    prob = lines[n_line]
    n_line += 1
    n = int(lines[n_line])  # number of nodes
    G = nx.DiGraph()
    n_line += 1
    for i in range(n + 1):
        array = []
        while 1:
            line = lines[n_line].split()
            array.extend(line)
            if len(array) >= n + 1:
                break
            n_line += 1
        for j, cost in enumerate(array):
            if i != j:
                G.add_edge(i, j, weight=int(cost))
        n_line += 1
    n_line += 1
    nx.write_weighted_edgelist(G, "../data/capmst/" + prob[:-6].strip() + ".txt")

例として,tc40-1.txtの問題を読み込んで解いてみる.容量Qは別途設定するが,以下のコードのコメントにあるように,点数nによって変える必要がある.

fname = "../data/capmst/tc40-1.txt"
G = nx.read_weighted_edgelist(fname, create_using=nx.DiGraph, nodetype=int)
n = len(G)

# Q=3, 5, or 10 for n=40
# Q=5, 10, or 20 for n=80

Q = 10
print("n=", n, "Q=", Q)
n= 41 Q= 10
model = Model()
x, y = {}, {}
for (i, j) in G.edges():
    if i != j and j != n - 1:  # ルートであるn-1に入る枝はない
        x[i, j] = model.addVar(vtype="B", name=f"x[{i},{j}]")
        y[i, j] = model.addVar(vtype="C", name=f"y[{i},{j}]")
model.update()

model.addConstr(quicksum(x[i, j] for (i, j) in x) == n - 1)

for j in range(0, n - 1):  # ルート以外の点は入次数が1
    model.addConstr(
        quicksum(x[i, j] for i in range(n) if i != j) == 1, name=f"in_degree[{j}]"
    )
for j in range(0, n - 1):  # ルート以外の点に対するフロー保存式
    model.addConstr(
        quicksum(y[i, j] for i in range(n) if i != j)
        - quicksum(y[j, i] for i in range(0, n - 1) if i != j)
        == 1,
        name=f"flow_conservarion[{j}]",
    )
for i in range(n):
    for j in range(0, n - 1):
        if i != j:
            model.addConstr(x[i, j] <= y[i, j], name=f"lower_bound[{i},{j}]")
            if i == n - 1:  # ルートに入るフロー量はQ以下
                model.addConstr(y[i, j] <= Q * x[i, j], name=f"upper_bound[{i},{j}]")
            else:  # ルート以外の点に入るフロー量はQ-1以下
                model.addConstr(
                    y[i, j] <= (Q - 1) * x[i, j], name=f"upper_bound[{i},{j}]"
                )
model.setObjective(quicksum(G[i][j]["weight"] * x[i, j] for (i, j) in x), GRB.MINIMIZE)
model.optimize()
Gurobi Optimizer version 9.1.1 build v9.1.1rc0 (mac64)
Thread count: 8 physical cores, 16 logical processors, using up to 16 threads
Optimize a model with 3281 rows, 3200 columns and 12760 nonzeros
Model fingerprint: 0x88f4acbb
Variable types: 1600 continuous, 1600 integer (1600 binary)
Coefficient statistics:
  Matrix range     [1e+00, 1e+01]
  Objective range  [1e+01, 1e+02]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 4e+01]
Presolve removed 1 rows and 0 columns
Presolve time: 0.02s
Presolved: 3280 rows, 3200 columns, 11160 nonzeros
Variable types: 1600 continuous, 1600 integer (1600 binary)
Found heuristic solution: objective 1607.0000000

Root relaxation: objective 4.691778e+02, 1790 iterations, 0.04 seconds

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0  469.17778    0   23 1607.00000  469.17778  70.8%     -    0s
H    0     0                     532.0000000  469.17778  11.8%     -    0s
     0     0  485.07553    0   34  532.00000  485.07553  8.82%     -    0s
     0     0  485.07553    0   25  532.00000  485.07553  8.82%     -    0s
H    0     0                     524.0000000  485.07553  7.43%     -    0s
H    0     0                     510.0000000  485.07553  4.89%     -    0s
     0     0  490.74762    0   45  510.00000  490.74762  3.77%     -    0s
H    0     0                     498.0000000  490.74762  1.46%     -    0s
     0     0  496.35996    0   29  498.00000  496.35996  0.33%     -    0s
     0     0  496.35996    0   29  498.00000  496.35996  0.33%     -    0s
     0     0  496.66667    0    8  498.00000  496.66667  0.27%     -    0s

Explored 1 nodes (4660 simplex iterations) in 0.41 seconds
Thread count was 16 (of 16 available processors)

Solution count 5: 498 510 524 ... 1607

Optimal solution found (tolerance 1.00e-04)
Best objective 4.980000000000e+02, best bound 4.980000000000e+02, gap 0.0000%
SolGraph = nx.DiGraph()
for (i, j) in x:
    if x[i, j].X > 0.1:
        SolGraph.add_edge(i, j)
nx.draw(SolGraph)