把局面压缩成(棋子格,空白邻格)的状态图,推动与绕行两类边按需缓存最短路后跑 Dijkstra
OJ: roj
题目 ID: 3613
难度:提高+/省选-
标签:搜索BFS最短路网格python
创建: 2026-10-02 10:49
更新: 2026-10-10 11:30
形式化题目
- 棋盘为
网格,每格标记为固定格(永远不动)或可移动格;恰有一个可移动格上是空白,其余格子各有一枚棋子。每秒执行一次操作:把与空白相邻(有公共边)的任意棋子移进空白,二者交换。 - 固定/可移动的布局在所有询问中保持不变,共
次询问;第 次给定空白初位置 、指定棋子初位置 、目标格 。 - 对每次询问求:把指定棋子移动到目标格的最少操作次数;不可能完成输出
。
样例输入与输出:
3 4 2
0 1 1 1
0 1 1 0
0 1 0 0
3 2 1 2 2 2
1 2 2 2 3 22
-1第一次询问的初始局面(空白
| 行 \ 列 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 1 | ✕ | ● | ○ | ○ |
| 2 | ✕ | ◎ | ○ | ✕ |
| 3 | ✕ | □ | ✕ | ✕ |
图例:✕ 固定格,○ 普通棋子,● 目标棋子(初始在
- 第一次询问答案
:空白先向上走一步( 的普通棋子下移填补),目标棋子再向下走一步进入 。 - 第二次询问答案
:目标格 是只与 相邻的死胡同。空白每次钻进 都只能从 挤进去,把 留给刚从 被顶出来的那枚棋子,此后唯一合法操作又只是把它换回去。而目标棋子踏入 必须同时满足"自己在 、空白在 "——这个局面永远不会出现,因此无解。
正解
思路
朴素思路与瓶颈。 最直接的想法是对"整盘棋子怎么摆"做广搜,状态数是组合级的,完全不可行。注意到除目标棋子外的其它棋子没有任何身份要求——空白走到任意可移动格,把那里的棋子顶出来即可——于是状态只需记录二元组
关键观察。
- 除第一次推动外,每次推动后空白都恰好落在棋子的原位置,也就是与新棋子位置相邻。空白走到不与棋子相邻的格子时不可能发生推动,它只是在绕向棋子的下一个邻格,中间位置不携带任何信息。
- 空白在除棋子格外的所有可移动格上都能自由穿行(顶开普通棋子),每步代价恒为
;换言之,"空白从 绕到 、全程避开棋子格 "的代价就是在 上的最短路,记作 。 - 因此状态只需记录"空白在棋子的哪个邻格上":压缩为
,其中 ( 的四方向邻格中可移动的格子),总数不超过 。
两类转移。 下图展示压缩状态之间的两条边:
flowchart LR
st["状态 (p, a)<br/>棋子在 p、空白在邻格 a"]
st -->|"推动:棋子走进空白 a,代价 +1"| push["状态 (a, p)"]
st -->|"绕行:空白避开 p 从 a 走到 b,代价 +d_p(a, b)"| circ["状态 (p, b)"]
推动边
初始段。 首次推动前,空白可能离
为什么用 Dijkstra。 推动代价为
预处理代价。 @cache 以
正确性。 充分性:压缩图的每条边都能展开成真实操作序列(推动是一步交换;绕行沿
边界。
代码
代码中 blank_dist 是“空白避开棋子格”的单源 BFS,同时服务初始段与绕行距离;near_dist 是 @cache 缓存的 adj[p] 对齐;play 是单次询问的 Dijkstra 主循环。
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 1000000000;
int n,m,qnum,kcells; vector<int> free_cell; vector<vector<int> > adj; map<ll, vector<int> > memo;
vector<int> blank_dist(int src,int blocked){ vector<int> dist(kcells,INF); if(src<0||src>=kcells) return dist; dist[src]=0; queue<int> q; q.push(src); while(!q.empty()){int u=q.front();q.pop(); for(int i=0;i<(int)adj[u].size();i++){int v=adj[u][i]; if(v!=blocked && dist[v]>dist[u]+1){dist[v]=dist[u]+1; q.push(v);}}} return dist; }
vector<int> near_dist(int p,int a){ ll key=(ll)p*kcells+a; if(memo.count(key)) return memo[key]; vector<int> d=blank_dist(a,p), res; for(int i=0;i<(int)adj[p].size();i++) res.push_back(d[adj[p][i]]); memo[key]=res; return res; }
struct Node{int c,p,a; bool operator<(const Node&o)const{ return c>o.c; }};
int play(int e0,int s,int t){ if(!free_cell[s]||!free_cell[t]||!free_cell[e0]) return -1; if(s==t) return 0; vector<int> first=blank_dist(e0,s); map<pair<int,int>, int> best; priority_queue<Node> pq; for(int i=0;i<(int)adj[s].size();i++){int a=adj[s][i]; if(first[a]<INF){best[{s,a}]=first[a]; pq.push({first[a],s,a});}} while(!pq.empty()){Node cur=pq.top();pq.pop(); pair<int,int> key={cur.p,cur.a}; if(best[key]!=cur.c) continue; if(cur.p==t) return cur.c; pair<int,int> nk={cur.a,cur.p}; if(cur.c+1 < (best.count(nk)?best[nk]:INF)){best[nk]=cur.c+1; pq.push({cur.c+1,cur.a,cur.p});} vector<int> nd=near_dist(cur.p,cur.a); for(int i=0;i<(int)adj[cur.p].size();i++){int b=adj[cur.p][i], step=nd[i]; pair<int,int> nk2={cur.p,b}; int old=best.count(nk2)?best[nk2]:INF; if(step<INF && cur.c+step<old){best[nk2]=cur.c+step; pq.push({cur.c+step,cur.p,b});}} } return -1; }
int main(){ios::sync_with_stdio(false); cin.tie(nullptr); if(!(cin>>n>>m>>qnum)) return 0; kcells=n*m; free_cell.resize(kcells); for(int i=0;i<kcells;i++) cin>>free_cell[i]; adj.assign(kcells, vector<int>()); for(int i=0;i<kcells;i++) if(free_cell[i]){int r=i/m,c=i%m; int js[4]={i-1,i+1,i-m,i+m}; bool ok[4]={c>0,c<m-1,r>0,r<n-1}; for(int z=0;z<4;z++) if(ok[z] && free_cell[js[z]]) adj[i].push_back(js[z]);} for(int qq=0;qq<qnum;qq++){int ex,ey,sx,sy,tx,ty;cin>>ex>>ey>>sx>>sy>>tx>>ty; ex--;ey--;sx--;sy--;tx--;ty--; cout<<play(ex*m+ey,sx*m+sy,tx*m+ty)<<'\n';} return 0;}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 10:51
# update_at: 2026-10-02 10:51
import heapq
import sys
from collections import deque
from functools import cache
INF = 10**9 # 不可达哨兵:远超棋盘最大步数 n*m
def blank_dist(src: int, blocked: int, adj: list[list[int]], k: int) -> list[int]:
"""空白从 src 出发、避开 blocked(目标棋子所在格)到每个格子的最短步数。"""
dist = [INF] * k
dist[src] = 0
dq = deque([src])
while dq:
u = dq.popleft()
for v in adj[u]:
if v != blocked and dist[v] > dist[u] + 1: # 只在可移动格之间行走
dist[v] = dist[u] + 1
dq.append(v)
return dist
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m, q = next(data), next(data), next(data)
k = n * m
free = [next(data) for _ in range(k)] # 1 = 可移动格(含空白),0 = 固定格
adj: list[list[int]] = [[] for _ in range(k)]
for i in range(k):
if free[i]: # 只有可移动格之间才相邻可达
r, c = divmod(i, m)
adj[i] = [
j
for j, inside in ((i - 1, c > 0), (i + 1, c < m - 1), (i - m, r > 0), (i + m, r < n - 1))
if inside and free[j]
]
@cache
def near_dist(p: int, a: int) -> tuple[int, ...]:
"""空白从邻格 a 绕开棋子 p 后,到 p 各邻格的最短步数(与 adj[p] 对齐)。"""
dist = blank_dist(a, p, adj, k)
return tuple(dist[b] for b in adj[p])
def play(e0: int, s: int, t: int) -> int:
"""一局游戏:在状态 (棋子格 p, 空白邻格 a) 上跑 Dijkstra,返回最少秒数或 -1。"""
if not free[s] or not free[t] or not free[e0]:
return -1
if s == t: # 棋子已在目标位置
return 0
first = blank_dist(e0, s, adj, k) # 首次推动前,空白绕开 S 的行走距离
best: dict[tuple[int, int], int] = {}
pq: list[tuple[int, int, int]] = []
for a in adj[s]:
if first[a] < INF: # 空白走到 S 的某个邻格后才可能推动棋子
best[(s, a)] = first[a]
heapq.heappush(pq, (first[a], s, a))
while pq:
c, p, a = heapq.heappop(pq)
if best.get((p, a)) != c: # 过期堆项
continue
if p == t: # 棋子抵达目标,首次弹出即最短
return c
# 棋子走进空白格:空白换到原棋子格 p(p 与 a 相邻,状态不变量保持)
if c + 1 < best.get((a, p), INF):
best[(a, p)] = c + 1
heapq.heappush(pq, (c + 1, a, p))
# 空白绕开棋子走到 p 的其他邻格,为下一次推动占位
for b, step in zip(adj[p], near_dist(p, a)):
if step < INF and c + step < best.get((p, b), INF):
best[(p, b)] = c + step
heapq.heappush(pq, (c + step, p, b))
return -1
out: list[str] = []
for _ in range(q):
ex, ey, sx, sy, tx, ty = (next(data) - 1 for _ in range(6))
out.append(str(play(ex * m + ey, sx * m + sy, tx * m + ty)))
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
- 时间:单次询问为初始 BFS
加 Dijkstra ,合计 ;绕行距离缓存整个运行至多 次 BFS,总计 。整体 ,与 C++ 正解(压缩状态图 + 绕行最短路 + Dijkstra)同阶。 - 空间:邻接表、缓存(
个键 每键 个距离)、单询问状态与堆均为 ,峰值远低于 MiB。 - 实测:官方样例输出
/ ; verify.sh全量 20 组真实数据 PASS,单组最慢s、峰值内存 MB。
总结
- 其它棋子可互换 → 状态先缩为 (棋子格, 空白格);再观察"推动后空白恒贴棋子" → 最终只记 (棋子格, 空白邻格),状态数从
降到 。 - 空白绕行不逐格展开,收成一条权为
的边; 依赖固定棋盘,用 @cache按需生成、跨询问共享。 - 推动
与绕行 边权不等,故用 Dijkstra;终点在首次弹出时判定。 直接输出 ,不可达输出 ;死锁局面(如样例二)会被图上的不可达自然覆盖。
图示解析
这张图串起本题从"整盘配置"到"最少秒数"的建模主线:
整盘配置(组合级,朴素 BFS 不可行)
`- 观察一:其它棋子可互换 → 状态 (棋子格 p, 空白格 e),仍有 O(K²)/询问
`- 观察二:推动后空白恒贴棋子 → 压缩状态 (p, a),a ∈ N(p),≤ 4K 个
`- 两类边:推动 +1 / 绕行 +d_p(a,b)(按需 BFS,@cache 跨询问共享)
`- Dijkstra:首次弹出 p = T 即最少秒数,堆空则 -1先看每层节点代表的推理结论:它先砍掉无身份的普通棋子,再砍掉空白的中间位置。顺着分支检查两类边如何还原真实操作,就能理解压缩图的最短路为什么等于原问题的最少步数。图中只保留正文已论证的关键关系,样例推演和正确性细节以正文为准。

