USACO 1399: Test Tubes

(Silver)

算法思路与可视化讲解

Topic: Constructive Algorithms, Greedy, Stack

题目背景与目标

  • 输入: $N$ (液体总数), $P$ (是否部分分), 两个试管 $A, B$ 的液体序列。
  • 工具: 试管 A, 试管 B, 烧杯 C (初始为空)。
  • 操作: 将一个容器顶部的液体倒在这个容器中。
  • 目标: 试管 A 全为液体 1,试管 B 全为液体 2 (或为空,反之亦然)。
Tube A Tube B

核心步骤 1:去重 (Dedup)

思考: 如果试管里有 111221,倒出最上面的 1 和倒出三个 1 是一次操作吗?

是的! 只要是连续的相同颜色,就可以一次性倒完。

1
2
2
1
1
1
原始数据
$\Rightarrow$
1
2
1
Dedup后

void dedup(vector &t, char *s) {
    t.clear();
    for (int i = 0; i < n; i++) {
        if (i == 0 || t[t.size()-1] != s[i]-'1')
            t.push_back(s[i]-'1');
    }
}
                    

核心步骤 2:栈模拟与贪心

将试管看作栈(Stack) A, B,烧杯为 C。

1. 平凡情况:相同即消

Top(A) == Top(B):将元素个数较多的栈顶倒入较少的栈(合并消除)。

2. 利用烧杯 (C)

若 C 为空:选择 A, B 中较高的一个,将其栈顶移入 C。

3. 烧杯配对

Top(A) == Top(C)Top(B) == Top(C):将匹配的栈顶倒入 C 中合并。

核心步骤 3:边界与收尾

重要观察: 上述过程中,烧杯 C 只有在空的时候才会被填入,因此 C 的元素个数始终 $\le 1$

情况 A: 双单收尾

Size(A)==1Size(B)==1 且栈顶异色:
$\to$ 将 C (若有) 倒回匹配的管子。
$\to$ 大功告成

情况 B: 单空处理

若 A 为空 (B 同理):
1. 若 Size(B) == 1: C 倒给 A,结束。
2. 否则: B 栈顶给 A (拆分),继续循环。

算法模拟演示

假设 A: [1, 2, 2] -> [1, 2] (Top: 2), B: [2, 1] (Top: 1)
2
1
A
1
2
B
C

状态:Top(A)=2 != Top(B)=1。C为空。
操作:将 B(1) 移入 C。

2
1
A
2
B
1
C

状态:Top(A)=2 == Top(B)=2
操作:A 比 B 高,将 A(2) 倒入 B (合并)。

1
A
2
B
1
C

状态:A, B 各剩1层且异色(边界情况)。
操作:将 C(1) 倒回 A。

1
A
2
B
C

完成!A 全 1,B 全 2。

核心代码实现:主循环 (1)


// 策略 1: 顶部颜色相同,进行合并
if (!ts[0].empty() && !ts[1].empty() && ts[0].back() == ts[1].back()) {
    // 贪心:把较高的那一管倒入较矮的,减少层数
    if (ts[0].size() > ts[1].size()) {
        ts[0].pop_back();
        ans.push_back({1, 2});
    } else {
        ts[1].pop_back();
        ans.push_back({2, 1});
    }
    continue;
}
                

核心代码实现:单空处理 (2)


// 边界情况 B: 移动到空管
// 如果某管是空的,且另一管不只一层,或者那一层不该待在那
if (ts[0].empty() && ts[1].size() > 1 && ts[1].back() != beaker ||
    ts[1].empty() && ts[0].size() > 1 && ts[0].back() != beaker) {
    
    int i = ts[0].empty() ? 0 : 1; // 目标是空管 i
    ts[i].push_back(ts[1-i].back());
    ts[1-i].pop_back();
    ans.push_back({(1-i)+1, i+1});
}
                

核心代码实现:收尾与烧杯 (3)


// 边界情况 A: 结束判断与烧杯回填
if (ts[0].size() <= 1 && ts[1].size() <= 1) {
    if (beaker != -1) {
        // 烧杯非空,倒回匹配的管子
        // 若 T0 是空的或者 T0 颜色和烧杯一样,就倒回 T0
        int i = (ts[0].empty() || ts[0].back() == beaker) ? 0 : 1;
        ans.push_back({3, i+1});
    }
    break; // 完成
}

// 策略 2/3: 借用/填入烧杯
int i;
if (beaker == -1) i = ts[0].size() > ts[1].size() ? 0 : 1; // 烧杯空,取高的
else i = ts[0].back() == beaker ? 0 : 1; // 烧杯非空,取匹配的
beaker = ts[i].back();
ts[i].pop_back();
ans.push_back({i+1, 3});
                

完整 AC 代码


#include <bits/stdc++.h>
using namespace std;

int t,n,p,m;
char s1[100005], s2[100005];
vector<int> ts[2];
using pi = pair<int,int>;
vector<pi> ans;
int beaker;

void dedup(vector<int> &t, char *s) {
    t.clear();
    for (int i = 0; i < n; i++) {
        if (i == 0 || t[t.size()-1] != s[i]-'1')
            t.push_back(s[i]-'1');
    }
}

int main() {
    scanf("%d", &t);
    while (t--) {
        scanf("%d %d", &n, &p);
        scanf("%s", s1);
        scanf("%s", s2);
        // dedup the colors
        dedup(ts[0], s1);
        dedup(ts[1], s2);
        beaker = -1;
        ans.clear();

        while (1) {
            // merge the same color to the lower tube
            if (!ts[0].empty() && !ts[1].empty() && ts[0].back() == ts[1].back()) {
                if (ts[0].size() > ts[1].size()) {
                    ts[0].pop_back();
                    ans.push_back({1, 2});
                } else {
                    ts[1].pop_back();
                    ans.push_back({2, 1});
                }
                continue;
            }
            // move to an empty tube
            if (ts[0].empty() && ts[1].size() > 1 && ts[1].back() != beaker ||
                ts[1].empty() && ts[0].size() > 1 && ts[0].back() != beaker) {
                int i = ts[0].empty() ? 0 : 1;
                ts[i].push_back(ts[1-i].back());
                ts[1-i].pop_back();
                ans.push_back({(1-i)+1, i+1});
            }
            // done if both have <= 1, move beaker liquid back if necessary
            if (ts[0].size() <= 1 && ts[1].size() <= 1) {
                if (beaker != -1) {
                    int i = (ts[0].empty() || ts[0].back() == beaker) ? 0 : 1;
                    ans.push_back({3, i+1});
                }
                break;
            }
            // move liquid from tube to beaker
            int i;
            if (beaker == -1)
                i = ts[0].size() > ts[1].size() ? 0 : 1;
            else
                i = ts[0].back() == beaker ? 0 : 1;
            beaker = ts[i].back();
            ts[i].pop_back();
            ans.push_back({i+1, 3});
        }
        printf("%d\n", (int)ans.size());
        if (p > 1) {
            for (auto &a: ans)
                printf("%d %d\n", a.first, a.second);
        }
    }
    return 0;
}
                    

总结

  • Run-Length Encoding (去重): 极大简化了状态空间,是本题关键。
  • 构造性算法: 不需要搜索或DP,只需要明确的逻辑优先级。
  • 观察: 烧杯 C 只需要 1 的容量即可完成周转。
  • 复杂度: 每次操作至少减少一层液体或移动一次,线性复杂度 $O(N)$。
感谢观看!