题目描述
你有 nnn 个不同颜色的弹珠,这些弹珠分布在两个盒子中。每次操作中,你可以将一个弹珠从一个盒子移动到另一个盒子。你需要通过移动弹珠,使得第一个盒子依次包含所有可能的弹珠组合,每个组合恰好出现一次。总共有 2n2^n2n 种弹珠组合。
例如,当 n=4n = 4n=4 时,如果盒子 111 初始包含弹珠 111 和 333,盒子 222 包含弹珠 222 和 444,那么需要找到一个移动序列,使得盒子 111 依次遍历所有 161616 种可能的子集。
输入格式
输入包含多个测试用例。第一行是 TTT (1≤T≤201 \leq T \leq 201≤T≤20),表示测试用例的数量。每个测试用例包含两行:
- 第一行:nnn (1≤n≤101 \leq n \leq 101≤n≤10) 和 b1b_1b1 (0≤b1≤n0 \leq b_1 \leq n0≤b1≤n)
- 第二行:b1b_1b1 个整数,表示盒子 111 中弹珠的编号(在 111 到 nnn 之间,且互不相同)
输出格式
对于每个测试用例,输出 2n2^n2n 行:
- 前 2n−12^n - 12n−1 行:移动操作,格式为
Move x from B1 to B2或Move x from B2 to B1 - 最后一行:空行
题目分析
问题本质
这个问题要求我们找到一个移动序列,使得盒子 111 的状态依次遍历所有 2n2^n2n 个可能的子集,且相邻两个状态之间只能相差一个元素(即每次只能移动一个弹珠)。
这实际上是在寻找一个哈密顿路径(Hamiltonian Path\texttt{Hamiltonian Path}Hamiltonian Path)问题,在超立方体图(Hypercube Graph\texttt{Hypercube Graph}Hypercube Graph)上从给定起点出发,访问所有顶点且每个顶点只访问一次。在超立方体图中,顶点表示子集,边连接那些相差一个元素的子集。
关键观察
-
状态表示:可以用位掩码来表示盒子 111 的状态,第 iii 位为 111 表示弹珠 i+1i+1i+1 在盒子 111 中。
-
相邻状态:每次只能移动一个弹珠,意味着相邻状态的海明距离(Hamming Distance\texttt{Hamming Distance}Hamming Distance)必须为 111。
-
路径存在性:对于超立方体图,从任意顶点出发都存在哈密顿路径,因此问题总是有解。
解题思路
我们可以使用回溯法来构造从初始状态出发的哈密顿路径:
- 初始化:读取初始状态,用位掩码表示。
- 路径搜索:从初始状态开始,尝试所有可能的单步移动(即翻转一个位),如果新状态未被访问过,则加入路径。
- 回溯:如果当前路径无法访问所有状态,则回溯到上一个状态尝试其他移动。
- 输出结果:找到完整路径后,根据相邻状态的差异输出移动操作。
算法复杂度
- 状态总数:2n2^n2n,其中 n≤10n \leq 10n≤10,最多 102410241024 个状态。
- 每个状态最多尝试 nnn 种可能的移动。
- 总时间复杂度:O(n×2n)O(n \times 2^n)O(n×2n),在 n≤10n \leq 10n≤10 时完全可行。
代码实现
// Set of Marbles
// UVa ID: 11535
// Verdict: Accepted
// Submission Date: 2025-11-25
// UVa Run Time: 0.000s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
void solve() {
int n, b1;
cin >> n >> b1;
vector<int> initial(b1);
for (int i = 0; i < b1; i++) cin >> initial[i];
int startState = 0;
for (int x : initial) startState |= (1 << (x - 1));
int total = 1 << n;
vector<int> visited(total, 0);
vector<int> sequence;
sequence.push_back(startState);
visited[startState] = 1;
while (sequence.size() < total) {
int current = sequence.back();
for (int i = 0; i < n; i++) {
int next = current ^ (1 << i);
if (!visited[next]) {
sequence.push_back(next);
visited[next] = 1;
break;
}
}
}
for (int i = 0; i < total - 1; i++) {
int changed = sequence[i] ^ sequence[i + 1];
int marble = __builtin_ffs(changed);
if (sequence[i] & changed)
cout << "Move " << marble << " from B1 to B2\n";
else
cout << "Move " << marble << " from B2 to B1\n";
}
cout << endl;
}
int main() {
int T;
cin >> T;
while (T--) solve();
return 0;
}
代码解释
核心函数 solve()
-
读取输入:
- 读取弹珠数量 nnn 和盒子 111 的初始弹珠数 b1b_1b1
- 读取盒子 111 中的弹珠编号
-
初始化状态:
- 使用位掩码
startState表示初始状态 - 第 iii 位为 111 表示弹珠 i+1i+1i+1 在盒子 111 中
- 使用位掩码
-
生成路径:
- 使用
visited数组记录已访问的状态 - 使用
sequence向量存储状态序列 - 从初始状态开始,每次尝试翻转一个位来生成新状态
- 使用
-
输出移动序列:
- 遍历相邻状态,找出变化的位(弹珠)
- 根据变化方向输出移动操作
- 使用
__builtin_ffs函数快速找到最低有效位的位置
关键技巧
- 位运算:使用异或运算
^来翻转单个位,高效地生成相邻状态 - 内置函数:
__builtin_ffs返回最低有效位的位置(从 111 开始计数) - 贪心构造:实际上不需要完全的回溯,因为超立方体图具有很好的性质,简单的贪心策略就能找到解
总结
本题通过位运算和状态搜索,巧妙地解决了弹珠移动序列的生成问题。关键在于将组合遍历问题转化为图上的哈密顿路径问题,并利用位运算高效地实现状态转移和差异检测。算法在 n≤10n \leq 10n≤10 的范围内具有很好的效率,能够快速生成符合要求的移动序列。
121

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



