UVa 11535 Set of Marbles

题目描述

你有 nnn 个不同颜色的弹珠,这些弹珠分布在两个盒子中。每次操作中,你可以将一个弹珠从一个盒子移动到另一个盒子。你需要通过移动弹珠,使得第一个盒子依次包含所有可能的弹珠组合,每个组合恰好出现一次。总共有 2n2^n2n 种弹珠组合。

例如,当 n=4n = 4n=4 时,如果盒子 111 初始包含弹珠 111333,盒子 222 包含弹珠 222444,那么需要找到一个移动序列,使得盒子 111 依次遍历所有 161616 种可能的子集。

输入格式

输入包含多个测试用例。第一行是 TTT (1≤T≤201 \leq T \leq 201T20),表示测试用例的数量。每个测试用例包含两行:

  • 第一行:nnn (1≤n≤101 \leq n \leq 101n10) 和 b1b_1b1 (0≤b1≤n0 \leq b_1 \leq n0b1n)
  • 第二行:b1b_1b1 个整数,表示盒子 111 中弹珠的编号(在 111nnn 之间,且互不相同)

输出格式

对于每个测试用例,输出 2n2^n2n 行:

  • 2n−12^n - 12n1 行:移动操作,格式为 Move x from B1 to B2Move x from B2 to B1
  • 最后一行:空行

题目分析

问题本质

这个问题要求我们找到一个移动序列,使得盒子 111 的状态依次遍历所有 2n2^n2n 个可能的子集,且相邻两个状态之间只能相差一个元素(即每次只能移动一个弹珠)。

这实际上是在寻找一个哈密顿路径Hamiltonian Path\texttt{Hamiltonian Path}Hamiltonian Path)问题,在超立方体图(Hypercube Graph\texttt{Hypercube Graph}Hypercube Graph)上从给定起点出发,访问所有顶点且每个顶点只访问一次。在超立方体图中,顶点表示子集,边连接那些相差一个元素的子集。

关键观察

  1. 状态表示:可以用位掩码来表示盒子 111 的状态,第 iii 位为 111 表示弹珠 i+1i+1i+1 在盒子 111 中。

  2. 相邻状态:每次只能移动一个弹珠,意味着相邻状态的海明距离(Hamming Distance\texttt{Hamming Distance}Hamming Distance)必须为 111

  3. 路径存在性:对于超立方体图,从任意顶点出发都存在哈密顿路径,因此问题总是有解。

解题思路

我们可以使用回溯法来构造从初始状态出发的哈密顿路径:

  1. 初始化:读取初始状态,用位掩码表示。
  2. 路径搜索:从初始状态开始,尝试所有可能的单步移动(即翻转一个位),如果新状态未被访问过,则加入路径。
  3. 回溯:如果当前路径无法访问所有状态,则回溯到上一个状态尝试其他移动。
  4. 输出结果:找到完整路径后,根据相邻状态的差异输出移动操作。

算法复杂度

  • 状态总数:2n2^n2n,其中 n≤10n \leq 10n10,最多 102410241024 个状态。
  • 每个状态最多尝试 nnn 种可能的移动。
  • 总时间复杂度:O(n×2n)O(n \times 2^n)O(n×2n),在 n≤10n \leq 10n10 时完全可行。

代码实现

// 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()

  1. 读取输入

    • 读取弹珠数量 nnn 和盒子 111 的初始弹珠数 b1b_1b1
    • 读取盒子 111 中的弹珠编号
  2. 初始化状态

    • 使用位掩码 startState 表示初始状态
    • iii 位为 111 表示弹珠 i+1i+1i+1 在盒子 111
  3. 生成路径

    • 使用 visited 数组记录已访问的状态
    • 使用 sequence 向量存储状态序列
    • 从初始状态开始,每次尝试翻转一个位来生成新状态
  4. 输出移动序列

    • 遍历相邻状态,找出变化的位(弹珠)
    • 根据变化方向输出移动操作
    • 使用 __builtin_ffs 函数快速找到最低有效位的位置

关键技巧

  • 位运算:使用异或运算 ^ 来翻转单个位,高效地生成相邻状态
  • 内置函数__builtin_ffs 返回最低有效位的位置(从 111 开始计数)
  • 贪心构造:实际上不需要完全的回溯,因为超立方体图具有很好的性质,简单的贪心策略就能找到解

总结

本题通过位运算和状态搜索,巧妙地解决了弹珠移动序列的生成问题。关键在于将组合遍历问题转化为图上的哈密顿路径问题,并利用位运算高效地实现状态转移和差异检测。算法在 n≤10n \leq 10n10 的范围内具有很好的效率,能够快速生成符合要求的移动序列。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值