数理最適化超入門
佐賀大学経済学部
2026年6月19日
| 概念 | 数学の言葉 | 通学の例 |
|---|---|---|
| 変えられない条件 | 制約条件 | 始業時刻・体力・費用 |
| 何を良くしたいか | 目的関数 | 所要時間を最小化 |
| 何を決められるか | 決定変数 | ルート・出発時刻 |
最適化とは
制約のもとで、目的を最もよく達成する選択肢を見つけること
消費者問題(ミクロ)
最適化問題(一般形)
ヒント
💡 ミクロ経済学 = 最適化問題の応用だった!
| チョコ(1枚) | クッキー(1袋) | |
|---|---|---|
| 価格 | 150円 | 100円 |
| カロリー | 150 kcal | 300 kcal |
| 満足度 | 4 | 3 |
ノート
ミクロ経済学の「効用最大化問題」とまったく同じ構造です
viewof uSlider = Object.assign(
step === "最適解を表示"
? Inputs.range([1, 22], {value: 17, step: 1, label: "満足度 U = "})
: step === "目的関数を動かす"
? Inputs.range([1, 22], {value: 8, step: 1, label: "満足度 U = "})
: Inputs.range([1, 22], {value: 8, step: 1, label: "満足度 U = ", disabled: true}),
{ id: "uslider-input", style: "font-size: 1.6rem;" }
){
const W = 1200, H = 560; // キャンパスサイズ
const mx = 60, my = 20, mr = 40, mb = 55; // マージン
const pw = W - mx - mr, ph = H - my - mb; // プロット領域のサイズ
const xmax = 9, ymax = 7.5;
const sx = x => mx + (x / xmax) * pw;
const sy = y => my + ph - (y / ymax) * ph;
function utilityClipped(U) {
// 4x+3y=U → y=(U-4x)/3、傾き -4/3
const pts = [];
const y_at_x0 = U / 3;
const x_at_y0 = U / 4;
if (y_at_x0 <= ymax) pts.push([0, y_at_x0]);
else pts.push([(U - 3*ymax)/4, ymax]);
if (x_at_y0 <= xmax) pts.push([x_at_y0, 0]);
else pts.push([xmax, (U - 4*xmax)/3]);
return pts;
}
const feasible = [[0,0],[4,0],[2,3],[0,4],[0,0]];
const showCal = step !== "予算制約のみ";
const showU = step === "目的関数を動かす" || step === "最適解を表示";
const showOpt = step === "最適解を表示";
const U = uSlider;
const svg = d3.create("svg").attr("width", W).attr("height", H)
.style("font-family", "sans-serif").style("font-size", "16px");
const regionPts = showCal ? feasible : [[0,0],[4,0],[0,6],[0,0]];
svg.append("polygon")
.attr("points", regionPts.map(([x,y]) => `${sx(x)},${sy(y)}`).join(" "))
.attr("fill", "#cce5ff").attr("opacity", 0.55);
for(let i=1;i<=8;i++){
// 縦線:y=0からymaxまで
svg.append("line").attr("x1",sx(i)).attr("y1",sy(0)).attr("x2",sx(i)).attr("y2",sy(ymax))
.attr("stroke","#ddd").attr("stroke-width",1);
// 横線:x=0からxmaxまで
svg.append("line").attr("x1",sx(0)).attr("y1",sy(i)).attr("x2",sx(xmax)).attr("y2",sy(i))
.attr("stroke","#ddd").attr("stroke-width",1);
}
svg.append("line").attr("x1",sx(0)).attr("y1",sy(0)).attr("x2",sx(xmax)).attr("y2",sy(0))
.attr("stroke","#333").attr("stroke-width",2.5);
svg.append("line").attr("x1",sx(0)).attr("y1",sy(0)).attr("x2",sx(0)).attr("y2",sy(ymax))
.attr("stroke","#333").attr("stroke-width",2.5);
for(let i=1;i<=8;i++){
svg.append("text").attr("x",sx(i)).attr("y",sy(0)+22).attr("text-anchor","middle")
.attr("font-size","16px").text(i);
svg.append("line").attr("x1",sx(i)).attr("y1",sy(0)).attr("x2",sx(i)).attr("y2",sy(0)+6)
.attr("stroke","#333").attr("stroke-width",1.5);
}
for(let i=1;i<=7;i++){
svg.append("text").attr("x",sx(0)-12).attr("y",sy(i)+5).attr("text-anchor","end")
.attr("font-size","16px").text(i);
svg.append("line").attr("x1",sx(0)).attr("y1",sy(i)).attr("x2",sx(0)-6).attr("y2",sy(i))
.attr("stroke","#333").attr("stroke-width",1.5);
}
svg.append("text").attr("x",sx(xmax)-80).attr("y",sy(0)-10)
.attr("font-size","18px").attr("font-weight","bold").text("x(チョコ)");
svg.append("text").attr("x",sx(0)+10).attr("y",sy(ymax)+15)
.attr("font-size","18px").attr("font-weight","bold").text("y(クッキー)");
// 予算制約: 150x+100y=600 → (0,6)〜(4,0)
svg.append("line")
.attr("x1",sx(0)).attr("y1",sy(6)).attr("x2",sx(4)).attr("y2",sy(0))
.attr("stroke","#1a6fc4").attr("stroke-width",3);
svg.append("text").attr("x",sx(1)).attr("y",sy(5)-14)
.attr("fill","#1a6fc4").attr("font-size","18px").attr("font-weight","bold")
.text("予算制約");
// カロリー制約: 150x+300y=1200 → (0,4)〜(8,0)
if(showCal){
svg.append("line")
.attr("x1",sx(0)).attr("y1",sy(4)).attr("x2",sx(8)).attr("y2",sy(0))
.attr("stroke","#e07b00").attr("stroke-width",3);
svg.append("text").attr("x",sx(5)).attr("y",sy(1.5)-14)
.attr("fill","#e07b00").attr("font-size","18px").attr("font-weight","bold")
.text("カロリー制約");
}
if(showU){
const [[x1,y1],[x2,y2]] = utilityClipped(U);
svg.append("line")
.attr("x1",sx(x1)).attr("y1",sy(y1)).attr("x2",sx(x2)).attr("y2",sy(y2))
.attr("stroke","#22a862").attr("stroke-width",3).attr("stroke-dasharray","8,5");
svg.append("text").attr("x",sx((x1+x2)/2)+10).attr("y",sy((y1+y2)/2)-20)
.attr("fill","#22a862").attr("font-size","16px").attr("font-weight","bold")
.text(`4x + 3y = ${U}`);
}
// 頂点:制約追加時はA,C(グレー)
const pts = showCal
? [
{x:4, y:0, label:"A(4, 0)", opt:false},
{x:0, y:4, label:"C(0, 4)", opt:false},
...(showOpt ? [{x:2, y:3, label:"B(2, 3)", opt:true}] : [])
]
: [
{x:4, y:0, label:"A(4, 0)", opt:false},
{x:0, y:6, label:"C(0, 6)", opt:false}
];
pts.forEach(({x,y,label,opt})=>{
svg.append("circle").attr("cx",sx(x)).attr("cy",sy(y)).attr("r", opt?11:7)
.attr("fill", opt?"#e63946":"#555").attr("stroke","white").attr("stroke-width",2);
svg.append("text").attr("x",sx(x)+15).attr("y",sy(y)+5)
.attr("fill", opt?"#e63946":"#333").attr("font-size","15px")
.attr("font-weight", opt?"bold":"normal").text(label);
});
return svg.node();
}ノート
これが線形計画法の基本定理です
| 分野 | 最適化の問題 |
|---|---|
| 企業経営 | 生産計画/資源配分/メディアミックス |
| 政策 | 予算配分・社会厚生の最大化 |
| 金融 | ポートフォリオ最適化(リスク vs リターン) |
| サプライチェーン | コスト最小の輸送計画 |
| AI・機械学習 | 損失関数の最小化(モデルの学習) |
→ 次々に手法が開発され、多くの問題が解けるようになっています
線形計画(LP)
整数計画(IP)
{
const W = 1100, H = 460;
const mx = 55, my = 20, mr = 30, mb = 50;
const pw = W - mx - mr, ph = H - my - mb;
const xmax = 9, ymax = 5;
const sx = x => mx + (x / xmax) * pw;
const sy = y => my + ph - (y / ymax) * ph;
const svg = d3.create("svg").attr("width", W).attr("height", H)
.style("font-family", "sans-serif");
// 実行可能領域: (0,0),(4,0),(2,3),(0,4)
const region = [[0,0],[4,0],[2,3],[0,4]];
svg.append("polygon")
.attr("points", region.map(([x,y]) => `${sx(x)},${sy(y)}`).join(" "))
.attr("fill","#cce5ff").attr("opacity",0.4);
// 予算制約: 150x+100y=600 → (0,6)〜(4,0)
svg.append("line")
.attr("x1",sx(0)).attr("y1",sy(6)).attr("x2",sx(4)).attr("y2",sy(0))
.attr("stroke","#1a6fc4").attr("stroke-width",1.5).attr("stroke-dasharray","4,3").attr("opacity",0.7);
svg.append("text").attr("x",sx(1.8)).attr("y",sy(3.2))
.attr("fill","#1a6fc4").attr("font-size","13px").text("予算制約");
// カロリー制約: 150x+300y=1200 → (0,4)〜(8,0)
svg.append("line")
.attr("x1",sx(0)).attr("y1",sy(4)).attr("x2",sx(8)).attr("y2",sy(0))
.attr("stroke","#e07b00").attr("stroke-width",1.5).attr("stroke-dasharray","4,3").attr("opacity",0.7);
svg.append("text").attr("x",sx(5)).attr("y",sy(1.5))
.attr("fill","#e07b00").attr("font-size","13px").text("カロリー制約");
// 格子点(x=0〜8, y=0〜4)
for(let i=0;i<=8;i++)
for(let j=0;j<=4;j++){
const inLP = 150*i+100*j<=600 && 150*i+300*j<=1200 && i>=0 && j>=0;
svg.append("circle").attr("cx",sx(i)).attr("cy",sy(j)).attr("r",inLP?6:3)
.attr("fill",inLP?"#1a6fc4":"#ccc").attr("opacity",inLP?0.8:0.4);
}
// 軸
svg.append("line").attr("x1",sx(0)).attr("y1",sy(0)).attr("x2",sx(xmax)).attr("y2",sy(0))
.attr("stroke","#333").attr("stroke-width",2);
svg.append("line").attr("x1",sx(0)).attr("y1",sy(0)).attr("x2",sx(0)).attr("y2",sy(ymax))
.attr("stroke","#333").attr("stroke-width",2);
for(let i=0;i<=8;i++){
svg.append("text").attr("x",sx(i)).attr("y",sy(0)+20).attr("text-anchor","middle")
.attr("font-size","14px").text(i);
}
for(let j=0;j<=4;j++){
svg.append("text").attr("x",sx(0)-10).attr("y",sy(j)+5).attr("text-anchor","end")
.attr("font-size","14px").text(j);
}
// LP最適解 (2,3)
svg.append("circle").attr("cx",sx(2)).attr("cy",sy(3)).attr("r",12)
.attr("fill","none").attr("stroke","#e63946").attr("stroke-width",3);
svg.append("text").attr("x",sx(2)+16).attr("y",sy(3)-10)
.attr("fill","#e63946").attr("font-size","15px").attr("font-weight","bold")
.text("LP最適解 (2,3) ✓ 整数!");
// 軸ラベル
svg.append("text").attr("x",sx(xmax)-20).attr("y",sy(0)-8)
.attr("font-size","15px").attr("font-weight","bold").text("x(チョコ)");
svg.append("text").attr("x",sx(0)+8).attr("y",sy(ymax)+14)
.attr("font-size","15px").attr("font-weight","bold").text("y(クッキー)");
// 凡例
svg.append("circle").attr("cx",sx(6)).attr("cy",sy(4.3)).attr("r",6)
.attr("fill","#1a6fc4").attr("opacity",0.8);
svg.append("text").attr("x",sx(6)+12).attr("y",sy(4.3)+5)
.attr("font-size","14px").text("整数の実行可能解");
return svg.node();
}今回はたまたま LP 最適解が整数 → 一般にはずれる
決定変数:\(x_{ig} \in \{0, 1\}\quad i=\{1,2,\ldots , 8\}\quad g=\{1, 2\}\)
\[x_{ig} = \begin{cases} 1 & \text{学生} i \text{ がグループ} g \text{ に入る} \\ 0 & \text{それ以外} \end{cases}\]
変数の数:8人 × 2グループ = 16個の0/1変数
| g \ i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 1 | \(x_{11}\) | \(x_{21}\) | \(x_{31}\) | \(x_{41}\) | \(x_{51}\) | \(x_{61}\) | \(x_{71}\) | \(x_{81}\) |
| 2 | \(x_{12}\) | \(x_{22}\) | \(x_{32}\) | \(x_{42}\) | \(x_{52}\) | \(x_{62}\) | \(x_{72}\) | \(x_{82}\) |
例えば、グループ1={1, 2, 3, 4}、グループ2={5, 6, 7, 8}のとき、
| g \ i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
| 2 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
| g \ i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 1 | \(x_{11}\) | \(x_{21}\) | \(x_{31}\) | \(x_{41}\) | \(x_{51}\) | \(x_{61}\) | \(x_{71}\) | \(x_{81}\) |
| 2 | \(x_{12}\) | \(x_{22}\) | \(x_{32}\) | \(x_{42}\) | \(x_{52}\) | \(x_{62}\) | \(x_{72}\) | \(x_{82}\) |
グループ1={1, 3, 5, 7}、グループ2={2, 4, 6, 8}のとき、
| g \ i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
| 2 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
制約①:各学生は必ずどちらか1つのグループに所属
\[x_{i1} + x_{i2} = \sum_gx_{ig} = 1 \quad \text{(全員分)}\]
制約②:グループサイズ(上限4人)
\[x_{1g} + x_{2g} + \cdots x_{8g} = \sum_ix_{ig} \leq 4 \quad \text{(グループごと)}\]
目的関数:過去ペアの重複を最小化
補助変数 \(y_{ij} \in \{0,1\}\) を導入:
\[y_{ij} = \begin{cases} 1 & \text{学生}i\text{と}j\text{が今回同じグループに入る} \\ 0 & \text{それ以外} \end{cases}\]
\[\min \sum_{i,j} w_{ij} \cdot y_{ij}\]
\(w_{ij}\):過去に同じグループだった回数 (\(y_{ij}\) と \(x_{ig}\) の関係は制約式で規定)
【練習1】8人・履歴1回・2チーム(4+4)
| チーム1 | チーム2 |
|---|---|
| ? | ? |
スコア(残った過去ペアの数):____ 組
【練習2】8人・履歴2回・2チーム(4+4)
できるだけ過去ペアが少なくなるよう2チームに分けてください。
| チーム1(4人) | チーム2(4人) |
|---|---|
| ? | ? |
スコア:____ 組 ←「これが本当に最小か」自信を持って言えますか?
スコア8を達成するすべての組み合わせ:
| チーム1 | チーム2 | |
|---|---|---|
| ① | A, B, E, F | C, D, G, H |
| ② | A, B, E, G | C, D, F, H |
| ③ | A, B, F, H | C, D, E, G |
| ④ | A, B, G, H | C, D, E, F |
| ⑤ | A, C, E, F | B, D, G, H |
| ⑥ | A, C, E, G | B, D, F, H |
| ⑦ | A, C, F, H | B, D, E, G |
| ⑧ | A, C, G, H | B, D, E, F |
| ⑨ | A, D, F, G | B, C, E, H |
ノート
どれを選んでも同じ「最適」。追加条件(男女比など)で絞ることもできる
\[ \frac{{}_{24}C_{6}\times {}_{18}C_{6}\times {}_{12}C_{6}}{4!} = \text{9,663,972,130} \]
\[\min \sum_{i < j} w_{ij} \cdot y_{ij}\]
\[\text{s.t.} \quad \sum_{g} x_{ig} = 1 \quad \forall i\]
\[\sum_{i} x_{ig} \leq 6 \quad \forall g\]
\[y_{ij} \geq x_{ig} + x_{jg} - 1 \quad \forall i,j,g\]
\[x_{ig},\, y_{ij} \in \{0, 1\}\]
さっきの表で見ていたものを、まとめて書くとこうなります
→ すべて「制約のもとで目的を最小化/最大化する」整数計画問題
→ 1店舗だけ出店する場合とは考え方が変わりますよね。
{
const W = 900, H = 380;
const svg = d3.create("svg").attr("width", W).attr("height", H)
.style("font-family","sans-serif");
const cities = [
{name:"東京", x:720, y:140},
{name:"大阪", x:480, y:220},
{name:"名古屋", x:580, y:190},
{name:"福岡", x:180, y:270},
{name:"札幌", x:730, y:40},
{name:"仙台", x:750, y:110},
{name:"広島", x:330, y:250},
{name:"那覇", x:160, y:360},
];
const route = [0,5,4,1,2,6,3,7];
for(let k=0;k<route.length;k++){
const a = cities[route[k]], b = cities[route[(k+1)%route.length]];
svg.append("line")
.attr("x1",a.x).attr("y1",a.y).attr("x2",b.x).attr("y2",b.y)
.attr("stroke","#1a6fc4").attr("stroke-width",2).attr("opacity",0.6);
}
cities.forEach(c=>{
svg.append("circle").attr("cx",c.x).attr("cy",c.y).attr("r",10)
.attr("fill","#e63946").attr("stroke","white").attr("stroke-width",2);
svg.append("text").attr("x",c.x+13).attr("y",c.y+5)
.attr("font-size","15px").attr("font-weight","bold").text(c.name);
});
return svg.node();
}ヒント
次回:空間最適化の考え方を使って、自転車観光ルートを考えてみよう
tamura.eco.saga-u.ac.jp/gis