Gurobiは「条件を守りながら、一番よい選択を探す計算エンジン」です。
人間が総当たりで考えるのが不可能な組合せや、制約が複雑に絡み合う意思決定を、現実的な時間で解ける形にしてくれます。
まずはこの3点を日本語で説明できるようになると、最適化が一気にわかりやすくなります。
gurobi() を呼ぶ2種類の製品を、限られた資源(原料・作業時間など)の中で、どう作れば利益が最大になるかを考える典型例です。
ポイントは、数式から入るのではなく、「何を決めて」「何を良しとして」「どんなルールを守るか」を先に固めることです。
容量や予算に制限がある中で、品目を「選ぶ/選ばない」で最適な組合せを求めます。
供給地から需要地へ、どれだけ運ぶかを決め、総輸送コストを最小化します。
「誰にどの仕事を担当させるか」を、総時間(または総コスト)最小で決める組合せ最適化です。
割当問題は、ルールを日本語で言えることがすべてです。
Gurobiの出力は、0/1(YES/NO)の形で返ってきます。
したがって、読む側は 「1になっている組合せだけを見る」 で割当結果を正しく把握できます。
割当問題は専用アルゴリズム(ハンガリアン法など)もありますが、Gurobiのような汎用MIPソルバーでも高速に解けるため、実務ではそのまま解いて問題になることはほとんどありません。
"<", ">", "=")エラーは悪いことではなく、モデルを改善するヒントです。原因になっている制約やデータの矛盾を潰していくと、理解が一気に深まります。
記号は「以下(超えない)」「以上(下回らない)」「等号(ちょうど一致)」の3種類だけです。業務の言葉に直せれば、そのままモデルになります。
# 利益最大(A=40万円/単位, B=30万円/単位)
# 原料:Aは2、Bは1 消費(合計100まで)
# 作業時間:Aは1、Bは2 消費(合計80まで)
library(gurobi)
m <- list()
m$modelsense <- "max"
m$obj <- c(40, 30) # 利益(万円): [A, B]
m$A <- matrix(c(
2, 1, # 原料
1, 2 # 作業時間
), nrow = 2, byrow = TRUE)
m$rhs <- c(100, 80)
m$sense <- c("<", "<") # "<" は「以下(超えない)」
m$lb <- c(0, 0) # 0以上
res <- gurobi(m, list(OutputFlag = 0))
res$objval # 最大利益(万円)
res$x # [A, B] の最適生産量
knap_res$x が 1 の品目が「選ばれた品目」# モデルの準備
knap_model <- list()
knap_model$modelsense <- "max"
knap_model$obj <- c(60, 90, 120) # 価値
knap_model$A <- matrix(c(10, 20, 30), # 重さ係数
nrow=1) # 1行(容量制約)
knap_model$rhs <- 50
knap_model$sense <- "<"
knap_model$vtype <- "B" # 全品目を0-1変数に
# 最適化実行
knap_res <- gurobi(knap_model, list())
print(knap_res$objval) # 最適総価値
print(knap_res$x) # 選択された品物 (変数値)
res$x)を見る# 2供給地(S1,S2)→ 3需要地(T1,T2,T3)に配分して輸送コスト最小化
library(gurobi)
# 変数順:S1→T1,S1→T2,S1→T3,S2→T1,S2→T2,S2→T3(計6本)
cost <- c(2, 4, 5,
3, 1, 7)
supply <- c(35, 50) # S1=35, S2=50
demand <- c(30, 25, 30) # T1=30, T2=25, T3=30
m <- list()
m$modelsense <- "min"
m$obj <- cost
A <- matrix(0, nrow = 5, ncol = 6)
# 供給制約(<=)
A[1, 1:3] <- 1
A[2, 4:6] <- 1
# 需要制約(>=)
A[3, c(1,4)] <- 1
A[4, c(2,5)] <- 1
A[5, c(3,6)] <- 1
m$A <- A
m$rhs <- c(supply, demand)
m$sense <- c("<","<", ">",">",">") # "<" 以下 / ">" 以上
m$lb <- rep(0, 6)
res <- gurobi(m, list(OutputFlag = 0))
res$objval
res$x
assign_res$x の中で 1 になっている組合せが「採用された割当」読むコツ:0/1変数はスイッチです。1の場所だけ拾えば割当が確定します。
# 変数 9個: 順に x_A1,x_A2,x_A3, x_B1,x_B2,x_B3, x_C1,x_C2,x_C3
assign_model <- list()
assign_model$modelsense <- "min"
assign_model$obj <- c(8,4,3, 2,6,5, 7,8,4)
# 制約行列 A (6本: Aさん行, Bさん行, Cさん行, Job1列, Job2列, Job3列)
assign_model$A <- matrix(c(
# 各人=1:
1,1,1, 0,0,0, 0,0,0, # Aさん担当数
0,0,0, 1,1,1, 0,0,0, # Bさん担当数
0,0,0, 0,0,0, 1,1,1, # Cさん担当数
# 各仕事=1:
1,0,0, 1,0,0, 1,0,0, # Job1割当数
0,1,0, 0,1,0, 0,1,0, # Job2割当数
0,0,1, 0,0,1, 0,0,1 # Job3割当数
), nrow=6, byrow=TRUE)
assign_model$rhs <- c(1,1,1, 1,1,1)
assign_model$sense <- c("=", "=", "=", "=", "=", "=")
assign_model$vtype <- "B"
assign_res <- gurobi(assign_model, list())
print(assign_res$objval) # 最小総時間
print(assign_res$x) # 各 x_ij の値