题解:P10191 [USACO24FEB] Test Tubes S

· · 题解

说在前面

码农题目,思路是简单的,就是有点废手。

在只能交 3 次的且只有 2.5h 时间的考场上我居然过了大模拟题目,我太强了。

前置芝士:贪心、一个清醒的大脑、一对敏锐的眼睛和十根勤劳的手指大模拟。

注意:

  1. 该文章篇幅较长,给出了详细的证明和算法步骤,可自行选择观看。
  2. 题目是有问题的,“输入保证两个字符串包含至少一个 1 和一个 2”,而样例三与题目描述矛盾。题目一开始提到的“输入保证两种颜色的液体至少各有一个单位”符合样例三,故我认为,第一句话应该把“均”字改成“总共”。
  3. 该题解中所有的思路的构建与证明都基于“输入保证两种颜色的液体至少各有一个单位”这句话之上,请务必牢记在心。

题目分析

思路是比较好想的。

:::info[思路]{open} 建立在贪心之上,我们尽可能多合并能合并的颜色。

  1. 指定一个烧杯中唯一可能出现的颜色,令其为 x
  2. 分别查看两个试管,如果最顶层存在 x,将其倒入烧杯,每次倾倒花费代价 1
  3. 否则,将颜色多的试管的顶层倒入另一个试管中,花费代价 1
  4. 在两个试管内的颜色的段数至少有一个大于 1 的情况下,重复步骤 2、3,否则,分类讨论:
    • 如果烧杯是空的,直接退出,此时必定分拣完成。
    • 否则,讨论两试管液体存在性及颜色。
    • 若试管 1 和试管 2 均为非空。
      • 若两试管内颜色相同,则将试管 1 的液体倒入试管 2,最后将试管 3 的倒入试管 1,花费代价 2
      • 否则,检查哪个试管的剩下的颜色为 x,将烧杯中的颜色倒入该试管,花费代价 1
    • 若仅有试管 1 非空。
      • 将烧杯中的液体倒入试管 2,花费代价 1
    • 若仅有试管 2 非空。
      • 将烧杯中的液体倒入试管 1,花费代价 1
  5. x 换一个数,重复上述过程,答案取两次代价的最小值。 :::

由于这个算法是我们凭直觉搞出来的,所以这里给出证明。

:::info[算法思路的证明]{open} 先证总体思路:

故总体思路正确。

再证退出循环前的思路:

故退出循环前的思路正确。

最后证明退出循环后的思路。

故退出循环后的思路正确。

综上,该算法思路全部正确。

::: :::warning[注意事项] 1. $P = 1$ 只能输出一个数字即最小代价。 2. 类似于样例 $3$ 的情况代码无法处理,需要特殊判断。 ::: :::success[AC 代码] ~~长达 3.71KB 的代码。~~ ```cpp #include <bits/stdc++.h> #define PII pair<int, int> #define fir first #define sec second using namespace std; namespace zcq_qwq { const int N = 1e5 + 5, inf = 0x3f3f3f3f; int t, n, mn = inf, res[3]; deque<PII> q[4], p[4]; vector<PII> ans[3]; void merge(int x, PII v) { // 把 v 这个东西放到 q[x] 里面 if (q[x].empty()) q[x].push_back(v); else if (q[x].back().fir == v.fir) q[x].back().sec += v.sec; else q[x].push_back(v); } bool check() {return q[1].size() <= 1 && q[2].size() <= 1;} void clear() {for (int i = 1; i <= 3; i++) q[i].clear(); ans[1].clear(), ans[2].clear(), res[1] = res[2] = 0;} void big_question(int x) { // 确定 q[3] 里面存的是啥颜色 for (int i = 1; i <= 3; i++) {p[i] = q[i];} int sum = 0; while (!check()) { if (!q[1].empty() && q[1].back().fir == x) { merge(3, q[1].back()), q[1].pop_back(), sum++; ans[x].push_back({1, 3}); } else if (!q[2].empty() && q[2].back().fir == x) { merge(3, q[2].back()), q[2].pop_back(), sum++; ans[x].push_back({2, 3}); } else { int tmp = max(q[1].size(), q[2].size()); if (tmp == q[1].size()) { merge(2, q[1].back()), q[1].pop_back(), sum++; ans[x].push_back({1, 2}); } else { merge(1, q[2].back()), q[2].pop_back(), sum++; ans[x].push_back({2, 1}); } } } // 接下来是大分讨 if (!q[1].empty() && !q[2].empty() && !q[3].empty()) { if (q[1].back().fir == q[2].back().fir) { ans[x].push_back({1, 2}), ans[x].push_back({3, 1}), sum += 2; } else { if (q[1].back().fir == x) { ans[x].push_back({3, 1}), sum++; } else { ans[x].push_back({3, 2}), sum++; } } } else if (!q[1].empty() && !q[3].empty()) { ans[x].push_back({3, 2}), sum++; } else if (!q[2].empty() && !q[3].empty()) { ans[x].push_back({3, 1}), sum++; } res[x] = sum; mn = min(mn, sum); for (int i = 1; i <= 3; i++) {q[i] = p[i];} } void solve() { mn = inf; clear(); int P; cin >> n >> P; for (int i = 1; i <= n; i++) { char c; int x; cin >> c; x = c - '0'; PII t = {x, 1}; merge(1, t); } for (int i = 1; i <= n; i++) { char c; int x; cin >> c; x = c - '0'; PII t = {x, 1}; merge(2, t); } if (q[1].size() == 1 && q[2].size() == 2) { if (q[1].back().fir == q[2].back().fir) { cout << "1\n2 1" << endl; return; } } else if (q[1].size() == 2 && q[2].size() == 1) { if (q[1].back().fir == q[2].back().fir) { cout << "1\n1 2" << endl; return; } } big_question(1), big_question(2); cout << mn << endl; if (P == 1) return; if (mn == res[1]) { for (auto i : ans[1]) cout << i.fir << " " << i.sec << endl; } else { for (auto i : ans[2]) cout << i.fir << " " << i.sec << endl; } } void main() { cin >> t; while (t--) { solve(); } } } signed main() { ios::sync_with_stdio(false); cin.tie(nullptr); zcq_qwq::main(); return 0; } /* 这个东西显然是策略的问题,只要策略对了即可 看起来是这样的策略:每次遇到指定颜色段,就放到烧杯里,这样烧杯里面一定是一种颜色的。 最后再把烧杯里面的倒回来 由于我们指定的哪一个颜色放到烧杯里面可能对答案有影响,所以我们跑两次取最小值。 接下来只需要思考如何判断可以直接倒了。 如果两边烧杯的段数同时 <= 1,就可以直接倒。 */ ``` ::: ## 说在后面 若代码、思路讲解有误,欢迎提出!