經典貪吃蛇問題:在地圖中控制蛇移動,蛇身不可自撞、不可撞牆、不可進入障礙物。給定初始蛇身座標與障礙物,求蛇頭到達目標位置 (0,0) 的最小步數。蛇長固定,每步蛇頭移動一格,蛇尾收縮一格(除非吃到食物,本題無食物機制)。
觀察:蛇身相鄰節點曼哈頓距離恆為 1,即方向僅有上下左右 4 種。以蛇頭座標 + 相對方向序列編碼:
(x, y):各 5 bits(0~19),共 10 bits。L-1 段方向:每段 2 bits(0:上, 1:右, 2:下, 3:左),共 2*(L-1) bits。L=8 → 10 + 14 = 24 bits,完美置入 32 位整數。編碼函數:
int encodeState(const Node& snake) {
int code = snake.x * 20 + snake.y; // 頭部座標壓縮為 0~399
for (int i = 1; i < snake.len; ++i) {
int dir = getDirection(snake.p[i-1], snake.p[i]); // 上一節點→當前節點方向
code = (code << 2) | dir;
}
return code;
}
struct Node {
uint8_t x, y; // 蛇頭座標
uint8_t len; // 蛇長
uint8_t dirs[7]; // 相對方向序列 (len-1)
uint16_t step; // 步數
};
int bfs() {
std::queue<Node> q;
std::vector<bool> visited(1 << 24, false); // 狀態訪問標記
Node start = parseInput();
int startCode = encodeState(start);
visited[startCode] = true;
q.push(start);
const int dx[4] = {-1, 0, 1, 0};
const int dy[4] = {0, 1, 0, -1};
while (!q.empty()) {
Node cur = q.front(); q.pop();
if (cur.x == 0 && cur.y == 0) return cur.step; // 到達目標
for (int d = 0; d < 4; ++d) {
int nx = cur.x + dx[d], ny = cur.y + dy[d];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue; // 越界
if (obstacle[nx][ny]) continue; // 障礙物
// 構造新狀態
Node nxt = cur;
nxt.x = nx; nxt.y = ny; nxt.step++;
// 更新方向序列:新方向插入頭部,尾部捨棄
for (int i = nxt.len - 1; i > 0; --i) nxt.dirs[i] = nxt.dirs[i-1];
nxt.dirs[0] = opposite(d); // 新頭部相對舊頭部方向
// 自身碰撞檢測(排除蛇尾同步移動的位置)
if (selfCollision(nxt)) continue;
int code = encodeState(nxt);
if (!visited[code]) {
visited[code] = true;
q.push(nxt);
}
}
}
return -1; // 無解
}
利用方向序列還原蛇身所有座標,存入 bool body[20][20],檢查新頭部是否落在蛇身上(蛇尾座標除外,因同步移動會騰空)。
bool selfCollision(const Node& s) {
bool body[20][20] = {false};
int x = s.x, y = s.y;
body[x][y] = true; // 頭部
for (int i = 0; i < s.len - 1; ++i) {
x += dx[s.dirs[i]];
y += dy[s.dirs[i]];
if (i != s.len - 2) body[x][y] = true; // 蛇尾不標記
}
return body[s.x][s.y]; // 新頭部是否撞身體
}
400 × 4⁷ = 655,360,000,實際可達狀態遠小於此(牆壁、障礙、自身限制)。visited 數組 16M bool ≈ 16 MB,隊列同級,符合限制。| 技巧 | 效果 |
|---|---|
| 相對方向編碼 (2 bits/段) | 狀態壓縮 24 bits,數組索引 O(1) |
| 循環隊列而非 std::queue | 減少記憶體分配開銷,提升 30%+ 速度 |
| 方向序列更新用移位而非重建 | O(L) → O(1) 更新蛇身拓撲 |
| 蛇尾不標記碰撞 | 正確處理「頭追尾」合法移動 |
| 預計算方向向量 | 消除分支預測失敗 |
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 20;
const int MAX_STATE = 1 << 24;
const int MAXQ = 500000;
int n, m, L;
bool obstacle[MAXN][MAXN];
bool visited[MAX_STATE];
Node q[MAXQ];
int head, tail;
int main() {
while (cin >> n >> m >> L) {
if (n == 0 && m == 0 && L == 0) break;
memset(obstacle, 0, sizeof(obstacle));
memset(visited, 0, sizeof(visited));
// 讀取障礙物、蛇身初始座標...
cout << bfs() << endl;
}
return 0;
}
本題是「狀態壓縮 BFS」教科書級範例。關鍵在於:
這類技巧廣泛應用於滑塊謎、推箱子、多實體移動等狀態空間搜索問題。