题目描述
Venture MFG\texttt{Venture MFG}Venture MFG 公司设计了一个带有 151515 个洞的游戏板,初始状态除一个指定的洞为空外,其余洞均插有木钉。游戏规则是:木钉可以沿直线跳过一个或多个连续的木钉,跳到最近的空洞。被跳过的木钉将被移除。
目标:找到最短的移动序列,使得最后只剩下一个木钉,并且该木钉位于最初为空的那个洞中。如果不存在这样的序列,输出 IMPOSSIBLE\texttt{IMPOSSIBLE}IMPOSSIBLE。
输入格式
第一行包含测试用例数 TTT,每个测试用例是一个整数,表示初始空洞的编号(111 到 151515)。
输出格式
对于每个测试用例,第一行输出最短序列的跳跃次数,第二行输出跳跃序列。每个跳跃由两个用空格分隔的整数表示:移动的木钉所在洞的编号和目标空洞的编号。
样例输入
1
5
样例输出
10
12 5 3 8 15 12 6 13 7 9 1 7 10 8 7 9 11 14 14 5
重要补充说明
题目描述不完整:根据实际评测系统的要求,本题需要输出字典序最小的最短序列。虽然原始题目描述中没有明确说明这一要求,但评测系统实际上按照字典序进行比对。
字典序定义
对于两个操作序列 A=[(a1,b1),(a2,b2),...,(an,bn)]A = [(a_1, b_1), (a_2, b_2), ..., (a_n, b_n)]A=[(a1,b1),(a2,b2),...,(an,bn)] 和 B=[(c1,d1),(c2,d2),...,(cn,dn)]B = [(c_1, d_1), (c_2, d_2), ..., (c_n, d_n)]B=[(c1,d1),(c2,d2),...,(cn,dn)]:
- 比较第一个操作的起始洞编号:如果 a1<c1a_1 < c_1a1<c1,则 A<BA < BA<B
- 如果 a1=c1a_1 = c_1a1=c1,则比较第一个操作的目标洞编号:如果 b1<d1b_1 < d_1b1<d1,则 A<BA < BA<B
- 如果第一个操作完全相同,则比较第二个操作,依此类推
解题思路分析
方法一:实时生成解(BFS\texttt{BFS}BFS + 位运算 + 字典序优化)
这种方法通过广度优先搜索(BFS\texttt{BFS}BFS)探索所有可能的状态,并通过排序保证找到字典序最小的解。
1. 状态表示
使用 151515 位二进制数表示棋盘状态,第 iii 位(1≤i≤151 \le i \le 151≤i≤15)表示洞 iii 是否有木钉:111 表示有木钉,000 表示空。
- 初始状态:除了指定的空洞外,其他所有洞都有木钉
- 目标状态:只有指定的空洞有木钉
2. 跳跃规则预计算
棋盘上合法的跳跃必须满足:
- 起始洞必须有木钉
- 目标洞必须为空
- 起始洞和目标洞必须在同一条直线上
- 必须跳过至少一个木钉
- 目标洞必须是跳跃方向上的第一个空洞
我们为每条直线生成所有可能的跳跃规则,并进行字典序排序:
// 按字典序排序:先按from排序,from相同按to排序
sort(jumps.begin(), jumps.end(), [](const jump& a, const jump& b) {
if (a.from != b.from) return a.from < b.from;
return a.to < b.to;
});
3. BFS\texttt{BFS}BFS 搜索策略
使用 BFS\texttt{BFS}BFS 从初始状态开始搜索,确保按层扩展,从而找到最短路径。由于跳跃规则已按字典序排序,BFS\texttt{BFS}BFS 会优先尝试字典序小的操作。
关键算法步骤:
- 初始化队列和访问集合
- 从队列中取出状态
- 按排序后的顺序尝试所有合法跳跃
- 生成新状态并加入队列
- 找到目标状态时返回路径
4. 算法正确性证明
- 最短性保证:BFS\texttt{BFS}BFS 按层扩展,第一次到达目标状态时路径最短
- 字典序最小保证:对于同一层的状态,按字典序顺序尝试跳跃,确保找到的第一个解字典序最小
- 完备性:遍历所有可达状态,确保不会遗漏解
5. 算法复杂度分析
- 状态总数:215=327682^{15} = 32768215=32768 种可能状态
- 每个状态的合法跳跃数:约 363636 种
- 时间复杂度:O(32768×36)≈1.2×106O(32768 \times 36) \approx 1.2 \times 10^6O(32768×36)≈1.2×106,在可接受范围内
- 空间复杂度:O(32768)O(32768)O(32768) 存储访问状态
方法二:硬编码方案
由于已知所有测试用例的正确答案,可以直接硬编码解决方案。这种方法的优势是 100%100\%100% 可靠且效率极高。
// Moving Pegs
// UVa ID: 1533
// Verdict: Accepted
// Submission Date: 2025-12-17
// UVa Run Time: 0.000s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <iostream>
using namespace std;
int main()
{
int cnt[15] = {9, 9, 9, 9, 10, 9, 9, 10, 10, 9, 9, 9, 9, 9, 9};
string moves[15] = {
"4 1 6 4 11 2 14 11 15 6 1 10 10 7 11 4 4 1",
"7 2 1 4 10 1 14 2 1 7 11 14 15 13 13 4 7 2",
"10 3 1 6 7 1 12 3 1 10 14 12 11 13 13 6 15 3",
"1 4 6 1 13 6 10 3 11 13 3 12 15 11 11 2 1 4",
"12 5 3 8 15 12 6 13 7 9 1 7 10 8 7 9 11 14 14 5",
"1 6 4 1 13 4 7 2 15 13 2 14 11 15 15 3 1 6",
"1 7 6 1 11 4 9 7 14 11 11 2 15 6 6 4 1 7",
"3 8 12 5 15 3 13 6 1 10 2 9 11 2 14 5 2 9 10 8",
"2 9 11 2 12 5 3 8 13 4 1 7 14 5 15 3 3 8 7 9",
"1 10 4 1 11 4 4 6 12 5 10 8 15 12 12 3 1 10",
"4 11 1 4 10 1 14 2 1 7 11 14 15 13 13 4 4 11",
"14 12 2 14 7 2 1 4 15 13 3 15 11 14 4 13 15 12",
"4 13 1 4 11 2 13 11 15 13 2 14 3 15 15 12 11 13",
"11 14 2 11 3 12 10 3 1 6 11 13 15 12 6 13 12 14",
"6 15 1 6 7 1 12 3 1 10 15 12 11 13 13 6 6 15"
};
int T; cin >> T;
while (T--) {
int emptyHole; cin >> emptyHole;
cout << cnt[emptyHole - 1] << '\n';
cout << moves[emptyHole - 1] << '\n';
}
return 0;
}
代码实现(实时生成解)
以下是提交通过的实时生成解代码:
// Moving Pegs
// UVa ID: 1533
// Verdict: Accepted
// Submission Date: 2025-12-17
// UVa Run Time: 0.050s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
// 棋盘布局(洞编号):
// (1)
// (2) (3)
// (4) (5) (6)
// (7) (8) (9) (10)
//(11)(12)(13)(14)(15)
// 所有可能的直线(至少3个洞在一条直线上)
const vector<vector<int>> lines = {
{1, 2, 4, 7, 11},
{3, 5, 8, 12},
{6, 9, 13},
{4, 5, 6},
{7, 8, 9, 10},
{11, 12, 13, 14, 15},
{1, 3, 6, 10, 15},
{2, 5, 9, 14},
{4, 8, 13},
};
struct jump { int mask, bit1, bit2, from, to; };
vector<jump> jumps;
void getPossibleMoves() {
for (auto line : lines) {
for (int i = 0; i < line.size(); i++)
for (int j = i + 2; j < line.size(); j++) {
jump jmp = jump {0, 0, 0, line[j], line[i]};
for (int k = i; k <= j; k++) jmp.mask |= 1 << (line[k] - 1);
for (int k = i + 1; k <= j; k++) jmp.bit1 |= 1 << (line[k] - 1);
jmp.bit2 = 1 << (line[i] - 1);
jumps.push_back(jmp);
}
for (int i = line.size() - 1; i >= 0; i--)
for (int j = i - 2; j >= 0; j--) {
jump jmp = jump {0, 0, 0, line[j], line[i]};
for (int k = i; k >= j; k--) jmp.mask |= 1 << (line[k] - 1);
for (int k = i - 1; k >= j; k--) jmp.bit1 |= 1 << (line[k] - 1);
jmp.bit2 = 1 << (line[i] - 1);
jumps.push_back(jmp);
}
}
// 按字典序排序:先按from排序,from相同按to排序
sort(jumps.begin(), jumps.end(), [](const jump& a, const jump& b) {
if (a.from != b.from) return a.from < b.from;
return a.to < b.to;
});
}
struct state {
int bits;
vector<pair<int, int>> moves;
};
vector<pair<int, int>> solve(int emptyHole) {
int end = 1 << (emptyHole - 1);
unordered_set<int> visited;
state start;
start.bits = ~(1 << (emptyHole - 1)) & ((1 << 15) - 1);
queue<state> q;
q.push(start);
visited.insert(start.bits);
while (!q.empty()) {
state s = q.front(); q.pop();
if (s.bits == end) return s.moves;
for (auto j : jumps) {
if ((s.bits & j.mask) == j.bit1) {
state next = {s.bits & (~j.mask) | j.bit2, s.moves};
next.moves.push_back(make_pair(j.from, j.to));
if (!visited.count(next.bits)) {
visited.insert(next.bits);
q.push(next);
}
}
}
}
return {};
}
int main() {
getPossibleMoves();
int T; cin >> T;
while (T--) {
int emptyHole; cin >> emptyHole;
auto moves = solve(emptyHole);
if (moves.empty()) cout << "IMPOSSIBLE\n";
else {
cout << moves.size() << '\n';
for (size_t i = 0; i < moves.size(); i++) {
if (i) cout << ' ';
cout << moves[i].first << ' ' << moves[i].second;
}
cout << '\n';
}
}
return 0;
}
关键代码解析
1. 跳跃规则数据结构
struct jump { int mask, bit1, bit2, from, to; };
mask:涉及的所有洞的位掩码bit1:需要检查的木钉位置的位掩码(起始洞和中间洞)bit2:目标洞的位掩码from:起始洞编号to:目标洞编号
2. 跳跃有效性检查
if ((s.bits & j.mask) == j.bit1)
这个条件检查:
- 起始洞有木钉(包含在
bit1中) - 所有中间洞都有木钉(包含在
bit1中) - 目标洞为空(不在
bit1中但在mask中)
3. 状态转移
state next = {s.bits & (~j.mask) | j.bit2, s.moves};
s.bits & (~j.mask):清除涉及的所有洞| j.bit2:在目标洞放置木钉
两种方案的比较
| 特性 | 实时生成解 | 硬编码方案 |
|---|---|---|
| 通用性 | 高,可扩展 | 低,仅限于当前问题 |
| 通过率 | 100%(正确实现时) | 100% |
| 时间复杂度 | O(1.2×106)O(1.2 \times 10^6)O(1.2×106) | O(1)O(1)O(1) |
| 空间复杂度 | O(32768)O(32768)O(32768) | O(270)O(270)O(270) |
| 实现难度 | 中等 | 简单 |
| 学习价值 | 高 | 低 |
经验总结
- 仔细阅读题目要求:即使题目描述不完整,也要通过测试样例推断隐含要求
- 字典序处理:当输出序列不唯一时,需要考虑字典序要求
- 状态压缩技巧:使用位运算可以有效表示和操作棋盘状态
- 预计算优化:提前计算所有可能的跳跃规则可以显著提高效率
- BFS\texttt{BFS}BFS 的正确性:通过排序跳跃规则,可以保证 BFS\texttt{BFS}BFS 找到字典序最小的解
扩展思考
- 如果棋盘布局变化(如更多洞),算法如何扩展?
- 如何证明这个 BFS\texttt{BFS}BFS 算法一定能找到最短路径?
- 如果要求输出所有最短序列,算法应如何修改?
- 是否存在启发式搜索(如 A*\texttt{A*}A*)的优化空间?
本题展示了状态空间搜索、位运算优化和字典序处理的综合应用,是一个优秀的算法练习题。

1583

被折叠的 条评论
为什么被折叠?



