k-メディアン問題に対する定式化

準備

%matplotlib inline
import random
import math
from gurobipy import Model, quicksum, GRB, multidict
# from mypulp import Model, quicksum, GRB, multidict
import networkx as nx
import matplotlib.pyplot as plt
from amplpy import AMPL, Environment, DataFrame
AMPL_PATH = r"/Users/mikiokubo/Dropbox/My Mac (mikionoiMac-Pro.local)/Documents/ampl/ampl.macos64"

メディアン問題

メディアン問題は,顧客から最も近い施設への距離の「合計」を最小にするように グラフ内の点上または空間内の任意の点から施設を選択する施設配置問題の総称である.

メディアン問題においては,選択される施設の数があらかじめ決められていることが多く, その場合には選択する施設数 $k$ を頭につけて $k$-メディアン問題($k$-median problem)とよばれる. 施設数を表す記号としては基本的にはどんな文字でも良いが, 慣用では $p$ または $k$ を用いることが多いようである. 以下では $k$ を用いることにする.

顧客 $i$ から施設 $j$ への距離を $c_{ij}$ とし,以下の変数を導入する.

$$ x_{ij}= \left\{ \begin{array}{ll} 1 & 顧客 i の需要が施設 j によって満たされるとき \\ 0 & それ以外のとき \end{array} \right. $$$$ y_j = \left\{ \begin{array}{ll} 1 & 施設 j を開設するとき \\ 0 & それ以外のとき \end{array} \right. $$

顧客数を $n$ とし,顧客の集合を $I$,施設の配置可能な点の集合を $J$ とする. 通常の $k$-メディアン問題では,施設の候補地点は顧客上と仮定するため,$I=J=\{1,2,\ldots,n\}$ となる.

上の記号および変数を用いると,$k$-メディアン問題は以下の整数最適化問題 として定式化できる.

$$ \begin{array}{l l l} minimize & \sum_{i \in I} \sum_{j \in J} c_{ij} x_{ij} & \\ s.t. & \sum_{j \in J} x_{ij} =1 & \forall i \in I \\ & \sum_{j \in J} y_{j} = k & \\ & x_{ij} \leq y_j & \forall i \in I, j \in J \\ & x_{ij} \in \{ 0,1 \} & \forall i \in I, j \in J \\ & y_j \in \{ 0,1 \} & \forall j \in J \end{array} $$

Pythonによる求解

def kmedian(I, J, c, k):
    """kmedian -- minimize total cost of servicing
    customers from k facilities
    Parameters:
        - I: set of customers
        - J: set of potential facilities
        - c[i,j]: cost of servicing customer i from facility j
        - k: number of facilities to be used
    Returns a model, ready to be solved.
    """

    model = Model("k-median")
    x, y = {}, {}
    for j in J:
        y[j] = model.addVar(vtype="B", name="y(%s)" % j)
        for i in I:
            x[i, j] = model.addVar(vtype="B", name="x(%s,%s)" % (i, j))
    model.update()

    for i in I:
        model.addConstr(quicksum(x[i, j] for j in J) == 1, "Assign(%s)" % i)
        for j in J:
            model.addConstr(x[i, j] <= y[j], "Strong(%s,%s)" % (i, j))
    model.addConstr(quicksum(y[j] for j in J) == k, "Facilities")

    model.setObjective(quicksum(c[i, j] * x[i, j] for i in I for j in J), GRB.MINIMIZE)

    model.update()
    model.__data = x, y
    return model
def distance(x1, y1, x2, y2):
    return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)

def make_data(n, m, same=True):
    """
    施設と顧客が同じ場合にはsameにTrueを,異なる場合にはFalseを入れる.
    """
    if same == True:
        I = range(n)
        J = range(m)
        x = [random.random() for i in range(max(m, n))]
        y = [random.random() for i in range(max(m, n))]
    else:
        I = range(n)
        J = range(n, n + m)
        x = [random.random() for i in range(n + m)]
        y = [random.random() for i in range(n + m)]
    c = {}
    for i in I:
        for j in J:
            c[i, j] = distance(x[i], y[i], x[j], y[j])

    return I, J, c, x, y
random.seed(67)
n = 300
m = n
I, J, c, x_pos, y_pos = make_data(n, m, same=True)
k = 30
model = kmedian(I, J, c, k)
# model.Params.Threads = 1
model.Params.LogFile = "gurobi.log"
model.optimize()
EPS = 1.0e-6
x, y = model.__data
edges = [(i, j) for (i, j) in x if x[i, j].X > EPS]
facilities = [j for j in y if y[j].X > EPS]
print("Optimal value=", model.ObjVal)
print("Selected facilities:", facilities)
Set parameter Username
Academic license - for non-commercial use only - expires 2023-11-07
Set parameter LogFile to value "gurobi.log"
Gurobi Optimizer version 9.5.2 build v9.5.2rc0 (mac64[x86])
Thread count: 8 physical cores, 16 logical processors, using up to 16 threads
Optimize a model with 90301 rows, 90300 columns and 270300 nonzeros
Model fingerprint: 0xeaee31b2
Variable types: 0 continuous, 90300 integer (90300 binary)
Coefficient statistics:
  Matrix range     [1e+00, 1e+00]
  Objective range  [1e-03, 1e+00]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 3e+01]
Presolve time: 1.14s
Presolved: 90301 rows, 90300 columns, 270300 nonzeros
Variable types: 0 continuous, 90300 integer (90300 binary)
Found heuristic solution: objective 26.3670076

Deterministic concurrent LP optimizer: primal and dual simplex
Showing first log only...

Concurrent spin time: 0.60s

Solved with dual simplex

Root relaxation: objective 1.770454e+01, 4061 iterations, 1.62 seconds (1.25 work units)

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

*    0     0               0      17.7045359   17.70454  0.00%     -    3s

Explored 1 nodes (4061 simplex iterations) in 3.13 seconds (2.36 work units)
Thread count was 16 (of 16 available processors)

Solution count 2: 17.7045 26.367 

Optimal solution found (tolerance 1.00e-04)
Best objective 1.770453589243e+01, best bound 1.770453589243e+01, gap 0.0000%
Optimal value= 17.704535892425152
Selected facilities: [6, 8, 14, 44, 48, 93, 101, 103, 110, 115, 116, 117, 122, 124, 139, 151, 160, 162, 170, 199, 200, 207, 210, 213, 221, 238, 241, 248, 274, 295]
position = {}
for i in range(len(x_pos)):
    position[i] = (x_pos[i], y_pos[i])

G = nx.Graph()
facilities = set(facilities)
unused = set(j for j in J if j not in facilities)
client = set(i for i in I if i not in facilities and i not in unused)
G.add_nodes_from(facilities)
G.add_nodes_from(unused)
for (i, j) in edges:
    if i!=j:
        G.add_edge(i, j)

nx.draw(G, position, with_labels=False, node_color="r", nodelist=facilities, node_size=80)
nx.draw(G, position, with_labels=False, node_color="c", nodelist=unused, node_size=30)

AMPLによる求解

AMPLモデル(kmedian.mod)

param n;  # number or customers
param m;  # number or facilities
param c {0..n-1, 0..m-1};
param k;

var x {0..n-1, 0..m-1} binary;
var y {0..m-1} binary;

minimize cost: sum {i in 0..n-1, j in 0..m-1} c[i,j] * x[i,j];

subject to
Service {i in 0..n-1}: sum {j in 0..m-1} x[i,j] = 1;
Kfacil: sum {j in 0..m-1} y[j] = k;
Activate {i in 0..n-1, j in 0..m-1}: x[i,j] <= y[j];
ampl = AMPL(Environment(AMPL_PATH))
ampl.setOption('solver', 'highs')
ampl.read("../ampl/kmedian.mod")
ampl.param['n'] = n
ampl.param['m'] = n
ampl.param['k'] = k
ampl.param['c'] = c
ampl.solve()

cost = ampl.obj['cost']
print("Objective is:", cost.value())

facilities,edges = [],[]
x_ = ampl.var['x']
y_ = ampl.var['y']
HiGHS 1.2.2:HiGHS 1.2.2: optimal solution; objective 17.70453589
1 branching nodes
Objective is: 17.704535892425145

容量制約付き施設配置問題

容量制約付き施設配置問題(capacitated facility location problem)は,以下のように定義される問題である.

顧客数を $n$, 施設数を $m$ とし, 顧客を $i=1,2,\ldots,n$, 施設を $j=1,2,\ldots,m$ と番号で表すものとする. また,顧客の集合を $I=\{1,2,\ldots,n\}$,施設の集合を $J=\{1,2,\ldots,m\}$ と記す.

顧客 $i$ の需要量を $d_i$,顧客 $i$ と施設 $j$ 間に $1$ 単位の需要が移動するときにかかる 輸送費用を $c_{ij}$,施設 $j$ を開設するときにかかる固定費用を $f_j$,容量を $M_j$ とする.

以下に定義される連続変数 $x_{ij}$ および $0$-$1$ 整数変数 $y_j$ を用いる.

$$ x_{ij}= 顧客 i の需要が施設 j によって満たされる量 $$$$ y_j = \left\{ \begin{array}{ll} 1 & 施設 j を開設するとき \\ 0 & それ以外のとき \end{array} \right. $$

上の記号および変数を用いると,容量制約付き施設配置問題は以下の混合整数最適化問題 として定式化できる.

$$ \begin{array}{l l l } minimize & \sum_{j \in J} f_j y_j +\sum_{i \in I} \sum_{j \in J} c_{ij} x_{ij} & \\ s.t. & \sum_{j \in J} x_{ij} =d_i & \forall i \in I \\ & \sum_{i \in I} x_{ij} \leq M_j y_j & \forall j \in J \\ & x_{ij} \leq d_i y_j & \forall i \in I; j \in J \\ & x_{ij} \geq 0 & \forall i \in I; j \in J \\ & y_j \in \{ 0,1 \} & \forall j \in J \end{array} $$
def flp(I, J, d, M, f, c):
    """flp -- model for the capacitated facility location problem
    Parameters:
        - I: set of customers
        - J: set of facilities
        - d[i]: demand for customer i
        - M[j]: capacity of facility j
        - f[j]: fixed cost for using a facility in point j
        - c[i,j]: unit cost of servicing demand point i from facility j
    Returns a model, ready to be solved.
    """

    model = Model("flp")
    x, y = {}, {}
    for j in J:
        y[j] = model.addVar(vtype="B", name="y(%s)" % j)
        for i in I:
            x[i, j] = model.addVar(vtype="C", name="x(%s,%s)" % (i, j))
    model.update()

    for i in I:
        model.addConstr(quicksum(x[i, j] for j in J) == d[i], "Demand(%s)" % i)

    for j in M:
        model.addConstr(quicksum(x[i, j] for i in I) <= M[j] * y[j], "Capacity(%s)" % j)

    for (i, j) in x:
        model.addConstr(x[i, j] <= d[i] * y[j], "Strong(%s,%s)" % (i, j))

    model.setObjective(
        quicksum(f[j] * y[j] for j in J)
        + quicksum(c[i, j] * x[i, j] for i in I for j in J),
        GRB.MINIMIZE,
    )

    model.update()
    model.__data = x, y
    return model


def make_data():
    I, d = multidict({1: 80, 2: 270, 3: 250, 4: 160, 5: 180})  # demand
    J, M, f = multidict(
        {1: [500, 1000], 2: [500, 1000], 3: [500, 1000]}
    )  # capacity, fixed costs
    c = {
        (1, 1): 4,
        (1, 2): 6,
        (1, 3): 9,  # transportation costs
        (2, 1): 5,
        (2, 2): 4,
        (2, 3): 7,
        (3, 1): 6,
        (3, 2): 3,
        (3, 3): 4,
        (4, 1): 8,
        (4, 2): 5,
        (4, 3): 3,
        (5, 1): 10,
        (5, 2): 8,
        (5, 3): 4,
    }
    return I, J, d, M, f, c
I, J, d, M, f, c = make_data()
model = flp(I, J, d, M, f, c)
model.optimize()

EPS = 1.0e-6
x, y = model.__data
edges = [(i, j) for (i, j) in x if x[i, j].X > EPS]
facilities = [j for j in y if y[j].X > EPS]
print("Optimal value=", model.ObjVal)
print("Facilities at nodes:", facilities)
print("Edges:", edges)
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 23 rows, 18 columns and 63 nonzeros
Model fingerprint: 0x96676946
Variable types: 15 continuous, 3 integer (3 binary)
Coefficient statistics:
  Matrix range     [1e+00, 5e+02]
  Objective range  [3e+00, 1e+03]
  Bounds range     [1e+00, 1e+00]
  RHS range        [8e+01, 3e+02]
Presolve time: 0.00s
Presolved: 23 rows, 18 columns, 63 nonzeros
Variable types: 15 continuous, 3 integer (3 binary)

Root relaxation: objective 5.610000e+03, 10 iterations, 0.00 seconds

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

*    0     0               0    5610.0000000 5610.00000  0.00%     -    0s

Explored 0 nodes (10 simplex iterations) in 0.01 seconds
Thread count was 16 (of 16 available processors)

Solution count 1: 5610 

Optimal solution found (tolerance 1.00e-04)
Best objective 5.610000000000e+03, best bound 5.610000000000e+03, gap 0.0000%
Optimal value= 5610.0
Facilities at nodes: [2, 3]
Edges: [(1, 2), (2, 2), (3, 2), (3, 3), (4, 3), (5, 3)]

非線形施設配置問題

容量制約付き施設配置問題において 倉庫における費用が出荷量の合計の凹費用関数になっている場合を考える.

ここでは施設 $j$ に対する固定費用 $f_j$ のかわりに, 施設 $j$ から輸送される量の合計 $X_j$ に対して以下の凹費用関数がかかるものと仮定する.

$$ f_j \sqrt{X_j} $$

他の記号は,容量制約付き施設配置問題と同じである.

上の記号および変数を用いると,凹費用関数をもつ施設配置問題は以下の混合整数最適化問題 として定式化できる.

$$ \begin{array}{l l l } minimize & \sum_{j \in J} f_j \sqrt{\sum_{j \in J} x_{ij}} +\sum_{i \in I} \sum_{j \in J} c_{ij} x_{ij} & \\ s.t. & \sum_{j \in J} x_{ij} =d_i & \forall i \in I \\ & \sum_{i \in I} x_{ij} \leq M_j y_j & \forall j \in J \\ & x_{ij} \geq 0 & \forall i \in I; j \in J \end{array} $$

まず,凸関数を最小化する問題を線形最適化問題に(近似的に)帰着する方法を考えよう. 凸関数を線形関数の繋げたものとして近似する.このように一部をみれば線形関数であるが, 繋ぎ目では折れ曲がった関数を区分的線形関数(piecewise linear function)と呼ぶ.

最初に,1変数の凸関数$f(x)$を区分的線形関数で近似する方法を考える.

変数 $x$ の範囲を $L \leq x \leq U$ とし, 範囲 $[L,U]$ を $K$ 個の区分 $[a_k,a_{k+1}] (k=0,1,\ldots,K-1)$ に分割する. ここで,$a_k$ は, $$ L= a_0 < a_1 < \cdots < a_{K-1} < a_K =U $$ を満たす点列である. 各点 $a_k$ における関数 $f$ の値を $b_k$ とする. $$ b_k =f(a_k) \ \ k=0,1,\ldots,K $$ 各区分 $[a_k,a_{k+1}]$ に対して,点 $(a_k,b_k)$ と点 $(a_{k+1},b_{k+1})$ を通る線分を引くことによって区分的線形関数を構成する.

線分は端点の凸結合で表現できる. $k$番目の点 $a_k$ に対する変数を$z_k$ とする. いま,$f(x)$ は凸関数であるので,隣り合う2つの添え字 $k, k+1$ に対してだけ $z_k$ が正となるので, $$ f(x) \approx \sum_{k=0}^K b_k z_k $$ $$ x= \sum_{k=0}^K a_k z_k $$ $$ \sum_{k=0}^K z_k=1 $$ $$ z_k \geq 0 \ \ \ \forall k=0,1,\ldots,K $$ が区分的線形近似を与える.

ここで考える平方根関数は凹関数なので,タイプ2の特殊順序集合(Special Ordered Set: SOS)と呼ばれる制約を付加するものである. タイプ2の特殊順序集合は,順序が付けられた集合(順序集合)に含まれる変数のうち, (与えられた順序のもとで)連続するたかだか2つが $0$ でない値をとることを規定する.

def convex_comb_sos(model, a, b):
    """convex_comb_sos -- add piecewise relation with gurobi's SOS constraints
    Parameters:
        - model: a model where to include the piecewise linear relation
        - a[k]: x-coordinate of the k-th point in the piecewise linear relation
        - b[k]: y-coordinate of the k-th point in the piecewise linear relation
    Returns the model with the piecewise linear relation on added variables x, f, and z.
    """
    K = len(a) - 1
    z = {}
    for k in range(K + 1):
        z[k] = model.addVar(
            lb=0, ub=1, vtype="C"
        )  # do not name variables for avoiding clash
    x = model.addVar(lb=a[0], ub=a[K], vtype="C")
    f = model.addVar(lb=-GRB.INFINITY, vtype="C")
    model.update()

    model.addConstr(x == quicksum(a[k] * z[k] for k in range(K + 1)))
    model.addConstr(f == quicksum(b[k] * z[k] for k in range(K + 1)))

    model.addConstr(quicksum(z[k] for k in range(K + 1)) == 1)
    model.addSOS(GRB.SOS_TYPE2, [z[k] for k in range(K + 1)])

    return x, f, z
def flp_nonlinear_sos(I, J, d, M, f, c, K):
    """flp_nonlinear_sos --  use model with SOS constraints
    Parameters:
        - I: set of customers
        - J: set of facilities
        - d[i]: demand for customer i
        - M[j]: capacity of facility j
        - f[j]: fixed cost for using a facility in point j
        - c[i,j]: unit cost of servicing demand point i from facility j
        - K: number of linear pieces for approximation of non-linear cost function
    Returns a model, ready to be solved.
    """
    a, b = {}, {}
    for j in J:
        U = M[j]
        L = 0
        width = U / float(K)
        a[j] = [k * width for k in range(K + 1)]
        b[j] = [f[j] * math.sqrt(value) for value in a[j]]

    model = Model("nonlinear flp -- use model with SOS constraints")
    x = {}
    for j in J:
        for i in I:
            x[i, j] = model.addVar(
                vtype="C", name=f"x(i,j)"
            )  # i's demand satisfied from j
    model.update()

    # total volume transported from plant j, corresponding (linearized) cost, selection variable:
    X, F, z = {}, {}, {}
    for j in J:
        # add constraints for linking piecewise linear part:
        X[j], F[j], z[j] = convex_comb_sos(model, a[j], b[j])
        X[j].ub = M[j]

    # constraints for customer's demand satisfaction
    for i in I:
        model.addConstr(quicksum(x[i, j] for j in J) == d[i], f"Demand(i)")

    for j in J:
        model.addConstr(quicksum(x[i, j] for i in I) == X[j], f"Capacity(j)")

    model.setObjective(
        quicksum(F[j] for j in J) + quicksum(c[i, j] * x[i, j] for j in J for i in I),
        GRB.MINIMIZE,
    )

    model.update()
    model.__data = x, X, F
    return model


def distance(x1, y1, x2, y2):
    return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)


def make_data(n, m, same=True):
    x, y = {}, {}
    if same == True:
        I = range(1, n + 1)
        J = range(1, m + 1)
        for i in range(1, 1 + max(m, n)):  # positions of the points in the plane
            x[i] = random.random()
            y[i] = random.random()
    else:
        I = range(1, n + 1)
        J = range(n + 1, n + m + 1)
        for i in I:  # positions of the points in the plane
            x[i] = random.random()
            y[i] = random.random()
        for j in J:  # positions of the points in the plane
            x[j] = random.random()
            y[j] = random.random()

    f, c, d, M = {}, {}, {}, {}
    total_demand = 0.0
    for i in I:
        for j in J:
            c[i, j] = int(100 * distance(x[i], y[i], x[j], y[j])) + 1
        d[i] = random.randint(1, 10)
        total_demand += d[i]

    total_cap = 0.0
    r = {}
    for j in J:
        r[j] = random.randint(0, m)
        f[j] = random.randint(100, 100 + r[j] * m)
        M[j] = 1 + 100 + r[j] * m - f[j]
        # M[j] = int(total_demand/m) + random.randint(1,m)
        total_cap += M[j]
    for j in J:
        M[j] = int(M[j] * total_demand / total_cap + 1) + random.randint(0, r[j])
        # print "%s\t%s\t%s" % (j,f[j],M[j])

    # print "demand:",total_demand
    # print "capacity:",sum([M[j] for j in J])

    return I, J, d, M, f, c, x, y


def example():
    I, d = multidict({1: 80, 2: 270, 3: 250, 4: 160, 5: 180})  # demand
    J, M, f = multidict(
        {10: [500, 100], 11: [500, 100], 12: [500, 100]}
    )  # capacity, fixed costs
    c = {
        (1, 10): 4,
        (1, 11): 6,
        (1, 12): 9,  # transportation costs
        (2, 10): 5,
        (2, 11): 4,
        (2, 12): 7,
        (3, 10): 6,
        (3, 11): 3,
        (3, 12): 4,
        (4, 10): 8,
        (4, 11): 5,
        (4, 12): 3,
        (5, 10): 10,
        (5, 11): 8,
        (5, 12): 4,
    }
    x_pos = {
        1: 0,
        2: 0,
        3: 0,
        4: 0,
        5: 0,
        10: 2,
        11: 2,
        12: 2,
    }  # positions of the points in the plane
    y_pos = {1: 2, 2: 1, 3: 0, 4: -1, 5: -2, 10: 1, 11: 0, 12: -1}
    return I, J, d, M, f, c, x_pos, y_pos
I, J, d, M, f, c, x_pos, y_pos = example()
K = 4
model = flp_nonlinear_sos(I, J, d, M, f, c, K)
x, X, F = model.__data
model.Params.OutputFlag = 1  # silent/verbose mode
model.optimize()

edges = []
flow = {}
for (i, j) in sorted(x):
    if x[i, j].X > EPS:
        edges.append((i, j))
        flow[(i, j)] = x[i, j].X

print("obj:", model.ObjVal, "\nedges", sorted(edges))
print("flows:", flow)
Parameter OutputFlag unchanged
   Value: 1  Min: 0  Max: 1  Default: 1
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 17 rows, 36 columns and 78 nonzeros
Model fingerprint: 0xe3da8fc6
Model has 3 SOS constraints
Variable types: 36 continuous, 0 integer (0 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+03]
  Objective range  [1e+00, 1e+01]
  Bounds range     [1e+00, 5e+02]
  RHS range        [1e+00, 3e+02]
Presolve added 12 rows and 6 columns
Presolve time: 0.00s
Presolved: 29 rows, 42 columns, 108 nonzeros
Variable types: 33 continuous, 9 integer (9 binary)

Root relaxation: objective 7.573808e+03, 7 iterations, 0.00 seconds

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

     0     0 7573.80780    0    2          - 7573.80780      -     -    0s
H    0     0                    7938.3393289 7573.80780  4.59%     -    0s
     0     0 7757.94556    0    2 7938.33933 7757.94556  2.27%     -    0s

Cutting planes:
  Gomory: 1
  Cover: 1
  MIR: 3
  Flow cover: 5
  RLT: 1
  Relax-and-lift: 1

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

Solution count 1: 7938.34 

Optimal solution found (tolerance 1.00e-04)
Best objective 7.938339328889e+03, best bound 7.938339328889e+03, gap 0.0000%
obj: 7938.33932888946 
edges [(1, 11), (2, 11), (3, 11), (3, 12), (4, 12), (5, 12)]
flows: {(1, 11): 80.0, (2, 11): 270.0, (3, 11): 150.0, (3, 12): 100.0, (4, 12): 160.0, (5, 12): 180.0}

ハブ立地問題

ここでは,$p$-ハブ・メディアン問題 ($p$-hub median problem) に対する定式化を示す.

地点間には輸送量と距離(費用)が与えられており,各地点は,単一のハブに割り当てる必要があり,ハブ間は直送を行うものと仮定する. 目的は,総輸送費用の最小化である.

最初に記号を導入しておく.

  • $n$: 点数
  • $t_{ij}$: 地点 $i,j$ 間の輸送量
  • $d_{ij}$: 地点 $i,j$ 間の距離
  • $p$: 選択するハブの数

輸送費用は,距離に以下のパラメータを乗じて計算する.

  • $\chi$: 地点からハブへの輸送
  • $\alpha$: ハブ間の輸送
  • $\delta$: ハブから地点への輸送

ベンチマーク問題例は,以下のサイトから入手できる.

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

http://grafo.etsii.urjc.es/optsicom/uraphmp/

以下のデータ読み込みのコードでは,上のベンチマーク問題例が "../data/p-hub/" ディレクトリに保管されている場合を想定している. データの保管場所は適宜変更されたい.

folder = "../data/p-hub/"
f = open(folder + "AP40.txt")
data = f.readlines()
f.close()
n = int(data[0].split()[0])
t, d = {}, {}
data_ = []
for row in data[2:]:
    data_.extend(list(map(float, row.split())))
count = 0
for i in range(n):
    for j in range(n):
        t[i, j] = data_[count]
        count += 1
for i in range(n):
    for j in range(n):
        d[i, j] = data_[count]
        count += 1
# 座標の読み込み
folder = "../data/p-hub/"
f = open(folder + "phub1.txt")
data2 = f.readlines()
f.close()
pos = {}
for i, row in enumerate(data2):
    if row == "200\n":
        break
for j in range(200):
    xx, yy = list(map(int, data2[i + 1 + j].split()))
    pos[j] = (xx, yy)

$p$-ハブ・メディアン問題に対する非凸2次関数定式化

最初の定式化は,非凸2次関数を目的関数としたものであるが,数理最適化ソルバーGurobiだと求解可能である.

以下の変数を用いる.

$x_{ik}$: 地点 $i$ がハブ $k$ に割り当てられるとき $1$ の $0$-$1$ 変数; ただし $x_{kk}=1$ のときは 地点 $k$ がハブであることを表す.

この変数を用いると,非凸2次な目的関数をもつ定式化は,以下のように書くことができる.

$$ \begin{array}{l l l} minimize & \sum_{i,j, k, \ell} t_{ij} ( \chi d_{ik} x_{ik} + \alpha d_{k \ell} x_{ik} x_{j\ell} +\delta d_{\ell j} x_{j\ell} ) & \\ s.t. & x_{ik} \leq x_{kk} & \forall i,k \\ & \sum_{k} x_{ik} = 1 & \forall i \\ & \sum_{k} x_{kk} = p & \\ & x_{ik} \in \{0,1\} & \forall i,k \end{array} $$
p = 3
chi, alpha, delta = 3.0, 0.75, 2.0

N = list(range(n))
model = Model("p-hub")

x = {}
for i in N:
    for k in N:
        x[i, k] = model.addVar(vtype="B", name=f"x[{i},{k}]")
model.update()

for i in N:
    model.addConstr(quicksum(x[i, k] for k in N) == 1)

model.addConstr(quicksum(x[k, k] for k in N) == p)

for i in N:
    for k in N:
        if k != i:
            model.addConstr(x[i, k] <= x[k, k])

model.setObjective(
    quicksum(sum(t[i, j] for j in N) * chi * d[i, k] * x[i, k] for i in N for k in N)
    + quicksum(
        sum(t[i, j] for i in N) * delta * d[j, l] * x[j, l] for j in N for l in N
    )
    + quicksum(
        t[i, j] * alpha * d[k, l] * x[i, k] * x[j, l]
        for i in N
        for k in N
        for j in N
        for l in N
    ),
    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 1601 rows, 1600 columns and 4760 nonzeros
Model fingerprint: 0xeddbbabe
Model has 1248000 quadratic objective terms
Variable types: 0 continuous, 1600 integer (1600 binary)
Coefficient statistics:
  Matrix range     [1e+00, 1e+00]
  Objective range  [7e+02, 1e+05]
  QObjective range [1e+00, 1e+04]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 3e+00]
Found heuristic solution: objective 648901.57937
Presolve time: 0.30s
Presolved: 3200 rows, 3199 columns, 1255958 nonzeros
Variable types: 1598 continuous, 1601 integer (1601 binary)

Root simplex log...

Iteration    Objective       Primal Inf.    Dual Inf.      Time
       0    0.0000000e+00   2.624000e+03   0.000000e+00      8s
     485    1.3558416e+05   0.000000e+00   0.000000e+00      8s

Root relaxation: objective 1.355842e+05, 485 iterations, 0.21 seconds
Total elapsed time = 8.10s

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

     0     0 135584.158    0  138 648901.579 135584.158  79.1%     -    8s
H    0     0                    236342.72984 135584.158  42.6%     -    8s
     0     0 136974.028    0  121 236342.730 136974.028  42.0%     -   10s
     0     0 138030.944    0  153 236342.730 138030.944  41.6%     -   10s
     0     0 138055.325    0  148 236342.730 138055.325  41.6%     -   11s
     0     0 138142.167    0  159 236342.730 138142.167  41.6%     -   12s
     0     0 138336.699    0  167 236342.730 138336.699  41.5%     -   12s
     0     0 138375.307    0  168 236342.730 138375.307  41.5%     -   13s
     0     0 138402.326    0  158 236342.730 138402.326  41.4%     -   13s
     0     0 138409.593    0  172 236342.730 138409.593  41.4%     -   13s
     0     0 138524.315    0  154 236342.730 138524.315  41.4%     -   15s
H    0     0                    176721.18652 138524.315  21.6%     -   15s
     0     0 138608.242    0  183 176721.187 138608.242  21.6%     -   15s
     0     0 138613.049    0  180 176721.187 138613.049  21.6%     -   17s
H    0     0                    169916.72664 138613.049  18.4%     -   17s
     0     0 138621.245    0  182 169916.727 138621.245  18.4%     -   17s
     0     0 138622.921    0  182 169916.727 138622.921  18.4%     -   19s
     0     0 138622.921    0  182 169916.727 138622.921  18.4%     -   21s
     0     2 138622.921    0  182 169916.727 138622.921  18.4%     -   21s
H   17    24                    160941.82639 141367.288  12.2%  89.5   22s
   295   188 158471.772   33   96 160941.826 141367.288  12.2%  28.4   25s
H  296   188                    160811.15062 141367.288  12.1%  28.3   25s
H  301   188                    158830.13131 141367.288  11.0%  28.0   25s

Cutting planes:
  Gomory: 9
  MIR: 110
  Zero half: 3
  RLT: 12

Explored 1553 nodes (33626 simplex iterations) in 29.63 seconds
Thread count was 16 (of 16 available processors)

Solution count 7: 158830 160811 160942 ... 648902

Optimal solution found (tolerance 1.00e-04)
Best objective 1.588301313146e+05, best bound 1.588301313146e+05, gap 0.0000%
G = nx.Graph()
for i, k in x:
    if x[i, k].X > 0.001:
        G.add_edge(i, k)
plt.figure()
nx.draw(G, pos=pos, with_labels=True, node_size=100, node_color="yellow")
plt.show()

$p$-ハブ・メディアン問題に対する線形定式化

ここでは,非凸2次関数に対応していない数理最適化ソルバーでも求解可能な定式化を示す.

以下の変数を用いる.

  • $x_{ik}$:地点 $i$ がハブ $k$ に割り当てられるとき $1$ の $0$-$1$ 変数; ただし $z_{kk}=1$ のときは 地点 $k$ がハブであることを表す.
  • $y_{ik\ell}$: 地点 $i$ で発生したものが,ハブ $k$ からハブ $\ell$ に輸送される量
$$ \begin{array}{l l l} minimize & \sum_{i,k} \chi (\sum_{j} t_{ij}) d_{ik} x_{ik} + \sum_{j,\ell} \delta (\sum_{i} t_{ij}) d_{\ell j} x_{j \ell} + \sum_{i,k,\ell } \alpha d_{k \ell} y_{ik\ell } & \\ s.t. & \sum_{k} x_{ik} = 1 & \forall i \\ & x_{ik} \leq x_{kk} & \forall i,k \\ & \sum_{\ell} y_{ik\ell } -\sum_{\ell} y_{i\ell k} = (\sum_{j} t_{ij})x_{ik} - \sum_{j} t_{ij} x_{jk} & \forall i,k \\ & x_{ik} \in \{0,1\} & \forall i,k \\ & y_{ik\ell } \geq 0 & \forall i,k,\ell \\ \end{array} $$

最後の式は,地点 $i$ からハブ $k$ に運ばれた量に対するフロー保存式であり, これによって実数変数 $y_{ik\ell}$ が計算される.

p = 3
chi, alpha, delta = 3.0, 0.75, 2.0

N = list(range(n))
model = Model("p-hub")

x, y = {}, {}
for i in N:
    for k in N:
        x[i, k] = model.addVar(vtype="B", name=f"x[{i},{k}]")
for i in N:
    for k in N:
        for l in N:
            y[i, k, l] = model.addVar(vtype="C", name=f"y[{i},{k},{l}]")
model.update()

for i in N:
    model.addConstr(quicksum(x[i, k] for k in N) == 1)

model.addConstr(quicksum(x[k, k] for k in N) == p)

for i in N:
    for k in N:
        if k != i:
            model.addConstr(x[i, k] <= x[k, k])

for i in N:
    for k in N:
        model.addConstr(
            quicksum(y[i, k, l] for l in N) - quicksum(y[i, l, k] for l in N)
            == (
                sum(t[i, j] for j in N) * x[i, k]
                - quicksum(t[i, j] * x[j, k] for j in N)
            )
        )

model.setObjective(
    quicksum(sum(t[i, j] for j in N) * chi * d[i, k] * x[i, k] for i in N for k in N)
    + quicksum(sum(t[i, j] for i in N) * delta * d[l, j] * x[j, l] for j in N for l in N)
    + quicksum(alpha * d[k, l] * y[i, k, l] for i in N for k in N for l in N),
    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 3201 rows, 65600 columns and 193560 nonzeros
Model fingerprint: 0x85381248
Variable types: 64000 continuous, 1600 integer (1600 binary)
Coefficient statistics:
  Matrix range     [3e-01, 6e+02]
  Objective range  [1e+00, 1e+05]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 3e+00]
Presolve removed 0 rows and 1600 columns
Presolve time: 0.31s
Presolved: 3201 rows, 64000 columns, 193560 nonzeros
Variable types: 62400 continuous, 1600 integer (1600 binary)

Root relaxation: objective 1.574994e+05, 2846 iterations, 0.38 seconds

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

     0     0 157499.357    0  176          - 157499.357      -     -    0s
H    0     0                    209239.21067 157499.357  24.7%     -    0s
H    0     0                    192152.81606 157499.357  18.0%     -    1s
     0     0 157858.476    0  174 192152.816 157858.476  17.8%     -    1s
     0     0 157936.811    0  170 192152.816 157936.811  17.8%     -    2s
     0     0 157937.244    0  170 192152.816 157937.244  17.8%     -    2s
     0     0 157937.244    0  170 192152.816 157937.244  17.8%     -    2s
     0     0 157991.935    0  170 192152.816 157991.935  17.8%     -    2s
     0     0 158076.756    0  172 192152.816 158076.756  17.7%     -    2s
     0     0 158076.756    0  172 192152.816 158076.756  17.7%     -    2s
H    0     0                    181217.38158 158791.481  12.4%     -    3s
     0     0 158791.481    0  172 181217.382 158791.481  12.4%     -    3s
H    0     0                    158830.13131 158793.879  0.02%     -    3s
     0     0 158793.879    0  172 158830.131 158793.879  0.02%     -    3s

Cutting planes:
  MIR: 144
  Zero half: 2
  RLT: 57

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

Solution count 4: 158830 181217 192153 209239 

Optimal solution found (tolerance 1.00e-04)
Best objective 1.588301313146e+05, best bound 1.588301313146e+05, gap 0.0000%
G = nx.Graph()
for i, k in x:
    if x[i, k].X > 0.001:
        G.add_edge(i, k)
plt.figure()
nx.draw(G, pos=pos, with_labels=True, node_size=100, node_color="yellow")
plt.show()

$r$-割当 $p$-ハブ・メディアン問題

$r$-割当 $p$-ハブ・メディアン問題 ($r$-allocation $p$-hub median problem) とは, 各地点が高々 $r$ 個のハブに割当可能と仮定した拡張である. .

まず,定式化に必要な記号を導入しておく.

  • $n$: 点数
  • $t_{ij}$: 地点 $i,j$ 間の輸送量
  • $d_{ij}$: 地点 $i,j$ 間の距離
  • $p$: 選択するハブの数
  • $r$: 各点が接続可能なハブの数の上限

輸送費用は,距離に以下のパラメータを乗じて計算する.

  • $\chi$: 地点からハブへの輸送
  • $\alpha$: ハブ間の輸送
  • $\delta$: ハブから地点への輸送

標準定式化

最初の定式化は変数は多いが,下界は強い標準的な定式化である.

以下の変数を用いる.

  • $f_{ijk\ell}$: 地点 $i$ から地点 $j$ へ輸送する量が,パス $i \rightarrow k \rightarrow \ell \rightarrow j$ を通過する比率
  • $z_{ik}$: 地点 $i$ がハブ $k$ に割り当てられるとき $1$ の $0$-$1$ 変数; ただし $z_{kk}=1$ のときは 地点 $k$ がハブであることを表す.

上の変数を使うと,$r$-割当 $p$-ハブ・メディアン問題は以下のように定式化できる.

$$ \begin{array}{l l l} minimize & \sum_{i,j, k, \ell} t_{ij} ( \chi d_{ik}+ \alpha d_{k \ell} +\delta d_{\ell j} ) f_{ijk \ell} & \\ s.t. & \sum_{k} z_{ik} \leq r & \forall i \\ & z_{ik} \leq z_{kk} & \forall i,k \\ & \sum_{k} z_{kk} = p & \\ & \sum_{k, \ell} f_{ijk\ell} = 1 & \forall i,j \\ & \sum_{\ell} f_{ijk\ell} \leq z_{ik} & \forall i,j,k \\ & \sum_{k} f_{ijk\ell} \leq z_{j\ell} & \forall i,j,\ell \\ & f_{ijk \ell} \geq 0 & \forall i,j, k, \ell \\ & z_{ik} \in \{0,1\} & \forall i,k \end{array} $$
p, r = 3, 2
chi, alpha, delta = 3.0, 0.75, 2.0

N = list(range(n))
model = Model("p-hub")

f, z = {}, {}
for i in N:
    for j in N:
        for k in N:
            for l in N:
                f[i, j, k, l] = model.addVar(vtype="C", name=f"f[{i},{j},{k},{l}]")

for i in N:
    for k in N:
        z[i, k] = model.addVar(vtype="B", name=f"z[{i},{k}]")
model.update()

for i in N:
    model.addConstr(quicksum(z[i, k] for k in N) <= r)

for i in N:
    for k in N:
        if k != i:
            model.addConstr(z[i, k] <= z[k, k])

model.addConstr(quicksum(z[k, k] for k in N) == p)

for i in N:
    for j in N:
        model.addConstr(quicksum(f[i, j, k, l] for k in N for l in N) == 1)

for i in N:
    for j in N:
        for k in N:
            model.addConstr(quicksum(f[i, j, k, l] for l in N) <= z[i, k])

for i in N:
    for j in N:
        for l in N:
            model.addConstr(quicksum(f[i, j, k, l] for k in N) <= z[j, l])

model.setObjective(
    quicksum(
        t[i, j] * (chi * d[i, k] + alpha * d[k, l] + delta * d[l, j]) * f[i, j, k, l]
        for i, j, k, l in f
    ),
    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 131201 rows, 2561600 columns and 7812760 nonzeros
Model fingerprint: 0x2bd673a7
Variable types: 2560000 continuous, 1600 integer (1600 binary)
Coefficient statistics:
  Matrix range     [1e+00, 1e+00]
  Objective range  [2e+00, 2e+04]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 3e+00]
Presolve removed 0 rows and 0 columns (presolve time = 5s) ...
Presolve removed 0 rows and 0 columns (presolve time = 11s) ...
Presolve time: 10.95s
Presolved: 131201 rows, 2561600 columns, 7812760 nonzeros
Variable types: 2560000 continuous, 1600 integer (1600 binary)
Found heuristic solution: objective 269423.15969

Deterministic concurrent LP optimizer: primal simplex, dual simplex, and barrier
Showing barrier log only...

Root barrier log...

Elapsed ordering time = 21s
Ordering time: 23.45s
Elapsed ordering time = 23s
Ordering time: 24.50s

Barrier statistics:
 Dense cols : 1600
 AA' NZ     : 2.821e+06
 Factor NZ  : 1.178e+07 (roughly 1.2 GBytes of memory)
 Factor Ops : 2.352e+09 (less than 1 second per iteration)
 Threads    : 6

                  Objective                Residual
Iter       Primal          Dual         Primal    Dual     Compl     Time
   0   7.02411423e+07 -1.42469880e+06  1.60e+02 0.00e+00  3.43e+01    75s
   1   6.00896171e+07 -1.25989163e+06  1.37e+02 7.35e+01  2.92e+01    75s
   2   2.63905929e+06 -1.06517924e+06  5.31e+00 1.09e-05  1.37e+00    76s
   3   1.28664243e+06 -7.75948111e+05  2.16e+00 6.27e-06  6.20e-01    76s
   4   1.21228751e+06 -7.14751176e+05  1.99e+00 4.81e-06  5.62e-01    77s
   5   9.89374504e+05 -5.83620556e+05  1.48e+00 2.85e-06  4.22e-01    77s
   6   9.59527761e+05 -5.10749901e+05  1.42e+00 1.89e-06  3.83e-01    77s
   7   9.11552962e+05 -4.34673569e+05  1.31e+00 1.03e-06  3.37e-01    78s
   8   7.74072743e+05 -3.66538909e+05  1.01e+00 7.56e-07  2.69e-01    78s
   9   7.22293869e+05 -2.62277858e+05  9.01e-01 2.41e-09  2.19e-01    79s
  10   5.92718150e+05 -1.77670226e+05  6.36e-01 3.45e-07  1.60e-01    80s
  11   5.03850997e+05 -1.05218206e+05  4.66e-01 1.87e-06  1.20e-01    80s
  12   4.29240327e+05 -3.64160666e+04  3.33e-01 3.20e-06  8.79e-02    81s
  13   3.66920057e+05  2.71436380e+04  2.29e-01 4.99e-06  6.17e-02    82s
  14   3.21896978e+05  5.95514593e+04  1.60e-01 7.08e-06  4.69e-02    83s
  15   2.93448442e+05  9.19488130e+04  1.22e-01 5.08e-06  3.53e-02    83s
  16   2.59826917e+05  1.07621051e+05  7.90e-02 8.03e-06  2.68e-02    84s
  17   2.46419879e+05  1.19574988e+05  6.45e-02 8.24e-06  2.22e-02    85s
  18   2.32332916e+05  1.34028955e+05  5.07e-02 9.02e-06  1.71e-02    86s
  19   2.19228340e+05  1.41300173e+05  3.94e-02 6.53e-06  1.35e-02    86s
  20   2.10227595e+05  1.47097007e+05  3.25e-02 3.02e-06  1.09e-02    87s
  21   2.04900210e+05  1.49463400e+05  2.85e-02 1.73e-06  9.56e-03    88s
  22   1.98703695e+05  1.51880322e+05  2.43e-02 4.36e-06  8.06e-03    88s
  23   1.94430926e+05  1.52931545e+05  2.17e-02 3.13e-06  7.13e-03    89s
  24   1.91228400e+05  1.53686263e+05  1.97e-02 2.96e-06  6.45e-03    90s
  25   1.84361150e+05  1.54292201e+05  1.56e-02 2.86e-06  5.17e-03    90s
  26   1.79963755e+05  1.54580459e+05  1.31e-02 2.26e-06  4.36e-03    91s
  27   1.76520991e+05  1.54799406e+05  1.13e-02 2.29e-06  3.73e-03    92s
  28   1.74067631e+05  1.54983636e+05  9.93e-03 4.00e-06  3.28e-03    93s
  29   1.72781231e+05  1.55176423e+05  9.26e-03 4.40e-06  3.02e-03    93s
  30   1.69382621e+05  1.55325001e+05  7.38e-03 3.79e-06  2.41e-03    94s
  31   1.59490086e+05  1.55569835e+05  1.89e-03 1.27e-06  6.79e-04    95s
  32   1.56400863e+05  1.55793442e+05  2.64e-04 1.63e-07  1.06e-04    96s
  33   1.55798864e+05  1.55798298e+05  2.61e-10 2.50e-07  1.08e-07    96s
  34   1.55798858e+05  1.55798298e+05  1.91e-07 2.48e-07  1.07e-07    97s
  35   1.55798321e+05  1.55798305e+05  5.63e-09 5.87e-11  3.11e-09    97s
  36   1.55798321e+05  1.55798305e+05  1.61e-07 2.82e-08  3.10e-09    98s
  37   1.55798321e+05  1.55798305e+05  2.70e-07 2.89e-08  3.10e-09    98s
  38   1.55798322e+05  1.55798305e+05  1.49e-07 2.71e-08  3.10e-09    99s
  39   1.55798321e+05  1.55798305e+05  1.94e-06 7.02e-08  3.10e-09   100s
  40   1.55798321e+05  1.55798305e+05  1.89e-06 6.81e-08  3.10e-09   101s
  41   1.55798321e+05  1.55798305e+05  1.89e-06 6.99e-08  3.10e-09   102s
  42   1.55798320e+05  1.55798305e+05  1.90e-06 1.19e-07  3.10e-09   102s
  43   1.55798321e+05  1.55798305e+05  3.30e-06 1.19e-07  3.10e-09   103s

Barrier performed 43 iterations in 103.34 seconds
Barrier solve interrupted - model solved by another algorithm

Concurrent spin time: 25.83s (can be avoided by choosing Method=3)

Solved with dual simplex

Root relaxation: objective 1.557983e+05, 82147 iterations, 81.62 seconds

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

*    0     0               0    155798.30506 155798.305  0.00%     -  103s

Explored 0 nodes (82147 simplex iterations) in 104.12 seconds
Thread count was 16 (of 16 available processors)

Solution count 2: 155798 269423 

Optimal solution found (tolerance 1.00e-04)
Best objective 1.557983050567e+05, best bound 1.557983050567e+05, gap 0.0000%
G = nx.Graph()
for i, k in z:
    if z[i, k].X > 0.001:
        G.add_edge(i, k)
plt.figure()
nx.draw(G, pos=pos, with_labels=True, node_size=100, node_color="yellow")
plt.show()

コンパクトな定式化

上の定式化は変数の数が $O(n^4)$ であり,強い下界を算出するが,大規模問題例には不向きである. 以下では $O(n^3)$ の変数を用いた定式化を示す.

以下の変数を用いる.

  • $x_{ik}$: 地点 $i$ からハブ $k$ へ輸送する量
  • $X_{i\ell j}$: 地点 $i$ から地点 $j$ への輸送が,ハブ $\ell$ から運ばれる量
  • $y_{ik\ell}$: 地点 $i$ で発生したものが,ハブ $k$ からハブ $\ell$ に輸送される量
  • $z_{ik}$: 地点 $i$ がハブ $k$ に割り当てられるとき $1$ の $0$-$1$ 変数; ただし $z_{kk}=1$ のときは 地点 $k$ がハブであることを表す.

上の変数を使うと,$r$-割当 $p$-ハブ・メディアン問題は以下のように定式化できる.

$$ \begin{array}{l l l} minimize & \sum_{i,k} \chi d_{ik} x_{ik} + \sum_{i,j,\ell} \delta d_{\ell j} X_{i\ell j}+ \sum_{i,k,\ell } \alpha d_{k \ell} y_{ik\ell } & \\ s.t. & \sum_{k} z_{ik} \leq r & \forall i \\ & z_{ik} \leq z_{kk} & \forall i,k \\ & \sum_{k} z_{kk} = p & \\ & \sum_{k} x_{ik} = \sum_{j} t_{ij} & \forall i \\ & x_{ik} \leq (\sum_{j} t_{ij}) z_{ik} & \forall i,k \\ & \sum_{\ell} X_{i\ell j} = t_{ij} & \forall i,j \\ & X_{i\ell j} \leq t_{ij} z_{j\ell } & \forall i,j,\ell \\ & \sum_{\ell} y_{ik\ell } -\sum_{\ell} y_{i\ell k} = x_{ik} - \sum_{j} X_{ikj} & \forall i,k \\ & x_{ik} \geq 0 & \forall i,k \\ & X_{i\ell j} \geq 0 & \forall i,\ell ,j \\ & y_{ik\ell } \geq 0 & \forall i,k,\ell \\ & z_{ik} \in \{0,1\} & \forall i,k \end{array} $$
p, r = 3, 2
chi, alpha, delta = 3.0, 0.75, 2.0

N = list(range(n))
model = Model("p-hub")

x, X, y, z = {}, {}, {}, {}
for i in N:
    for k in N:
        x[i, k] = model.addVar(vtype="C", name=f"x[{i},{k}]")

for i in N:
    for l in N:
        for j in N:
            X[i, l, j] = model.addVar(vtype="C", name=f"X[{i},{l},{j}]")

for i in N:
    for k in N:
        for l in N:
            y[i, k, l] = model.addVar(vtype="C", name=f"y[{i},{k},{l}]")

for i in N:
    for k in N:
        z[i, k] = model.addVar(vtype="B", name=f"z[{i},{k}]")
model.update()

for i in N:
    model.addConstr(quicksum(z[i, k] for k in N) <= r)

for i in N:
    for k in N:
        if k != i:
            model.addConstr(z[i, k] <= z[k, k])

model.addConstr(quicksum(z[k, k] for k in N) == p)

for i in N:
    model.addConstr(quicksum(x[i, k] for k in N) == sum(t[i, j] for j in N))

for i in N:
    for k in N:
        model.addConstr(x[i, k] <= sum(t[i, j] for j in N) * z[i, k])

for i in N:
    for j in N:
        model.addConstr(quicksum(X[i, l, j] for l in N) == t[i, j])

for i in N:
    for j in N:
        for l in N:
            model.addConstr(X[i, l, j] <= t[i, j] * z[j, l])

for i in N:
    for k in N:
        model.addConstr(
            quicksum(y[i, k, l] for l in N)
            + quicksum(X[i, k, j] for j in N)
            - quicksum(y[i, l, k] for l in N)
            - x[i, k]
            == 0
        )

model.setObjective(
    quicksum(chi * d[i, k] * x[i, k] for i in N for k in N)
    + quicksum(delta * d[j, l] * X[i, l, j] for i in N for j in N for l in N)
    + quicksum(alpha * d[k, l] * y[i, k, l] for i in N for k in N for l in N),
    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 70441 rows, 131200 columns and 391960 nonzeros
Model fingerprint: 0x1fffc9e9
Variable types: 129600 continuous, 1600 integer (1600 binary)
Coefficient statistics:
  Matrix range     [3e-01, 6e+02]
  Objective range  [1e+00, 2e+02]
  Bounds range     [1e+00, 1e+00]
  RHS range        [3e-01, 6e+02]
Presolve removed 0 rows and 1600 columns
Presolve time: 0.54s
Presolved: 70441 rows, 129600 columns, 391960 nonzeros
Variable types: 128000 continuous, 1600 integer (1600 binary)

Deterministic concurrent LP optimizer: primal and dual simplex
Showing first log only...


Root simplex log...

Iteration    Objective       Primal Inf.    Dual Inf.      Time
  109354    2.8461050e+05   0.000000e+00   1.806613e+07      5s
  120882    2.8011164e+05   0.000000e+00   3.554569e+07     10s
Concurrent spin time: 0.00s

Solved with dual simplex

Root relaxation: objective 1.552017e+05, 30152 iterations, 10.89 seconds

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

     0     0 155201.689    0  198          - 155201.689      -     -   12s
H    0     0                    215072.82433 155201.689  27.8%     -   12s
H    0     0                    175196.13129 155201.689  11.4%     -   13s
H    0     0                    172861.35789 155201.689  10.2%     -   13s
     0     0 155258.527    0  204 172861.358 155258.527  10.2%     -   15s
H    0     0                    171910.83774 155258.527  9.69%     -   16s
     0     0 155333.323    0  267 171910.838 155333.323  9.64%     -   17s
     0     0 155333.469    0  268 171910.838 155333.469  9.64%     -   17s
     0     0 155333.470    0  268 171910.838 155333.470  9.64%     -   17s
     0     0 155355.893    0  273 171910.838 155355.893  9.63%     -   19s
     0     0 155356.137    0  274 171910.838 155356.137  9.63%     -   20s
     0     0 155356.139    0  274 171910.838 155356.139  9.63%     -   20s
     0     0 155412.521    0  252 171910.838 155412.521  9.60%     -   24s
H    0     0                    171860.07912 155412.521  9.57%     -   25s
     0     0 155423.063    0  268 171860.079 155423.063  9.56%     -   26s
     0     0 155425.029    0  267 171860.079 155425.029  9.56%     -   26s
     0     0 155425.030    0  267 171860.079 155425.030  9.56%     -   26s
     0     0 155457.171    0  249 171860.079 155457.171  9.54%     -   30s
     0     0 155463.488    0  247 171860.079 155463.488  9.54%     -   31s
     0     0 155464.231    0  248 171860.079 155464.231  9.54%     -   31s
     0     0 155464.491    0  251 171860.079 155464.491  9.54%     -   32s
     0     0 155464.505    0  251 171860.079 155464.505  9.54%     -   32s
     0     0 155472.209    0  253 171860.079 155472.209  9.54%     -   33s
     0     0 155472.299    0  253 171860.079 155472.299  9.54%     -   34s
     0     0 155472.318    0  253 171860.079 155472.318  9.54%     -   34s
     0     0 155474.281    0  253 171860.079 155474.281  9.53%     -   35s
     0     0 155474.304    0  253 171860.079 155474.304  9.53%     -   35s
     0     0 155474.468    0  253 171860.079 155474.468  9.53%     -   36s
     0     0 155474.472    0  254 171860.079 155474.472  9.53%     -   37s
     0     0 155474.495    0  252 171860.079 155474.495  9.53%     -   37s
     0     0 155474.495    0  250 171860.079 155474.495  9.53%     -   46s
H    0     0                    166150.07051 155474.495  6.43%     -   47s
H    0     0                    166031.88465 155474.495  6.36%     -   49s
H    0     0                    162592.30477 155474.495  4.38%     -   51s
     0     0 155474.495    0  197 162592.305 155474.495  4.38%     -   62s
     0     0 155474.495    0  200 162592.305 155474.495  4.38%     -   64s
     0     0 155474.495    0  202 162592.305 155474.495  4.38%     -   66s
     0     0 155547.542    0  168 162592.305 155547.542  4.33%     -   67s
     0     0 155547.542    0  251 162592.305 155547.542  4.33%     -   68s
     0     0 155547.542    0  250 162592.305 155547.542  4.33%     -   69s
     0     0 155548.004    0  268 162592.305 155548.004  4.33%     -   69s
     0     0 155548.004    0  250 162592.305 155548.004  4.33%     -   69s
     0     0 155548.004    0  250 162592.305 155548.004  4.33%     -   70s
H    0     0                    156457.33779 155548.004  0.58%     -   70s
H    0     0                    155798.30506 155548.004  0.16%     -   70s

Cutting planes:
  MIR: 146
  Flow cover: 110
  RLT: 4

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

Solution count 10: 155798 156457 162592 ... 215073

Optimal solution found (tolerance 1.00e-04)
Best objective 1.557983050567e+05, best bound 1.557983050567e+05, gap 0.0000%
G = nx.Graph()
for i, k in z:
    if z[i, k].X > 0.001:
        G.add_edge(i, k)
plt.figure()
nx.draw(G, pos=pos, with_labels=True, node_size=100, node_color="yellow")
plt.show()

ロジスティクス・ネットワーク設計問題

実際には,ロジスティクス・ネットワーク全体を最適化する必要がある.以下に簡単な例を示す.

集合・パラメータ・変数を以下に示す.

集合:

  • $Cust$ : 顧客(群)の集合
  • $Dc$ : 倉庫の集合
  • $Plnt$ : 工場の集合
  • $Prod$ : 製品(群)の集合

パラメータ:

需要量,固定費用,取扱量(生産量)上下限の数値は,単位期間(既定値は365日)に変換してあるものとする.

  • $c_{ij}$ : 地点 $ij$ 間の1単位重量あたりの移動費用
  • $w_{p}$ : 製品 $p$ の重量
  • $d_{kp}$ : 顧客 $k$ における製品 $p$ の需要量
  • $LB_j, UB_j$ : 倉庫 $j$ の取扱量の下限と上限;入庫する製品量の下限と上限を表す.
  • $NLB, NUB$ : 開設する倉庫数の下限と上限
  • $f_j$ : 倉庫 $j$ を開設する際の固定費用
  • $v_j$ : 倉庫 $j$ における製品1単位あたりの変動費用
  • $PLB_{ip}, PUB_{ip}$ : 工場 $i$ における製品 $p$ の生産量上限

変数:

  • $x_{ijp}$ : 地点 $ij$ 間の製品 $p$ のフロー量

  • $y_{j}$: 倉庫 $j$ を開設するとき $1$, それ以外のとき $0$ を表す$0$-$1$変数

定式化:

$$ \begin{array}{lll} minimize & \displaystyle\sum_{ i \in Plnt, j \in Dc, p \in Prod} (w_p c_{ij} + v_j) x_{ijp} + & \displaystyle\sum_{j \in Dc, k \in Cust, p \in Prod} w_p c_{jk} x_{jkp} + \displaystyle\sum_{j \in Dc} f_j y_j & \\ s.t. & \displaystyle\sum_{j \in Dc} x_{jkp} = d_{kp} & \forall k \in Cust, p \in Prod \\ & \displaystyle\sum_{ i \in Plnt} x_{ijp} = \sum_{k \in Cust} x_{jkp} & \forall j \in Dc, p \in Prod \\ & x_{jkp} \leq d_{kp} y_j & \forall j \in Dc, k \in Cust, p \in Prod \\ & LB_j y_j \leq \displaystyle\sum_{i \in Plnt, p \in Prod} x_{ijp} \leq UB_j y_j & \forall j \in Dc \\ & \displaystyle\sum_{j \in Dc} x_{ijp} \leq PUB_{ip} & \forall i \in Plnt, p \in Prod \\ & NUB \leq \displaystyle\sum_{j \in Dc} y_j \leq NUB & \end{array} $$

上のモデルの拡張を組み込んだロジスティクス・ネットワーク最適化システムとしてMELOS (MEta Logistic network Optimization System: https://www.logopt.com/melos/ ) が開発されている.