D - 交互移動ロボット / Alternating Movement Robot Editorial by admin
Gemini 3.8 Flash (High)概要
マス目上を「縦移動」と「横移動」を交互に行いながら、スタートからゴールまでの最短手数を求める問題です。直前の移動方向を状態に含めて幅優先探索(BFS)を行うことで解くことができます。
考察
素朴なアプローチの問題点
通常の最短経路問題では「現在いるマス \((r, c)\)」のみを状態として管理し、訪問済みかどうかを記録します。 しかし本問では、「直前に縦に移動してきたか、横に移動してきたか」によって、次に進める方向(縦か横か)が制限されます。
そのため、単に「マス \((r, c)\) に到達した」という情報だけを記録していると、 - 「縦移動で到達したルート(次は横にしか動けない)」 - 「横移動で到達したルート(次は縦にしか動けない)」
が混同されてしまいます。例えば、あるマスに手数が同じで先に到達したルートがあっても、そのルートの向きのせいでゴールへ進めない場合、後から異なる向きで到達したルートを探索できずに誤った答え(WA)となってしまいます。
状態の拡張
この問題は、グラフの頂点(状態)を増やす「状態の拡張」によって解決できます。 ロボットの状態を以下のように \(3\) つ組で表します。
- \((r, c, \text{dir})\)
- \(r, c\): 現在のマス(行番号、列番号)
- \(\text{dir} = 0\): 直前に「縦移動」をした(次は「横移動」のみ可能)
- \(\text{dir} = 1\): 直前に「横移動」をした(次は「縦移動」のみ可能)
このように状態を分けることで、同じマスであっても「縦から入ってきたか」「横から入ってきたか」を区別して最短手数を管理することができます。
アルゴリズム
すべての移動コストは \(1\) であるため、幅優先探索(BFS) を用いて最短手数を求めます。
- 初期化:
- 距離配列
dist[r][c][dir]を \(-1\) で初期化します。 - スタート地点 \((1, 1)\) からの最初の \(1\) 手目は縦・横のどちらでもよいため、
- 上または下に \(1\) マス進めるなら、そのマスを
dist[nr][nc][0] = 1としてキューに追加します。 - 左または右に \(1\) マス進めるなら、そのマスを
dist[nr][nc][1] = 1としてキューに追加します。
- 上または下に \(1\) マス進めるなら、そのマスを
- 距離配列
- BFS の遷移:
- キューから状態 \((r, c, \text{dir})\) を取り出します。
- 取り出したマスがゴール \((H, W)\) であれば、そのときの移動回数が最小値となります(BFS の性質より、最初に取り出された時点で最小手数が確定します)。
- \(\text{dir} = 0\) の場合(次は横移動):
- 左右に隣接するマス \((r, c \pm 1)\) のうち、グリッド内で障害物のないマスへ移動します。
- 未訪問(
dist[r][nc][1] == -1)であれば距離を更新し、状態 \((r, nc, 1)\) をキューに追加します。
- \(\text{dir} = 1\) の場合(次は縦移動):
- 上下に隣接するマス \((r \pm 1, c)\) のうち、グリッド内で障害物のないマスへ移動します。
- 未訪問(
dist[nr][c][0] == -1)であれば距離を更新し、状態 \((nr, c, 0)\) をキューに追加します。
- 終了判定:
- キューが空になるまでにゴールに到達できなかった場合は、到達不能であるため
NONEを出力します。
- キューが空になるまでにゴールに到達できなかった場合は、到達不能であるため
計算量
- 時間計算量: \(O(HW)\)
- 状態数は \(H \times W \times 2\) 通りです。各状態からの移動の候補は \(2\) 通り(縦なら上下、横なら左右)なので、探索する辺の総数は最大でも \(4HW\) 本です。\(H, W \le 1000\) より、状態数は最大で約 \(2 \times 10^6\) となり、実行時間制限に余裕で間に合います。
- 空間計算量: \(O(HW)\)
- 盤面の情報 \(A\)、距離配列
dist、BFS のキューを保持するために \(O(HW)\) のメモリを使用します。
- 盤面の情報 \(A\)、距離配列
実装のポイント
初手の扱い: スタート地点 \((1, 1)\) 自身を「向きを持たない特殊な状態」としてキューに入れるよりも、初手で進める隣接マスを距離 \(1\) としてあらかじめキューに入れておくことで、BFS ループ内の遷移を「縦なら横へ、横なら縦へ」というシンプルな反復だけに統一できます。
0-indexed の活用: プログラム内ではマス目の座標を \(0\) から \(H-1\)、\(0\) から \(W-1\) の 0-indexed で扱うと、境界値判定や配列の参照が簡潔になります。
ソースコード
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int H, W;
if (!(cin >> H >> W)) return 0;
vector<vector<int>> A(H, vector<int>(W));
for (int i = 0; i < H; ++i) {
for (int j = 0; j < W; ++j) {
cin >> A[i][j];
}
}
// dist[r][c][dir]
// dir = 0: 直前に縦移動した(次は横移動)
// dir = 1: 直前に横移動した(次は縦移動)
vector<vector<vector<int>>> dist(H, vector<vector<int>>(W, vector<int>(2, -1)));
queue<pair<pair<int, int>, int>> q;
// 縦方向の初手 (上または下)
int dr[] = {-1, 1};
for (int d = 0; d < 2; ++d) {
int nr = 0 + dr[d];
int nc = 0;
if (nr >= 0 && nr < H && A[nr][nc] == 0) {
dist[nr][nc][0] = 1;
q.push({{nr, nc}, 0});
}
}
// 横方向の初手 (左または右)
int dc[] = {-1, 1};
for (int d = 0; d < 2; ++d) {
int nr = 0;
int nc = 0 + dc[d];
if (nc >= 0 && nc < W && A[nr][nc] == 0) {
dist[nr][nc][1] = 1;
q.push({{nr, nc}, 1});
}
}
int ans = -1;
while (!q.empty()) {
auto [pos, dir] = q.front();
q.pop();
auto [r, c] = pos;
if (r == H - 1 && c == W - 1) {
ans = dist[r][c][dir];
break;
}
int d = dist[r][c][dir];
if (dir == 0) {
// 次は横移動
for (int i = 0; i < 2; ++i) {
int nr = r;
int nc = c + dc[i];
if (nc >= 0 && nc < W && A[nr][nc] == 0) {
if (dist[nr][nc][1] == -1) {
dist[nr][nc][1] = d + 1;
q.push({{nr, nc}, 1});
}
}
}
} else {
// 次は縦移動
for (int i = 0; i < 2; ++i) {
int nr = r + dr[i];
int nc = c;
if (nr >= 0 && nr < H && A[nr][nc] == 0) {
if (dist[nr][nc][0] == -1) {
dist[nr][nc][0] = d + 1;
q.push({{nr, nc}, 0});
}
}
}
}
}
if (ans != -1) {
cout << ans << "\n";
} else {
cout << "NONE\n";
}
return 0;
}
この解説は gemini-3.8-flash-high によって生成されました。