経路1
局所探索
有効な答えを一つ作れて、その一部分を変更しながら改善したい。
入口と読む順番を見る →AHCは、制限時間内にできるだけよい答えを求めるコンテストです。「何を保存し、どの順に処理を書くか」を、小さな例と図で学びます。まず共通準備から始め、その後は必要な経路を一つ選んでください。
高校数学とアルゴリズム競プロの基礎を前提にします。全39単元。導入、演習とC++17の完成コードを備えた36単元、二つの実装テンプレートで構成します。
まず一つの経路を選びます。別の方法が必要になったら、他の経路へ戻れます。
↓ 必要な経路を一つ選ぶ
経路1
有効な答えを一つ作れて、その一部分を変更しながら改善したい。
入口と読む順番を見る →経路2
手順や選択を一つずつ決める問題で、途中の一候補に決め切らず先へ進めたい。
入口と読む順番を見る →経路3
隠れた値や未来が分からず、返事や観測を得ながら次の行動を決めたい。
入口と読む順番を見る →↓ どの経路からも、同じ入力で効果を確認する
検証の後は、必要な題材を選ぶ発展や総合練習へ進めます。全体の目次は画面の目次から開けます。
初めてなら1.1で仕事を移す例を読み、1.2〜1.8で状態・制約・評価・最初の答え・保存をそろえます。その後は経路を一つ選び、その経路の「次へ」をたどってください。各経路の入口に、使う題材・処理の流れ・到達点を示しています。
局所探索なら2.2、ビーム探索なら3.2、観測と判断なら4.1で、まず小さく動く実装を確かめられます。経路を一巡したら5.1で検証し、必要な題材や実装部品へ広げます。
各章は「本文+演習」で開きます。図と説明を読みながら、隣の演習で確かめてください。「本文」「演習・コード」で片方だけにも切り替えられます。狭い画面では本文を先に表示します。新しい題材や条件へ変わる単元では問題文を開いて表示し、実装手順は画面上部から開けます。
変数・配列・関数、計算量・全探索・ソート・貪欲法・BFSなどの基礎と、高校数学を前提にします。コード例はC++17です。
詳しく扱うのは、何を状態に持つか、一手で何を更新するか、どの候補や行動を選ぶか、正しさと改善をどう検証するかです。共通準備の内容を既に説明できる場合は、選んだ経路から始め、必要な章へ戻れます。
図と演習はこのページだけで試せます。コピーしたコードの動かし方は実行方法にまとめています。
| 章 | 学ぶこと | 収録状況 |
|---|---|---|
| 1.1 | はじめに:仕事を一つ移す | 導入の図解・確認問題 |
| 1.2 | 制約と評価を分け、実装する条件を整理する | 本文・演習・完成コード |
| 1.3 | 有効な答えと、その検査処理を先に作る | 本文・演習・完成コード |
| 1.4 | 探索で変える情報を、状態として持つ | 本文・演習・完成コード |
| 1.5 | 一手の変更が守る条件を決める | 本文・演習・完成コード |
| 1.6 | 検査と評価を分け、全計算を基準にする | 本文・演習・完成コード |
| 1.7 | 貪欲法で初期解を作る | 本文・演習・完成コード |
| 1.8 | 探索中の状態と、提出する最良解を分ける | 本文・演習・完成コード |
| 章 | 学ぶこと | 収録状況 |
|---|---|---|
| 2.1 | 制約に合う近傍を用意する | 本文・演習・完成コード |
| 2.2 | 山登り法を、コピーして試す形で組む | 本文・演習・完成コード |
| 2.3 | 試した変更を、元どおりに戻す | 本文・演習・完成コード |
| 2.4 | 変わった分だけを計算する | 本文・演習・完成コード |
| 2.5 | 焼きなまし法で、悪化する変更も受け入れる | 本文・演習・完成コード |
| 2.6 | 処理ごとに測り、ボトルネックを見つける | 本文・演習・完成コード |
| 2.7 | 焼きなましの実装テンプレート | 実装テンプレート・完成チェック |
| 章 | 学ぶこと | 収録状況 |
|---|---|---|
| 3.1 | 途中の答えを、何で比べるか考える | 本文・演習・完成コード |
| 3.2 | ビーム探索で、途中の候補を複数残す | 本文・演習・完成コード |
| 3.3 | ビーム探索の履歴を復元し、重複を取り除く | 本文・演習・完成コード |
| 3.4 | ビーム探索のメモリと、多様性を管理する | 本文・演習・完成コード |
| 3.5 | ビーム探索の実装テンプレート | 実装テンプレート・完成チェック |
| 章 | 学ぶこと | 収録状況 |
|---|---|---|
| 4.1 | インタラクティブ問題の入出力と、観測履歴を管理する | 本文・演習・完成コード |
| 4.2 | ベイズ推定で、観測から仮説の確率を更新する | 本文・演習・完成コード |
| 4.3 | 質問の情報量と費用を比べ、測定を選ぶ | 本文・演習・完成コード |
| 4.4 | プレイアウトとUCBで、行動を比較する | 本文・演習・完成コード |
| 章 | 学ぶこと | 収録状況 |
|---|---|---|
| 5.1 | いくつかの入力で、本当によくなったか比べる | 本文・演習・完成コード |
| 5.2 | 最短路を、ヒューリスティック探索の部品にする | 本文・演習・完成コード |
| 5.3 | 大まかな計画を、実際の手順へ変える | 本文・演習・完成コード |
| 章 | 学ぶこと | 収録状況 |
|---|---|---|
| 6.1 | 行き詰まったら、始め方や一部分を変える | 本文・演習・完成コード |
| 6.2 | 訪れる順番を、交換したり逆にしたりする | 本文・演習・完成コード |
| 6.3 | 必要な場所を、選んだもの全部で覆う | 本文・演習・完成コード |
| 6.4 | 物を置いたとき、はみ出しや重なりを調べる | 本文・演習・完成コード |
| 6.5 | 辺の変更と、連結性の検査を組み合わせる | 本文・演習・完成コード |
| 章 | 学ぶこと | 収録状況 |
|---|---|---|
| 6.6 | 大まかに決めてから、細かく直す | 本文・演習・完成コード |
| 6.7 | 作業の順番から、開始・終了の時刻を決める | 本文・演習・完成コード |
| 6.8 | 同じ時刻に、同じ場所へ入らないようにする | 本文・演習・完成コード |
| 6.9 | 繰り返す手順を、短い書き方へまとめる | 本文・演習・完成コード |
| 章 | 学ぶこと | 収録状況 |
|---|---|---|
| 7.1 | 覚えた部品を組み合わせ、一つの問題を解く | 本文・演習・完成コード |
「C++ 全文をコピー」を押し、普段使うC++の編集・実行画面へ貼り付けます。C++17を選んで実行すると、入力を追加しなくても結果が出ます。コードの欄を開く必要はありません。
自分でコードを読みたいときだけ「完成コードを開く」を押してください。画面の設定を変えると、コピーするコードの値も変わります。
g++が使える環境では、「実行一式をコピー」を下で選んだ端末へ貼り付けられます。これはコード本文に加え、保存・コンパイル・実行の指示も含むものです。C++の編集欄へ貼る場合は「C++ 全文をコピー」を使ってください。
実行一式は、一時フォルダへコードを保存し、C++17でコンパイルして実行します。
g++の準備がまだなら、まずページ内の実験から進められます。
ページ内ではJavaScriptで計算します。コピーするC++も、同じ設定で同じ結果になるように作っています。自由に編集したC++をこのページ内で実行する機能はありません。
自動コピーを使えないブラウザでは、全文を選択した画面を表示します。そこからCtrl+Cなどでコピーできます。
各章のコードは、その章の処理を単独で試せる形にしています。共通準備で状態と評価をそろえ、各経路で次の役割を組み合わせます。局所探索では2.2で一つの探索ループになります。
| 役割 | 確認する点 |
|---|---|
| 状態を持つ | 固定入力と変更する情報が分かれているか。追加で保存した情報は、状態から再計算した結果と一致するか。 |
| 一手を適用する | 前提条件を満たさないとき、状態を変えずに失敗するか。 |
| 検査・評価する | 制約を満たす候補だけを比較しているか。全計算で評価値を再現できるか。 |
| 採用・保存する | 採用時に状態と評価値をそろえて更新し、棄却時に変更を残さないか。最良の答えを提出できるか。 |
画面の設定はコード内の定数へ反映されます。まず同じ設定で実行結果を照合し、その後に一つの処理を変更すると、結果が変わった原因を追いやすくなります。
1.1の小例から始め、1.2〜1.8で三つの経路に共通する部品をそろえます。探索方法を選ぶ前に、有効な答えを作り、値を計算し、よかった答えを保存できるようにします。
共通準備 / 1段階目・全8段階
重さ8・7・6・5・4・3の六仕事を、A・B・Cの三人へ分けます。重さは手間の大きさです。各仕事を一人に任せ、負担の偏りを小さくします。
Aの負荷は15、Cは7です。Aの重さ7の仕事をCへ移すと、何が変わるか考えてみましょう。
assign[item] = destination;
ここを見る:assignは担当表です。item=1(重さ7の仕事)、destination=2(C)なら、この一行でAからCへ担当を移します。負荷や評価値は、この変更後の表から求めます。
完成コードの L23 を見る →各人の負荷をLA・LB・LCとし、この問題では二乗和S=LA²+LB²+LC²を小さくします。よさを比べるこの数が評価値です。負荷の総和33はどの割当でも同じなので、総和だけでは比べられません。
変更前:S=15²+11²+7²=395
変更後も同じ式で計算します。演習で、担当と負荷の変化、評価値の増減を確かめてください。
平均11は固定なので、S=(LA−11)²+(LB−11)²+(LC−11)²+363です。二乗和の最小化は、平均からの偏差平方和の最小化と同じです。
8と3、7と4、6と5を組にすると全員11、下限363になります。実際のコンテストでは、指定された採点方法に合わせて評価式を作ります。
次の単元から、状態を持つ → 変更する → 条件と値を確かめる処理を順に実装します。
Aの重さ7の仕事をCへ移すと、各人の負荷と評価値はいくつになりますか。変わらない情報も挙げてください。
変わるのは重さ7の担当と、A・Cの負荷です。変更後は(8,11,14)。Bの負荷11と全体の総和33は変わりません。
変更後:S=8²+11²+14²=381
差分Δ=381−395=−14なので改善です。
負荷も保存する実装では、担当表だけでなくA・Cの負荷も更新します。
共通準備 / 2段階目・全8段階
前の単元と同じ仕事割当です。設定の読み直しは不要です。
前の単元と同じ六仕事・三人の割当問題です。設定の読み直しは不要です。ここから、制約を検査する実装へ進みます。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品重さ7の仕事だけを取り出し、担当者が0人・1人・2人の場合の有効性を判定します。探索や得点計算はまだ行いません。
制約は答えが必ず守る条件、評価は答えのよさを数で比べることです。条件の検査と、よさの計算を別々に作ります。
重さ 8, 7, 6, 5, 4, 3
各仕事の担当者
全仕事が一人ずつに割当済み?
いいえ → 比較しない
負荷 15, 11, 7
二乗和 395 を小さくする
仕事の重さと人数は入力、担当者は自分で決める情報です。各仕事を一人に任せるのが制約、負荷の二乗和を小さくするのが目的です。制約を満たす答えを実行可能解、最小化・最大化する式を目的関数、その値を目的値と呼びます。
担当者ごとに仕事一覧を持つなら、全一覧を通じて各仕事が一回ずつ現れるか調べます。負荷の合計だけでは、重複や欠落を検出できません。
1.4のように各仕事へ担当者IDを一つ持たせれば、重複割当は構造上起こりません。状態の表現によって、必要な検査も変わります。
const int owners = 1;
bool ok = (owners == 1);
ここを見る:この小例で変えるのは、一仕事を担当する人数ownersです。評価値がよくても、okがfalseなら答えとして使えません。
完成コードの L6–7 を見る →演習では一仕事だけ担当者数を0・1・2へ変え、欠落と重複を拒否できるか確かめます。ほかの五仕事は正しいままです。次の1.3で、この検査を答え全体へ広げます。
担当者ごとの仕事一覧で答えを持つ場合、各仕事の重複や欠落は何を調べれば検出できますか?
全員の一覧を横断して、各仕事の出現回数がちょうど1か調べます。担当者ごとの負荷だけを見ても、この制約は確認できません。
出発点探索はまだ書かず、固定した一つの答えについて「使えるか」と「よさ」を判定するところから始めます。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
まず「各仕事を一人に任せる」を一条件として書きます。担当人数を数える処理と、負荷を比べる処理を別の場所へ置きます。
ここで確認担当人数0・1・2の三例で、1だけが有効になる。
内蔵のownersを使い、一条件の判定を出力します。この段階では、得点がよいかを採否の条件へ混ぜません。
ここで確認未担当や二重担当を、負荷が小さいという理由で通していない。
次の1.3では担当表を入力として受け取り、まず長さ、次に各担当者の範囲を調べる形へ移します。
ここで確認制約を追加したら、その条件だけを破る入力を一つ用意できる。
担当表から人数や負荷を集計した後に上限判定を追加します。目的値の関数へ大きな罰点を足す前に、有効性の条件として確定させます。
追加後の確認:上限ちょうどは通り、一つ超えた例は拒否される。
完成コードで見る場所:owners:担当人数の小例 / bool ok:有効性だけを判定する場所
仕事の重さは8,7,6,5,4,3です。各仕事を三人の誰か一人に割り当て、各人の負荷の二乗和を最小化します。仕事を未割当にして値を下げても、有効な答えにはなりません。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:1.1 はじめに:仕事を一つ移す
1.4では、各仕事の担当者を一つだけ保存する表現を使います。表現の選び方によって、毎回調べずに済む制約があります。
訪問順を決める問題でも、「全地点を一度ずつ訪れる」という制約と「総距離を短くする」という目的を分けます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
1.2 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
0人は未担当、2人は二重の担当です。1人だけが条件を満たします。
共通準備 / 3段階目・全8段階
前の章と同じ仕事割当です。設定の読み直しは不要です。
六仕事・三人の入力、各仕事を一人へ任せる条件、負荷の二乗和を小さくする目的は同じです。前の章で理解していれば、問題設定を読み直す必要はありません。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品担当表を一つ出力し、要素数の不足と担当者IDの範囲外を検出します。よい担当表を探す前の、出力と検査の部品です。
ベースラインは、改良の効果を比べる基準となる実装です。まず有効な答えを必ず出す簡単な方法と、その答えの検査を作ります。
担当の欠落
↓
COUNT
個数は正しいがIDが範囲外
↓
PERSON
この表現の制約を満たす
↓
OK
まず assign = {0,0,1,1,2,2} を返します。担当者IDはA=0、B=1、C=2です。これを基準に、後で加える探索の効果を比べます。
一般の入力でも有効解を作れる簡単な方法を用意し、探索が途中で終わっても、その答えを返せるようにします。
vector<int> assign = {0, 0, 1, 1, 2, 2};
ここを見る:要素の位置が仕事ID、値が担当者IDです。この対応を、出力・検査・評価で共通に使います。
完成コードの L18 を見る →担当表の長さが6で、全IDが0〜2なら、各仕事に一人の担当がいます。個数違いは COUNT、範囲外は PERSON、通過は OK を返します。
生成と検査を別にしておけば、初期解や候補の作り方を変えた後も、同じ基準で正しさを確かめられます。
if (assign.size() != 6) {
cout << "check=COUNT\n";
return 0;
}
for (int person : assign) {
if (person < 0 || person >= 3) {
cout << "check=PERSON\n";
return 0;
}
}
ここを見る:仕事の書き忘れと、存在しない担当者を別々に検出します。評価を始める前の入口に、この検査を置きます。
完成コードの L24–33 を見る →正常な表に加え、担当を一つ消した表と、担当者3を含む表を渡します。それぞれ正しい理由で拒否されるか確認してください。正常例だけでは、常にOKを返す不具合を見抜けません。
answer= などは教材用の診断表示です。提出時は指定の出力形式を守り、診断情報は標準エラー出力などへ分けます。
答えを作る処理の中で担当者の範囲を確認していても、答え全体の検査を別に持つ利点は何ですか?
生成方法や変更処理を差し替えても、同じ検査を使えることです。途中の操作が正しくても、最終的な答えに欠落などがないか独立に確認できます。
出発点固定の担当表を出力できる最小プログラムを用意し、その出力を検査できるようにします。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
六仕事の担当表を固定値で持ち、print_arrayで出力します。実問題に移す際は、この出力部分を指定形式に置き換えます。
ここで確認探索なしで一つの有効な答えを出せる。
長さの確認を先に書き、合っている場合だけ各担当者IDを調べます。条件違反を見つけたら理由を返して処理を止めます。
ここで確認要素を一つ削っても範囲外アクセスせずCOUNTになり、担当者3ならPERSONになる。
教材のcheckなどの表示は動作確認用です。提出版では出力関数に答えだけを渡し、検査結果や計測ログは標準エラーへ送ります。
ここで確認検査用ログを増やしても、問題が読む出力形式は変わらない。
mainの長さ・範囲の判定を、担当表を受け取る関数へ移します。その後、候補生成後と最終出力前の両方から呼びます。
追加後の確認:正常例と二種類の不正例が、切り出す前と同じ判定になる。
完成コードで見る場所:print_array:出力する処理 / assign.size():長さを先に調べる場所
最初の答えを [0,0,1,1,2,2] とします。A=0、B=1、C=2です。担当の欠落と範囲外の担当者を含む答えも用意し、検査が区別できることを確かめます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:1.2 制約と評価を分け、実装する条件を整理する
教材のコードは入力を内蔵しています。実際の問題へ移すときは、入力の読取りと出力形式を合わせ、診断用の表示は提出用の標準出力に混ぜません。
訪問順の問題なら、距離の計算とは別に、全地点が一度ずつ含まれていることを検査します。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
1.3 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
COUNTは個数、PERSONは担当者番号の間違いです。正常な答えへ戻すとOKになります。
共通準備 / 4段階目・全8段階
前の章と同じ仕事割当です。設定の読み直しは不要です。
六仕事・三人の入力、各仕事を一人へ任せる条件、負荷の二乗和を小さくする目的は同じです。前の章で理解していれば、問題設定を読み直す必要はありません。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品担当表の一要素を変更します。仕事の重さと、変更できる担当を別のデータとして持ちます。
探索状態は、候補を表し、次の変更を試すために持つ情報です。この章では仕事の担当表を状態とし、負荷も保存する場合との違いを考えます。
仕事ID → 重さ[8,7,6,5,4,3]
仕事ID → 担当者ID[0,0,1,1,2,2]
[15,11,7]
ここでは毎回計算する
weight[job] は固定の重さ、assign[job] は変更する担当者です。候補を作るときはassignだけをコピーします。担当者ごとの仕事一覧や負荷は、重さと担当表から求められます。
const vector<int> weight = {8, 7, 6, 5, 4, 3};
ここを見る:探索で変えるのは担当です。仕事の重さweightは固定し、候補ごとに変更しません。
完成コードの L15 を見る →各仕事に担当者IDを一つ持たせれば、重複割当は起きません。長さとIDの範囲を保ち、仕事1をCへ移すなら assign[1] = 2; とします。
担当者別の一覧も持つ場合は、移動元から削除し、移動先へ追加する更新が必要です。まず担当表だけで実装し、遅い操作が分かってから補助情報を足します。
assign[item] = destination;
ここを見る:担当表assignの一要素だけを変えます。この段階では負荷の配列を持たず、必要になったら担当表から計算します。
完成コードの L23 を見る →演習の負荷は担当表から毎回計算します。負荷も保存する設計なら、移動時に担当表と二人分の負荷を更新し、取消しでも全て戻す必要があります。保存する情報を増やした分だけ、整合性を保つ更新も増えます。
担当表に加えて、担当者ごとの仕事一覧も保存すると、一つの仕事を移すときに何を更新する必要がありますか?
担当表、移動元の一覧、移動先の一覧をそろえて更新する必要があります。取得は速くなりますが、更新漏れが起きる場所も増えます。最初は担当表だけから始めます。
出発点1.3の担当表を使い、一回の代入で答えを変えられる状態にします。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
weightとassignを別々に宣言します。変更前のassignを出力し、どの情報を書き換えるかを一つに絞ります。
ここで確認担当を変えてもweightの内容が変わらない。
探索や乱数を加える前に、itemとdestinationを固定してassign[item]を変更します。
ここで確認変更前後を比べると指定した一要素だけが変わる。
負荷が必要になったらassignとweightから毎回集計します。速さのためにloadを保存するのは、この集計と照合できるようになってからです。
ここで確認同じassignから、いつでも同じ負荷を再計算できる。
初期化時に担当表からloadを作り、一手で変わる二人の負荷も更新します。検証中は毎回、担当表からの再計算と照合します。
追加後の確認:担当表は正しいのにloadだけ古い、という不一致を検出できる。
完成コードで見る場所:weight:固定入力 / assign[item]:状態を変える一行
固定入力は weight=[8,7,6,5,4,3]、探索状態は assign=[0,0,1,1,2,2] です。assign[job] に担当者を保存すれば、一つの仕事が二人に重複して割り当てられることはありません。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:1.2 制約と評価を分け、実装する条件を整理する
2.3・2.4では負荷も保存し、変更と取消しで更新します。まずは情報を重複して保存しない実装を、正しさの基準として作ります。
訪問順ならorder[pos]に地点IDを持ちます。担当表とは添字・値の役割が違うため、変更処理がどちらを扱うかを明確にします。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
1.4 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
番号1は重さ7です。同じ担当者を指定した場合は、代入しても表の数字が変わりません。
共通準備 / 5段階目・全8段階
前の章と同じ仕事割当です。設定の読み直しは不要です。
六仕事・三人の入力、各仕事を一人へ任せる条件、負荷の二乗和を小さくする目的は同じです。前の章で理解していれば、問題設定を読み直す必要はありません。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品一仕事の担当を変える関数を作ります。仕事IDや担当者IDが不正な場合は、変更せずに失敗を返します。
一手は「仕事1をCへ移す」のような一回の変更です。状態に一手を適用する関数を作り、失敗時には状態を変えないようにします。
[0,0,1,1,2,2]
適用する[0,2,1,1,2,2]
拒否する[0,0,1,1,2,2]
移動は仕事IDと移動先IDで指定します。apply は状態と指定を受け取り、適用できたかを返します。どの一手を選ぶかは探索側の役割です。固定の一手列でapplyを検証でき、選び方を乱択へ変えても使い回せます。
仕事IDが0〜5、移動先が0〜2かを確認し、通った場合だけ担当を更新します。失敗時は状態を変えないと決めると、呼出し側の処理が簡単になります。容量などの制約を足す場合も、書換え前に検査するか、失敗時に全変更を取り消します。
bool apply(vector<int>& assign, int item, int destination) {
if (item < 0 || item >= int(assign.size())) return false;
if (destination < 0 || destination >= 3) return false;
assign[item] = destination;
return true;
}
ここを見る:どちらかの番号が範囲外なら、書き換え前にreturnします。失敗を返したときに状態が元のまま、という約束を守ります。
完成コードの L15–21 を見る →仕事6や移動先3では applied=0 となり、状態が変わらないことを確かめます。有効な指定では、指定した担当だけが変わることを確認します。
同じ担当者への移動も、この実装では成功です。ただし状態は変わりません。こうした候補を探索から除くかは、生成側で決めます。
bool applied = apply(assign, item, destination);
ここを見る:appliedが成功・失敗、assignが変更後の状態です。戻り値だけでなく、失敗時にassignが変わっていないことも確かめます。
完成コードの L28 を見る →変更を拒否したとき、戻り値以外に何を確かめる必要がありますか?
状態が変更前と完全に一致することです。一部を書き換えた後で失敗を返すと、呼び出し側が気付かないまま壊れた状態で探索を続けてしまいます。
出発点担当表の一要素を変える処理を、失敗しても状態を壊さない関数へまとめます。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
applyの先頭に仕事IDと担当者IDの範囲判定を書きます。どちらも通るまで担当表へ触れません。
ここで確認仕事ID−1・6、移動先−1・3を渡しても配列外を読まない。
検査の後に代入を置き、成功を返します。呼び出し側は返り値を見て、失敗した操作を評価へ送らないようにします。
ここで確認不正な操作の後も、担当表が変更前と完全一致する。
この関数は有利な変更かを選びません。採否は外側で、コピーした候補の検査・評価を終えてから決めます。
ここで確認悪化するが合法な一手もapply自体は実行できる。
二つの仕事IDを受け取る操作を追加し、両方の範囲を検査してから交換します。移動か交換かを選ぶ処理は呼び出し側へ置きます。
追加後の確認:同じ仕事同士なら変化せず、一方のIDが不正なら両方とも変更されない。
完成コードで見る場所:bool apply:一手の窓口 / bool applied:成功・失敗を受け取る場所
仕事1を担当者2へ移すと、assignは [0,0,1,1,2,2] から [0,2,1,1,2,2] へ変わります。仕事6や担当者3への指定は拒否し、状態を変えません。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章では同じ担当者への移動も受け付けますが、状態は変化しません。探索ではこのような候補を生成時に除く選択もあります。
長方形を動かす処理なら、境界と重なりを確認したうえで更新する、といった前提条件を決めます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
1.5 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
仕事の番号を6、移動先を3へ、それぞれ変えてみましょう。拒否したときに元の表が残るかも見ます。
共通準備 / 6段階目・全8段階
前の章と同じ仕事割当です。設定の読み直しは不要です。
六仕事・三人の入力、各仕事を一人へ任せる条件、負荷の二乗和を小さくする目的は同じです。前の章で理解していれば、問題設定を読み直す必要はありません。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品担当表から負荷と二乗和を全計算します。担当者が欠けている表は、計算へ入る前に拒否します。
全計算は、以前の計算結果を使わず、状態から評価値を計算し直す方法です。担当表から負荷と二乗和を求め、後の高速化の答え合わせに使います。
[0,0,1,1,2,2]
[15,11,7]
225 + 121 + 49 = 395
担当表を検査し、負荷を0から load[assign[job]] += weight[job]; で集計します。過去の負荷や評価値は使いません。同じ表から常に同じ値を返す全計算を、後で差分評価の答え合わせに使います。
vector<int> get_load(const vector<int>& assign) {
vector<int> load(3, 0);
for (int job = 0; job < int(weight.size()); ++job) {
load[assign[job]] += weight[job];
}
return load;
}
ここを見る:毎回loadを0から作るので、以前の候補の値が混ざりません。この全計算を、後で差分評価を検証する基準にします。
完成コードの L25–31 を見る →score は負荷の二乗和です。初期表は395、重さ7の移動後は381、均等なら363。小さい値を改善とし、採用条件と差分の符号をそろえます。
この入力はintで足ります。大きい入力では負荷と二乗和の上限を見積もり、乗算の途中も収まる型を使います。必要なら long long にします。
int score(const vector<int>& assign) {
vector<int> load = get_load(assign);
int value = 0;
for (int total : load) value += total * total;
return value;
}
ここを見る:scoreは担当表を変更せず、負荷の二乗和を返します。この小入力ではintで足りますが、大きな入力では積と合計の上限から型を選びます。
完成コードの L33–38 を見る →未担当−1が残る表は拒否し、NOT_EVALUATED とします。0を返すと、最小化では不正な表が最良に見えてしまいます。未完成の状態を探索する場合は、完成解とは別の評価を設計します。
未割当の状態に評価値0を返すと、この最小化問題では何が起きますか?
本来使えない状態が、全ての有効な答えよりよい候補に見えてしまいます。制約を満たすかどうかは、評価値とは別に扱います。
出発点有効な担当表を一つ渡すと、全体の目的値を返す基準の計算を作ります。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
人数分のloadを0で初期化し、各仕事の重さを担当者へ加えます。途中の集計を使い回さず、呼ぶたびに作り直します。
ここで確認初期表から負荷15・11・7、総和33が得られる。
scoreからget_loadを呼び、各負荷の二乗を足します。最小化か最大化かを呼び出し側とそろえます。
ここで確認初期表395、均等な表363になる。
mainではvalidが偽なら終了し、真の場合だけscoreを呼びます。範囲外の担当者を集計の添字に使わせません。
ここで確認未担当−1の例は、負の良い得点ではなくNOT_EVALUATEDになる。
既存のscoreを基準として残し、追加項を単独で計算する関数を用意してから合算します。公式の目的値と探索用の補助評価は別に返します。
追加後の確認:各項を表示して総和を再現でき、追加項だけが変わる入力を確認できる。
完成コードで見る場所:get_load:負荷の全計算 / score:目的値の全計算
各人の負荷15,11,7から二乗和395を計算します。重さ7を移した状態では381です。一方、未割当の仕事が残る状態は、完成した答えの比較から除きます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:1.5 一手の変更が守る条件を決める
3.1では探索用の評価と最終的な目的値を分けます。未完成の状態を扱う場合にも、完成した答えと同じ基準で比較できるとは限りません。
経路の問題なら、訪問順から全区間の距離を合計する関数を、差分計算の検証用に残します。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
1.6 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
三つの正常例の値は395・381・363です。担当なしの例には、比較する値を付けません。
共通準備 / 7段階目・全8段階
前の章と同じ仕事割当です。設定の読み直しは不要です。
六仕事・三人の入力、各仕事を一人へ任せる条件、負荷の二乗和を小さくする目的は同じです。前の章で理解していれば、問題設定を読み直す必要はありません。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品仕事を一つずつ負荷の小さい人へ割り当て、探索の出発点になる有効解を作ります。
初期解は、改善を始める出発点の答えです。ここでは貪欲法で仕事を一つずつ割り当て、処理順による違いを比べます。
担当を全て−1、負荷を0で始め、各仕事を最も負荷の小さい人へ任せます。未担当を許すのは構築中だけです。完成後に1.6の検査と評価を通します。
一般の入力では、仕事IDを重さでソートした順に処理します。重さと担当表の対応は保ち、処理順だけを変えます。
int person = 0;
for (int p = 1; p < 3; ++p) {
if (load[p] < load[person]) person = p;
}
assign[job] = person;
load[person] += weight[job];
ここを見る:各仕事jobについてこの処理を行います。担当の確定と負荷の更新を続けて行い、次の仕事は更新後の負荷で選びます。
完成コードの L49–55 を見る →負荷が同じなら小さい担当者IDを選びます。構築順と同点規則を固定すれば初期解を再現でき、その後の探索を比較できます。
N仕事・M人なら全員を調べる構築はO(NM)です。今回は三人なので走査で十分です。大きい場合は優先度付きキューも選べますが、構築後に残る探索時間も見て判断します。
vector<int> order = {0, 1, 2, 3, 4, 5};
if (!heavy) reverse(order.begin(), order.end());
ここを見る:この入力ではID順が重い順です。heavyを外すと軽い順へ変わります。担当の選び方を固定して、順序だけの効果を比べられます。
完成コードの L43–44 を見る →重い順では負荷11・11・11で363、軽い順では9・11・13で371です。「次の一手」で割当の違いを追ってください。
この例で重い順が最適でも、一般の入力や探索後の優劣は保証されません。2.2では改善を観察するため、偏りのある固定解から始めます。
この小入力で最適値363が出たことだけで、同じ貪欲法を他の入力でも最適解を求める処理として扱えますか?
扱えません。この構築法に最適性の保証はありません。初期解として使い、その後の探索も含めた結果と計算時間で比較します。
出発点1.6の検査・全評価を残したまま、固定の答えを作る部分を貪欲な構築へ置き換えます。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
assignを未担当−1、loadを0で作り、まず仕事ID順のorderを用意します。未完成な表はscoreへ渡しません。
ここで確認一仕事も割り当てていないとき、全負荷が0になる。
現在のloadから最小の担当者を選び、assignとloadを同時に更新します。同点なら小さいIDを選ぶと決めます。
ここで確認一件追加するたび、負荷の総和がその仕事の重さだけ増える。
orderの全仕事を処理し終えてからvalidとscoreを呼びます。その後で重い順・軽い順にorderだけを変えます。
ここで確認全仕事が一回ずつ処理され、保存したloadと全計算が一致する。
最小の担当者を探すループで、容量を超える候補を除外します。一人も置けない場合は構築失敗を返し、別の順番から再試行します。
追加後の確認:置ける担当者がない例で、−1を残したまま完成と報告しない。
完成コードで見る場所:vector<int> order:処理順 / load[person] +=:割当と負荷をそろえる場所
重い仕事から、現在の負荷が最小の人へ割り当てます。同点ならA・B・Cの順です。最初の8,7,6を配った後、重さ5は負荷6のCへ割り当てます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:1.6 検査と評価を分け、全計算を基準にする
この六つの仕事では重い順で全員11となるため、2.2では改善を観察できるよう、偏りのある1.3の答えから始めます。
被覆でも、未被覆の対象を多く覆う候補を選んで初期解を作り、削除・交換による改善へつなげられます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
1.7 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
重い順で363、軽い順で371です。一手ずつ見て、どの仕事を誰に配ったか説明してみましょう。
共通準備 / 8段階目・全8段階
前の章と同じ仕事割当です。設定の読み直しは不要です。
六仕事・三人の入力、各仕事を一人へ任せる条件、負荷の二乗和を小さくする目的は同じです。前の章で理解していれば、問題設定を読み直す必要はありません。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品順に調べた担当表の中から、最良の表とその評価値を組で保存します。候補の作り方は固定して比較します。
ここでいう最良解は、これまでに見つけた有効な答えのうち最もよいものです。探索をいつ止めても返せるよう、状態と評価値を組で保存します。
現在 395
最良 395
現在 381
最良 381
現在 389
最良 381
current は今調べる状態、best は提出用の最良解です。演習では評価値395・381・389の表を順に調べ、最後の表に替わっても381を残せるか確かめます。悪化する変更を試したり、別の答えからやり直したりする場合にも使えます。
vector<int> best = answers[0];
int best_value = score(best);
ここを見る:最初の有効解を状態と値の組で保存します。後続の候補を一つも調べられなくても、この組を返せます。
完成コードの L41–42 を見る →有効な初期解をbestへコピーし、その値を best_value に保存します。改善時は両方を更新します。値だけ381に替えても、表が395のままでは正しい最良解を返せません。保存した表を全計算し、値との一致を検査します。
if (!keep_best || value < best_value) {
best = current;
best_value = value;
}
ここを見る:通常はkeep_best=trueなので、改善時だけ二つを更新します。falseは毎回上書きする不具合を試す演習用の切替です。
完成コードの L46–49 を見る →一表だけ調べたら395、二表以上なら381が残ります。「よくなったときだけ保存」を外すと、三表目の389で上書きする失敗を確認できます。
まず固定回数で候補列をそろえて検証します。乱数の初期設定値をseedと呼びます。時間で打ち切る版は試行数が環境で変わるため、seedだけでは再現できません。常に最良解を保存し、出力時間を残して終了します。
best_valueだけを381へ更新し、bestの担当表を更新し忘れると、どのような不整合が起きますか?
記録上の最良値と、実際に提出する答えの値が一致しなくなります。保存時に状態と評価値をまとめて更新し、最後に状態から再計算すると確認できます。
出発点候補を作る方法は固定し、複数の答えを受け取って最良の一つを保存する処理だけを作ります。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
bestに最初の担当表をコピーし、その表をscoreで評価してbest_valueへ入れます。
ここで確認探索を0回で終えても、出力可能な答えが残る設計になっている。
候補を評価する場所の直後に比較を置きます。値が小さければ、bestとbest_valueを同じ分岐の中で更新します。
ここで確認381の後に389を調べても、保存されている値は381のまま。
終了時は最後のcurrentではなくbestを出します。検証用にbestを全計算し、best_valueと比べます。
ここで確認数値381だけでなく、その値を出す担当表も保存できている。
一回の探索が終わるごとにこの保存処理を呼びます。再出発するcurrentだけを初期化し、外側のbestは消さないようにします。
追加後の確認:最後の探索が悪い結果でも、以前の最良解を提出できる。
完成コードで見る場所:best_value:保存する評価値 / best = current:答えそのものをコピーする場所
候補の値が395→381→389と変わったとします。最後に調べた候補は389ですが、提出用には381の担当表を残す必要があります。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:1.7 貪欲法で初期解を作る
まず固定回数で動作を確認します。時間制限を使う版では、出力までの余裕を残して探索を打ち切り、常に保存済みの有効な答えを返します。
焼きなましや複数の初期解からの探索でも、最良値とその状態を保存する部分は共通して使えます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
1.8 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
二つ目まで調べれば381が見つかります。毎回上書きする設定では、三つ目で389に戻ってしまいます。
共通準備から選べる経路 · 経路1
有効な答えを一つ作れて、その一部分を変更しながら改善したい。
局所探索は、現在の答えを少しずつ変えて、よい答えを探す方法です。仕事の担当を移したり交換したりする例で、改善だけを採用する山登りから、悪化も確率で採用する焼きなましへ進みます。
共通準備で作った担当表を出発点にします。各試行でも全仕事の担当が決まった状態を持ち、その一部を変更します。
入口:共通準備 1.1〜1.8の状態・制約・評価・答えの保存。共通準備の後、この経路の最初の章から始められます。
到達点:最良の答えを保存しながら探索を回し、採否・差分・取消しの正しさと処理時間を確認できる。
経路1 局所探索 / 1段階目・全7段階
共通準備の仕事割当を使います。基本ルールの読み直しは不要ですが、追加する件数制約を確認してください。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品一仕事の移動と二仕事の交換を試し、条件を守れる候補かを確認します。ここでは一手を作るところまでです。
近傍は一回の変更で作る候補の集まりです。制約を保ち、別の答えへ進める変更を選びます。
A:8,7 / B:6,5 / C:4,3
仕事数 (2,2,2)
A:8 / C:4,3,7
仕事数 (1,2,3)
× 個数の制約を破る
A:8,4 / C:7,3
仕事数 (2,2,2)
✓ 個数の制約を保つ
移動は (item, destination)、交換は (item, other) とします。一手を状態と別に表すと、生成・適用・記録を分けられます。重さ7と4の交換なら、Aの負荷は15→12、Cは7→10になります。
「各人ちょうど2件」では、別の人への移動は必ず不正ですが、交換は件数を保ちます。不正な候補ばかり作ると、検査が正しくても探索は進みません。
負荷の上限などは交換でも破ることがあります。近傍が自動的に保つ条件と、変更後に検査する条件を分けます。
vector<int> candidate = current;
if (swap_move) swap(candidate[item], candidate[other]);
else candidate[item] = destination;
ここを見る:交換は二仕事の担当者を入れ替えるため、各人の担当件数を保てます。移動は一要素だけを書き換えるので、件数が変わる場合があります。
完成コードの L22–24 を見る →演習はコピーに一手を適用し、有効か調べるところまでです。評価と採用は2.2でつなぎます。
同じ仕事同士・同じ担当者の仕事同士の交換や、同じ担当者への移動は無変化です。検証では試し、探索では必要に応じて生成から除きます。
vector<int> counts(3, 0);
for (int person : candidate) ++counts[person];
bool allowed = true;
if (exact_two) {
for (int count : counts) if (count != 2) allowed = false;
}
ここを見る:候補生成と合法性の判定は別処理です。exact_two=trueなら、各人2件でない候補を採用へ渡しません。
完成コードの L25–30 を見る →「一人ちょうど2個」を守るため、不正な移動を全て棄却するだけで探索は進みますか?
別の担当者への移動は全て棄却され、同じ担当者への移動は状態を変えないため、進みません。個数を保つ交換など、別の近傍が必要です。
出発点1.5の一手の適用を使い、現在の答えからどんな候補を作るかを定義します。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
候補をcurrentからコピーし、一仕事の担当を変えます。変更方法の選択と、候補へ適用する処理を分けて書きます。
ここで確認試すだけではcurrentは変わらない。
候補から担当人数を数え、「各人2件」の場合に移動が拒否されることを確認します。
ここで確認合法率が低い原因を、評価値でなく人数の違反として説明できる。
二仕事の担当者を交換する操作を追加します。人数が保たれても、他の制約があれば共通の検査へ通します。
ここで確認移動で行けない担当変更へ、人数を保ったまま進める。
候補を生成する入口だけで、移動と交換を選ぶ割合を変更します。種類ごとに試行数・合法数・採用数を数えます。
追加後の確認:変更の種類を増やした効果と、不正な候補が増えただけの場合を区別できる。
完成コードで見る場所:swap_move:変更の種類の切替 / counts:制約を検査する集計
仕事を一つ移す近傍と、二つの仕事の担当を交換する近傍を比べます。「一人ちょうど2個」の制約を足すと、別の人への移動は使えませんが、交換は使えます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:1.8 探索中の状態と、提出する最良解を分ける
訪問順の区間反転や木の辺の交換も、状態と制約に合わせて近傍を設計する例です。それぞれの題材で更新箇所を具体化します。
訪問順の二地点を交換すれば、各地点が一度ずつ現れる性質は保たれます。ただし訪問の順序制約まで保てるとは限りません。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
2.1 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
最初の設定の交換では、全員二つのままです。移動へ切り替えると、移動元は一つ、移動先は三つになります。
経路1 局所探索 / 2段階目・全7段階
題材は同じ仕事割当ですが、初期設定では「各人ちょうど2件」の制約を外します。変更後の条件を確認し、一手の確認を繰り返す探索へつなぎます。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品移動・交換で候補を作り、条件を守り二乗和が減る場合だけ採用する探索を組みます。
山登り法は、今の答えを少し変え、よくなる変更を採用することを繰り返す探索です。この章では悪化と同点を採用せず、コピー上で候補を試します。
評価値 395
ここから毎回コピーする
改善 → 採用
現在の状態と値を 381 へ
悪化 → 棄却
現在の状態と値は 395 のまま
「山登り」という名前でも、この最小化問題では評価値を下げる向きに進みます。現在の状態をコピーし、変更・検査・全評価を行います。allowed && value < current_value なら採用し、同点は棄却します。コピーと全計算は遅くても検証しやすいため、高速化の比較用に残します。
vector<int> candidate = current;
if (move[0] == 0) candidate[move[1]] = move[2];
else swap(candidate[move[1]], candidate[move[2]]);
ここを見る:moveは「種類・仕事ID・移動先または交換相手」です。candidateだけを変更するため、棄却してもcurrentを戻す処理は要りません。
完成コードの L50–52 を見る →仕事0〜5と移動先A・B・Cの18通り、交換ありなら続けて仕事ペア15通りを試します。一巡したら戻り、指定回数で止めます。
改善はその場で採用するので、次の候補は更新後の状態から作ります。全近傍から最良の一手を選ぶ方式ではありません。順序を固定して動きを追います。
採用ならcurrentとcurrent_valueを両方置き換え、棄却なら触れません。山登り法では現在値は増えないので、増えたら採否か更新を疑います。2.5のように悪化も許す探索では、最良解を別に保存します。
bool accepted = allowed && value < current_value;
if (accepted) {
current = candidate;
current_value = value;
}
ここを見る:allowedで制約、valueで改善を判定します。採用した場合だけ、状態currentと対応する評価値を同時に更新します。
完成コードの L62–66 を見る →「各人2件」で交換を無効にすると、有効な移動がなく395から進みません。交換を有効にして比べてください。試行数を増やす前に、有効で状態を変えられる候補があるかを確認します。
使っている近傍のどの有効な候補も改善しない状態を局所最適と呼びます。全体で最もよい答えとは限らず、別の変更方法や2.5の焼きなまし法を試す理由になります。
候補を棄却した後に、次の候補を作る元の状態と評価値には、どのような条件が必要ですか?
棄却した変更が残らず、その候補を試す前の状態・評価値と一致していることです。コピー方式なら、採用時以外にcurrentへ触らないことで保てます。
出発点一手の適用、候補の検査、全評価を用意し、最初は状態をコピーする山登りループへつなぎます。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
コピー→一手を適用→検査→全評価→改善なら採用、をmainに一回だけ書きます。不正な候補は評価比較を飛ばします。
ここで確認改善候補では表と値が両方更新され、悪化候補では両方とも元のまま。
検証しやすいmovesを順に試すループへ囲みます。この段階では乱数や時間制限を入れず、同点は棄却すると決めます。
ここで確認同じ試行回数なら採否の列を再現でき、現在値は増えない。
交換をmovesへ加え、採用処理は共通のままにします。各人2件の制約を有効にして、合法な候補が得られるか確認します。
ここで確認移動だけで止まった場合でも、交換を加えると探索できる。
候補を選ぶ箇所をseed付き乱数へ置き換えます。固定回数で再現を確認してから、外側の終了条件を時計へ変えます。
追加後の確認:時間版も初期解を必ず保存し、期限が短くても有効な答えを返す。
完成コードで見る場所:vector<vector<int>> moves:一手の一覧 / if (accepted):候補を確定する場所
1.3の偏った担当から始めます。重さ7をAからCへ移す候補は395→381なので採用します。重さ5をBからCへ移す候補は395→405なので棄却します。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:2.1 制約に合う近傍を用意する
正しく動くこの版を基準に、2.3で取消し、2.4で差分計算を導入します。高速化の前後で同じ変更列を試し、結果が一致するか確認できます。
経路や配置でも、コピー・変更・検査・評価・採用の流れは共通です。状態の表現と近傍を題材に合わせて差し替えます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
2.2 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
追加ルールなしでは、移動だけなら18回でも60回でも値365です。交換も使う60回では363へ進みます。次に「一人ちょうど二つ」を加えると、移動だけでは395のまま、交換を使う60回では363です。回数だけでなく、変更の種類も大切だと確かめられます。
経路1 局所探索 / 3段階目・全7段階
前の章と同じ仕事割当です。基本ルールの読み直しは不要です。
同じ仕事割当の、一手を試して元へ戻す部分を取り出します。基本の問題設定を理解していれば、読み直す必要はありません。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品担当表と負荷を直接変更し、不採用のときに両方を元へ戻す部品を作ります。
undo(取消し)は、試した変更を元に戻す処理です。現在の状態を直接変更し、不採用なら記録した旧値を書き戻すことで、全体のコピーを省きます。
担当 0,0,1,1,2,2
負荷 15,11,7
担当Aと、A・Cの負荷を記録する。
担当 0,2,1,1,2,2
負荷 8,11,14
担当表と負荷を一緒に更新する。
担当 0,0,1,1,2,2
負荷 15,11,7
保存した値で両方を戻す。
2.2のコピーを省き、現在の状態を直接変更します。不採用なら旧値を書き戻します。今回は負荷も保存するため、常に load == get_load(assign) を保ちます。
仕事1をAからCへ移すなら、Undo に仕事ID・移動元・移動先・二人の旧負荷を保存します。
変更:assign[1]: 0→2、load[0]: 15→8、load[2]: 7→14
戻す値:担当0、Aの負荷15、Cの負荷7
逆向きに加減算する方法もありますが、まずは旧値をそのまま書き戻す形にします。
const int from = state.assign[item];
Undo log{item, from, to, state.load[from], state.load[to]};
state.assign[item] = to;
state.load[from] -= weight[item];
state.load[to] += weight[item];
return log;
ここを見る:logには変更前の担当者と二人分の負荷を保存します。書き換え後に記録すると、元へ戻すための値を失います。
完成コードの L44–49 を見る →採用なら変更を保持し、棄却ならundoします。演習は取消しの確認用なので、毎回戻します。「負荷も戻す」を外すと表だけ戻り、負荷は8・11・14のままです。表から再計算して不一致を検出します。
void undo(State& state, const Undo& log, bool restore_load) {
state.assign[log.item] = log.from;
if (restore_load) {
state.load[log.from] = log.old_from_load;
state.load[log.to] = log.old_to_load;
}
}
ここを見る:通常はrestore_load=trueです。falseにすると担当表だけが戻り、補助情報loadとの不一致を観察できます。
完成コードの L52–59 を見る →consistent は表と負荷の整合性、restored は変更前との完全一致です。両方1を確認します。別の有効な表になっていても、復元成功ではありません。
before の全コピーは検証用です。異なる担当者への移動でも一致を確認してから、探索ではUndoだけを使います。複数手は逆順に戻し、評価値などの補助情報を追加したら、それらも取消し対象にします。
担当表だけを戻し、各人の合計を戻し忘れると、何が起きますか?
担当表と合計が食い違います。担当表から計算すると395なのに、保存した合計からは381になる、といった誤りが起きます。
出発点2.2のコピー版を比較用に残し、一手の適用と取消しだけを先に往復させます。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
applyが触るassign[item]、load[from]、load[to]を列挙し、変更前の値をUndoへ入れてから書き換えます。
ここで確認記録の作成が変更後になっていない。移動元と移動先が同じ場合も扱える。
undoで記録した値を代入し直します。探索へ入れる前に、各一手についてapply→undoを実行します。
ここで確認担当表・負荷が元のコピーと一致し、get_loadによる全計算とも一致する。
コピー版の候補作成を直接適用に置き換え、検査失敗と評価での棄却の両方からundoを呼びます。採用時は変更を残します。
ここで確認同じ一手列に対する採否と最終状態がコピー版と一致する。
新しい情報を書き換える場所と、Undoへ保存して戻す場所を同時に追加します。状態を比較する検査にもその欄を足します。
追加後の確認:担当表が同じでも補助情報だけ壊れたケースを検出できる。
完成コードで見る場所:struct Undo:取消し用の記録 / void undo:記録を戻す処理
ここからは、各人の負荷も配列へ保存します。重さ7の仕事をAからCへ移す前に、元の担当Aと、A・Cの負荷15・7を記録します。取り消すときは、担当と二人の負荷を元へ戻します。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:2.2 山登り法を、コピーして試す形で組む
変更した値が増えた場合も、その全てを戻す必要があります。複数の手を戻すときは、最後の手から逆順に戻します。
対象を覆う問題では、担当者の合計の代わりに「何個の候補が覆うか」という数も戻します。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
2.3 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
仕事1をCへ移し、負荷の復元を外してください。担当表は戻っても負荷は8・11・14のままです。次に同じ担当者Aへの移動を試すと、不具合が表れません。状態が変わるケースも検証に必要です。
経路1 局所探索 / 4段階目・全7段階
前の章と同じ入力・制約・目的値です。設定の読み直しは不要です。
入力・制約・目的値は前の章と同じです。問題設定を読み直す必要はありません。今回は一手で変わる評価値だけを計算します。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品一仕事の移動について、二乗和の変化だけを計算し、変更前後の全計算と照合します。
差分評価は、一手で影響を受ける項だけから評価値の変化を求める方法です。仕事を移す二人分だけを計算し、全計算と一致するか確かめます。
8² − 15²
−161
11² − 11²
0
14² − 7²
+147
Δ = −161 + 147 = −14
更新後の値 = 395 − 14 = 381
重さwを担当者aからbへ移すと、二人の負荷だけが変わります。a≠bなら、二乗和の差は次の式です。
Δ = {(La−w)²−La²} + {(Lb+w)²−Lb²}
他の項は相殺されます。new_value = old_value + delta と更新し、Δ<0を改善とします。
if (from == to) return 0;
long long delta = square(load[from] - w) - square(load[from]);
if (include_destination) {
delta += square(load[to] + w) - square(load[to]);
}
return delta;
ここを見る:wは移す仕事の重さです。移動元の減少と移動先の増加を足します。include_destination=falseは後者を落とす不具合の実験です。
完成コードの L37–43 を見る →a=bなら何も変わらずΔ=0です。二人分として上の式へ代入せず、delta_move の先頭で0を返します。交換や反転でも、同じ対象や短い区間を確認します。
予測値 predicted_after と全計算 full_after を比べ、改善・悪化・無変化の全てで delta_ok=1 を確かめます。
移動先の項を省くと、初期例はΔ=−161、予測234となり、正しい381とずれます。小さい入力で毎回照合してから、本番用の全計算を減らします。
cout << "predicted_after=" << before + delta << "\n";
cout << "full_after=" << full_after << "\n";
cout << "delta_ok=" << (before + delta == full_after) << "\n";
ここを見る:full_afterは変更後の担当表をscoreで全計算した値です。差分からの予測と一致して初めて、高速な計算を探索へ組み込みます。
完成コードの L62–64 を見る →N仕事・M人の全評価はO(N+M)。負荷を保存した一仕事の移動は、差分・更新ともO(1)です。ただし区間反転などは、距離差分がO(1)でも配列の更新に時間がかかります。
2.3へ組み込むなら、差分を求めてから変更し、棄却時は表と負荷を戻して評価値も元のままにします。
差分式が全計算と一致することを確認できたら、2.6で一試行全体の処理を計測します。状態コピーなど、評価以外に残る処理も比べます。
Bの項11²を計算し直さなくてよいのは、なぜですか?
Bの負荷は変わらず、差分を取るとその項が相殺されるためです。更新対象をA・Cに限定できる根拠になります。
出発点1.6のscoreを正解確認用に残し、候補の採否を変えずに評価計算だけを差分へ置き換えます。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
移動元と移動先の二乗項を列挙し、それぞれ新しい項−古い項を足します。同じ担当者への移動は先に0を返します。
ここで確認重さ7をAからCへ動かす差が−14になる。
候補をコピー上に作り、score(candidate)−score(current)とdelta_moveを全ての仕事・移動先で比較します。
ここで確認移動先の項を外すと不一致を検出し、元と先が同じ場合は差0になる。
一致したら、探索中の候補評価をcurrent_value+deltaへ置き換えます。採用時だけ保存値へdeltaを加え、棄却時は加えません。
ここで確認状態の全評価、保存した評価値、採否が基準版と一致する。
一人から引く重さと加える重さをまとめ、影響する担当者の集合から各項を一回だけ計算します。単純な二移動の差を同じ古い状態で足さないようにします。
追加後の確認:同じ担当者の二仕事を交換する場合と、重さが等しい場合も全計算と一致する。
完成コードで見る場所:delta_move:差だけを求める関数 / full_after:独立した全計算による照合
重さ7の仕事をAからCへ移すと、Aは15→8、Cは7→14になります。評価値の差分は (8²+14²)−(15²+7²)=−14。元の395へ加えると381です。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:2.3 試した変更を、元どおりに戻す
別の題材への応用は、この計算が説明できてから開きます。速い計算を作った後も、全計算を答え合わせ用に残します。
経路では変わった道の費用を、被覆では覆う数が変わる対象を調べます。同じ数式を使うのではなく、変わる部分を見つける手順を使います。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
2.4 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
仕事1をCへ移すと、移動元の寄与は−161、移動先は+147で、差分は−14です。移動先を省くと予測値234になり、全計算381と一致しません。同じ担当者への移動では差分0の専用分岐が働きます。
経路1 局所探索 / 5段階目・全7段階
前の章と同じ問題です。探索の出発点だけ変えます。基本ルールの読み直しは不要です。
問題の入力・制約・目的値は同じなので、読み直す必要はありません。採用判定を変える効果が見えるよう、探索の出発点だけを変えます。
六つの仕事を三人に分担してもらいます。重い仕事ほど担当者の負担が大きくなるため、できるだけ負担が偏らない分け方を探します。仕事を行う順序や開始時刻は、この問題では決めません。
仕事IDは仕事を区別する番号、重さはその仕事の負担です。担当者の番号とは別物です。入力として仕事の重さが決まっており、答えとして各仕事の担当者を選びます。
| 仕事ID | 重さ | ある答えでの担当 |
|---|---|---|
| 0 | 8 | A(担当者ID 0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
| 4 | 4 | C(2) |
| 5 | 3 | C(2) |
この表の答えは[0,0,1,1,2,2]です。配列の左から仕事0、仕事1、…の担当者を並べています。例えば二つ目の0は「重さ0」ではなく「仕事1をAへ任せる」という意味です。
Aは8+7=15、Bは6+5=11、Cは4+3=7を担当します。評価値は15²+11²+7²=395です。全仕事の重さの合計33は配り方によらないため、負荷の総和だけでは答えのよさを比べられません。
仕事1をAからCへ移すと答えは[0,2,1,1,2,2]、負荷は(8,11,14)、評価値は381になります。通常のルールでは395よりよい答えです。さらに8+3、7+4、6+5と分ければ全員11、評価値363にできます。
ここで間違えやすいこと全仕事を一人ずつへ割り当てることが必須です。担当が欠けた表や、存在しない担当者3を含む表は採点しません。「各人2件」の追加条件を使う章では、上の一仕事の移動はAが1件・Cが3件になるため不正です。
この章で作る部品一仕事の担当を変える探索に、温度に応じて悪化も採用する判定を加えます。提出する表は探索中の最良を保存します。
焼きなまし法は、悪化する変更も確率で採用し、その採用しやすさを徐々に下げながら探す方法です。採用しやすさを調整する値を温度と呼び、高温で広く試してから低温で改善を重ねます。
A:8,3 / B:7,5 / C:6,4
負荷 (11,12,10)
A:8,3 / B:7 / C:6,4,5
Δ = +30
A:8,3 / B:7,4 / C:6,5
Δ = −32
現在値:365 → 395 → 363
保存する最良値:365 → 365 → 363
初期値365は一仕事の移動では改善しません。しかし重さ5をBからCへ移して395に悪化した後、重さ4をCからBへ移すと363になります。このような経路も試すため、悪化を確率で採用します。成功が保証されるわけではありません。
Δ=変更後−変更前、温度T>0とします。
Δ≤0なら採用。Δ>0なら p=exp(−Δ/T)。
一様乱数 u∈[0,1) を引き、u<pなら採用。
Δ=30、T=20ならp≈0.223。u=0.1なら採用、0.7なら棄却です。評価式を100倍にしたら、同じ確率を保つ温度も100倍にします。
2.2と異なり同点は採用します。「悪化も採用」を外した比較でも、同点の扱いは変えません。
const int delta = delta_move(current, item, to);
const double probability = delta <= 0 ? 1.0 :
(allow_worse ? exp(-double(delta) / temperature) : 0.0);
const Undo log = apply(current, item, to);
const bool take = u < probability;
ここを見る:uは[0,1)の乱数です。改善・同値は確率1、悪化は温度に応じた確率にします。この完成版は先に適用し、棄却時にundoします。
完成コードの L94–98 を見る →試行の進捗rに対し T = T_start + (T_end − T_start) × r とし、初期設定は20→1です。序盤は悪化も受け入れ、終盤は改善を中心にします。温度の下げ方を冷却スケジュールと呼び、この例は直線的に下げます。
始終を同温度にすれば固定温度を試せます。まず固定回数で確認し、時間制限を使う版ではrを経過時間の割合へ替えます。
const double progress = trials == 1 ? 0.0 : double(i) / (trials - 1);
const double temperature = start_temp + (end_temp - start_temp) * progress;
ここを見る:iは0始まりの試行番号です。最初と最後を指定温度に合わせ、1試行だけの場合も0除算を避けます。
完成コードの L92–93 を見る →差分を計算 → 旧値を記録して変更 → 採否判定、の順です。採用なら current_value += delta、棄却ならundoし、評価値は変えません。bestは改善時だけ更新するので、現在値が悪化しても最良解は残ります。
完成コードは毎回全計算とも照合し、consistent=1 を確認します。この検証を残した実行では、差分化の速度効果は測れません。
if (take) {
current_value += delta;
if (delta > 0) ++accepted_worse;
if (current_value < best_value) {
best = current.assign;
best_value = current_value;
}
} else {
undo(current, log);
}
ここを見る:悪化を採用してもbestは上書きしません。棄却時は状態をundoし、まだ変更していないcurrent_valueはそのまま残します。
完成コードの L99–108 を見る →毎試行「仕事・移動先・採否」の三乱数を消費し、seedと回数を固定して再現します。120回・seed42・温度20→1では363、悪化なしでは365ですが、seedによっては改善しません。5.1で複数入力・seed・同じ予算で比較します。
確率だけを確かめるには温度と悪化幅の小実験を使えます。
悪い変更を試して今の値が増えたら、最良の答えも上書きしますか?
上書きしません。今の答えは次を試すためのもの、最良の答えは最後に出すためのものです。
出発点改善限定の探索と最良解の保存を用意します。差分・取消しを使うなら、それぞれの照合を済ませておきます。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
delta≤0を採用し、悪化ならu<exp(−delta/T)で採用する処理を、以前の改善判定の場所へ置きます。まず正の固定温度で試します。
ここで確認同じ悪化量なら温度が高い方が採用されやすく、符号を反転していない。
採用したら現在値を更新し、bestは最良時だけ更新します。棄却したら適用した変更を取り消します。
ここで確認現在値が一時的に悪化しても、最良値は悪化しない。
固定回数で乱数の消費順をそろえた後、進捗0〜1から温度を求めます。試行1回の除算と、終了温度が正であることも確認します。
ここで確認同じseed・設定で採否を再現し、最終状態の全計算も一致する。
温度計算へ渡す進捗だけを「経過時間/探索時間」に変更し、0〜1へ収めます。最良解を返すための時間は探索予算から先に除きます。
追加後の確認:固定回数版を再現用に残し、時間版は複数seedの最終得点で比較できる。
完成コードで見る場所:probability:悪化を採る確率 / best_value:最良解を別に保つ場所
負荷11・12・10の状態は評価値365で、仕事を一つ移すだけでは改善しません。重さ5をBからCへ動かして395へ悪化した後、重さ4をCからBへ動かすと363へ改善できます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:2.4 変わった分だけを計算する
本編では線形に温度を下げます。発展では幾何的な冷却、実行時間による進捗、悪化幅からの温度設定を比較します。必ず改善する方法ではありません。
移動距離を比べる問題へ移すときは、値の大きさも変わります。温度の数字はそのまま流用せず、悪い変更が採用される割合を確かめます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの仕事を三人に分け、負担の偏りを小さくする — 問題設定・具体例を読む
2.5 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
既定の120回・seed42・温度20→1では最良値363になります。「悪化も採用」を外すと365のままです。seed1では既定の温度・回数でも365なので、悪化の採用が成功を保証しないことも確かめられます。一手送りで悪化の採用と棄却後の状態を見てください。
経路1 局所探索 / 6段階目・全7段階
同じ割当の計算を測ります。問題の考え方は読み直さず、入力サイズと実行方法の違いを確認してください。
計算を速くする方法を比べるには、全ての方式に同じ処理をさせる必要があります。この章では、仕事の割当を決められた順番で変更し、評価値を求める処理だけを比べます。
下の六仕事の並びを繰り返してN仕事へ広げます。仕事IDは0〜N−1で、重さも担当者も六仕事ごとに同じ並びです。初期設定のN=120なら、この組が20回現れます。
| 組の中の仕事 | 重さ | 初期担当 |
|---|---|---|
| 0・1 | 8・7 | A・A |
| 2・3 | 6・5 | B・B |
| 4・5 | 4・3 | C・C |
試行番号tを0から数え、仕事t%Nを、担当者(t+1)%3へ移します。したがって最初は仕事0をBへ、次は仕事1をCへ動かします。どの方式でもこの列を変えません。
全計算なら毎試行N仕事を読み直します。差分なら移動元・移動先の負荷を使います。担当表のコピーを行う設定では、評価が差分でもN要素のコピーが残ります。
最終評価値と途中評価値の合計checksumが一致していることを先に確認します。その後で、参照数・コピー数と、手元のC++が出す実時間を比較します。
ここで間違えやすいことこの計測用ループは、評価値が悪くなる変更も全て実行します。ここで最終値が大きくなったことは、計測方式の不具合とは限りません。探索としての強さを比べるときは、別途同じ時間予算で評価します。
この章で作る部品全計算/差分、コピーあり/なしを切り替えます。ブラウザーでは仕事量、手元のC++では実時間も確認します。
処理ごとの回数や時間を測ることをプロファイリングと呼びます。全体の速度を大きく左右する処理(ボトルネック)を見つけ、そこから改善します。
今回の操作を決める
仕事ID・移動先
N要素を複製
差分化しても残る
全計算:N仕事
差分:最大2人の負荷
2.4の差分評価を使い、一試行全体の処理量を測ります。全計算/差分、コピーあり/なしを切り替え、全方式に同じ変更列を適用します。採否で経路が変わらないよう、変更は全て実行します。
画面では、評価で参照した要素数とコピー数を別に数えます。全評価はN仕事、差分は変更する二人の負荷を参照します。
差分を使っても担当表のコピーにはN要素分の処理が残ります。仕事数を増やし、評価がO(1)でも一試行はO(N)になりうることを確かめます。
if (copy_state) {
vector<int> candidate = assign;
copied += n;
candidate[item] = to;
assign.swap(candidate);
}
ここを見る:差分評価を使っていても、このコピーには仕事数nに比例する処理が残ります。copiedで評価の参照数とは別に数えます。
完成コードの L17 を見る →C++は steady_clock で測り、elapsed_us を標準エラーへ出します。ブラウザが示すのは処理量であり、参照数の比は速度比ではありません。
入力・試行数・コンパイル設定をそろえて複数回測り、中央値とばらつきを見ます。短すぎる測定や測定中の大量ログを避け、結果のchecksumも照合します。
auto elapsed = chrono::duration_cast<chrono::microseconds>(
chrono::steady_clock::now() - start).count();
ここを見る:startは試行ループの直前に取得しています。全試行の終了後に差を取り、評価だけでなく候補のコピーや更新も含めて測ります。
完成コードの L33 を見る →コピーだけ外す、次に差分へ替える、と一要素ずつ比べます。最終値とchecksumの一致が前提です。実際の探索で直接変更する場合は、取消しや変更前の採否判定も必要です。
候補生成・ソート・メモリ確保が遅ければ、そこを改善します。候補数を減らす変更は探索範囲も変えるので、同じ時間での最終得点まで比べます。
差分評価をO(1)にしても、毎回N要素の担当表をコピーしています。一試行全体もO(1)になったと言えますか?
言えません。担当表のコピーにO(N)の仕事量が残ります。評価の高速化と、一試行全体の計測を分けて考えます。実時間も同じ条件で測り、次に改善する処理を選びます。
出発点2.4の正しい差分評価を基準にし、同じ一手列を処理する比較用のプログラムを用意します。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
同じ入力・一手列を用意し、全計算と差分、コピーありとなしを切り替えます。まず探索の採否を入れず結果をそろえます。
ここで確認scoreとchecksumが切替前後で一致する。
visitsとcopiedを更新し、まとめた試行の前後をsteady_clockで測ります。出力は計測区間の外へ出します。
ここで確認仕事数を増やしたとき、どのカウンタが増えたか説明できる。
支配的な処理を一つ選びます。コピーを減らす場合は取消しの正しさを再確認し、同じ条件で複数回計測します。
ここで確認結果一致を保ったまま実時間が改善し、別の処理が支配的になっていないか分かる。
候補配列の生成箇所にカウンタを置き、毎試行の確保が多いなら作業用バッファをループ外で確保します。clearとreserveの役割を分けます。
追加後の確認:古い要素が残らず、速度だけでなく同じ実時間の試行数・最終得点も比較できる。
完成コードで見る場所:checksum:同じ変更列の結果を比べる値 / steady_clock:実時間を測る区間
同じ変更列で、全計算/差分評価と、状態コピーあり/なしを切り替えます。評価の参照数とコピー数、手元の実時間をそれぞれ比べます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:2.4 変わった分だけを計算する
逆引き配列、bitset、距離の事前計算などを、頻繁に行う操作に合わせて選びます。計算量の削減だけでなく、実測で増えた試行回数と最終的な解の品質も確認します。
別の問題でも、速くする前後で同じ入力を使い、結果が一致するかを確認する流れは共通です。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材仕事の割当を変える処理に、どれだけ時間がかかるか測る — 問題設定・具体例を読む
2.6 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
まず差分評価を有効にしたまま、コピーだけを外します。評価の参照数は同じでもコピー数が減ります。次に全計算へ戻し、仕事数を増やしてみてください。手元のelapsed_usは標準エラー出力に出ます。参照数の比をそのまま速度比とは考えません。
経路1 局所探索 / 7段階目・全7段階
状態・評価・一手の作り方が決まれば、焼きなましのループを組めます。まずは状態をコピーし、毎回全評価する構成で完成させます。
新しい問題は使いません。これまでの仕事割当を例に、自分の問題へ置き換える部品を整理します。負荷の二乗和を小さくする設定は2.5と同じなので、読み直しは不要です。
担当表がState、一仕事の移動が候補生成、負荷の二乗和がcostに当たります。以下は最小化の形です。最大化なら採否と最良値の比較をそろえて反転します。
| 持つもの | 用途 |
|---|---|
| 固定入力 | 重さ・距離・制約など。候補をコピーするときに複製する必要はありません。 |
| State / current / best | Stateは答えを復元できる情報。currentは探索中の答え、bestは見つけた最良の有効解のコピーです。 |
| current_cost / best_cost | それぞれの状態を評価した値。状態と値を必ず組で更新します。 |
| 試行予算・温度・乱数 | まず試行回数、開始温度・終了温度(ともに正)、乱数seedを設定します。 |
関数名は役割を表す例です。手元のコードでは、同じ役割を担う処理へ対応させてください。
| 部品 | 入出力と守る条件 |
|---|---|
| make_initial() | 有効な完成解を一つ作る。探索0回でも提出できる答えにする。 |
| valid(state) | 全制約を検査する。不正な状態を評価・採用へ渡さない。 |
| cost(state) | 状態を変えずに目的値を返す。最初は全計算し、後の高速化の基準にする。 |
| propose(current, rng) | currentのコピーに一手を適用して候補を返す。候補なしは失敗として返し、currentを変えない。 |
| write_answer(best) | 保存したbestを指定形式へ変換する。必要なら操作列を復元し、再生して検査する。 |
vector<int> candidate = current;
if (move[0] == 0) candidate[move[1]] = move[2];
else swap(candidate[move[1]], candidate[move[2]]);
ここを見る:2.2のコピー方式です。これがproposeの中身に当たります。次に候補を検査・全評価し、採用する場合だけcurrentへ戻します。
完成コードの L50–52 を見る →最小化では Δ = candidate_cost − current_cost。Δ≤0なら採用し、Δ>0なら exp(−Δ / T) の確率で採用します。Tは正に保ち、試行の進捗に合わせて下げます。
乱数uを[0,1)から取り、u < exp(−Δ / T) で判定します。棄却時はcurrentに触れません。bestの更新は、best_costを改善したときだけです。
const double probability = delta <= 0 ? 1.0 :
(allow_worse ? exp(-double(delta) / temperature) : 0.0);
ここを見る:2.5から採用確率だけを抜き出しています。コピー方式ならdeltaは候補の全評価値−現在値です。allow_worse=trueとし、乱数uがprobability未満なら採用します。
完成コードの L95–96 を見る →if (current_value < best_value) {
best = current.assign;
best_value = current_value;
}
ここを見る:2.5ではcurrent.assignが担当表です。自分のStateでは、答えを復元するのに必要な状態全体をbestへコピーします。
完成コードの L102–105 を見る →make_initial → valid → cost → write_answerをつなぎ、探索0回で有効な答えを出します。
候補を一つ作り、合法で改善する場合だけ採用します。採用・棄却・候補なしを固定した例で確認します。
正の温度、進捗、乱数比較を追加します。悪化してもbestが残るか、同じ入力・seed・回数で動きを追います。
保存したbestの再評価・出力検査を通します。時間制限へ替える場合は、一試行の所要時間と出力時間も見込んで終了します。
差分評価・undo・近傍の混合・温度調整は、基準版の完成後に一つずつ足します。特に差分と取消しは全計算・コピー版と照合し、同じ時間での改善を確認します。
自分のコードで確認できた項目にチェックしてください。全項目を確認できたら、最小構成の完成です。
チェックはこのページを開いている間、保持されます。
差分評価もundoも未実装ですが、コピーした候補を全評価し、温度による採否・最良解の保存・出力まで動いています。焼きなましの最小構成は完成していますか?
完成しています。差分評価とundoは高速化の選択肢です。まず上の完成チェックで正しさを確かめ、処理時間を測ってから追加します。探索が正しく動くことと、得点が十分高いことは別に検証します。
コードは参照先で、閉じたまま全文をコピーできます。
コピー・検査・全評価・採用を確認する。
温度・採否・最良保存を参照する。この版は差分評価とundoも含む。
目的値・時間・失敗の比較方法を確認する。
共通準備から選べる経路 · 経路2
手順や選択を一つずつ決める問題で、途中の一候補に決め切らず先へ進めたい。
ビーム探索は、途中の候補から一手先を作り、評価のよい候補を一定数だけ残す方法です。数を「選ぶ・選ばない」と順に決めながら、この操作を繰り返します。
1.7では、その時点でよい選択を一つ決めました。この経路では複数の選択肢を残します。まず3.1で、未完成の状態を比べる評価が必要になる理由を考えます。
入口:共通準備 1.1〜1.8の状態・制約・評価・答えの保存。共通準備の後、この経路の最初の章から始められます。
到達点:層ごとに候補を選び、親の記録から手順を復元し、重複排除・多様性・記録の寿命を確認できる。
経路2 ビーム探索 / 1段階目・全5段階
8・6・5をそれぞれ使うか選び、合計と11の絶対差を小さくします。今回は8と6を処理した途中から、合計8と合計6の二候補を比べます。どちらにも最後の5が残っています。
ここから数を選ぶ問題へ移ります。仕事の担当表ではなく、各数を使うかどうかを答えます。経路1を読んでいなくても、この設定から始められます。
数8・6・5について、各数を選ぶか選ばないかを決め、選んだ合計を11に近づけます。合計が11を超えても不正ではなく、11からの距離で評価します。
この章は、8と6を検討し終えた途中から始めます。「8だけを選んだ合計8」と「6だけを選んだ合計6」の二候補を比較します。どちらにも最後の5を選ぶ余地が残っています。
| 途中の合計 | 最後の5 | 完成した合計 | 目標11との誤差 |
|---|---|---|---|
| 8 | 選ばない | 8 | 3 |
| 8 | 選ぶ | 13 | 2 |
| 6 | 選ばない | 6 | 5 |
| 6 | 選ぶ | 11 | 0 |
合計8は目標との差が3、合計6は差が5なので、現在値だけなら8の方がよく見えます。
しかし残り5を使って完成させると、合計8からは最善でも誤差2です。合計6からなら11を作れます。途中では不利だった候補の方が、完成後にはよくなります。
完成解そのものを探すループではなく、この二候補へ順位を付ける途中評価を作ります。残りの数と目標を変えた場合も、完成後の誤差と途中評価を別に確かめます。
ここで間違えやすいこと「合計8〜13まで届きそう」という範囲には11が含まれますが、残りが5一個なら実際の選択肢は8と13だけです。範囲の下界が0でも、誤差0の答えが存在するとは限りません。
この章で作る部品途中候補を比較する評価を作ります。完成後の誤差と、途中で順位を付ける値を分けます。
評価関数は、候補のよさを比較する数を返す関数です。途中評価では未完成の候補の見込みを数にし、完成後の実際のよさとは分けて扱います。
今の誤差 3 → 近く見える
残り5を使わない:8(誤差3)
残り5を使う:13(誤差2)
完成後の最良誤差 2
今の誤差 5 → 遠く見える
残り5を使わない:6(誤差5)
残り5を使う:11(誤差0)
完成後の最良誤差 0
今の誤差で選ぶ → Aを残す
残りの二通りを見て選ぶ → Bを残す
合計8と6から、残り5を使うか選びます。目標11への現在の差は8が有利ですが、完成後は6+5=11が有利です。完成解の目的値 abs(final_sum − target) が、そのまま途中の順位付けに向くとは限りません。
現在の合計s、目標t、残り一個rに対し、次の値が小さい方を残します。同点なら候補Aです。
|s−t|。max(s−t, 0, t−(s+r))。区間[s,s+r]内を自由に選べると仮定した見積もりです。min(|s−t|, |s+r−t|)。この例では完成後の最小誤差を正確に求めます。残りが多い場合は、先読みを限定する、貪欲に埋めるなど、精度と計算時間を両立する評価に替えます。
int evaluate(int sum, int target, int remaining, int mode) {
if (mode == 0) return final_cost(sum, target);
if (mode == 1) {
return max({sum - target, 0, target - (sum + remaining)});
}
return min(final_cost(sum, target), final_cost(sum + remaining, target));
}
ここを見る:mode=0は現在の誤差、1は到達範囲からの下界、2は残り一個を使う・使わないの比較です。いずれも候補を残す順位付けに使います。
完成コードの L11–19 を見る →Aの範囲[8,13]、Bの範囲[6,11]はどちらも11を含み、下界は0です。しかしAが作れるのは8か13だけ。Bは6か11を作れます。下界による順位付けでは同点規則でAが残り、最適解を失います。
下界は実際の最小誤差以下の値です。下界が既知の最良値以上なら、その先を調べなくても改善しません。このように不要な分岐を省くのが枝刈りです。単に下界の順位で候補を捨てる場合は、最適解を失うことがあります。
evaluate で選んでから完成させ、final_cost で誤差を測ります。途中評価0を最終誤差0として記録しません。全候補での最小誤差 reference_error は答え合わせ専用で、候補の差し替えには使いません。
初期設定では現在の差と下界がAを残して誤差2、二通りの先読みはBを残して0です。残りを1へ変えるとAが有利になります。
const int chosen = evaluation[1] < evaluation[0] ? 1 : 0;
const int reference = min(completed_cost[0], completed_cost[1]);
ここを見る:chosenは途中評価だけで決めます。全候補の完成後の値から求めたreferenceは検証用で、選び直しには使いません。
完成コードの L38–39 を見る →評価の改良に加え、3.2のビーム探索で複数候補を残す方法も使えます。未来の入力が未知なら、既知の残りを試すこの例とは条件が変わります。予測して先を試す方法は4.4で扱います。
今の目標との差だけで合計8を残すと、何を見落としますか?
合計6から11に届く道を見落とします。途中では遠く見えても、残りの選択によって逆転することがあります。
出発点完成した答えの目的値を残し、未完成の候補を並べるための評価を別に作ります。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
partialの二候補から先を全て試し、完成後の最小誤差を答え合わせとして求めます。まず探索ループは書きません。
ここで確認合計8と6を、残り5でどう完成させられるか手で確認できる。
evaluateへ途中状態を渡し、現在の差・到達範囲の下界・小例の正確な先読みを切り替えます。全候補で同じ規則を使います。
ここで確認下界は実際の最小誤差を超えないが、下界0でも誤差0へ届くとは限らない。
途中評価と同点規則だけで残す候補を選び、完成後の値は選別後の答え合わせに使います。この比較器を3.2の選別へ渡します。
ここで確認正解を知ってから候補を選び直す処理が混ざっていない。
途中評価の内訳へ残り資源など一項だけ加え、同じ候補集合で順位を比較します。その後、同じ探索予算で完成後の目的値を比べます。
追加後の確認:途中評価だけが改善し、完成後の結果は悪化する場合も記録できる。
完成コードで見る場所:evaluate:途中評価の窓口 / completed_cost:完成後の答え合わせ
目標11に近い合計を作り、最後に5を足すか選べるとします。今の合計8は11に近いですが、最後は8か13。今の合計6なら5を足して11にできます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:1.7 貪欲法で初期解を作る
ルール違反に罰点を付けて探索する方法は後半の話です。その場合も、提出用にはルールを守る答えを別に保存します。
作業の順序を選ぶ問題では、これまでの時間だけでなく、残った作業に必要な時間も見積もります。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材数を選んで、合計を目標値へ近づける — 問題設定・具体例を読む
3.1 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
目標11・残り5では「現在の差」と「下界」がAを残し、完成後の誤差は2です。「二通りを試す」ではBを残して0になります。残りを1に変えるとAが有利になります。途中評価の値と、実際の完成後の誤差を別々に比べてください。
経路2 ビーム探索 / 2段階目・全5段階
前の章と同じ数選びです。設定の読み直しは不要です。
数8・6・5から選んで11へ近づける、前の章と同じ問題です。設定の読み直しは不要です。途中の二候補の比較から、最初から最後までの探索へ進みます。
3.1と同じ数選びを、まだ何も選んでいない状態から解きます。入力の順に8、6、5を見て、それぞれについて「選ばない」「選ぶ」を決めます。数の並べ替えは答えに含めません。
答えは各数の採否を並べた列です。0は選ばない、1は選ぶという意味で、三つの数なら長さ3になります。ビーム幅Wは、この途中の列を同時に何個持つかという探索側の設定です。
| 採否の列 | 選んだ数 | 合計 | 誤差 |
|---|---|---|---|
| [1,0,0] | 8 | 8 | 3 |
| [1,0,1] | 8と5 | 13 | 2 |
| [0,1,1] | 6と5 | 11 | 0 |
| [0,0,0] | なし | 0 | 11 |
空の選択列から、8を選ばない合計0と、選ぶ合計8が生まれます。幅1で今の差が小さいものだけを残すと、合計0の候補を失います。
合計0の候補が残っていれば、6を選んで合計6へ進めます。次に5を選ぶことで合計11を作れます。
最後の5まで検討した列を完成解とし、|合計−目標|を計算します。途中で目標に近い候補を選ぶ規則と、完成後に比較する値を区別します。
ここで間違えやすいこと途中の列を残す数Wを増やすのは、問題のルールを緩めることではありません。計算時間と記憶量を使って、より多くの選択を後まで検討することです。
この章で作る部品一段ずつ候補を展開し、上位W件を残すビーム探索を作ります。途中の順位が完成後の順位と一致するとは限りません。
ビーム探索は、複数の途中候補から一手先を作り、評価のよい候補を一定数だけ残すことを繰り返す探索です。各段階で残す数の上限をビーム幅と呼びます。
残す合計:8, 0
0も次の段へ渡す
残す合計:8, 14, 6
0から6へ進める
残す合計:11, 13, 8
6から11へ到達
同じ段階の候補の集まりを層、候補から次の候補を作ることを展開と呼びます。この例は一段で数を一つ処理し、状態に合計と選択列を保存します。beam の各状態から「使わない・使う」を next へ作り、全て展開してから選別します。展開中にbeamを書き換えると、段数が混ざります。
std::vector<State> next;
for (const State& state : beam) {
next.push_back({state.sum, state.path + "0"});
next.push_back({state.sum + value, state.path + "1"});
}
ここを見る:beamが今の層、nextが次の層です。valueを使わない子と使う子を作り、pathに選択を記録します。
完成コードの L37–41 を見る →abs(target − sum) の昇順、同点は選択列の辞書順に並べ、幅Wまで残します。現在の誤差が小さくても、完成後によいとは限りません。
分岐数Bなら候補は最大BW件、全ソートはO(BW log(BW))です。状態のコピーも別にかかります。まず全ソートで検証し、必要なら部分選択やヒープへ替えます。
std::sort(next.begin(), next.end(), better);
if (int(next.size()) > WIDTH) next.resize(WIDTH);
beam = next;
ここを見る:betterで途中評価と同点規則をそろえ、上位WIDTH件だけを次の探索対象にします。親ごとに幅を切る処理ではありません。
完成コードの L42–44 を見る →幅1は最初に8だけを残し、最終的に13・誤差2です。幅3なら0→6→11を残せます。演習の「次の段階」で親と選別結果を追ってください。幅を増やしても、評価・同点規則・予算次第で改善しない場合があります。
この例は全数を処理したら完成です。一般の問題では途中の完成や手詰まりも扱い、完成解を別に保存します。next.empty() でも未完成の状態を答えにしません。
完成コードは選択列をコピーして持ちます。path=011 は6と5を選んだ答えです。3.3では親IDから復元し、履歴の寿命も管理します。
std::cout << "path=" << beam.front().path << '\n';
ここを見る:この小例では全数を処理した時点で全候補が完成しています。最良候補が保持するpathをそのまま取り出せるため、親IDはまだ不要です。
完成コードの L55 を見る →同じ層・同じ合計なら残りの数も同じで、一方を代表にできます。層が違えばまとめられません。3.3で状態比較とハッシュ、3.4で共有履歴と種類別上限を扱います。一手の進捗が異なる問題では、手数ではなく進捗別に層を作ります。
最初の8を選ばない候補を残すと、どんな答えを作れますか?
次の6と5を選んで、ちょうど11を作れます。見本では残す数を1と3に変え、どの段階で候補が消えるかを見ます。
出発点一段で何を決めるかを固定し、3.1の途中評価と、一候補から子を作る処理を用意します。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
根をbeamへ入れ、各候補から選ぶ・選ばないの二候補をnextへ追加します。走査中のbeamへ追加しないようにします。
ここで確認一段だけなら全ての子を列挙でき、親の状態が書き換わっていない。
nextを同じ比較器で並べ、幅W件まで残してからbeamと入れ替えます。次の段ではnextを空にします。
ここで確認幅1では一候補、幅を十分大きくすれば小さい全探索と同じ候補が残る。
全段の後で完成解を実目的値で比較します。一般の問題では完成解を別に保存し、nextが空の場合や期限終了でも返す答えを決めます。
ここで確認幅を増やすと候補数・時間・保存量も増えることを測れる。
3.3の完全な状態比較を、nextを生成した後、幅で切る前へ入れます。層ごとに重複判定表を作り直します。
追加後の確認:同じ状態が幅を占有せず、異なる未来を持つ状態まで消していない。
完成コードで見る場所:bool better:評価と同点規則 / next.resize:次の層の幅を制限する場所
8,6,5を順に「選ぶ・選ばない」と決め、合計を11へ近づけます。最初に合計8の候補だけを残すと、6と5だけを選ぶ道が消えます。合計0の候補も残せば、その答えを作れます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
同点なら決めた順で選びます。幅を増やすと時間も使うため、必ずよい答えになるとは限りません。
作業を一つずつ追加する問題でも、「次の一つを選ぶ→いくつか残す」という部分を使えます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材選ぶ・選ばないを順に決め、複数の候補を残す — 問題設定・具体例を読む
3.2 の教材体験見本 この場で動きます
8、6、5を順に、選ぶか選ばないか決め、合計を11へ近づけます。途中では、今の合計が11に近い候補から残します。同じ差なら、選び方を0と1で書いた列の小さい順です(0は選ばない、1は選ぶ)。
JavaScript が有効なブラウザで、値を変えて実験できます。
| これまでの選び方 | 合計 | 目標との差 | 残すか |
|---|
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
経路2 ビーム探索 / 3段階目・全5段階
数選びのルールは同じです。重複する状態が生まれるよう入力を一つ増やすため、その違いだけ確認してください。
数選びの入力を8・6・5・5へ変更します。値が等しい二つの5にも別々のIDを付け、それぞれ使うかどうかを決めます。答えは長さ4の採否の列です。
同じ合計へ至る別々の選び方が生じます。最後の合計を保存するだけでは、何を選んだ答えだったのか分からないため、親の候補と今回の選択も記録します。
| 採否の列 | 選んだ要素 | 合計 |
|---|---|---|
| [0,1,1,0] | 6と一つ目の5 | 11 |
| [0,1,0,1] | 6と二つ目の5 | 11 |
| [0,0,1,1] | 二つの5 | 10 |
同じ段階で合計が同じなら、この問題では残りの数も評価基準も同じです。その二候補は一方を代表にできます。
各候補に親IDと今回の0/1を保存します。終端から親をたどると選択が逆順に集まるので、反転すれば答えの列になります。
例えば[0,1,0,1]なら8を使わず、6を使い、一つ目の5を使わず、二つ目を使います。合計6+5=11を、復元した列から再現します。
ここで間違えやすいこと段階が違う合計は、その後に使える数が違います。また、ハッシュの値が同じでも合計が違う場合があります。「同じ層・同じ合計」を確認してから候補をまとめます。
この章で作る部品親IDから選択列を復元し、同じ層の重複を取り除きます。ハッシュが一致しただけで同じ状態と決めないようにします。
復元は、最終候補に至る選択の列を取り出すことです。今後の選択と結果が同じになる候補を一つにまとめる重複排除と合わせ、必要な履歴を保存します。
現在残す記録ID
ソート・選別してよい
各IDの親と操作
参照中の記録は残す
親 → 親 → 根
操作列を反転して再生
候補の記録をノード、その候補を生んだ一つ前の記録を親と呼びます。保存領域 pool に合計・親ID・直前の選択を保存し、beam は残すIDだけを持ちます。記録のIDを固定すれば、ビームをソートしても親をたどれます。祖先はビームから消えても必要なので、最初は全記録を最後まで保持します。
Node node{pool[parent].sum + bit * values[step], parent, bit};
int id = pool.size();
pool.push_back(node);
ここを見る:parentは履歴配列pool内のID、bitは今回使うかどうかです。ビーム内の位置とは分け、並べ替えても親をたどれるようにします。
完成コードの L18–19 を見る →この問題は同じ層・同じ合計なら残りの数も目的も同じで、一方を代表にできます。層をまたぐ比較では段階もキーに必要です。別の問題では残り資源や訪問済み集合など、以後の選択を変える情報も含めます。
ハッシュは状態を短い値へ写したものです。異なる状態が同じ値になる衝突もあります。演習は sum % buckets で候補を分け、同じハッシュの中で合計を比較します。buckets=1でも完全比較があれば区別できます。比較を外すと、衝突しただけの候補まで消えます。
盤面には、各要素と値に割り当てた乱数をXORでまとめるZobrist hashも使えます。一要素の変更は hash ^= table[item][old] ^ table[item][new]。全状態からの再計算と照合し、ハッシュ一致だけを状態一致の証明にしません。
for (int other : bucket) {
if (pool[other].sum == node.sum) {
duplicate = true;
break;
}
collisions++;
if (!exact) { duplicate = true; break; }
}
ここを見る:同じ層なので、合計sumが同じなら残りの選択肢も同じです。exact=falseは、衝突しただけで異なる合計も消す不具合を試す設定です。
完成コードの L21–25 を見る →終端から親をたどり、選択列を反転して再計算します。replay_ok は終端の合計との一致です。復元に成功しても重複排除で有望な候補を失う場合があるため、両者は別に検証します。
この版のプール量は選別前の候補数にも比例します。状態のコピー、履歴、比較用キーを分けて数え、3.4の記録共有へ進みます。
int best = beam.front();
string path;
for (int id = best; pool[id].parent != -1; id = pool[id].parent)
path += char('0' + pool[id].bit);
reverse(path.begin(), path.end());
ここを見る:終端から集めるため、操作は最後のものから並びます。反転してから初期状態へ適用し、保存した合計と一致するか確かめます。
完成コードの L37–39 を見る →親IDにビーム配列の添字を使い、その配列を次の段階で上書きすると、何が起きますか?
同じ添字が別の候補を指し、誤った手順を復元する可能性があります。復元用の記録に安定したIDを付け、ビームの並び順とは分けます。
出発点コピー版のビームを残し、まず復元を通してから重複排除を追加します。同時に両方を変えません。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
子を生成するときに親IDと操作をpoolへ追加し、beamにはIDを入れます。ソートするのはID一覧で、記録本体は並べ替えません。
ここで確認beamの順番を変えても、親IDが別の記録を指さない。
終端IDから親をたどり、集めた操作列を反転します。根から操作を再実行して終端状態と照合します。
ここで確認出力した操作列から、保存した合計と誤差を再現できる。
次の層を幅で切る前に、ハッシュが同じ候補の状態を比較します。この小例では同じ層の同じ合計をまとめられます。
ここで確認ハッシュの種類数1でも、完全比較ありなら異なる合計を残せる。
残り資源などを加えたら、完全比較とハッシュの両方へその項目を足します。先に全記録を保持する版で正しさを確認し、解放の最適化は3.4へ分けます。
追加後の確認:盤面や合計が同じでも、次に使える手が異なる二状態を区別できる。
完成コードで見る場所:struct Node:親と操作の記録 / map<int,vector<int>> seen:層ごとの重複判定
8・6・5・5から数を選びます。候補は親IDを持つ記録へ保存し、同じ層の同じ合計をまとめます。ハッシュをわざと衝突させ、完全比較の有無も試します。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:3.2 ビーム探索で、途中の候補を複数残す
導入例は整数の合計を完全比較します。大きな状態ではZobrist hashなどで候補を絞り、衝突を完全比較で確認します。履歴の寿命と共有は3.4へ進みます。
最短路の経路復元と同様に親をたどりますが、ビームでは候補の削除や配列の再利用があるため、記録の寿命を別に管理します。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材同じ合計へ進んだ候補をまとめ、選択の履歴を復元する — 問題設定・具体例を読む
3.3 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
ハッシュの種類数1でも、完全比較ありなら異なる合計を区別できます。完全比較を外すと全て衝突扱いになり、必要な候補を失います。復元成功と探索結果のよさは別に確認してください。
経路2 ビーム探索 / 4段階目・全5段階
候補4〜9の六つから、評価値が小さい順に幅3まで残します。種類は順に0・0・0・1・1・2。「各種類1件まで」を加えると、残る候補は4・5・6から4・7・9へ変わります。
前の章のビーム探索から、候補の選別と履歴の保持だけを切り出します。数選びの入力をそのまま使うのではなく、固定した六候補の表へ置き換えるので、表の意味を確認してください。
ビーム探索の一段で、次へ残す候補が六つできた場面を考えます。今回は数を選ぶ処理自体を省き、候補の評価値・大まかな種類・履歴だけを固定データとして与えます。
「種類」は候補を粗く分類したラベルです。同じ種類だからといって、同じ状態とは限りません。「完成後の費用」は選別結果を後から比較するための教材データです。
| 候補ID | 種類 | 途中評価 | 完成後の費用(設定0) |
|---|---|---|---|
| 4 | 0 | 10 | 30 |
| 5 | 0 | 11 | 31 |
| 6 | 0 | 12 | 32 |
| 7 | 1 | 13 | 8 |
| 8 | 1 | 14 | 9 |
| 9 | 2 | 15 | 7 |
幅3で種類の上限がなければ、途中評価10・11・12の候補4・5・6を残します。三つとも種類0で、完成後の最良費用は30です。
各種類1件までなら、候補4・7・9を残します。設定0では完成後に費用7の候補が得られます。ただし設定1では候補5の完成費用が5なので、上限で有望な候補を捨てます。
候補4〜6は親1、7〜8は親2、9は親3を持ち、親1〜3は全て根0を指します。候補4・7・9を残すなら、祖先も含め{0,1,2,3,4,7,9}の7記録が必要です。
ここで間違えやすいこと選別で読むのは途中評価と種類です。完成後の費用を見て残す候補を選び直すと、未知の未来を使うことになり、多様性を入れる効果を比較できません。
この章で作る部品似た候補への偏りを抑える処理と、必要な祖先を特定する処理を作ります。数選びや盤面の探索全体を動かす演習ではありません。
多様性は、異なる選び方の候補が残っていることです。似た候補だけでビーム幅を使い切らないようにし、候補が共有する履歴も管理します。
種類0・種類0・種類0
途中評価10・11・12
種類0・種類1・種類2
途中評価10・13・15
選んだ候補+祖先
共通の親は共有する
途中評価の上位だけでは、似た候補で幅が埋まる場合があります。六候補を三種類に分類し、種類ごとの上限を設けます。同じ種類でも未来は異なるので、3.3の重複排除とは別の選別です。
評価順に走査し、その種類が上限未満なら残します。幅に達したら終了し、候補が足りなくても無理には埋めません。上限0は制限なしです。
完成後の費用は答え合わせ専用です。設定0では多様性が有利ですが、設定1では同種の有望な候補を失います。上限の有無を実際の結果で比べます。
for (int id = 4; id < 10; id++) {
int g = pool[id].group;
if (cap > 0 && counts[g] >= cap) continue;
kept.push_back(id);
counts[g]++;
if (int(kept.size()) == width) break;
}
ここを見る:この入力のID 4〜9は評価値のよい順です。全体の幅widthと、種類gごとの上限capを別々に管理します。
完成コードの L14–17 を見る →根0、親1〜3、候補4〜9の履歴から、残す候補と全祖先を集合にします。共通の親を一度だけ数えることで記録を共有できます。ビームから消えた親も、子が残れば必要です。
割当済み記録数は10、必要数はこの集合の大きさです。ノード数の比較であり、実メモリ量や時間とは区別します。
for (int id : kept) {
best = min(best, pool[id].finished);
for (int p = id; p != -1; p = pool[p].parent) alive.insert(p);
}
ここを見る:aliveには候補自身から根までのIDを入れます。共有する祖先は集合で一度だけ数え、どの候補からも参照されない記録と区別します。
完成コードの L19 を見る →演習は必要な記録の特定までです。プール再利用では不要な記録だけを解放します。共有木をたどる実装なら、親へundo、子へapplyして状態コピーを減らせます。
選別の部分選択・ヒープ化や、進捗別のビームも選択肢です。同じ候補集合と同点規則で結果を照合してから、時間・メモリを測ります。
大まかな特徴が同じなら、完全に同じ候補として消してよいですか?
必ずしもよくありません。残りの手や資源が違うかもしれません。「似ている」と「同じ」を分けて扱います。
出発点3.3の復元できるビームを基準にし、候補を残す処理と記録を保持する処理を別々に改善します。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
評価順の候補から幅W件を選ぶ版を基準にします。教材の候補は評価順に並んでいますが、一般の入力では先に並べます。
ここで確認種類別上限0で、以前と同じ候補を残せる。
候補を評価順に読み、種類の件数が上限未満なら追加します。完成後の結果finishedは答え合わせ専用で、選別には使いません。
ここで確認種類を分散できる一方、同種の有望な候補を捨てる場合も確認できる。
選別後の各IDから親をたどり、必要な祖先を印付けします。まず解放せず、全記録数と必要数だけを比べます。
ここで確認選別で落ちた候補でも、残った子の親なら必要な記録に含まれる。
必要な記録の印付けが正しいことを確認してから、不要IDだけを空きリストへ入れます。vectorの途中削除で他のIDをずらさない設計にします。
追加後の確認:複数段にわたり再利用しても、全保持版と復元した操作列が一致する。
完成コードで見る場所:counts:種類ごとの選別上限 / set<int> alive:祖先を含めた必要な記録
途中評価10・11・12の候補は同じ種類で、13・14は別の種類、15はさらに別の種類です。幅3の選択に種類別上限を加え、完成後の結果と必要な祖先記録を比べます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:3.3 ビーム探索の履歴を復元し、重複を取り除く
発展の実装で参照する章:2.3 試した変更を、元どおりに戻す、2.6 処理ごとに測り、ボトルネックを見つける。この章の導入例を読むための必修ではありません。
候補が増えてメモリを使いすぎる場合は、共通する履歴を共有する方法も学びます。まずコピーする版を比較用に残します。
作業計画なら、早く終える候補と資源を多く残す候補を分けて残すことを考えます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材ビームの一段から、残す候補と履歴を選ぶ — 問題設定・具体例を読む
3.4 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
幅3で上限0なら同じ種類の三候補、上限1なら三種類が残ります。結果の設定を1へ変えると、同種内の有望な候補を上限で落とす例になります。多様性は必ず有利になる規則ではありません。
経路2 ビーム探索 / 5段階目・全5段階
一段で何を決めるか、候補をどう広げて比べるかが決まれば、ビーム探索を組めます。まずは各候補に状態と操作列をコピーして持たせます。
新しい問題は使いません。3.2の数選び(8・6・5から選んで11へ近づける)を例に、別の問題でも使う部品を整理します。設定の読み直しは不要です。
Stateは合計と選択列、一段は数を一つ処理することです。子は「使う・使わない」の二つ。途中評価と完成後の目的値は、この例ではどちらも11との差ですが、役割は分けて実装します。
| 持つもの | 用途 |
|---|---|
| Stateと操作列 | 途中の答え・進捗・次の選択に必要な情報。最初は選択列をStateと一緒にコピーします。 |
| beam / next | 現在の層と、次の層の候補を別の配列で持ちます。同じ層では進捗の意味をそろえます。 |
| 幅W・探索予算・同点規則 | Wは1以上。最大段数や期限、同じ評価の候補を残す順を決めます。 |
| best_complete / final_cost | 有効な完成解とその目的値。基準の完成解を先に用意できれば、打切り時もそれを返せます。 |
struct State {
int sum;
std::string path;
};
ここを見る:3.2の最小構成です。合計だけでなく選択列pathもコピーするので、親IDを導入しなくても答えを取り出せます。
完成コードの L13–16 を見る →関数名は役割を表す例です。手元のコードでは、同じ役割を担う処理へ対応させてください。
| 部品 | 入出力と守る条件 |
|---|---|
| make_root() | 最初の途中状態を作る。既に完成している場合も判定する。 |
| expand(state) | 一段先の子を列挙する。親を変えず、途中状態としての制約を守る子だけを返す。 |
| rank(state) | 未完成候補を残す順を決める途中評価。同点規則と合わせて比較する。 |
| is_complete(state) | 必要な決定が全て済んだか判定する。完成後は全制約も検査する。 |
| cost(complete) | 有効な完成解の実際の目的値。途中評価と混同せず、完成解同士を比較する。 |
| restore / write_answer | 候補の操作列を取り出し、初期状態から再生・検査して出力する。 |
完成した子は、幅で切る前に保存します。途中評価が悪くても、完成解としては有効な場合があります。全ての候補が同じ段で完成する問題なら、最終段を実目的値で比較する形でも構いません。
期限までに完成解が見つからなければ、有効な基準解を返します。基準解も用意できない設計では「完成解なし」を明示し、途中状態を答えとして出しません。
std::vector<State> next;
for (const State& state : beam) {
next.push_back({state.sum, state.path + "0"});
next.push_back({state.sum + value, state.path + "1"});
}
std::sort(next.begin(), next.end(), better);
if (int(next.size()) > WIDTH) next.resize(WIDTH);
beam = next;
ここを見る:一つの数valueを処理する部分です。この小例は全候補が同じ最終段で完成します。途中でも完成する問題では、完成した子を選別前に別保存する処理を加えます。
完成コードの L37–44 を見る →一段の意味を固定し、全ての子と操作列を手計算で確かめます。親が変更されていないことも確認します。
全親の展開後にnextを並べ、上位W件を残して入れ替えます。同点の順を固定し、次の段ではnextを空にします。
完成判定と実目的値の比較を追加します。途中の完成・候補なし・期限終了の全てで返す答えを決めます。
保存した操作列から状態を再現し、制約と目的値を照合します。小入力で幅を十分広げ、全候補を残した結果とも比較します。
親IDによる履歴共有・重複排除・多様性の上限・選別の高速化は、基準版の完成後に足します。重複をまとめるなら将来の選択も等しいことを確認し、履歴を共有するなら参照中の祖先を残します。
自分のコードで確認できた項目にチェックしてください。全項目を確認できたら、最小構成の完成です。
チェックはこのページを開いている間、保持されます。
重複排除と親IDは未実装ですが、各候補が操作列をコピーして持ち、一段展開・幅での選別・完成解の保存・出力まで動きます。ビーム探索の最小構成は完成していますか?
完成しています。重複排除は候補枠の使い方を、親IDは履歴の持ち方を改善する追加実装です。まず完成チェックを通し、コピー量や候補数を測ってから必要なものを足します。幅を増やしただけで最適解が保証されるわけではありません。
コードは参照先で、閉じたまま全文をコピーできます。
状態と操作列をコピーし、各層を全ソートする版。幅を変える演習も試せる。
基準版の完成後に、親IDと状態比較を加える。
候補の種類別上限と、必要な祖先の保持を確認する。
共通準備から選べる経路 · 経路3
隠れた値や未来が分からず、返事や観測を得ながら次の行動を決めたい。
推定は、観測から見えない情報を見積もること、意思決定は、その見積もりを使って行動を選ぶことです。箱の中のカードの色を測る例で、追加測定と回答のどちらを選ぶか考えます。
共通準備の固定入力に加えて、まだ分からない情報を扱います。実装を「返事を受け取る窓口」「推定の更新」「次の判断」に分け、順に作ります。
入口:共通準備 1.1〜1.8の状態・制約・評価・答えの保存。共通準備の後、この経路の最初の章から始められます。
到達点:通信を守り、観測から推定を更新し、質問・停止・先読みの行動比較をそれぞれ実装できる。
経路3 観測・推定・意思決定 / 1段階目・全4段階
箱の中のカードは赤か青。最大3回質問し、返事の多数決で答えます。模擬相手は指定した一回だけ誤答し、途中で返事が来なくなる場合もあります。質問と返事の順序を守る練習です。
ここから、箱の中のカードの色を、センサーへの質問から当てる問題を使います。値を受け取る順番も問題のルールに含まれます。経路1・2の履修は前提にしません。
箱の中のカードは赤か青のどちらか一色で、途中で色は変わりません。センサーへ質問すると「赤」か「青」と返ります。センサーは誤る場合があるので、一回の返事だけで色を断定しません。
完成コードでは相手も同じプログラム内に作ります。カードの色・誤る位置・受信可能数は相手側の設定で、答える側のsolveには渡しません。コードではRED=赤、BLUE=青として扱います。
| 初期設定の項目 | 意味 |
|---|---|
| カードは青 | 模擬相手だけが知る実際の色 |
| 質問上限3 | 送信できる質問は最大3回 |
| 反転位置1 | 0始まりで二回目だけ反対の色を返す |
| 受信可能数5 | 初期設定の三質問では途中終了しない |
質問?を送ると返事BLUE、次は誤ったRED、三回目はBLUEが返ります。質問→受信→記録の順番を三回繰り返します。
返事は青・赤・青。多数決で青を選び、最終回答! BLUEを一回送ります。同数なら赤、質問上限0でも赤を基準回答にします。
受信可能数を1へ変えると、二回目の質問後に入力終了となります。赤・青の返事として数えず、中断し、最終回答も送らない規則です。
ここで間違えやすいことこの章の誤答は「指定した一回を反転する」固定の仕組みです。次のベイズ更新で用いる「毎回独立に一定確率で誤る」モデルとは区別してください。
この章で作る部品同じプログラム内の模擬相手と通信します。指定した一回だけ反対の色を返す設定は通信確認用で、4.2の確率的な測定モデルとは別です。
インタラクティブ問題(対話型問題)は、質問や行動を出し、返事を受け取りながら解く形式です。通信の形式・順序・終了条件を定めた規則をプロトコルと呼びます。
予算を確認
質問を送信する
返事を読む
終了・不正形式なら停止
観測を追加
次の質問か最終回答
解く側のプログラムをソルバ、質問に返事をする問題側をジャッジと呼びます。Judge は質問するaskと最終回答のfinishを公開します。ソルバはこの窓口と予算だけを受け取り、真値は受け取りません。まず相手を手元で再現する模擬ジャッジ(MockJudge)で、返事の反転や受信打切りを試します。4.2の確率的な誤差とは別の設定です。
struct Judge {
virtual int ask() = 0;
virtual void finish(int answer) = 0;
virtual ~Judge() = default;
};
ここを見る:ソルバが受け取るのはこの窓口です。MockJudgeが持つ実際のカードの色を、推定処理へ直接渡しません。
完成コードの L7–11 を見る →予算を確認して質問し、返事を読んでから観測を更新します。この例は多数決で答え、同数や観測なしなら赤です。推定法を替えても通信順序は保ちます。
回数は送信時に増やします。受信に失敗しても、送った質問は予算へ戻しません。
RED・BLUE以外や入力終了を受けたら中断し、以後の質問も最終回答も送りません。予算3・受信可能数1で、二回目の受信後に止まるか確認します。正常終了時は最終回答を一回だけ送ります。実問題では、その終了規則に合わせます。
while (queries < budget) {
queries++;
int y = judge.ask();
if (y != RED && y != BLUE) return {-1, queries, true};
if (y == RED) reds++;
else blues++;
}
ここを見る:不正な返事や入力終了は、askから赤・青以外として返ります。ここでsolveを終了するので、後続の質問や最終回答へ進みません。
完成コードの L35–39 を見る →同梱のStreamJudgeは ?・! 形式を使います。flushは、出力バッファに残った内容を送り出す処理です。質問の出力後に行ってから返事を読み、読取り成功を確認します。mainは模擬相手を使うので単独で実行できます。
本番ではコマンド形式を合わせ、診断出力を標準出力へ混ぜません。真値や未来の返事をソルバへ渡さず、4.2の推定、4.3の質問選択へつなぎます。
cout << "?\n" << flush;
string y;
if (!(cin >> y)) return -1;
ここを見る:StreamJudgeのask内です。flushで質問を送り出し、その後で返事を読みます。読めなければ失敗を呼出し側へ返します。
完成コードの L27 を見る →練習用の相手が知っている実際の色を、答えを選ぶ処理へ渡してよいですか?
いけません。本番では見えない情報を使ってしまいます。測定の返事と実際の色は、別々に管理します。
出発点推定を入れる前に、質問・受信・終了を一巡できる通信の窓口を作ります。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
MockJudgeを作り、solveからaskを一回呼んで返事を記録します。まず固定の返事で接続を確かめます。
ここで確認ソルバが使える情報は返ってきた値だけで、真値へ直接アクセスしていない。
質問予算を送信前に確認し、入力終了・不正な応答では更新せず停止します。最後の回答を送る条件も決めます。
ここで確認予算0で質問せず、受信が途中で切れたら無効な値で推定を続けない。
StreamJudgeへ問題の質問形式、受信形式、flushを実装します。solveの判断処理を変えずに窓口だけを入れ替えます。
ここで確認送信後に返事を受け取れ、デバッグログが質問の出力へ混ざらない。
多数決している観測の集計箇所を、4.2の推定更新へ置き換えます。通信ログから同じ返事列を再生し、窓口の変更と推定の変更を別々に検証します。
追加後の確認:同じ観測列なら、模擬通信と再生で同じ事後分布になる。
完成コードで見る場所:Result solve:ソルバと相手を分ける窓口 / StreamJudge:本番の入出力へつなぐ場所
箱の中のカードは赤か青です。模擬ジャッジは一回だけ反対の返事をしたり、途中で受信を終了したりします。ソルバは質問の窓口と予算だけを受け取り、返事を多数決に使います。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
まず一つのプログラム内で相手とのやり取りを練習し、その後に実際の入出力へつなぎます。
要求が一つずつ到着する問題でも、未来の要求を先に読まず、今までに見えた情報で判断します。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材箱の中のカードが赤か青かを、センサーに質問して答える — 問題設定・具体例を読む
4.1 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
質問上限3、受信可能数1なら二回目に入力終了を受け、最終回答を送らず止まります。真値を切り替えても、ソルバへ渡されるのは返事だけです。返事を反転すると、多数決の答えが変わる場合もあります。
経路3 観測・推定・意思決定 / 2段階目・全4段階
カードが赤か青という題材は同じですが、測定の誤り方が変わります。前の章の反転設定と混同しないよう、次の条件を確認してください。
同じカードを使い、今度はセンサーの誤り方が確率で分かっているものとします。カードの色は固定ですが、測定ごとに正しい色を返すか、反対の色を返すかが変わります。
初期設定では赤・青を半々と考えます。正答率75%なので、カードが赤なら返事は赤75%・青25%、カードが青なら青75%・赤25%です。
| 仮説 | 最初の確率 | 「赤」が返る確率 | 積 |
|---|---|---|---|
| カードは赤 | 1/2 | 3/4 | 3/8 |
| カードは青 | 1/2 | 1/4 | 1/8 |
「赤」という返事は実際に赤の場合に出やすいものの、青でも出ます。各仮説の元の確率と、その返事が出る確率を掛けます。
積の合計は3/8+1/8=1/2です。各積をこの合計で割ると、更新後の確率は赤3/4、青1/4になります。
赤→赤→青の順に更新すると、カードが赤である確率は1/2→3/4→9/10→3/4と変わります。最後に「青」と返っても、過去の二回の「赤」が消えるわけではありません。
ここで間違えやすいこと求めるのは、今ある情報から見積もった色の確率です。初期設定では赤をより有力と判断しますが、確率3/4は正解を保証する数字ではありません。
この章で作る部品尤度を掛けて正規化する更新と、長い観測列でも計算しやすい対数での実装を作ります。測定するかの判断は4.3で扱います。
ベイズ推定は、観測前の確率と観測の起こりやすさを使い、仮説の確率を更新する方法です。ここでは「カードは赤」「カードは青」という二つの仮説を、返事を得るたびに比べ直します。
H=赤:50%
H=青:50%
H=赤:75%
H=青:25%
H=赤:90%
H=青:10%
H=赤:75%
H=青:25%
Hはカードの色、Yは返事で、どちらも赤か青です。センサーが正しい色を返す確率をq、反対を1−qとします。仮説ごとの観測前の確率を事前分布、観測後の確率を事後分布と呼びます。Hを仮定したとき、実際に得た返事が出る確率が尤度です。
| 仮説 | 返事「赤」の尤度 | 返事「青」の尤度 |
|---|---|---|
| H=赤 | q | 1−q |
| H=青 | 1−q | q |
Hとqを固定したとき各回の誤差は独立とします。別々の観測で同じ値を得ることと、一つの観測の二重取込みは区別します。
const double likelihood_red = reply == RED ? accuracy : 1.0-accuracy;
const double likelihood_blue = reply == BLUE ? accuracy : 1.0-accuracy;
ここを見る:replyは受け取った色です。カードが赤という仮説と青という仮説に対し、この返事の起こりやすさを別々に求めます。
完成コードの L28–29 を見る →返事yに対し w[h] = prior[h] × likelihood(y | h) を計算し、合計Zで割って総和を1にします。これを正規化と呼びます。
P(H=h | Y=y) = P(Y=y | H=h) P(H=h) / Z
Z = Σh P(Y=y | H=h) P(H=h)
事前1/2ずつ、q=3/4、返事「赤」なら、重み3/8・1/8から事後3/4・1/4になります。これを次の事前に使います。確率はモデルの下での推定なので、高くなっても真値の確定ではありません。
積が小さくなりすぎるのを避け、log_probability に対数尤度を加えます。更新後の対数重みをa、最大値をmとして、「指数の和の対数」を安定して求めるlog-sum-expを使って正規化します。
log Z = m + log(Σ exp(a[h] − m))
最大値を引くと少なくとも一項が1になり、数値が安定します。log_probability[h] = a[h] − logZ とし、表示時だけ指数関数で確率へ戻します。
const double a = log_probability[0] +
(likelihood_red > 0 ? log(likelihood_red) : negative_infinity);
const double b = log_probability[1] +
(likelihood_blue > 0 ? log(likelihood_blue) : negative_infinity);
ここを見る:log_probabilityは前回までの確率の対数です。尤度の対数を足すことで、通常の確率での掛け算に対応させます。
完成コードの L30–33 を見る →const double log_normalizer = top + log(exp(a-top) + exp(b-top));
log_probability = {a-log_normalizer, b-log_normalizer};
posterior = {exp(log_probability[0]), exp(log_probability[1])};
ここを見る:topはaとbの最大値です。最大値を引いてから指数を取り、正規化した対数確率を保存します。posteriorは表示・判断に使う通常の確率です。
完成コードの L39–41 を見る →正答率100%なのに「赤→青」と返ると、固定したどちらの色でも説明できず、全重みが0になります。mismatch_at と posterior=NOT_DEFINED を出して止め、モデルや読取りを確認します。0で割ったり一様分布へ戻したりしません。
演習では正答率50%で更新なし、75%で更新あり、100%で矛盾の検出を確認できます。
const double top = max(a, b);
if (!isfinite(top)) {
mismatch_at = used;
break;
}
ここを見る:二つとも対数重みが−∞なら、どちらの色でも説明できない観測です。正規化せずに、矛盾が出た観測番号を保存します。
完成コードの L34–38 を見る →演習は観測済みの列を再生し、真値を推定側へ渡しません。4.1の返事ごとに一回更新し、4.3へ事後分布を渡す形で接続できます。
ここでは有限仮説を実装しました。連続値の平均・分散、観測式、最小二乗へ広げる場合も、推定値と不確かさを判断へ渡します。
最初が1/2ずつなら、「赤」という返事の後はどんな計算になりますか?
赤には1/2×3/4=3/8、青には1/2×1/4=1/8が付きます。合計1/2でそれぞれ割ると、赤は3/4、青は1/4になります。
出発点観測済みの返事列を入力にし、二仮説の更新を一回ずつ確かめます。通信は4.1の窓口へ任せます。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
まず二仮説・事前1/2・正答率3/4で、事前×尤度を計算して総和で割る基準版を作ります。
ここで確認返事「赤」の後に確率3/4と1/4になり、総和が1になる。
更新した事後を次の事前にします。更新関数は新しい観測一つだけを受け取る形にし、履歴全体を再度掛けないようにします。
ここで確認2→2で9/10へ更新され、同じ一観測を二重に反映していない。
長い観測列向けに、積を対数の加算へ置き換え、最大の対数重みを引いて正規化します。全て負の無限大なら矛盾として返します。
ここで確認短い列では基準版と一致し、起こりえない観測を無理に正規化しない。
二変数の更新を、仮説ごとの配列とループへ置き換えます。尤度の計算を別関数にし、最大値・指数の和・正規化を配列全体で行います。
追加後の確認:仮説の並びを変えても対応する確率は同じで、確率0の仮説も扱える。
完成コードで見る場所:log_probability:保持する対数重み / log_normalizer:正規化と矛盾検出の付近
カードは赤か青で、最初は同じくらいありそうだとします。センサーは一回ごとに3/4の確率で実際の色を返す設定です。「赤」という返事を得たら、赤の方を有力にします。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この計算はセンサーの誤り方を仮定しています。測定を重ねる例では、各回の誤りが独立に起きる設定を使います。
道の長さを推定する場合は、返事が一つの道の長さなのか、通った複数の道の合計なのかを先に決めます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材センサーの返事から、カードが赤・青である確率を更新する — 問題設定・具体例を読む
4.2 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
事前50%・正答率75%で返事が赤→赤→青なら、H=赤の確率は50%→75%→90%→75%です。正答率50%では更新されません。正答率100%・返事が赤→青では二つ目でモデル矛盾になります。一手送りで事前・尤度・事後を対応させてください。
経路3 観測・推定・意思決定 / 3段階目・全4段階
二つの仮説とベイズ更新は前の章と同じです。確率を求めるだけでなく、質問を選ぶために費用と損失を加えます。
同じカードの色について、今度は「もっと測る価値があるか」を決めます。測定は有料なので、正確さだけでなく、減らせる誤答の損失と費用を比べます。
一回だけ先を考えます。今すぐ色を答えるか、センサーAまたはBで一回測ってから答えるかを選びます。測った後は返事に応じて確率を更新し、確率の高い色を最終回答します。
| 選択 | 正答率 | 測定費用 | 誤答時の損失 |
|---|---|---|---|
| 今すぐ答える | 追加測定なし | 0 | 10 |
| センサーAを使う | 75% | 1 | 10 |
| センサーBを使う | 90% | 2 | 10 |
事前確率が半々なら、赤・青どちらを答えても誤答率は1/2です。期待損失は10×1/2=5になります。
事前が半々なら、測定後に返事と同じ色を答えるのが最善です。Aなら1+10×1/4=3.5、Bなら2+10×1/10=3なので、Bを選びます。
赤の確率が90%なら、今すぐ赤と答える損失は10×0.1=1です。初期の測定費用では、追加測定するより安くなります。
ここで間違えやすいこと「情報が増える」と「費用を払う価値がある」は同じではありません。事前が偏った場合は、返事をそのまま答えると決めず、返事ごとの事後確率から色を選び直します。
この章で作る部品可能な返事ごとに確率と事後を仮計算し、期待損失で三択を比較します。情報利得も表示しますが、採否の基準は費用込みの損失です。
情報利得は、観測によって不確実さがどれだけ減るかを表す量です。よく分かる質問が得とは限らないため、ここでは測定費用も含めた期待損失で質問か停止かを選びます。
事前50%ずつ
期待誤答損失5
正答率75%
費用込み期待損失3.5
正答率90%
費用込み期待損失3
H=赤の確率p、誤答損失Lなら、確率の高い方を今答える期待損失は L × min(p, 1−p) です。これと、AかBで一回測って答える費用を比べます。追加質問だけを比べると、不要な測定を続けてしまいます。
vector<double> risk = {loss * min(p, 1-p)};
ここを見る:pはカードが赤である確率です。有力な色を答えると誤答確率はmin(p,1−p)なので、誤答時の損失lossを掛けます。
完成コードの L13 を見る →返事yに対し a=p P(y|H=赤)、b=(1−p) P(y|H=青) とします。返事の確率はa+b、事後のH=赤の確率はa/(a+b)です。
返事後に確率の高い仮説を答えると、期待誤答損失への寄与は L × min(a,b)。全返事について足し、測定費用を加えます。
double a = p * (reply == RED ? q : 1-q),
b = (1-p) * (reply == BLUE ? q : 1-q), z = a+b;
double posterior = a/z;
future_loss += loss * min(a,b);
ここを見る:a・bは「仮説と今回の返事が同時に成り立つ確率」です。min(a,b)には返事の確率zも含まれるため、zをさらに掛ける必要はありません。
完成コードの L18–20 を見る →エントロピーは確率分布の不確実さを表す数です。二仮説なら H(p)=−p log₂p−(1−p)log₂(1−p) で、半々のとき最大、どちらかに確定すると0です。観測による減少量の期待値が情報利得です。ただし不確実さが減っても、回答が変わらない、費用が高い、といった場合があります。
この教材は費用込み期待損失で選びます。情報利得・分散・予測の食い違いを、最終損失の代わりに比較する代理指標にする場合も、最終損失との関係を確認します。
事前50%・損失10・費用A=1、B=2なら、測らない5、Aは3.5、Bは3です。事前90%なら測らない損失1が最小になります。同点でも測らない方を優先します。
ここでは一回先まで比較します。繰り返すなら観測後に再判断し、複数回先まで考えるなら事後分布を状態にして、質問・停止の分岐を展開します。
int chosen = 0;
for (int i = 1; i < 3; i++)
if (risk[i] < risk[chosen] - 1e-12) chosen = i;
ここを見る:riskの0は測らない場合、1・2はA・Bの費用と期待誤答損失の合計です。同率では先にある「測らない」を残します。
完成コードの L27 を見る →正答率が高い測定は、いつでも選ぶべきですか?
いいえ。すでに答えがほぼ決まっていれば、追加の費用に見合わないことがあります。
出発点4.2の分布と観測モデルを使い、まず「今すぐ答える」場合の期待損失を計算します。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
現在の分布から最良の回答と期待損失を求め、riskの先頭へ入れます。測定候補がなくても選べるようにします。
ここで確認事前90%のとき、低い誤答確率を反映した停止値になる。
質問ごとに可能な返答を列挙し、返答確率と仮の事後を求めます。現在の分布を上書きせず、その事後で最良の回答を選び直します。
ここで確認返答確率の合計が1になり、質問を評価しただけでは現在の分布が変わらない。
各返答後の損失を返答確率で重み付けし、質問費用を一度だけ足します。停止を含む最小値の候補を採用します。
ここで確認測定精度が高くても、費用が大きければ停止を選べる。
質問候補を列挙した直後に予算不足の候補を除外します。実際に質問したときだけ予算を減らし、受信後に推定を更新して再選択します。
追加後の確認:仮の質問評価で予算を消費せず、最後の一回の後には停止できる。
完成コードで見る場所:vector<double> risk:停止も含めた候補比較 / future_loss:返答分岐の期待損失。コードのa・bは仮説と返答の同時確率なので、loss*min(a,b)で既に返答確率の重み付けを含む
事前50%ずつ・誤答損失10なら、測らない期待損失は5。正答率75%・費用1の測定Aは3.5、90%・費用2のBは3です。事前を90%へ変えると、測らず答える方がよくなります。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:4.2 ベイズ推定で、観測から仮説の確率を更新する
候補をよく区別できる質問と、実際の答えを変える質問は、いつも同じではありません。まず候補が二つの例で確かめます。
仕事の重さを測る場合は、測定によって担当の決め方が変わるかまで調べます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材もう一回測る費用と、色を答え間違える損失を比べる — 問題設定・具体例を読む
4.3 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
事前50%・損失10では費用込みでBが最小です。事前を90%にすると、既に有力な答えがあるため測らず答えます。情報利得が正でも、費用に見合うとは限りません。
経路3 観測・推定・意思決定 / 4段階目・全4段階
容量3を使って仕事を受けます。今の仕事は容量2で報酬3、次の仕事は容量2で報酬が8か0(半々)です。今受けると次は受けられません。二回の合計報酬を大きくする行動を選びます。
前の測定問題から、容量を使って仕事を受ける問題へ移ります。ここでは未知の固定値を当てるのではなく、まだ来ていない仕事を見越して今の行動を選びます。
使うと戻らない容量3を持っていて、仕事を受けるたびに容量を消費して報酬を得るとします。仕事は今と次の二回だけ来ます。今の選択が次を受けられるかを変えます。
今の仕事の内容は分かっていますが、次の報酬はまだ見えません。分かっているのは、8と0が半々で出るという生成モデルです。
| 仕事 | 必要な容量 | 得られる報酬 |
|---|---|---|
| 今の仕事 | 2 | 確定で3 |
| 次の仕事 | 2 | 確率1/2で8、1/2で0 |
容量3から2を使うので、残りは1です。次の仕事に必要な2を確保できず、合計報酬は必ず3で終わります。
容量3が残るため、次の仕事を受けられます。合計報酬は8か0で、期待値は(8+0)/2=4です。
「今受ける」と「待つ」を本物の状態のコピー上で試します。待つ側は試行ごとに8か0が出るため、平均が4付近へ近づくまでにはばらつきがあります。
ここで間違えやすいこと試行の回数は実際に受けた仕事の数ではありません。二つの行動を比較するためのシミュレーション回数です。最後に一つの行動を選んだ段階で、本物の意思決定を行います。
この章で作る部品二行動を交互に試す方法とUCBによる試行配分を比べ、最後は試行で得た平均報酬から行動を選びます。
プレイアウトは、仮の状態で先の行動を進め、最後の結果を調べることです。UCBは、成績のよい候補と、まだ試行が少ない候補の両方へ試行を振り分けるための指標です。
容量2・報酬3
次の要求は入らない
容量3を保持
次の報酬8または0
平均・試行数を保存
次に試す候補を選ぶ
今受けると報酬3、断って次を待つと報酬8か0で期待値4です。行動後の仮の状態から未来を生成し、最後の報酬を測って比べます。
本物の状態は変更しません。この小例の厳密な期待値は答え合わせに使い、複雑な未来はシミュレータと簡単な行動規則で試します。
int reward = arm == 0 ? 3 : (rng.next()/4294967296.0 < .5 ? 8 : 0);
counts[arm]++;
totals[arm] += reward;
ここを見る:arm=0は今受ける、1は次を待つ行動です。この小例は先の報酬を直接生成します。試行結果はcounts・totalsに記録し、本物の残り容量は変更しません。
完成コードの L23–24 を見る →報酬合計と試行数を持ち、未試行を一回ずつ試して平均を求めます。予算1では最初の候補しか試せません。交互に試す版を基準にし、「待つ」側の報酬列は配分によらず同じseedから生成します。
UCB(Upper Confidence Bound)は、平均に「試行が少なく、まだ期待できる分」を加えた指標です。報酬を0〜1にそろえ、試行済み候補を次の値で比べます。
平均 + c × sqrt(log(全試行数) / 候補の試行数)
平均が高い候補と、試行が少ない候補を両方調べる指標です。cを大きくすると試行の少ない側へ配分しやすくなります。最後の行動は平均報酬で選びますが、有限回では選び間違いもあります。
double a = totals[0]/(8*counts[0]) + c*sqrt(log(double(t))/counts[0]);
double b = totals[1]/(8*counts[1]) + c*sqrt(log(double(t))/counts[1]);
arm = b > a ? 1 : 0;
ここを見る:この部分は両候補を一回以上試した後に実行します。平均報酬を最大報酬8で正規化し、試行回数が少ない候補ほど大きい加算項を付けます。
完成コードの L18–20 を見る →モンテカルロ木探索(MCTS)は、行動の分岐を木で持ち、仮の試行結果を使って有望な枝を詳しく調べる方法です。候補選択→子の展開→プレイアウト→祖先への結果反映を繰り返します。未試行ノードの扱いと、試行後に状態を戻す処理も必要です。
誤った未来モデルは、試行を増やしても直りません。モデルの仮定、推定のばらつき、選んだ行動の実得点を分けて検証します。
候補を試すたびに、本物の残り容量も減らしてよいですか?
いけません。試すのはコピー側です。本物を変えるのは、採用する行動を決めた後の一回だけです。
出発点各行動を同じ回数だけ試す版を作り、未来の生成・試行・集計を分けてからUCBへ進みます。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
行動を一つ適用し、仮の未来で最後まで進め、報酬を返す処理を用意します。まず行動を交互に試します。
ここで確認試行しても本物の資源は減らず、countsの総和が試行数になる。
未試行の行動を先に選び、その後だけUCB値を比較します。報酬の尺度と探索係数をそろえます。
ここで確認回数0で割らず、予算1でも終了できる。
予算を使い終えたら、探索ボーナスを含まない平均報酬で行動を選びます。有限回の推定結果を、複数seedで比較します。
ここで確認試行を配る指標と、最後に実行する行動の基準が混ざっていない。
MCTSにするならノードへ子ID・回数・報酬合計を持たせ、選択→未展開の子を作る→そこから試行→通った祖先へ結果を加える順に足します。初めは状態コピーを使います。
追加後の確認:各試行で通ったノードだけを一度ずつ更新し、深さ1では今の行動比較へ戻る。
完成コードで見る場所:counts:行動ごとの試行数 / int chosen:試行終了後の選択
今受ける行動の報酬は3。次を待つ行動は、確率1/2ずつで8または0です。各行動を交互に試す方法とUCBで配分する方法を、同じ試行予算で比べます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:4.1 インタラクティブ問題の入出力と、観測履歴を管理する、4.3 質問の情報量と費用を比べ、測定を選ぶ
発展の実装で参照する章:3.1 途中の答えを、何で比べるか考える。この章の導入例を読むための必修ではありません。
まず各候補を同じ回数試します。有望な候補へ試行を多く割り当てる方法や、先読みを何段も続ける方法は後から扱います。
新しい仕事が後から来る日程作成でも、仮の到着を作って、今の受け入れ方を比べられます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材今の仕事を受けるか、次の仕事を待つか決める — 問題設定・具体例を読む
4.4 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
試行予算1では一方しか試せません。予算を増やし、交互の試行とUCBの回数配分を比べます。seedや探索係数で選択が変わります。解析上の期待報酬は、今受ける3、次を待つ4です。
ここは通読する一本の経路ではありません。今の問題で必要な項目を選び、各章の前提を確認します。ビーム探索と観測・推定の導入は、それぞれ独立した経路にまとめてあります。
局所探索で使った仕事割当の小さな別入力です。割当と二乗和の考え方は同じなので、人数・仕事数・初期表の違いを確認すれば進めます。
仕事の分担を、四仕事・二人に縮めて考えます。前と同じく重さは固定で、答えは各仕事の担当者です。仕事の個数と人数を小さくしたのは、改善できない理由を全候補について確認できるようにするためです。
初期状態ではAが8と7、Bが6と5を担当します。一仕事を反対側へ動かすだけでは、どの変更も偏りを大きくしてしまいます。
| 仕事ID | 重さ | 初期担当 |
|---|---|---|
| 0 | 8 | A(0) |
| 1 | 7 | A(0) |
| 2 | 6 | B(1) |
| 3 | 5 | B(1) |
負荷は15と11なので15²+11²=346です。重さ5をBからAへ動かすと20と6になり、評価値436へ悪化します。
8と5をA、7と6をBへ任せれば、13と13で評価値338です。一仕事の改善がなくても、もっとよい担当表は存在します。
仕事の一部を未担当−1にし、残した割当の負荷を集計してから、外した仕事を配置し直します。全て担当が決まった後で山登りを再開します。
ここで間違えやすいこと未担当を含む表は修復の途中です。その負荷が小さく見えても、完成した解の評価値として最良記録へ保存してはいけません。
この章で作る部品山登りの外側へ、ランダムな再出発と、一部の割当を外して修復する処理を加えます。
始める答えを変えて探索を繰り返すのが多点スタート、答えの一部を外して作り直すのが部分再構築です。一手では改善できなくなったところから、別の答えへ移るために使います。
8+7 / 6+5
一つ移すと悪化
未担当を−1で記録
残した負荷から再配置
現在値は変わってよい
最良値346→338を保存
負荷15・11、評価値346は、一仕事の移動では改善しません。13・13なら338なので、別の割当に移る余地はあります。
climb はID順で最初の改善を採用し、先頭から調べ直します。全候補が改善しなければ終了。この関数の外側に再出発を加えます。
ランダム再出発は全担当を乱数で決め直します。部分再構築は選んだ仕事だけを−1にし、残りの負荷を再計算してから、未担当を重い順に負荷の小さい人へ戻します。
−1をscoreへ渡しません。外した仕事を記録し、全件を戻せたか検査してから山登りを再開します。
int load[2] = {0,0};
for (int i = 0; i < 4; i++)
if (current[i] >= 0) load[current[i]] += weight[i];
for (int i = 0; i < 4; i++) if (current[i] < 0) {
int g = load[0] <= load[1] ? 0 : 1;
current[i] = g;
load[g] += weight[i];
}
ここを見る:未担当を−1で表します。既存の担当を保ったまま未担当だけを埋め、−1がなくなってからscoreで評価します。
完成コードの L37–43 を見る →再構築で悪化しても、この例では新しい出発点にします。bestは改善時だけ更新します。四仕事全てを外すと貪欲法で13ずつになりますが、一仕事だけなら元へ戻る場合もあります。
外す数を増やすと探索範囲と修復時間が増え、よい構造も壊れます。大きいほどよいとは限りません。
climb(current);
if (score(current) < score(best)) best = current;
ここを見る:再構築したcurrentを山登りで仕上げ、その結果が過去のbestを改善したときだけ保存します。出発点を変えてもbestは失いません。
完成コードの L45–46 を見る →外した直後・修復後・山登り後を追い、seed、再出発回数、外す数を変えます。bestが悪化しないことを確認し、実問題では同じ実時間で比べます。
外した部分を全探索やDPで埋める型も使えます。一部の決定を外し、その部分を解き直してまとめて変更する方法を大規模近傍探索(LNS)と呼びます。
作り直しに失敗したら、取り出した仕事を未担当のままにしてよいですか?
いけません。試す前の答えへ戻します。最初から作り直す方法でも、提出用の答えは別に残します。
出発点改善しなくなるまで動くclimbを先に完成させ、その外側へ再出発・再構築を追加します。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
初期解からclimbを呼び、単独移動では346で止まることを確認します。bestは外側に保存します。
ここで確認一手も改善しないことと、全体によりよい答えがないことを区別できる。
対象だけを−1へ戻し、残した仕事から負荷を再計算します。未担当だけを再配置し、全て戻してからscoreを呼びます。
ここで確認対象外の担当が変わらず、途中の−1を添字として使わない。
再構築後に検査してclimbを呼び直します。新しい出発点をcurrentへ渡し、bestは改善時だけ更新します。
ここで確認現在の出発点が悪化しても、保存した最良値は悪化しない。
未担当を埋める部分だけを全列挙へ置き換えます。残した仕事を固定し、対象が多すぎる場合は貪欲な修復へ戻します。失敗時は元の有効解を使います。
追加後の確認:全仕事を探索し直さず、外した部分だけの組合せを試している。
完成コードで見る場所:void climb:内側の山登り / current[i]<0:未担当だけを修復する場所
重さ8・7・6・5を二人に配ります。8+7と6+5は一仕事の移動では改善しませんが、一部を外して戻すと13ずつの割当を作れる場合があります。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:2.2 山登り法を、コピーして試す形で組む
数手だけわざと乱してから改善し直す方法も比べます。これは摂動と呼ばれますが、最初は名前より「何をどこまで変えるか」を追います。
経路の一部分を作り直す場合は、前後の道とつながるように両端を固定します。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材四つの仕事を二人へ分け直す — 問題設定・具体例を読む
6.1 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
山登りのみでは346で止まります。再構築で四つ全てを外すと、8+5と7+6の13ずつへ配り直せます。一つだけ外す場合やseedを変えた再出発とも比べ、最良値が悪化しないことを確認してください。
倉庫0から配達先1〜4を各一回訪れ、倉庫へ戻ります。答えは訪問順[1,3,2,4]のような配列です。各区間のマンハッタン距離(横と縦の移動量の和)を合計し、小さい順番を探します。
ここから一台の車の訪問順を扱います。前の仕事割当とは、答えの形も費用の計算も変わります。倉庫と配達先、順列の位置を最初に確認してください。
一台の車が倉庫を出発し、四つの配達先へ配ってから倉庫へ戻ります。全配達先へ行くことは必須で、配達先の座標は動かせません。工夫できるのは訪問する順番です。
ここでは一つの訪問順を四要素の配列で表します。要素は配達先IDであり、配列の位置とは別です。入力の座標から区間の費用を計算し、その合計で順番のよさを比べます。
| 地点ID | 役割 | 座標 |
|---|---|---|
| 0 | 倉庫 | (0,0) |
| 1 | 配達先 | (4,0) |
| 2 | 配達先 | (4,3) |
| 3 | 配達先 | (0,3) |
| 4 | 配達先 | (2,1) |
[1,3,2,4]という答えなら、倉庫0→1→3→2→4→倉庫0と進みます。倉庫を配列に入れないのは、どの答えでも出発点・帰着点が倉庫に固定されているためです。
通常の費用は縦横の移動量の和です。1→3なら|4−0|+|0−3|=7。全区間は4+7+4+4+3=22で、最後の4→倉庫の3も含めます。
位置a=1、b=2は、二つ目の地点3と三つ目の地点2を指します。その区間を反転すると[1,2,3,4]になり、費用は4+3+4+4+3=18へ改善します。
ここで間違えやすいこと[1,2,2,4]は地点3が欠け、地点2が重複するため不正です。非対称の設定ではu>vの区間に費用2を足し、元の順は26、反転後は20になります。通常の距離の18と混ぜて比較しないでください。
[1, 3, 2, 4]実際に回る順番倉庫0 → 1 → 3 → 2 → 4 → 倉庫0この章で作る部品訪問順を一回だけ交換・挿入・反転し、有効性と費用の変化を検査します。巡回路を繰り返し改善する探索ループは7.1で組みます。
2-optは、巡回路の二辺を別のつなぎ方へ替える変更です。向きのない巡回路では区間反転で表せます。この章は交換・挿入も含め、訪問順を変える近傍を作ります。
二つの位置を入れ替える
長さは変わらない
一つ抜いて別の位置へ
挿入位置は変更後の添字
連続区間を逆順にする
内部の辺も向きが逆になる
訪問順は [1,3,2,4]。倉庫は配列に入れず、評価時に出発・帰還の辺を加えます。位置1の要素は地点3です。逆引き position[id] も保存するなら、順列の変更時に更新します。
交換は二位置を入れ替えます。挿入は位置aから抜き、変更後の位置bへ入れます。反転は閉区間[min(a,b),max(a,b)]です。C++のreverseは終端を含まないため、右端にr+1を渡します。
a=b、先頭、末尾を試し、長さと訪問先の集合が変わらないか確かめます。
if (move == 1) {
int v = after[a];
after.erase(after.begin()+a);
after.insert(after.begin()+b, v);
}
ここを見る:a・bは地点IDではなく配列の位置です。bを削除後の配列での挿入位置と決め、実装と演習の意味をそろえます。
完成コードの L23–26 を見る →対称な費用では、反転区間の内側の費用は同じで、出入口の二辺だけが変わります。非対称の設定はu>vの辺に2を加えるため、内側の向きの差も必要です。
反転の予測差分を全計算と照合します。交換・挿入は全計算から始め、差分化するときは影響する辺を列挙して重複を除きます。
predicted = boundary;
for (int i = min(a,b); i < max(a,b); i++)
predicted += distance_between(before[i+1], before[i], directed)
- distance_between(before[i], before[i+1], directed);
ここを見る:boundaryは反転区間の出入口の差です。内部も全て逆向きになるため、その差を加えます。対称距離なら内部の加算は0になります。
完成コードの L36–38 を見る →距離差分がO(1)でも、反転や挿入には動かす要素数分の時間がかかります。採用時は順列と評価値を更新し、棄却時は元を保ちます。演習は一手の生成と照合まで。7.1で探索ループへ組み込みます。
行きと帰りで費用が違う道でも、反転した区間の中の費用を無視してよいですか?
いけません。中の道を逆向きに通るので、その費用も変わります。動く見本ではこの違いを切り替えられます。
出発点順列を一つ固定し、倉庫との往復を含む全費用を求めてから変更操作を足します。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
倉庫→各地点→倉庫の費用をcostで求めます。順列の長さ・範囲・重複・欠落を調べる検査も用意します。
ここで確認訪問順が同じなら同じ費用になり、帰還の一辺を落としていない。
交換を動かした後に挿入、次に反転を追加します。先頭・末尾・同じ位置を含む例で、変更後の配列そのものを確認します。
ここで確認挿入した要素が変更後の位置bへ入り、各IDが一回ずつ残る。
コピー上の全費用と、出入りの辺の差を比較します。非対称な費用なら内側の辺の向きの差も足してから比較します。
ここで確認全区間の反転でも、予測差分が変更後−変更前と一致する。
順列変更を確定する場所で、動いたIDの逆引きも更新します。最初は全順列から作り直してよく、一致を確認してから更新範囲を絞ります。
追加後の確認:全IDについてorder[position[id]]がidになり、拒否時は両方とも元に戻る。
完成コードで見る場所:distance_between:方向を含む辺の費用 / boundary:反転で出入りする辺の差
倉庫0と四地点を結ぶ順列[1,3,2,4]を、交換・挿入・反転で変更します。変更後の順列と、行き帰りが非対称な場合の差分も確かめます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:2.1 制約に合う近傍を用意する、2.4 変わった分だけを計算する
行き帰りの費用が同じなら、反転区間の中の費用は変わりません。ただし、配列そのものを反転する作業には区間の長さに応じた時間がかかります。
荷物が増えると移動費用も増える問題では、同じ距離の式を使えないため、運んでいる量も追います。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材四つの配達先を一回ずつ訪れ、倉庫へ戻る — 問題設定・具体例を読む
6.2 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
最初の反転は訪問順1→3→2→4の中央を入れ替えます。非対称の費用では区間内部の辺も向きが変わります。交換・挿入へ切り替え、a=bの何も変わらない場合も試してください。
対象0〜5を、五つの設備候補から選んで全て覆います。候補0は{0,1,2}、候補2は{3,4,5}を覆い、各費用4なので、この二つなら総費用8です。削除後の未被覆を修復し、安い有効な集合を作ります。
訪問順から、設備候補の組合せへ題材を変えます。対象の番号と候補の番号を分けて読み、何を選べば全対象を覆えるかを確認してください。
六つの場所へサービスを届けるため、設備候補を幾つか採用します。一候補を選ぶと、その候補に書かれた場所へまとめて届くものとします。場所の座標や、候補を巡る順序は使いません。
入力は「各候補が覆う対象の一覧」と「採用費用」です。出力は選んだ候補の集合であり、全対象が少なくとも一候補で覆われることが必要です。
| 候補ID | 覆う対象 | 費用 |
|---|---|---|
| 0 | 0・1・2 | 4 |
| 1 | 2・3 | 2 |
| 2 | 3・4・5 | 4 |
| 3 | 0・5 | 3 |
| 4 | 1・4 | 3 |
候補0と2を選ぶと、{0,1,2}と{3,4,5}で全対象を覆えます。費用は4+4=8。演習の初期集合5は、ビット0と2が立った値1+4を意味します。
候補0を外すと費用は4へ下がりますが、0・1・2が未被覆になります。このままでは安い有効解とは言えません。
初期の費用なら候補0を戻すと三対象を費用4で覆えます。候補1なら新しく覆うのは2だけ、候補3なら0だけ、候補4なら1だけなので、新しく覆う数当たりの費用で選びます。
ここで間違えやすいことある対象を二候補が覆っていれば、一方を外しても未被覆にはなりません。削除を扱うために「覆われているか」だけでなく、覆う候補の個数を数えます。
この章で作る部品一候補を削除し、足りなくなった場所を追加候補で覆い直します。対象ごとの被覆数を更新し、全計算で確認します。
被覆は、選んだ設備などで必要な対象を覆うことです。一つも覆っていない対象を未被覆と呼び、候補を追加して制約を満たす状態へ戻す処理を修復と呼びます。
その候補が覆う対象の
countを1減らす
count=0の対象が残る
費用だけで採用しない
費用/新たに覆う数で選ぶ
countを1増やす
状態は候補の選択集合、補助情報は対象ごとの count[u] です。一候補を外したら、それが覆う対象だけ1減らします。別の候補も覆っていれば0にはなりません。入力の範囲と費用は冒頭の表で確認できます。
if (selected[remove]) {
selected[remove] = 0;
for (int u : covers[remove]) count[u]--;
}
ここを見る:removeは設備候補ID、uは覆う対象IDです。count[u]が0になった対象を修復の対象にし、別設備で覆われている対象と区別します。
完成コードの L12 を見る →候補0と2で費用8です。0を外すと対象0・1・2が未被覆になります。修復を無効にすると、この安く見える不正状態を確認できます。修復後に missing==0 となってから完成解として扱います。
未選択候補について、新たに覆う対象数gainを数え、費用/gainが最小の候補を追加します。gain=0は除外し、全候補が0なら修復失敗です。失敗時に戻れる有効解を保存しておきます。
比は cost[a]*gain[b] < cost[b]*gain[a] で比較し、同率なら小さいIDを選びます。大きい入力では積のオーバーフローにも注意します。
int gain = 0;
for (int u : covers[i]) gain += count[u] == 0;
if (gain > 0 &&
(chosen < 0 || cost[i]*chosen_gain < cost[chosen]*gain)) {
chosen = i;
chosen_gain = gain;
}
ここを見る:gainは未被覆を何個減らせるかです。費用÷gainの比較を交差積にし、gain=0の候補は除きます。この小入力では積はintに収まります。
完成コードの L17–18 を見る →選択集合からcount・未被覆数・費用を再計算し、差分更新と照合します。処理が止まっただけでは成功とはしません。
貪欲な修復は最小費用を保証しません。複数候補の削除・再構築や、不要な候補の削除へ広げられます。逆引きでgainを更新する高速化も可能です。
ある場所を覆う数が2→1になったら、その場所は覆われなくなりますか?
なりません。まだ一つが覆っています。1→0になったときに、初めて覆うものがなくなります。
出発点選択集合から被覆数を全計算できるようにし、一候補の削除と修復を順に足します。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
countを0から作り、選んだ候補の対象へ1ずつ加えます。候補内に同じ対象が重複しない表現にします。
ここで確認同じ対象を二候補で覆えば2となり、一つ削除しても1が残る。
削除では選択中だけ1減らし、追加では未選択だけ1増やします。missingを差分管理するなら、1→0と0→1だけで更新します。
ここで確認同じ候補を二度消して負数にならず、全計算の被覆数と一致する。
未選択候補のgainを求め、費用/gainの小さい候補を加えます。gain=0を除外し、全て覆えたかを最後に判定します。
ここで確認修復が止まっただけで成功にせず、未被覆が残れば失敗として扱う。
対象から候補への逆引きを用意し、対象の被覆数が0かどうか変わったときだけ関係するgainを更新します。最初は毎回の全計算と比べます。
追加後の確認:2→1ではgainが変わらず、1→0で未選択候補のgainが増える。
完成コードで見る場所:chosen_gain:修復に使う新しい被覆数 / vector<int> full:補助情報の全計算による照合
六対象を五候補で覆います。一候補を削除して未被覆数を調べ、費用/新たに覆う数が小さい候補を追加して修復します。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:1.7 貪欲法で初期解を作る、2.4 変わった分だけを計算する
費用が減っても、必要な場所が覆われなくなれば完成した答えにできません。覆う数と費用を別々に確認します。
必要な機能を満たす道具の選択でも、場所を機能へ読み替えると同じ数え方を使えます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材六つの場所を、候補の組合せで全て覆う — 問題設定・具体例を読む
6.3 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
修復を外すと、未被覆が残った安い候補を観察できます。初期集合0、候補0の費用7で、追加順が変わることも確認します。新たに覆う数が0の候補は、比の計算へ入れません。
8×6の箱で、長方形0(左下(1,1)・3×2)を動かします。長方形1(左下(5,1)・2×3)は固定。箱の内側に収まり、他方と重ならない変更だけを反映します。辺・頂点の接触は初期設定では許します。
ここでは長方形の位置と向きを扱います。費用の最小化へ進む前に、箱の内側・重なり・接触という配置の条件を判定します。
箱の中に置いた一つの長方形を動かしたとき、はみ出したり他の長方形へ重なったりしないかを調べます。この章は配置の採点方法を作る前の、合法な変更かどうかを調べる練習です。
座標(x,y)は長方形の左下、wとhは幅と高さです。占める範囲は横がxからx+w、縦がyからy+hになります。回転は幅と高さを入れ替える90度回転です。
| 物 | 左下 | 幅×高さ | 占める範囲 |
|---|---|---|---|
| 箱 | (0,0) | 8×6 | x:0〜8、y:0〜6 |
| 長方形0 | (1,1) | 3×2 | x:1〜4、y:1〜3 |
| 長方形1 | (5,1) | 2×3 | x:5〜7、y:1〜4 |
長方形0を(2,1)へ動かすと、横2〜5・縦1〜3を占めます。箱からはみ出さず、長方形1とはx=5の辺で触れるだけです。
接触を許すなら、この変更は合法です。xを3へ変えると横の共通部分が5〜6、縦が1〜3となり、面積1×2=2だけ重なって不正になります。
全ての条件を満たした場合だけ長方形0を候補へ置き換えます。例えばx=8なら右端が11となり、箱の幅8を超えるため、元の位置を保ちます。
ここで間違えやすいこと「接触も禁止」を選ぶと、面積0でも辺や頂点が触れる候補を拒否します。回転する場合は、変更後の幅・高さで箱と相手の両方を検査します。
この章で作る部品はみ出し・内部の重なり・境界の接触を分けて検査し、全条件を通った場合だけ位置と向きを反映します。
移動・回転した長方形が、箱の内側に収まり、他の物と重ならないかを検査します。
x≥0、y≥0
x+w≤W、y+h≤H
横の共通幅>0
かつ縦の共通幅>0
共通幅が0の境界
許すかどうかを先に決める
長方形を左下(x,y)と幅w・高さhで持ち、範囲を[x,x+w)×[y,y+h)とします。箱の壁への接触は許し、長方形同士の辺・頂点の接触は設定で切り替えます。整数座標なので許容誤差は不要です。
箱の条件はx≥0、y≥0、x+w≤8、y+h≤6。二長方形の共通幅dx・dyが両方正なら重なり、面積はmax(0,dx)×max(0,dy)です。
片方が0、他方が非負なら境界の接触です。接触禁止ならこれも拒否します。面積0だけでは合法と判定できません。
int dx = min(candidate.x+candidate.w, other.x+other.w)
- max(candidate.x, other.x);
int dy = min(candidate.y+candidate.h, other.y+other.h)
- max(candidate.y, other.y);
int overlap = max(0,dx)*max(0,dy);
ここを見る:dx・dyが負なら、その軸では離れています。面積が0でも接触している場合があるので、接触の可否は別に判定します。
完成コードの L10–12 を見る →90度回転は幅と高さを交換します。変更後の位置・幅・高さで箱と相手を検査し、通った候補だけ現在値へ代入します。拒否したらstateに元の長方形が残ることを確認してください。
bool touching = dx >= 0 && dy >= 0 && overlap == 0;
bool legal = inside(candidate) && overlap == 0 && (allow_touch || !touching);
if (legal) current = candidate;
ここを見る:箱の内側、面積の重なりなし、接触の規則をまとめて確認します。一つでも違反したらcurrentを変更しません。
完成コードの L13–15 を見る →物が多い場合も、まず変更した一つと他の全てを比較します。次に格子で近くの候補を絞り、複数セルにまたがる物や境界を越す移動を漏らさず更新します。少数例では全比較と照合します。
円なら中心距離と半径、線分なら交差判定へ替えます。連続座標へ移る場合は、丸めと境界の規則も決め直します。
辺が触れている二つの長方形は、重なる面積が0なら常に有効ですか?
接触を許す規則かどうかで変わります。この演習では切り替えられるため、面積の重なりと接触の条件を別に判定します。
出発点一つの長方形候補と箱・他の長方形を受け取り、有効性を返す判定から作ります。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
insideで箱の四条件を調べます。次に二長方形の共通幅を求め、面積の重なりと辺・頂点の接触を分けます。
ここで確認離れている・接している・内部が重なる、の三例を区別できる。
位置を変え、必要なら幅と高さを交換した候補を別に作ります。全条件が通った後だけ現在の長方形へ代入します。
ここで確認回転後の寸法で箱を検査し、拒否された候補が状態へ残らない。
物を増やしたら、変更対象以外との全比較をループにします。最初はここを正解確認用の基準版として残します。
ここで確認離れた物だけでなく、ちょうど境界に接する物も判定対象になる。
各長方形をまたがる全セルへ登録し、候補の範囲に関係するIDを集めて重複を除いて比較します。接触禁止なら境界で隣接するセルも拾う規則にします。
追加後の確認:境界をまたぐ移動・回転を含め、全比較で検出する衝突を一つも見逃さない。
完成コードで見る場所:struct Rect:座標と寸法 / bool legal:全条件をそろえて反映する場所
8×6の箱で二つの長方形を置きます。一つの位置と向きを変え、箱の内外・面積の重なり・辺や頂点の接触を別々に判定します。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:2.1 制約に合う近傍を用意する
線が交差するかの計算や、近くの物だけを探す方法は別の節で扱います。整数では計算できる大きさ、実数では丸め誤差にも注意します。
長方形を点や線へ替えたとき、保存する値と「重なる」の意味を考え直します。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材箱の中で長方形を動かしても、配置条件を守れるか調べる — 問題設定・具体例を読む
6.4 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
x=2、y=1、回転なしでは別の長方形に辺で接します。接触を許すかで採否が変わります。x=3なら面積が重なり、x=8なら箱からはみ出します。拒否したときは元の位置を保存します。
五つの町を双方向の道路でつなぎます。初期は0—1—2—3—4の四道路を選び、費用は各2で合計8。道路を削除・追加した後も全町へ到達できるか、費用はいくらかを調べます。
ここでは町をつなぐ道路を選びます。配達先を訪れる順番ではなく、使う辺の集合が答えです。道路IDと町IDを区別して読みます。
五つの町の間に、建設できる道路の候補があります。どの道路を採用するかを選び、全町を行き来できるようにしつつ、建設費用を減らしたい問題です。道路を通る順序は答えに含めません。
道路IDは辺そのものの番号です。道路1は町1と町2を結ぶ辺で、「町1」という頂点を削除する操作とは異なります。全道路は双方向に通れます。
| 道路ID | 結ぶ町 | 費用 |
|---|---|---|
| 0 | 0–1 | 2 |
| 1 | 1–2 | 2 |
| 2 | 2–3 | 2 |
| 3 | 3–4 | 2 |
| 4 | 0–4 | 7 |
| 5 | 1–4 | 3 |
| 6 | 0–2 | 4 |
道路0・1・2・3を選ぶと0–1–2–3–4がつながり、費用は8です。どの町からでも他の全町へ、選んだ道路だけで到達できます。
道路1を削除すると、町{0,1}と{2,3,4}に分かれます。費用は6になりますが、この状態は連結という条件を満たしません。
さらに道路5(1–4)を加えると、0から1→4→3→2と通って全町へ届きます。費用は9です。合法になりましたが、初期の8より安くなったわけではありません。
ここで間違えやすいこと演習は変更後の連結性を検査して反映するところまでです。費用が改善した場合だけ採用する探索へ広げるときは、その比較を別に加えます。
この章で作る部品辺を削除・追加した候補の連結性を検査します。演習は合法なら反映する段階までで、改善した候補だけを採用する処理は別に足します。
辺を削除・追加した後も、全頂点がつながっているかを検査します。
辺1(1—2)を削除
二つの成分に分かれる
1—4を追加
迂回して全頂点へ届く
BFSで到達数を数える
その後に費用を比べる
状態は辺の集合です。候補から隣接リストを作り、頂点0からBFSして全5頂点へ届くか確認します。一辺の変更でも遠い頂点への到達性が変わります。この問題は閉路を許し、木である必要はありません。
for (int i = 0; i < 7; i++) if ((mask >> i) & 1) {
adj[edges[i].u].push_back(edges[i].v);
adj[edges[i].v].push_back(edges[i].u);
}
ここを見る:maskのビットiが道路iの採否です。この問題は双方向なので両向きを登録し、このグラフで到達数を数えます。
完成コードの L9 を見る →辺1を削ると{0,1}と{2,3,4}へ分かれますが、辺5を加えると再びつながります。予定した削除・追加を終えてから検査します。
削除だけなら費用6の不正状態、追加後は費用9の有効状態です。初期費用8より悪くても、この演習は有効なら反映します。改善時だけ採用する探索は別に加えます。
int current = 15, candidate = current & ~(1 << remove);
if (add >= 0) candidate |= 1 << add;
bool connected = reached(candidate) == 5, accepted = !check || connected;
if (accepted) current = candidate;
ここを見る:途中で一度分断しても、追加後に全5町へ届けば候補は有効です。通常はcheck=true。外す設定は、検査を省く不具合の実験です。
完成コードの L20–23 を見る →通常のUnion-Findは辺の追加に向きますが、削除後の分断はunionの逆操作では分かりません。まずBFSで再検査します。一般グラフの橋の判定や、処理順を設計したロールバック付きUnion-Findなどは、その後の選択肢です。
木が条件なら、連結性と辺数n−1を保ちます。一辺追加でできる閉路から別の辺を削れば、連結性を保つ交換になります。一辺削除で分かれた二成分をつなぐ型も使えます。一般ネットワークの検査と木専用の生成を区別します。
削除した道の近くに別の道があれば、全体もつながっていますか?
それだけでは判断できません。離れた部分が切り離されることもあるので、まず全体をたどって確認します。
出発点辺の集合から隣接リストを作り直し、到達数と費用をそれぞれ求める基準版を作ります。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
選んだ辺だけで隣接リストを作り、頂点0からBFSします。費用の良し悪しと関係なく、全頂点へ届くかを返します。
ここで確認初期の道は5頂点へ届き、途中の一辺だけを削除すると到達数が減る。
辺集合をコピーし、削除と追加を両方終えてから検査します。削除直後だけの連結性で棄却しないようにします。
ここで確認削除で一度分かれても、追加でつなぎ直した候補は通る。
教材の例は有効な変更を反映する段階までです。山登りへつなぐ場合は、有効な候補にだけcostを呼び、改善なら採用します。
ここで確認分断した費用6を採らず、有効でも費用9なら改善とは扱わない。
初期解を木にし、非木辺を一つ足してできた閉路の経路を探し、その中の別の辺を削除します。最初は毎回BFSでも検査します。
追加後の確認:辺数n−1と連結性を同時に保ち、関係ない辺の削除で分断していない。
完成コードで見る場所:int reached:到達性の全検査 / candidate:削除・追加をまとめる候補
0—1—2—3—4から辺1—2を削除すると分断します。辺1—4を加えてつなぎ直し、到達数と費用を別に確認します。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
通常のDSUは辺追加には使えますが、自由な削除には対応しません。毎回の再構築、変更の取消し、木の構造を使う方法を、近傍と計算時間に合わせて選びます。
白いマスが一つにつながっているか調べる場合も、上下左右の白いマスを順にたどる方法が使えます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材五つの町を、選んだ道路でつなぐ — 問題設定・具体例を読む
6.5 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
追加を−1にすると、辺1の削除で分断します。連結性の検査を外すと費用6の不正なネットワークを反映してしまいます。辺5を加えた有効な変更は費用9なので、有効性と改善は別です。
整数x=0〜16を選び、費用を小さくします。教材では費用を固定表で与えます。粗く調べてから有望な周辺を細かく調べ、評価回数と見つかった費用を比べます。
ここでは一つの整数パラメータを調整します。前の道路の選択とは別で、評価にかかる計算を減らすための、小さな費用表を使う練習です。
整数パラメータxを一つ選ぶと、その結果の費用が得られる問題です。本来は費用を求めるシミュレーションが重い場面を想定しますが、教材では計算の代わりに小さい表を引きます。
x=0〜16の全17候補に費用が決まっています。入力の表は答え合わせにも使えますが、ここでは粗い位置から調べる方法がどこを見落とすかを観察します。
| x | 0・1・2・3 | 4・5・6・7 | 8・9・10・11 | 12・13・14・15・16 |
|---|---|---|---|---|
| 費用 | 20・15・10・5 | 2・5・9・13 | 12・9・4・0 | 3・8・14・18・22 |
刻み8ならx=0・8・16を評価し、費用は20・12・22です。この段階だけを見るとx=8が最善なので、中心を一つ残すなら8を選びます。
次は刻み4で0・4・8・12・16を調べ、費用2のx=4を残します。さらに刻み2、1と細かくすると、その周辺だけを調べるためx=4に落ち着きます。
中心を二つ残すと、別の谷の周辺も引き継げます。この例では費用0のx=11へ届く一方、調べる候補数も増えます。
ここで間違えやすいこと刻みを1まで細かくしたことは、全17候補を調べたことを意味しません。どの範囲を残したかと、評価した回数を合わせて確認します。
この章で作る部品刻みを半分にして候補を作る処理と、複数の中心を残す処理を組み、評価回数と得られた費用を比較します。
全域を粗く調べてから有望な周辺を細かく調べ、評価回数を抑えます。
0・8・16を評価
候補の中心を残す
中心の周辺を調べ直す
一つの谷へ絞りすぎない
残った周辺だけを細かく
全域の最適性とは別
最初の刻み8ではx=0・8・16を評価します。以後は有望な中心の周辺だけを調べます。状態は中心の一覧、設定は刻み幅と残す数です。費用関数を分ければ、固定表を実際のシミュレーションへ替えられます。
各中心から前の刻み幅だけ左右へ広げ、新しい刻みで列挙します。範囲外と重複を除き、費用順・同点なら小さいx順で中心を残します。
演習の費用は全段で同じです。粗い盤面や少ない試行数で評価する実問題では、最後に細かい条件で再評価します。
if (centers.empty())
for (int x = 0; x <= 16; x += step) candidates.insert(x);
else for (int center : centers)
for (int x = max(0,center-previous);
x <= min(16,center+previous); x += step) candidates.insert(x);
ここを見る:stepが現在の刻み幅、previousが前の幅です。絞込み後はcentersの周囲だけが対象になるので、離れた谷を見落とす可能性があります。
完成コードの L13–14 を見る →if (step == 1) break;
previous = step;
step /= 2;
ここを見る:候補を評価して上位keep件をcentersに保存した後の処理です。刻み1まで来たら終了し、それ以外は周囲をより細かく調べます。
完成コードの L21–22 を見る →中心一つではx=8→4と進み、費用2の谷に残ってx=11の費用0を見落とします。中心二つなら、この例では11へ届きます。
全費用曲線は答え合わせ用です。中心の選別には、その段階で実際に調べた値だけを使います。
中心を増やすと範囲は広がり、評価回数も増えます。演習では別段で同じ位置を調べ直す場合も一回と数えます。粗い探索・細かい探索・最終確認へ時間を配分し、刻みだけでなく調べた範囲と最終費用を比べます。
刻み1まで細かくしたら、全域の最小費用を見つけたと言えますか?
言えません。細かく調べたのは残した中心の周辺だけです。前段で失った範囲に、よりよい位置が残っている場合があります。
出発点位置を受け取り費用を返す関数を固定し、粗い候補生成と細かい候補生成を同じ評価へつなぎます。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
範囲の両端と刻みを決め、最初の候補を列挙します。費用と同点規則で並べ、中心を指定個数だけ残します。
ここで確認刻み8では0・8・16を評価し、その三点の値だけで中心を選ぶ。
前の刻みを保存してから半分にし、残した中心の周辺だけを列挙します。範囲外を除き、集合で位置の重複を消します。
ここで確認候補が重複せず、最後は刻み1で停止する。
各段で調べた候補数と、残した中心を記録します。小例では全位置を列挙した最小値と比較します。
ここで確認細かくした範囲の外に、見落とした谷がある場合を説明できる。
粗い段では短いシミュレーションを呼び、絞った候補だけを精密な評価へ送る形にします。最後は精密な条件で再順位付けします。
追加後の確認:粗い値を最終得点として保存せず、評価費用を含めた同じ予算で比較する。
完成コードで見る場所:vector<int> centers:段をまたいで残す情報 / previous=step:細かい段へ移る場所
整数位置0〜16の費用表を、刻み8・4・2・1で調べます。中心を一つ残すと費用2の谷にとどまり、二つ残すと費用0の谷へ届きます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:5.3 大まかな計画を、実際の手順へ変える
大まかな段階で選べなかった形へ進めないこともあります。分け方を変えた場合との比較も行います。
設備を選ぶ部分と設備をつなぐ部分を分け、片方ずつ改善する形へ移してみます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材一つのパラメータを、粗い刻みから細かく調べる — 問題設定・具体例を読む
6.6 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
最初の刻み8、中心1個ではx=4の費用2へ進みます。中心2個なら別の谷も残り、x=11の費用0を見つけられます。評価回数と最終費用を一緒に比べてください。
作業0〜3を二台の機械で処理します。同じ機械は同時に使えず、0の完了後に2、1の完了後に3を始めます。優先順から開始時刻を作り、全作業の完了時刻を求めます。
ここから機械で作業する日程を扱います。仕事割当の「誰に任せるか」とは異なり、使用する機械は固定で、作業の順番と開始時刻を決めます。
四つの作業を二台の機械で処理します。どの機械を使うかは作業ごとに固定されており、別の機械へ移せません。また、先に完了させる必要がある作業が指定されています。
答えとして欲しいのは各作業の開始・終了時刻です。探索では作業の優先順を持ち、その順に「今配置できる作業」を選んで、実行可能な日程へ変換します。
| 作業ID | 使用機械 | 時間 | 先に終える作業 |
|---|---|---|---|
| 0 | 0 | 3 | なし |
| 1 | 1 | 2 | なし |
| 2 | 1 | 4 | 作業0 |
| 3 | 0 | 2 | 作業1 |
優先順[0,1,2,3]で機械が時刻0から使えるなら、作業0は機械0の0〜3、作業1は機械1の0〜2へ置けます。別の機械なので同時進行できます。
作業2の機械1は時刻2に空きますが、先行作業0が終わるのは3です。そのため作業2は3〜7になります。作業3は先行作業1の終了2と機械0の空き3を見て3〜5に置きます。
終了時刻は3・2・7・5で、全作業が終わる時刻は最大値7です。同じ機械の作業は重ならず、先行関係も守った日程になっています。
ここで間違えやすいこと優先順[2,3,0,1]でも、作業2を無条件で最初に実行する意味ではありません。先行作業が未配置なので、置ける作業0・1から処理します。機械1の利用開始を遅らせる設定も、この開始時刻へ影響します。
この章で作る部品優先順を日程へ変換し、先行関係と機械の重複を独立に検査します。作業を別の機械へ割り当て直す探索は含みません。
スケジューリングは、資源や順序の制約を守って作業の日程を決めることです。この章は機械の同時使用と先行関係(ある作業の完了後に別の作業を始める条件)を扱います。
作業0が終わる時刻3
それ以前に作業2は始めない
機械1は時刻2から空く
同じ機械を重ねて使わない
max(3,2)=3
終了=開始+所要時間
使用機械は固定です。探索では開始時刻ではなく作業の優先順を持ち、デコーダが未配置かつ先行作業を配置済みの作業を順に選びます。優先順[2,3,0,1]でも、準備できている0・1から置きます。
for (int j : priorities[priority]) if (start[j] < 0) {
if (ignore_precedence || parent[j] < 0 || finish[parent[j]] >= 0) {
chosen = j;
break;
}
}
ここを見る:start[j]<0は未配置です。通常はignore_precedence=falseなので、先行作業が未配置なら飛ばします。優先順の先頭を無条件に実行しません。
完成コードの L13–15 を見る →開始は max(先行作業の終了, 機械が空く時刻)。所要時間を足して終了とし、機械の空き時刻も更新します。先行作業が複数なら、その最大終了時刻を使います。
このデコーダは機械ごとに末尾へ追加します。途中の空きへ挿入する方式ではないため、空白が残る場合もあります。
int j = chosen, ready = 0;
if (!ignore_precedence && parent[j] >= 0) ready = finish[parent[j]];
start[j] = max(ready, free_at[machine[j]]);
finish[j] = start[j] + duration[j];
free_at[machine[j]] = finish[j];
ここを見る:readyは先行作業の終了、free_atは機械が空く時刻です。大きい方から開始し、配置後に機械の空き時刻を更新します。
完成コードの L17–20 を見る →各i→jで finish[i]≤start[j]、同じ機械の全作業ペアで時間区間の内部が重ならないことを確認します。終了と次の開始が同時なら合法です。
先行関係を無視すると、機械が重ならなくても不正です。未配置なのに置ける作業がなければ、循環や入力不備を含むデコード失敗として返します。
作業0を長くすると、依存する作業や機械を共有する後続も遅れます。最初は全体をデコードし直し、作業時間帯を横棒で表すガント図と最大終了時刻を比べます。
途中から再計算する高速化は全デコードと照合します。時間枠・複数資源・段取り費用を加える場合も、生成条件と生成後の検査を分けます。
先行作業が時刻3に終わり、機械が時刻5まで埋まっていたら、いつ始められますか?
時刻5です。二つの条件を両方満たす必要があります。
出発点探索する優先順と、そこから生成する開始・終了時刻を分け、まず一つの優先順を復元できるようにします。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
優先順を走査し、未配置で先行作業が配置済みの作業を選びます。見つからなければ循環などのデコード失敗を返す設計にします。
ここで確認優先順の先頭が未準備でも、その後の実行可能な作業を選べる。
先行作業の終了と機械の空きの最大値を開始にし、終了と機械の空き時刻を同時に更新します。全作業が配置されるまで繰り返します。
ここで確認同じ機械へ二作業を同時に置かず、先行作業の終了を待てる。
生成した時刻から全ての先行関係と機械上の重なりを調べます。有効なら最大終了時刻を返し、優先順を変える探索へつなぎます。
ここで確認先行関係を無視して短くなった日程を、有効な改善として採用しない。
まず全デコードを残します。新旧の優先順で実際に選ばれる作業列を比べ、最初に違う作業を選ぶ直前の機械の空き・配置済み・終了時刻から再計算します。優先順での変更位置を、そのまま再計算の開始位置にしないようにします。
追加後の確認:一作業だけを直して止めず、先行関係で選択順が変わる例でも全デコードと一致する。
完成コードで見る場所:parent:先行作業の条件 / start:開始・終了時刻の生成と検査
四作業を二台の機械へ配置します。先行作業の終了と機械の空きを両方見て開始時刻を決め、優先順を変えて完了時刻を比較します。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
一つの作業を遅らせると、後続も遅れることがあります。最初は後ろまで全て計算し直し、影響する範囲を確かめます。
部屋の予約でも、同じ部屋を使う時間帯が重ならないかを確認する部分を使えます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材四つの作業を、決められた機械で順番に処理する — 問題設定・具体例を読む
6.7 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
標準の順では作業2は作業0の終了時刻3まで待ちます。先行関係を無視すると機械は重ならなくても、この条件を破ります。優先順2では、実行可能な作業から順に選ぶことも確認できます。
通路0—1—2で、Aは0から2、Bは2から0へ進みます。1の横に待避場所3があります。Aの経路を固定し、同時刻の同じ位置と、同じ辺の逆向き通行を避けるBの経路を探します。
機械の日程から、二台の移動体が通路を共有する問題へ変わります。予約という考え方は共通ですが、位置の衝突と辺のすれ違いを新しく定義します。
二台の移動体A・Bが、一本の細い通路を反対方向へ進みます。位置1の横に待避場所3があり、片方がそこへ避ければ行き違える場合があります。
Aの経路を先に決め、Bだけの経路を探します。初期設定ではAは一単位待ち、0→1→2と進みます。Bは位置2から0へ向かい、一単位時間ごとに隣へ進むか、その場で待ちます。
| 時刻 | Aの位置 | 安全なBの例 | Bの動き |
|---|---|---|---|
| 0 | 0 | 2 | 出発位置 |
| 1 | 0 | 1 | 中央へ進む |
| 2 | 1 | 3 | 横へ避ける |
| 3 | 2 | 1 | 中央へ戻る |
| 4 | 2 | 0 | 目的地へ着く |
| 5・6 | 2 | 0 | 到着後もその位置に留まる |
時刻2ではAが位置1へ来るため、Bはそこで待てません。表の例では時刻1から2の間に、Bが位置1から3へ移ります。
Aが0→1、Bが1→0を同じ時間区間に移動すると、時刻の端では位置が違っても途中で衝突します。頂点の占有だけでなく逆向きの辺使用も禁止します。
表のBは時刻4に到着し、その後は位置0を占有します。経路を探すときも再生するときも、到着した車を消えたものとして扱いません。
ここで間違えやすいこと経路なしは、固定したAの経路・使える待避場所・時刻上限6の下で見つからないという意味です。Aの待ち時間や計画順を変えた場合まで不可能だとは言えません。
この章で作る部品時刻を状態に含むBFSと、頂点・辺の衝突検査を作ります。検査を外す設定は不具合の再現用で、問題の衝突規則は変わりません。
予約表は、各時刻に誰がどの場所や辺を使うかを記録する表です。先に決めた経路を予約し、次の移動体がその予約と衝突しない経路を探します。
同じ時刻に
AもBも位置1
A:0→1 と B:1→0
途中ですれ違う
状態=(位置,時刻)
移動と待機を展開
Aの経路を固定し、Bを状態(位置,時刻)のBFSで探します。次の時刻へ隣接移動か待機を選びます。同じ位置でも時刻が違えば別状態です。探索上限は時刻6です。
int next = (t+1)*4+v;
if (parent[next] >= 0) continue;
parent[next] = id;
q.push(next);
ここを見る:各時刻に4位置あるので、時刻×4+位置をIDにします。同じ位置でも到着時刻が違えば別状態になり、待機を含む探索ができます。
完成コードの L22–24 を見る →Bが時刻tのuからt+1のvへ動くとき、Aがt+1にvにいれば拒否します。Aが同じ区間でv→uへ動く場合も、途中ですれ違うため拒否します。
初期設定のBは2→1→3→1→0と避けます。辺の検査を外すと短い不正経路ができるので、独立の再生検査で確かめます。
if (v == a[t+1]) continue;
if (edge_check && u == a[t+1] && v == a[t]) continue;
ここを見る:Bがuからvへ動き、Aの予約位置がaです。一行目は同時刻の同位置、二行目は同じ辺のすれ違いを拒否します。通常はedge_check=trueです。
完成コードの L20–21 を見る →到着後も目的地を時刻6まで占有します。経路なしは空配列・到着時刻−1です。これは固定したAの経路と時間上限の下での失敗で、全体に解がない証明ではありません。Aの待機や計画順を変える余地があります。
頂点予約 occupied[t][v] と辺予約 used[t][u][v] を管理し、経路を取り消すときはその予約も消します。共有を許す規則なら、数や主体IDも必要です。
全主体の同時探索や衝突からの制約分岐へ広げても、衝突の定義・到着後の占有・再生検査は共通です。
二台が隣のマスを同時に交換する移動は、許されますか?
この小問題では許されません。移動後のマスが違っていても、途中ですれ違うためです。
出発点固定した一主体の経路を予約として受け取り、もう一主体の経路を時刻付きBFSで作ります。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
移動先に隣接頂点と現在位置を含め、時刻を一つ進めます。時間上限で展開を止め、未訪問を位置だけで判定しないようにします。
ここで確認同じ位置でも違う時刻の候補を残し、待機を経由できる。
移動先が次時刻に占有されているかを検査し、さらに同区間の逆向き移動を拒否します。復元後にも二条件を再検査します。
ここで確認頂点は別でも、辺のすれ違いがある経路を検出できる。
到着後も目的地を占有させます。一般の経路では到着以降の予約も確認してから成功にし、見つからなければ経路なしを返します。
ここで確認失敗を「全体に解がない」と決めつけず、この予約と時間上限での失敗として扱う。
一経路が確定したときだけ頂点・辺予約へ登録します。途中で失敗したら、計画順や先の経路を見直し、その経路の予約を取り消して再計画します。
追加後の確認:古い予約が残らず、全主体を同時に再生して二種類の衝突がない。
完成コードで見る場所:parent((horizon+1)*4:位置と時刻を含む探索記録 / edge_check:すれ違いの検査
0—1—2の通路と、位置1の横の待避場所3を使います。先に決めたAの移動に対し、Bの待機と迂回を時刻付きBFSで決めます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
経路が見つからない場合は、先に決めた主体の待機や計画順も見直します。全主体を同時に探索する方法へ広げる場合も、到着後の占有と二種類の衝突検査を保ちます。
機械を使う時間帯を予約する問題でも、時間が重なるかを調べる部分を使えます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材狭い通路で二台を衝突させずに行き違わせる — 問題設定・具体例を読む
6.8 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
待ち時間1・待避場所ありなら、Bは横へ避けてAを通せます。すれ違い検査を外すと、同時刻に同じ頂点がなくても衝突する経路が出ます。待ち時間0ではAを固定したこの計画順で経路が見つかりません。
操作列ABABABACを、X=ABと定義してXXXACと書きます。定義と呼出し列の計7文字に登録固定費用を加え、元の8文字より得かを比べます。展開後の操作列は元と完全一致させます。
ここでは既に与えられた操作列を短く表します。移動する経路や訪問先を変えるのではなく、同じ操作を再現できる記述を作ります。
実行したい操作がA・B・Cの文字列で与えられます。繰り返すまとまりへ名前Xを付け、Xの一文字でその列を呼べるようにして、記述の総費用を減らします。
Xの定義も伝える必要があるため、呼出し列だけが短くなればよいわけではありません。定義と呼出し列を両方保存し、展開すると元の操作を同じ順に実行できることを確認します。
| 保存するもの | 初期設定の内容 | 費用 |
|---|---|---|
| 元の列のまま | ABABABAC | 8 |
| Xの定義 | AB | 2+登録固定費用 |
| Xを使った呼出し列 | XXXAC | 5 |
ABABABACの先頭から、ABを三回Xへ置き換えるとXXXACです。末尾のACは定義と違うため、そのまま残します。
XXXACの各XをABへ展開するとABABABACへ戻ります。内容や順番が変わった短い列は、圧縮に成功した答えではありません。
固定費用0なら2+5=7なので元の8より得です。固定費用2なら2+2+5=9となり、Xを登録せず元の列を使う方がよくなります。
ここで間違えやすいこと例えばAAAAにAAを登録するなら、重ならない二組をXXにします。開始位置0・1・2の三一致を同時に置き換えると、同じ文字を重ねて消費してしまいます。
この章で作る部品置換・復元・費用計算を作り、登録費用込みで元の列より得かどうかを判定します。
マクロは、操作のまとまりに名前を付け、一回の呼出しでまとめて実行する仕組みです。呼出しを元の操作列へ戻すことを展開と呼び、元の列との一致を検査します。
ABABABAC
8文字
呼出し列XXXACは5文字
定義2文字で計7
XをABへ戻す
内容一致と総費用を別に検査
マクロXの費用は、呼出し列の長さ+定義長+登録固定費用です。X=ABならABABABAC→XXXACで、固定費用0なら5+2=7。使わなければ登録費用は不要です。今回は再帰や定義内のXを許しません。
int candidate_cost = encoded.size() + macro.size() + fee;
int estimated = ignore_registration ? int(encoded.size()) : candidate_cost;
bool used = estimated < int(original.size());
ここを見る:encodedは呼出し列、macroは登録する操作列です。通常はignore_registration=falseとし、短く見える呼出し列だけで採用しません。
完成コードの L14–16 を見る →左から一致を見つけたらXを出し、定義長だけ進みます。不一致なら一文字出して進みます。AAAA中のAAは開始位置0・1・2にありますが、三回同時には置換できません。複数マクロでも、頻度だけでなく実際の符号化費用で比べます。
for (int i = 0; i < int(original.size()); ) {
if (original.substr(i,macro.size()) == macro) {
encoded += 'X';
i += macro.size();
} else encoded += original[i++];
}
ここを見る:一致時は登録列の長さだけ進めるので、同じ文字を重ねて利用しません。不一致なら元の一文字を残します。
完成コードの L10–13 を見る →展開結果が元の列と完全一致するか検査し、その後に費用を比べます。固定費用2なら5+2+2=9で元の8より高く、復元成功でも改善ではありません。登録費用を無視する不具合で、expanded_ok と improved の違いを確認します。
マクロの定義・呼出し列に加え、入力と内部状態から次の行動を決める状態機械の規則も探索対象にできます。候補ごとに出力サイズや規則数を検査し、初期状態から実行して評価します。元の列との一致が不要な問題なら、到達地点など実行結果を検査します。
呼び出す列が短くなれば、費用も必ず減りますか?
必ずではありません。まとまりを登録する費用もかかるルールなら、それを含めて比べます。
出発点固定した一つのマクロで、符号化・復元・総費用の計算を別々に作ります。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
元の列を左から読み、一致ならマクロ長だけ、非一致なら一文字だけ進めます。候補は別の文字列へ追加します。
ここで確認AAAAにAAを使うと二回の置換となり、重なる三一致を二重計上しない。
呼出し列を左から読み、マクロ名を定義の列へ戻します。採否を決める前に、元の操作列と完全一致するかを調べます。
ここで確認短くなっていても、文字の欠落や順番の変化を見逃さない。
呼出し列の長さ・定義の長さ・固定費用を足します。使わない場合の費用と比較し、候補が高ければ元の列を返します。
ここで確認復元成功と費用改善を別に判定できる。
マクロ候補を列挙する外側ループを追加し、各候補を元の列から独立に符号化します。実費用が最良の定義と呼出し列を組で保存します。
追加後の確認:前の候補の圧縮結果を次の入力にせず、定義と呼出し列の対応が保たれる。
完成コードで見る場所:original.substr:一致と置換位置を進める場所 / candidate_cost:登録も含めた費用
ABABABACでX=ABを定義し、XXXACへ置き換えます。元の列への復元と、定義・固定費用を含む総費用を別々に確認します。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:5.3 大まかな計画を、実際の手順へ変える
次は、今の観測と内部の記憶から行動を決める小さな規則表へ進みます。同じ見た目でも、記憶が違えば別の行動が必要になることがあります。
移動ロボットでは、文字列が同じかだけでなく、実際に動かした結果が同じかも確認します。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材繰り返す操作を一つの名前にまとめ、総費用を減らす — 問題設定・具体例を読む
6.9 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
ABABABACをX=ABで置き換えるとXXXACの5文字、定義2文字で計7です。固定費用を2に上げると元の8文字より高くなります。登録費用を無視すると、復元は正しくても費用の採否を誤ります。
駒は位置0から出発し、毎回1/2で一つ進み、1/2で留まります。位置2に着いたら動きません。三回後に各位置にいる確率と、位置の期待値を求めます。
ここでは操作した後の位置が確率で変わります。測定から未知値を推定する問題とは別で、動く確率を既知として結果を計算します。
駒が位置0から出発し、同じ操作を何回か受けます。各操作で、確率pで一つ先へ進み、残りの確率でその位置へ留まります。位置2へ着いたら、それ以降は必ず位置2に留まります。
操作の結果はランダムなので、一つの到着位置ではなく、各位置にいる確率を答えます。pは既知で、測定結果から推定する問題ではありません。
| 操作回数(p=1/2) | 位置0 | 位置1 | 位置2 |
|---|---|---|---|
| 0 | 1 | 0 | 0 |
| 1 | 1/2 | 1/2 | 0 |
| 2 | 1/4 | 1/2 | 1/4 |
| 3 | 1/8 | 3/8 | 1/2 |
最初は位置0に確率1でいます。半分が0へ留まり、半分が1へ進むので[1/2,1/2,0]です。一回だけで位置2へは行けません。
二回目に位置1へいるのは「止まってから進む」と「進んでから止まる」の二通りで、各1/4を足して1/2です。各操作の間で現在と次の配列を分けます。
三回後の位置2への到達確率は1/2です。位置の期待値は0×1/8+1×3/8+2×1/2=11/8になります。
ここで間違えやすいこと確率の合計が1でも、更新が正しいとは限りません。今位置1へ届いた分を同じ操作中にさらに位置2へ動かすと、一回で二歩進む誤った分布になります。
この章で作る部品現在と次の分布を別配列にして更新します。後で操作列を探索する際に、その候補を比較する評価器として使う部品です。
各位置にいる確率を更新し、ランダムな移動後の結果を計算します。
確率[1,0,0]
一操作だけ進める
確率1−pで同じ位置
確率pで一つ先
[1−p,p,0]
まだ位置2へは届かない
既知の移動確率pから、将来の分布を計算します。観測から未知値を推定する4.2とは別です。p=1/2で三回後は[1/8,3/8,1/2]、位置の期待値はΣ位置×確率です。
nextを0にし、位置xの確率の(1−p)倍をnext[x]、p倍をnext[x+1]へ足します。位置2は全量をnext[2]へ残します。全位置を処理してから現在配列を置き換えます。複数経路が合流するので代入ではなく加算します。
vector<double> advance_distribution(const vector<double>& old, double p) {
vector<double> next(3,0);
for (int x = 0; x < 2; x++) {
next[x] += old[x]*(1-p);
next[x+1] += old[x]*p;
}
next[2] += old[2];
return next;
}
ここを見る:位置0・1から進む場合と留まる場合を分配します。位置2は吸収状態なので、その確率を全て位置2へ残します。oldは途中で更新しません。
完成コードの L9–13 を見る →同じ配列を左から更新すると、0→1に届いた分を同じ操作で1→2へ動かしてしまいます。総和1でも、一操作で二歩進む不具合です。
一回・p=1/2なら位置2は0か、p=0・p=1・操作0回は正しいかを確認します。二配列の基準計算と比べ、総和・期待値・最大誤差を出します。
sum += distribution[x];
expected += x*distribution[x];
reference_expected += x*reference[x];
error = max(error, abs(distribution[x]-reference[x]));
ここを見る:各位置xについて集計します。sumが1でも遷移が正しいとは限らないため、基準分布referenceとの最大誤差errorも確認します。
完成コードの L29–30 を見る →疎な遷移は行ける先だけ列挙します。固定した操作列の期待得点を返す評価器にすれば、外側で操作列を探索できます。
状態が多すぎれば、乱数で一部の結果を取り出すサンプリングや、分布を簡略化する方法を使い、ばらつきや捨てた確率量を確認します。小例の厳密計算を誤差の基準に残します。
一つ進む確率1/2で、3回後に位置2にいる確率はいくつですか?
1/2です。2回後に位置2にいた1/4に、位置1から進む分1/2×1/2=1/4を足します。
出発点位置0の確率だけ1にした分布から、一操作の遷移を純粋な関数として作ります。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
各位置の確率を、止まる分と進む分に分けてnextへ加えます。現在の配列は読み取り専用にし、吸収状態はそのまま残します。
ここで確認一操作・確率1/2で[1/2,1/2,0]になる。
一操作の全位置を処理し終えてからnextを現在の分布にします。操作0回は初期分布をそのまま返します。
ここで確認今届いた確率を同じ操作で再び動かしていない。
確率の総和・非負性・簡単な到達確率を検査してから期待位置を計算します。小例は基準の分布とも照合します。
ここで確認総和1という条件だけで、誤った遷移を正しいと判定しない。
一操作の関数へ操作の種類を引数として足します。固定した操作列を最後まで評価する窓口を作ってから、列を変える探索へ接続します。
追加後の確認:各行の遷移確率の総和が1で、短い操作列は全分岐の列挙と一致する。
完成コードで見る場所:advance_distribution:一操作の分布更新 / reference_expected:基準分布からの期待値
位置0から、確率pで一つ進みます。位置2は吸収状態です。今の配列と次の配列を分けた更新を、同じ配列を更新する不具合と比べます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:1.6 検査と評価を分け、全計算を基準にする
この章は動く確率が分かっている例です。分からない値を測定から推測する4.2とは、作る処理を分けます。
分岐する機械の中を物が流れる問題でも、分かれ道ごとに確率を分けて足していけます。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材確率で進む駒が、何回か動いた後の位置を求める — 問題設定・具体例を読む
6.10 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
操作回数1・確率50%で不具合を有効にすると、位置2へも確率が流れます。総和は1のままなので、総和の検査だけでは見つかりません。確率0%・100%、操作0回も試してください。
残り仕事数xから完成費用yを予測する直線を学習します。学習用は(1,3)・(2,5)・(3,7)・(4,9)、確認用は(5,15)・(6,17)。確認用を学習へ混ぜず、予測誤差を測ります。
ここでは入力と完成費用の記録から予測器を作ります。前の確率分布の更新とは異なり、学習用と確認用に分けたデータを使います。
探索途中の状態を完成させるには、どれくらい費用がかかりそうかを予測します。導入例では状態の情報を「残り仕事数x」一つに絞り、実測した完成費用yとの関係を学びます。
下の各行は別々の入力ケースです。費用を作る規則は教材のデータ生成用で、予測器は学習に許された行のxとyから係数を求めます。
| 用途 | x | y(追加費用4の初期設定) |
|---|---|---|
| 学習 | 1 | 3 |
| 学習 | 2 | 5 |
| 学習 | 3 | 7 |
| 学習 | 4 | 9 |
| 確認 | 5 | 15 |
| 確認 | 6 | 17 |
最初の四行はy=2x+1なので、線形モデルŷ=ax+bを当てるとa=2、b=1になります。確認用の15と17は、この計算へ混ぜません。
x=5・6の予測は11・13ですが、実際の費用は15・17です。誤差は両方−4、二乗誤差の平均は(16+16)/2=16です。
学習しない基準予測2xなら10・12となり、二乗誤差の平均は25です。確認用で16まで減りましたが、探索の得点がどれだけ変わるかは別途測ります。
ここで間違えやすいこと確認用を学習へ混ぜれば、この二入力への誤差が減ることがあります。しかしそれは、まだ見ていない入力での精度を確かめたことにはなりません。
この章で作る部品データ分割、学習、推論、誤差集計を作ります。探索の途中評価へ組み込むことは次の追加実装として扱います。
教師あり学習は、入力と正解の組から予測の規則を作る方法です。ここでは状態の情報と完成後の費用を記録し、途中候補のよさを予測する評価関数に使います。
ケース0〜3
残り仕事数と実測費用
予測 = a × 残り仕事数 + b
C++でも同じ式を使う
ケース4〜5
予測誤差と探索への効果
残り仕事数xから完成費用yを予測する ŷ=ax+b を作ります。六ケースの最初の四つで学習し、残り二つで確認します。費用は教材用の生成データです。
予測の入力に使う数値を特徴量、正解として学ばせる実測結果を教師値と呼びます。この例ではxとyです。特徴量には探索時点で得られる情報だけを使います。
xとyの偏差の積和をxの偏差平方和で割ってaを求め、b=平均y−a×平均x とします。学習した規則で新しい入力を予測することを推論と呼び、この例はa*x+bです。正規化する場合は平均・尺度も保存します。
基準予測2*xと、確認用データの平均二乗誤差(MSE)を比べます。学習側とC++推論側で特徴量・尺度・係数をそろえます。
double predicted = a*x[i]+b, err = predicted-y[i];
mse += err*err;
ここを見る:a・bは学習用データから求めた直線の係数です。確認用データでは係数を更新せず、予測値と正解yとの差だけを集計します。
完成コードの L18 を見る →同じ入力から得た状態は似ているため、状態単位のランダム分割では確認側にも似た例が混ざります。入力ケースごとに分け、確認用を学習に使いません。
混ぜる設定では誤差が下がっても independent=0 です。追加費用を変え、学習範囲外で規則が変わったときのずれも確認します。
int count = leak ? 6 : 4;
double mx = 0, my = 0;
for (int i = 0; i < count; i++) { mx += x[i]; my += y[i]; }
mx /= count;
my /= count;
ここを見る:通常はleak=falseで先頭4件だけを学習に使います。最後の2件まで混ぜると、確認用の正解を見て係数を決める不適切な比較になります。
完成コードの L10–11 を見る →予測が正確でも、推論が遅く探索回数が減れば得点は悪化しえます。基準版と同じ時間で走らせ、目的値・失敗率・推論回数を比べます。MSEだけで得点改善とは判断しません。
正解行動を手本にする模倣学習や、行動後の報酬から学ぶ強化学習へ広げる場合も、利用可能な情報、学習と確認の分離、実行時コストを守ります。
同じ入力から得た似た状態を学習側と検証側へ分けると、何が問題になりますか?
既に見た入力に近い状態で評価し、新しい入力への予測性能を過大に見積もる可能性があります。入力単位で分けます。
出発点探索へ学習器をつなぐ前に、入力ごとの特徴量と完成費用を集め、学習と確認を分けます。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
何を予測するかを一つ決め、入力IDで学習側・確認側へ分けます。同じ入力からの状態を両側へ混ぜません。
ここで確認探索時に見えない未来の値を、特徴量へ入れていない。
学習側だけから係数を求め、確認側は推論にだけ渡します。まず線形モデルで、手計算や既存の単純な予測と照合します。
ここで確認同じ特徴量・尺度で学習時と実行時の予測が一致する。
確認誤差を調べた後、候補を並べる直前の評価関数を差し替えます。最終目的値の計算と有効性の検査は引き続き実測します。
ここで確認予測誤差だけでなく、同じ時間での試行数と最終得点も比較できる。
記録時と推論時の特徴量生成を同じ定義に更新し、学習データから係数を求め直します。正規化の統計も学習側だけから計算して保存します。
追加後の確認:入力ごとの分割を保ち、新しい特徴量の追加で得点と推論時間がどう変わるか分かる。
完成コードで見る場所:variance:係数を学習する集計 / predicted:保存した係数で予測する場所
六入力の残り仕事数と完成費用から、最初の四入力で線形モデルを学習します。最後の二入力で予測誤差を測り、確認用を学習へ混ぜた場合との違いを見ます。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この章の前提:1.6 検査と評価を分け、全計算を基準にする、5.1 いくつかの入力で、本当によくなったか比べる
発展の実装で参照する章:3.1 途中の答えを、何で比べるか考える、4.4 プレイアウトとUCBで、行動を比較する。この章の導入例を読むための必修ではありません。
まず線形モデルで処理を一通り作ります。小さい学習器、模倣学習、強化学習は、推論コストも比較する発展課題です。
残り費用の予測を、候補を最後まで試したときの成功率の予測へ変え、特徴量と教師値を定義し直します。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材残り仕事数から、完成までの費用を予測する — 問題設定・具体例を読む
6.11 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
確認用の入力を学習へ混ぜると誤差が下がっても、未見入力の検証にはなりません。追加費用を0へ戻すと線形モデルが正確に合う例、正の値では学習範囲外でずれる例になります。
配送を題材に、計画・変更・検査・評価・比較をつなぐ課題です。状態・候補生成・検査・評価・採用・再生をつないだ完成コードと演習を収録しています。三つの探索方法を全て使うことを必須にはしていません。
訪問順の問題を二台の分担へ広げます。1.2の割当と6.2の巡回路を組み合わせる課題なので、変更点を確認してください。
一台で全て回る巡回路を、二台で分担する問題へ広げます。各車の訪問順だけでなく、どの配達先をどちらの車へ任せるかも選べます。
この章のID 0〜5は全て配達先で、倉庫は別に(0,0)として持ちます。各配達先を一台が一回担当し、どちらの車にも少なくとも一件を任せます。
| 配達先ID | 座標 | 初期の担当 |
|---|---|---|
| 0 | (1,0) | 車0 |
| 1 | (2,0) | 車0 |
| 2 | (0,1) | 車0 |
| 3 | (0,2) | 車1 |
| 4 | (2,2) | 車1 |
| 5 | (3,1) | 車1 |
初期計画は車0が[0,1,2]、車1が[3,4,5]です。それぞれの前後へ倉庫を補い、二本の巡回路として解釈します。
車0は倉庫→0→1→2→倉庫で1+1+3+1=6。車1は倉庫→3→4→5→倉庫で2+2+2+4=10。目的値は二台の合計16です。
車内反転なら担当集合は変わらず、並びだけが変わります。一地点を車間で移せば件数も変わり、交換なら件数を保ったまま担当を変えられます。
ここで間違えやすいこと配達先を一件消して距離を短くしても、不正です。また「各車ちょうど3件」を有効にした場合は、一地点の移動で2件と4件になる候補を拒否します。距離は短い方の車や最大値ではなく、二台分の合計で比べます。
この章で作る部品初期計画・候補生成・検査・評価・最良保存・再生をつないで探索を動かします。車同士の衝突や時間・積載量の制約は、この課題にはありません。
計画・変更・検査・評価をつなぎ、一つの探索プログラムを完成させます。
車ごとの訪問順
全配送先を一回ずつ担当
コピー→変更→検査→評価
改善したときに採用
初期位置から再生
費用と全探索の基準を照合
初期計画は車0が[0,1,2]、車1が[3,4,5]です。各配達先を一台が一回担当し、両車とも一件以上を回って倉庫へ戻ります。途中で別の配達先を通過しても配達とは数えず、列に指定した訪問だけを担当とします。
valid は空の車、ID範囲、重複・欠落、「各車3件」の追加条件を検査します。cost は倉庫との往復を含む二台の合計距離です。不正候補は採点しません。
現在の計画をコピーし、車内反転・車間移動・車間交換で候補を作ります。一件しかない車からの移動などは検査で拒否します。
bool valid(const Plan& p, bool balanced) {
vector<int> count(6,0);
for (const auto& route : p) {
if (route.empty() || (balanced && route.size() != 3)) return false;
for (int id : route) {
if (id < 0 || id >= 6) return false;
count[id]++;
}
}
return all_of(count.begin(), count.end(), [](int x){ return x == 1; });
}
ここを見る:pは車ごとの訪問順です。空の担当と範囲外IDを拒否し、全配達先の出現数が1であることを確認します。balancedは各車3件の追加制約です。
完成コードの L12–19 を見る →有効で現在より安い候補を採用し、同点は棄却します。最良計画も更新して、指定回数後に出力します。xorshift乱数は、変更方法によらず毎試行四つ使います。
一手送りで候補・検査・採否を追います。現在値と最良値はこの山登りでは一致しますが、焼きなましへ替えるなら別に保存します。
bool legal = valid(candidate, balanced);
int value = legal ? cost(candidate) : -1;
bool take = legal && value < cost(current);
if (!legal) rejected_invalid++;
if (take) { current = candidate; accepted++; }
if (cost(current) < cost(best)) best = current;
ここを見る:不正な候補の−1は評価しない目印です。legalと組で判定するため、−1を「小さくてよい値」と誤って採用しません。
完成コードの L70–74 を見る →replay_cost で原点から一歩ずつ再生し、帰還と歩数を評価式と照合します。六地点なら全順列と分割位置を列挙し、最小費用も答え合わせできます。
全探索の結果は候補選択や出力の差し替えに使いません。追加制約を変えたら全探索側にも適用し、探索で得た値と最適値との差を測ります。
試行0回、反転のみ、移動のみ、交換のみ、混合を比べます。反転は担当車を変えられず、「各車3件」では一地点移動も使えません。
同じseed・回数で動作を確認した後、複数入力・seed・同じ実時間で比較します。差分評価・再構築・焼きなましは一つずつ足し、変更の効果を記録します。
方法をたくさん入れれば、この課題は完成ですか?
数では判断しません。答えがルールを守り、改善を比較でき、各処理が何をするか説明できれば完成へ近づいています。
出発点初期計画をそのまま出せる版を最初に作り、候補生成・検査・評価を一つずつ接続します。
自分のプログラムへ組み込むときの分け方です。完成コードでは、短い処理をmain内に置いている場合もあります。
初期計画をvalidで検査し、costとreplay_costの両方で距離を求めて出力します。この時点で出力の形式を確定します。
ここで確認配送先の重複・欠落がなく、帰還を含む二つの距離が一致する。
まずコピー上の反転だけを実装し、検査→全評価→改善なら採用→最良保存を固定回数で繰り返します。
ここで確認一手ごとの採否を再現でき、変更しない候補でも処理が壊れない。
別の車への移動、次に車間交換を追加し、共通のvalidへ通します。「3件ずつ」の条件を加え、拒否理由と採用数を記録します。
ここで確認空の車や件数違反を拒否し、最終解の再生も成功する。
各変更の影響する辺を列挙して差を計算し、コピー版の全費用と照合します。一致してから評価の呼出しだけを置き換え、次に計測します。焼きなましなどの採否変更はその後に分けます。
追加後の確認:同じ一手列で採否・最終計画が基準版と一致し、小例の全探索は答え合わせ専用のまま。
完成コードで見る場所:bool valid:候補が満たす条件 / replay_cost:出力計画の独立した再生
六配送先を二台で分担します。車ごとの順列をコピーして反転・移動・交換し、有効な改善だけを残します。最終計画の再生と小例の全探索も照合します。
小例はこの範囲への入口です。収録済みの本編と、今後広げる実装は章内で区別します。
この完成例は、局所探索を配送計画へ組み込んだものです。ビーム探索なら担当や訪問先を一つずつ決める状態、観測と判断なら未確定の移動費用を加えるなど、次の総合課題へ広げられます。
車の積載量に上限を足し、担当変更の後に新しく何を調べる必要があるか説明します。
この章の演習では、設定を変えると図・結果・コピーする完成コードが対応して変わります。試してほしい変更は、演習の下の折りたたみに示しています。
この演習の題材二台の車で六つの配達先を分担して回る — 問題設定・具体例を読む
7.1 の教材体験見本 この場で動きます
結果を予想して「実験する」を押します。設定は完成コードにも反映されます。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
反転だけでは各車の担当集合が変わりません。移動・車間交換を加え、同じ試行数とseedで比較します。ちょうど3件の制約を有効にすると移動候補は不正となるので、車間交換が役立ちます。
各章の演習に加えて、仕事の重さを変えた差分評価、採用確率、経路、被覆を試せます。必要な見本を選んでください。ビーム探索・観測と判断の演習は、それぞれの経路の本編にあります。
1.5・2.3・2.4 の教材体験見本 この場で動きます
冒頭と同じ仕事を使います。仕事には左から0〜5の番号を付け、番号0・1をA、2・3をB、4・5をCへ任せます。各人の負荷の二乗和で評価し、小さい方をよい分け方とします。まず設定を変えずに「実験する」を押しても構いません。
外すと、担当だけ戻して合計を戻し忘れた場合を試せます。最初は変更しなくて構いません。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
2.5 の教材体験見本 この場で動きます
同じ差分に対する採用判定を繰り返し、exp(−差分/温度)と観測した採用率を比べます。温度・seed・試行回数を変えて確かめてください。探索全体は2.5の本編で扱います。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
仕事を移すときは、二人分の合計が変わりました。訪問順や候補の選び方を変える場合は、どこが変わるでしょうか。6.2・6.3を読んだら、この二つを試してみてください。
6.2・2.4 の教材体験見本 この場で動きます
地点0〜4の座標は順に (0,0),(4,0),(4,3),(0,3),(2,5) です。最初の訪問順は 0→2→1→3→4→0、費用はマンハッタン距離です。反転範囲は地点IDではなく、訪問順配列の添字で指定します。添字1〜2の反転なら、地点2→1が地点1→2になります。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
出入口だけで増減を計算できても、配列を逆順に並べ直す作業は別です。並べ直す範囲が長ければ、その作業には多くの時間がかかります。
6.3・2.4 の教材体験見本 この場で動きます
場所は0〜5の六つです。幾つかの場所をまとめて覆う候補を選びます。候補0は場所0・1・2を費用4で、候補1は2・3を費用2で、候補2は3・4・5を費用4で、候補3は0・5を費用3で覆います。全ての場所を少なくとも一つの候補で覆い、費用を減らすのが目標です。
JavaScript が有効なブラウザで、値を変えて実験できます。
下は、同じ設定のコードを実行すると表示される文字です。自分の実行結果と同じかを比べられます。図の説明を読む段階では、開かなくても進めます。
費用を減らすことと、全てを覆うルールを守ることの両方を確認します。
ここは後から引くための一覧です。最初に12種類を覚える必要はありません。「訪問順を変えたい」など、自分の問題に近い項目から章へ進めます。
仕事を三人に分ける。人ごとにまとめた数字を見て、仕事を移す。
作る部品:担当の表 → 人ごとの合計 → 移動 → 前後の比較
移した二人以外も変わるルールなら、その影響も調べます。
1.4 探索で変える情報を、状態として持つ / 2.1 制約に合う近傍を用意する / 2.4 変わった分だけを計算する / 6.7 作業の順番から、開始・終了の時刻を決める
倉庫から四つの配達先を全て訪れ、倉庫へ戻る。順番の一部を逆にする。
作る部品:訪問順の配列 → 距離 → 並べ替え → 合計の比較
行きと帰りで費用が違えば、逆にした区間の内側も計算し直します。
6.2 訪れる順番を、交換したり逆にしたりする / 2.4 変わった分だけを計算する / 5.2 最短路を、ヒューリスティック探索の部品にする
幾つかの場所を一度に担当する候補から、全てを覆う組合せを選ぶ。
作る部品:選んだかの表 → 場所ごとの覆う数 → 追加・削除
一つ消しても別の候補が覆っていることがあります。覆う数を記録します。
6.3 必要な場所を、選んだもの全部で覆う / 1.7 貪欲法で初期解を作る / 2.4 変わった分だけを計算する / 6.1 行き詰まったら、始め方や一部分を変える
二つの長方形を箱へ入れる。一方の位置や向きを少し変える。
作る部品:座標と大きさ → 重なりの確認 → 移動・回転
辺に触れてよいか、箱からはみ出してよいかを先に決めます。
町を結ぶ道を選ぶ。道を一つ変え、つながりと費用を調べる。
作る部品:隣の町の一覧 → 到達できる町の確認 → 道の変更
近くの道だけ見ても、遠い町が切り離されたことを見落とします。
6.5 辺の変更と、連結性の検査を組み合わせる / 5.2 最短路を、ヒューリスティック探索の部品にする / 6.1 行き詰まったら、始め方や一部分を変える
数を選んで11に近づける。今は不利な候補も少し残す。
作る部品:候補の一覧 → 一手先の候補 → 比較 → 残す数を制限
今の値だけでは、最後に良い答えになるか分からないことがあります。
1.7 貪欲法で初期解を作る / 3.2 ビーム探索で、途中の候補を複数残す / 3.3 ビーム探索の履歴を復元し、重複を取り除く / 3.4 ビーム探索のメモリと、多様性を管理する
訪問順を先に決め、地点間の道を後から探す。
作る部品:計画 → 手順へ変える処理 → 実行して確認
計画は短くても、手順に直したときに実行できるとは限りません。
5.2 最短路を、ヒューリスティック探索の部品にする / 5.3 大まかな計画を、実際の手順へ変える / 6.6 大まかに決めてから、細かく直す
二台の機械で作業する。同じ機械に作業を重ねない。
作る部品:使う物 → 開始・終了の時刻 → 重なりの確認
一つを遅らせると、後の作業まで遅れることがあります。
6.7 作業の順番から、開始・終了の時刻を決める / 6.8 同じ時刻に、同じ場所へ入らないようにする / 5.3 大まかな計画を、実際の手順へ変える
ABをXと呼ぶことにして、長い操作列を短く書く。
作る部品:元の列 → まとめる規則 → 短い列 → 元へ戻して確認
呼び出しが減っても、規則を登録する費用が増える場合があります。
6.9 繰り返す手順を、短い書き方へまとめる / 3.3 ビーム探索の履歴を復元し、重複を取り除く / 5.3 大まかな計画を、実際の手順へ変える
半分の確率で進む駒が、三回後にどこにいるか考える。
作る部品:各位置の確率 → 一回分の移動 → 確率を足し合わせる
動き方の確率は分かっている、という前提から始めます。
箱の中のカードは赤か青。ときどき間違うセンサーの返事から色を推定する。
作る部品:観測した値 → 候補の重み → 更新 → 次に測るか決める
本当の値と、センサーが返した値を別の変数にします。
4.1 インタラクティブ問題の入出力と、観測履歴を管理する / 4.2 ベイズ推定で、観測から仮説の確率を更新する / 4.3 質問の情報量と費用を比べ、測定を選ぶ
今仕事を受けると、後の大きな仕事を受けられないかもしれない。
作る部品:今の候補 → 仮の将来 → 最後まで試す → 結果を比べる
未来はまだ見えません。仮に作った将来を、本当の未来として扱いません。
4.1 インタラクティブ問題の入出力と、観測履歴を管理する / 4.4 プレイアウトとUCBで、行動を比較する / 5.1 いくつかの入力で、本当によくなったか比べる
代表的な十の題材をまとめています。各章の冒頭にも、その章の入力・答え・条件・評価方法を記載しています。同じ種類の題材でも、個数や条件を変える章では、その違いを明示しています。
重さ8、7、6、5、4、3を三人へ分けます。一つの仕事は一人だけが担当します。各人の負荷の二乗和を最小化します。
倉庫0=(0,0)から、配達先1=(4,0)、2=(4,3)、3=(0,3)、4=(2,1)を各一回訪れて戻ります。通常の費用はマンハッタン距離で、6.2では非対称の費用も試します。
対象は0〜5。五候補の担当範囲は{0,1,2}、{2,3}、{3,4,5}、{0,5}、{1,4}、費用は順に4・2・4・3・3です。全対象を覆い、合計費用を減らします。
町0〜4をつなぐ道路を選びます。最初は0—1—2—3—4の各辺を費用2ずつで使います。別の道路への置換で、全町への到達性と合計費用がどう変わるか調べます。
8×6の箱へ、3×2と2×3の長方形を置き、一方を移動・回転します。はみ出しと内部の重なりは禁止です。接触の許否を変えて、候補が合法かを調べます。
作業0〜3の所要時間は初期設定で3・2・4・2、使う機械は0・1・1・0です。0の後に2、1の後に3を処理し、機械の利用を重ねず全作業の完了を早めます。
8・6・5を順に選ぶか選ばないか決め、合計と11との差を小さくします。3.3では別要素の5をもう一つ足し、同じ合計になる選択と履歴を扱います。
3.1 途中の答えを、何で比べるか考える / 3.2 ビーム探索で、途中の候補を複数残す / 3.3 ビーム探索の履歴を復元し、重複を取り除く
箱の中のカードは赤か青。4.1は模擬相手との通信、4.2は誤差のある測定での確率更新、4.3は測定費用と誤答損失を含む質問選択を扱います。測定の規則は各章冒頭に示します。
4.1 インタラクティブ問題の入出力と、観測履歴を管理する / 4.2 ベイズ推定で、観測から仮説の確率を更新する / 4.3 質問の情報量と費用を比べ、測定を選ぶ
初期の列ABABABACをX=ABでまとめるとXXXACになります。呼出し列と定義の文字数に登録の固定費用を加え、元の列をそのまま使う場合と比べます。
位置0から出発します。毎回ある確率で一つ進み、残りの確率で止まります。位置2へ着いたら動きません。三回後に位置2へ着いている確率を求めます。
これは高校数学・プログラミング・アルゴリズム競プロの基礎を理解した、AHC初学者を想定する机上確認です。実際の読者に学習してもらった結果ではありません。各章を順にたどり、「前提知識と、それまでに説明したAHCの実装の知識で、設計の理由と確認問題を理解できるか」を見直しました。
| 読む場面 | つまずくと想定した点 | 説明の直し方 | 理解を確かめる行動 |
|---|---|---|---|
| 冒頭 | 35章・12分類を先に読むと、最初に何をするか分からない。 | 最初に一つの担当変更を紙で追う。分類や出典は後半へ移す。 | 担当の例を見て、重さ7の仕事の移動先を答える。 |
| 1.2〜1.4 | 配列にはできても、何を状態に保存するかの判断が見えにくい。 | 固定入力・探索状態・再計算できる情報を分け、表現で保証できる制約も示す。 | 担当表に負荷や仕事一覧を追加したとき、更新箇所がどう増えるか説明する。 |
| 1.5〜1.6 | 良い変更を選ぶ話と、変更する処理の区別がまだない。 | 決めた一手を動かしてから、ルールと良さを別々に調べる。 | 担当Dへの移動は変更せず拒否する。 |
| 1.5の図 | 担当表だけを保存する本文に、未学習の合計配列の更新が混ざっていた。 | この章の図は担当表の一か所だけを変える。合計の保存は2.3から導入する。 | 番号1の値だけが0から2へ変わると説明する。 |
| 数学の前提 | 高校数学を履修済みの読者に、二乗や座標の定義から説明していた。 | 高校数学の用語・記法を前提とし、評価式の意味と実装への対応に説明を集中する。 | 二乗和を選ぶ理由と、load配列から計算する手順を説明する。 |
| プログラミングの前提 | 競プロ経験のある読者に、演算子・配列・関数などの基礎を説明していた。 | 文法の説明を省き、変更の前提条件、失敗時の状態、検査・評価・採用の分担を扱う。 | 棄却後に何が一致すべきかと、全計算を残す理由を説明する。 |
| 1.6 | 負荷の二乗和を最小化する理由と、実装する処理の関係が見えにくい。 | 平均からの偏差平方和との関係を示し、負荷の集計と評価値の計算を分ける。 | 評価式をload配列とscore関数へ対応させる。 |
| 1.7〜1.8・2.1〜2.2 | 乱数・時計・保存・近傍を同時に導入すると追えない。 | 固定回数と最良解の保存を先にする。移動・交換以外の変更は後へ分ける。 | コピー側を変え、不採用なら元を残す流れを説明する。 |
| 1.7から2.2へ | この小入力では1.7で既に全員11となり、その答えからは改善を体験できない。 | 2.2は偏りのある1.3の担当表から始める、と明記する。 | 重さ7を移すと395から381へ改善し、重さ5を移すと405へ悪化すると比べる。 |
| 1.8の小例 | 保存の練習なのに、まだ学んでいない候補の作り方まで必要になる。 | 三つの担当表を示し、それを順に調べて最良の答えを残す。 | 今の値が389でも、381の担当表を保存して出せる。 |
| 2.3〜2.4 | 差分やundoの名前だけでは、省略する計算と戻す情報が分からない。 | 担当表と合計を分け、変更前後の数を並べる。 | 合計を戻し忘れた例と、Bの項を省ける理由を説明する。 |
| 2.5 | 採用確率の式と、温度・乱数による判定の関係が見えにくい。 | 温度の役割を説明し、指数関数の式をexpと乱数比較へ対応させる。 | 同じ差分で温度を上げたとき、採用確率がどう変わるか説明する。 |
| 2.6・3.1〜3.3 | 途中評価・幅・層・親・重複が一度に増える。 | 8,6,5の一例を共有する。候補を残す話、手順をたどる話、重複の話を順にする。 | 8を選ばない道が6+5へつながることを追う。 |
| 3.3・5.1〜5.3・7.1 | 経路復元やBFSを知っていても、探索の中でどう使うかが見えにくい。 | 復元用の記録の寿命と、距離表を再利用できる条件を具体化する。 | 候補を削除しても必要な親を残す理由と、壁を変更した場合に必要な再計算を説明する。 |
| 3.4・6.1・6.4・6.6・6.2〜6.3・6.5・6.7・6.9 | 新しい題材へ移った途端に抽象語が増える。 | 長方形、四つの仕事、三つの町などの具体例から各章を始める。 | その章の一つの変更について、前後と失敗例を説明する。 |
| 4.1〜4.4・6.8・6.10 | 確率を使う全ての問題を、未知の値の推定と混同しやすい。 | 測定から考える例と、動き方の確率が既知の例を分ける。 | 観測後の3/4と、3回後の到達確率1/2を別の計算として説明する。 |
| 1.2〜1.8・2.1〜2.4の図解 | 処理の分岐や更新する配列を文章だけでは追いにくい。 | 各章に処理の関係図を置き、2.3は担当表と負荷の三時点、2.4は評価値の項別の変化を示す。 | 不具合を有効にし、どの情報が一致しなくなるかを図から特定する。 |
| 網羅性 | 導入例だけを章の範囲にすると、ビーム管理・推定・評価学習が見えなくなる。 | 実装範囲を導入例と別に保存し、三つの経路と6.11の学習による評価を明示する。 | ビームの重複排除、質問選択、学習と検証の分割を、対応する章から探せる。 |
| 手元で実行 | 本文とコードの対応を確認するのに、複数のファイルを集める手間がある。 | 内蔵入力付きの完成コードと、保存・コンパイル・実行を行うスクリプトをそれぞれコピーできるようにする。 | 画面と同じ設定で実行し、出力を照合する。 |
| 2.4と2.6 | 差分評価の説明が両章で重なり、次に学ぶ理由が分からない。 | 2.4は差分式と全計算の照合、2.6はコピーと評価を分けた処理量・実時間の計測へ分担する。 | 差分評価を有効にしたまま仕事数を増やし、コピーの仕事量だけが増える設定を説明する。 |
| 3.3・3.4 | 重複排除と、多様性のために候補を落とす操作を混同する。 | 完全な状態一致、ハッシュ衝突、粗い種類分けを異なる演習で扱う。 | 衝突だけで消す不具合と、種類別上限で有望な候補を落とす例を再現する。 |
| 4.1〜4.4 | 通信、推定、質問選択、未来の試行を一度に読むと役割が見えない。 | 窓口→観測から更新→費用を含めた質問・停止→仮の未来の試行、の順で別の完成コードを使う。 | 真値をソルバへ渡さず、情報利得があっても停止を選ぶ条件を説明する。 |
| 5.1〜5.3・6.11 | 短い経路・小さい平均・小さい予測誤差を、そのまま改善と受け取る。 | 現在の壁での再生、帰還条件、失敗件数、学習と確認の分離を図と結果に表示する。 | 古い経路、帰路省略、実行失敗、確認用入力の混入をそれぞれ検出する。 |
| 三つの経路の境界 | 経路図は分かれているが、本文と「次へ」は章番号順なので、局所探索の途中でビーム探索へ移ってしまう。 | 共通準備・三つの経路・共通の検証・選択する発展へ本文を再配置し、入口・到達点・経路内の前後移動をそろえる。 | 2.5から2.6へ、3.3から3.4へ進めることと、3.4・4.4を他経路の必修なしで読めることを確認する。 |
| 章番号と経路 | 章の記号から所属する学習経路が分からない。 | 共通準備を1から始め、経路1・2・3はそれぞれ2.x・3.x・4.xとする。共通部品・発展・総合練習も続けて採番する。 | 目次・本文・図・演習・前提章・次へが同じ番号を使い、番号だけでも経路を判別できることを確認する。 |
| 6.1・6.2・6.3 | 再構築中の未完成な答えを、そのままよい解として採用しやすい。 | 取り外し・修復・検査を分け、各時点の担当表や被覆数を表示する。 | 一つだけ外す場合と全て外す場合、未被覆が残る場合を説明する。 |
| 6.4・6.5 | 面積0や費用減少を、制約を満たした証拠と受け取りやすい。 | 接触の許否と連結性を、面積・費用から独立に判定する。 | 接触だけで拒否される候補と、安くなった分断状態を再現する。 |
| 6.6・6.7・6.8 | 細かい探索なら最適、短い日程や経路なら有効、と思い込みやすい。 | 調べた範囲、先行関係、頂点衝突とすれ違いを図にする。 | 失った谷・先行関係違反・辺のすれ違いを、設定を変えて見つける。 |
| 6.9・6.10 | 復元一致や確率の総和1という一条件だけで、正しく改善したと判断しやすい。 | 登録費用を含めた採否と、一操作で到達可能な位置を別に検査する。 | 復元できても高くなる圧縮と、総和1でも間違った分布を再現する。 |
| 7.1 | 多数の関数が並ぶと、候補生成から出力までのつながりを追えない。 | 一手ごとの候補・有効性・評価・採否を並べ、最終解を再生して照合する。 | 試行0回から始め、3件ずつの制約で移動が拒否される理由を説明する。 |
| 各章へ直接入る場面 | 頂点・順列・作業時間が出てきても、何を決める問題なのか分からない。 | 全章の冒頭で入力・答え・条件・評価・作る部品を明示し、演習からも問題設定へ戻れるようにする。題材一覧も現在の演習に合わせる。 | 6.2だけを開き、四配達先を回って倉庫へ戻ること、配列に倉庫を含めないこと、初期経路の費用22を図から説明できる。 |
今後は実際の初学者にも試してもらい、説明を読み返した場所や、答えに迷った問題を記録して直します。
説明・図・操作できる演習・入力例・コードを、このHTMLに含めています。読者が作者のフォルダを開く必要はありません。コードは閉じたまま全文をコピーでき、設定を変えるとコピーされるコードも変わります。
現在の40種類の完成コードは、必要な処理と小さい入力を全て含み、追加ファイルなしで動きます。各章の導入例を実行でき、さらに広げる実装は発展課題として区別しています。
ページ内の実験はJavaScriptで計算します。コピーするC++のコードは、手元のC++実行環境で動かします。二つの結果が対応することを検査しています。
AHC001〜073の公式問題文の設定・操作・採点方法を見比べ、繰り返し現れる形を教材の十二の型へ整理しました。どの解法が何回使われたかを数えた調査ではありません。次の一覧は出典確認用で、学習の前に読む必要はありません。
2026年10月26日時点の番号付き AHC001〜AHC073の全73回について、公式問題文の問題設定・操作・目的を横断確認しました。下の分類は教材を設計するための整理です。全提出の解法調査や手法の使用率の集計ではありません。番号のない大会と開催前のAHC074は対象外です。公式の過去大会一覧
問題文から分かるのは必要な構造です。「この問題は焼きなましが最善」までは分かりません。そのため、複数時期・複数題材で現れる構造を拾い、手法の使い方は公式入門解説・個別の一次解説・既存実装と照合しました。下の典型の分け方と学習優先順位は、その確認をもとにした教材上の判断です。
| 範囲 | 読み取った共通構造 | 構造を確認できる公式問題文 |
|---|---|---|
| 001–020 | 配置・経路・観測・被覆・接続が既に併存 | AHC001・AHC003・AHC006・AHC007・AHC020 |
| 021–040 | 操作列・期間制約・推定と設計の組合せを確認 | AHC023・AHC025・AHC030・AHC033・AHC038・AHC040 |
| 041–060 | 木、規則、期待値、資源の累積など評価の形が多様 | AHC041・AHC044・AHC047・AHC050・AHC051・AHC056・AHC058 |
| 061–073 | 干渉、設計と運用、逐次割当、被覆の複合例も確認 | AHC064・AHC065・AHC066・AHC069・AHC071・AHC073 |
① 別の問題にも現れる構造である。② 保存する情報・操作・検証を小さく説明できる。③ 適用条件と反例を示せる。④ 他の部品と組み合わせられる。登場回数だけで順位を付けず、検証や時間管理のように多くの実装を支える部品も共通基礎に置きます。
一つの問題に複数のタグを付けています。全ての制約を列挙した表ではなく、教材へ取り出す構造の索引です。本文の小題材は独立した仕様なので、これらのリンクを開く必要はありません。
| 回・公式問題文 | 抽出した構造 | 分類 |
|---|---|---|
| AHC001 | 点を含む長方形の面積調整と非重複配置 | 配置・形状 |
| AHC002 | 同じタイルを再訪しない得点付き経路 | 順序・経路/操作列・状態遷移 |
| AHC003 | 未知の辺重みを経路の観測値から推定 | 順序・経路/グラフ・連結/未知情報・逐次観測 |
| AHC004 | 複数の文字列を格子の中に含める配置 | 文字列・規則/配置・形状/選択・被覆 |
| AHC005 | 道路を見渡す地点の選択と巡回 | 選択・被覆/順序・経路/グラフ・連結 |
| AHC006 | 配達する注文の選択と受取・配送の順序 | 選択・被覆/順序・経路/時間・資源 |
| AHC007 | 順に判明する辺費用に対する接続の決定 | グラフ・連結/未知情報・逐次観測 |
| AHC008 | 動く相手を観測しながら領域を仕切る | 配置・形状/グラフ・連結/操作列・状態遷移/未知情報・逐次観測 |
| AHC009 | 操作が確率的に失われる移動列 | 順序・経路/操作列・状態遷移/確率的な結果 |
| AHC010 | タイルの向きを選び大きな閉路を作る | 配置・形状/グラフ・連結 |
| AHC011 | スライド操作で木を表す配置へ近づける | 配置・形状/グラフ・連結/操作列・状態遷移 |
| AHC012 | 直線で点集合を分割し個数を調整する | 配置・形状/割当・分割 |
| AHC013 | 機器の移動と交差しない接続 | 配置・形状/グラフ・連結/操作列・状態遷移 |
| AHC014 | 既存の点・辺を使い図形を順に増やす | 配置・形状/操作列・状態遷移 |
| AHC015 | 次の配置位置を観測しながら盤面を操作 | 配置・形状/操作列・状態遷移/確率的な結果/未知情報・逐次観測 |
| AHC016 | ノイズのあるグラフ通信の設計と識別 | グラフ・連結/設計と運用/確率的な結果/未知情報・逐次観測 |
| AHC017 | 辺を工事日へ割り当て迂回費用を減らす | 割当・分割/グラフ・連結/時間・資源 |
| AHC018 | 未知の硬さの土地を掘って水路を接続 | グラフ・連結/未知情報・逐次観測/時間・資源 |
| AHC019 | 投影を満たす立体の共通部品を構成 | 配置・形状/選択・被覆/グラフ・連結 |
| AHC020 | 対象を覆う設備の選択・出力調整・接続 | 選択・被覆/配置・形状/グラフ・連結 |
| AHC021 | 隣接交換で大小関係を満たす配置を作る | 操作列・状態遷移/配置・形状 |
| AHC022 | 測定用の配置を設計し未知の対応を識別 | 設計と運用/配置・形状/未知情報・逐次観測/確率的な結果 |
| AHC023 | 作物の期間と作業時の到達可能性を両立 | 割当・分割/時間・資源/グラフ・連結/配置・形状 |
| AHC024 | 色の連結性と隣接関係を保ち領域を縮小 | 配置・形状/グラフ・連結 |
| AHC025 | 集合の大小比較で未知の重さを均等分割 | 割当・分割/未知情報・逐次観測 |
| AHC026 | 積み重なった物を順番に取り出す操作列 | 操作列・状態遷移/順序・経路 |
| AHC027 | 再訪間隔が評価に影響する巡回経路 | 順序・経路/時間・資源 |
| AHC028 | 単語の重なりと打鍵位置を考える順序 | 文字列・規則/順序・経路/選択・被覆 |
| AHC029 | 逐次補充される選択肢に対する資源運用 | 時間・資源/操作列・状態遷移/未知情報・逐次観測/確率的な結果 |
| AHC030 | ノイズのある集合観測から隠れた配置を推定 | 配置・形状/未知情報・逐次観測/確率的な結果 |
| AHC031 | 日ごとの区画割当と変更費用の両立 | 配置・形状/割当・分割/時間・資源 |
| AHC032 | 局所的な加算操作を回数制限内で組み合わせる | 配置・形状/操作列・状態遷移 |
| AHC033 | 複数台で干渉を避け指定順に物を搬出 | 操作列・状態遷移/時間・資源/順序・経路 |
| AHC034 | 積載量で費用が変わる運搬経路 | 順序・経路/時間・資源/操作列・状態遷移 |
| AHC035 | 確率的な継承を見込んで隣接配置を決める | 配置・形状/確率的な結果/未知情報・逐次観測 |
| AHC036 | 信号制御用の配置と訪問操作を設計 | 設計と運用/文字列・規則/グラフ・連結/順序・経路 |
| AHC037 | 中間物を共有して複数の目標を生成 | グラフ・連結/操作列・状態遷移 |
| AHC038 | 木構造の装置を設計して物を運ぶ | 設計と運用/グラフ・連結/配置・形状/操作列・状態遷移 |
| AHC039 | 周長制約のある単純な領域で対象を囲む | 配置・形状/選択・被覆 |
| AHC040 | 未知寸法の測定と長方形の梱包 | 配置・形状/未知情報・逐次観測/確率的な結果 |
| AHC041 | 高さ上限のある根付き森を構成 | グラフ・連結 |
| AHC042 | 行・列の操作で残す物と除く物を分ける | 操作列・状態遷移/配置・形状 |
| AHC043 | 資金と収入を考慮して被覆・接続を拡張 | 選択・被覆/グラフ・連結/時間・資源/操作列・状態遷移 |
| AHC044 | 遷移規則で訪問回数の目標を実現 | 文字列・規則/グラフ・連結/操作列・状態遷移 |
| AHC045 | 問い合わせで得る情報と分割・接続の設計 | 割当・分割/グラフ・連結/未知情報・逐次観測 |
| AHC046 | 移動環境も変更できる順序付き訪問 | 順序・経路/操作列・状態遷移/配置・形状 |
| AHC047 | 確率的な文字列生成器の設計 | 文字列・規則/設計と運用/確率的な結果 |
| AHC048 | 連続量の混合と容器・操作順の設計 | 配置・形状/設計と運用/操作列・状態遷移/時間・資源 |
| AHC049 | 累積損傷を制約とする積載と運搬 | 順序・経路/時間・資源/操作列・状態遷移 |
| AHC050 | 位置分布を変える配置順の期待値評価 | 順序・経路/配置・形状/確率的な結果 |
| AHC051 | 確率分岐する非交差ネットワークの構成 | グラフ・連結/配置・形状/確率的な結果 |
| AHC052 | 共通操作を割り当て複数体で全域を訪問 | 設計と運用/文字列・規則/操作列・状態遷移/選択・被覆 |
| AHC053 | 事前に数を設計し観測後に分割する | 設計と運用/割当・分割/未知情報・逐次観測 |
| AHC054 | 相手の逐次行動に応じ通路を変更する | グラフ・連結/配置・形状/未知情報・逐次観測/確率的な結果 |
| AHC055 | 獲得する資源を使い次の対象を処理する | 順序・経路/時間・資源/操作列・状態遷移 |
| AHC056 | 色と内部状態の遷移規則で巡回を実現 | 設計と運用/文字列・規則/順序・経路/操作列・状態遷移 |
| AHC057 | 時刻で変わる位置と結合後の運動を考える | グラフ・連結/時間・資源/割当・分割 |
| AHC058 | 投資順と生産の累積効果を評価する | 順序・経路/時間・資源/操作列・状態遷移 |
| AHC059 | スタックの組合せ条件を満たす回収経路 | 順序・経路/操作列・状態遷移 |
| AHC060 | グラフ上で種類の異なる列を作り納品する | 文字列・規則/グラフ・連結/順序・経路/操作列・状態遷移 |
| AHC061 | 複数の相手の行動を観測して領域を争う | 配置・形状/操作列・状態遷移/未知情報・逐次観測/確率的な結果 |
| AHC062 | 訪問順に応じた報酬を持つ全点訪問 | 順序・経路 |
| AHC063 | 形状と色の列が変化する移動操作 | 順序・経路/文字列・規則/操作列・状態遷移 |
| AHC064 | 干渉を避けながら複数の列を並べ替える | 操作列・状態遷移/時間・資源/順序・経路 |
| AHC065 | 循環装置を設計し順序付き搬出に使う | 設計と運用/グラフ・連結/操作列・状態遷移 |
| AHC066 | 繰り返す操作を記録して短く表現する | 文字列・規則/設計と運用/順序・経路/操作列・状態遷移 |
| AHC067 | 状態を持つ通路を設計し最短到達を遅らせる | グラフ・連結/設計と運用/配置・形状/操作列・状態遷移 |
| AHC068 | 領域単位の交換で目標配置へ変換する | 配置・形状/操作列・状態遷移/グラフ・連結 |
| AHC069 | 逐次到着する要求に期間付き領域を割り当てる | 割当・分割/配置・形状/時間・資源/未知情報・逐次観測 |
| AHC070 | 移動規則の設計と時系列の施設配置 | 設計と運用/順序・経路/選択・被覆/時間・資源 |
| AHC071 | 支持条件と非重複制約のある被覆 | 選択・被覆/配置・形状 |
| AHC072 | 積み重ねと移動を使う色別の搬送 | 操作列・状態遷移/順序・経路 |
| AHC073 | 初期配置と環境を変える移動の組合せ | 設計と運用/配置・形状/順序・経路/操作列・状態遷移 |
公式入門解説(wata):合法な出力と評価から始め、貪欲・局所探索・差分計算・焼きなましへ進む段階を、共通基礎の設計に反映しました。
AHC032公式解説:途中状態を残す探索と、確定部分・残り資源を評価する考え方を確認しました。加算が可換であることや盤面を確定する順序は、この問題の性質として扱います。
AHC020参加者解説(poka):使う対象の選択、接続、出力強度を分ける一実装例を確認しました。これを「外側の設計と内側の解き直し」の例とし、その分解で常に全体最適になるとは説明しません。
Ctrl+C / ⌘C でコピーできます。