题目
在《逆水寒》荡漾赛金殿中,有一类装备配件叫作“千机扣”。每个千机扣由 3 个部件 组合而成。
每个部件具有 3 个属性:
- 词条:部件的属性标识,用数字编号表示;
- 内外功倾向:
1表示外功,2表示内功; - 攻防类型:
1表示攻击型,2表示防御型。
要成功合成一个千机扣,需要选择 3 个部件,并满足以下条件:
- 词条限制:3 个部件的词条编号互不相同;
- 内外功限制:3 个部件的内外功倾向必须完全一致;
- 攻防限制:3 个部件的攻防类型必须完全一致。
请问,给定若干部件后,最多可以合成多少个千机扣?
输入描述
第一行一个整数:
n(1 ≤ n ≤ 100000)
表示可用部件总数。
接下来 n 行,每行 3 个整数:
x y z
分别表示:
x:部件词条编号(1 ≤ x ≤ 100000)y:内外功倾向(1外功,2内功)z:攻防类型(1攻击,2防御)
输出描述
输出一个整数,表示最多能合成的千机扣数量。
如果无法合成,则输出 0。
测试用例
样例 1
输入:
7
17 1 1
52 2 1
32 2 1
14 1 2
85 2 2
36 2 2
77 2 2
输出:
1
样例 2
你截图中的第二个样例底部被遮挡,输入没有完整显示。
为了避免错误复原,本文不列出该样例的输入内容。已知该样例输出为:
1
一开始容易想到的思路:按 (y,z) 分组后,随便拿 3 个不同词条就凑
这题我一开始有一个判断是对的:
因为同一个千机扣内部要求
y一致、z一致,所以可以先按(y,z)分成 4 组。
也就是说:
(1,1)一组(1,2)一组(2,1)一组(2,2)一组
不同组之间不可能互相配对,所以最后答案等于这 4 组的答案之和。
这一步是完全正确的。
但后面我一开始的错误思路是:
对于某一组,直接顺着
unordered_map扫,遇到 3 个不同词条就组成一个千机扣。
这个思路的问题在于:
它是“局部随便拿三个”,并不保证全局最优。
我在这题里犯过的错
错误 1:把“看到 3 个不同词条就拿走”当成贪心
这是这题最大的建模错误。
例如某一组里,词条次数是:
A:1
B:1
C:2
D:2
如果你第一次随便拿成:
A B C
那剩下:
C:1
D:2
只剩 2 种词条了,就凑不出第二个千机扣。
但其实最优可以组成 2 个:
- 第一个:
A C D - 第二个:
B C D
也就是说:
这题不是“能不能凑”,而是“怎么凑才能最大化轮数”。
错误 2:for (auto i : mp) 中的 i 是副本
我一开始写过类似:
for (auto i : _xx) {
i.second--;
}
这个写法的问题是:
i是 map 元素的副本- 改的是副本,不会改到原容器
如果你真想改原元素,至少得写:
for (auto& i : _xx)
但即便如此,这题也不应该靠“边遍历边减”来写,因为核心建模本身不对。
错误 3:一边遍历 unordered_map 一边 erase
我还写过这种逻辑:
_xx.erase(i.first);
这是很危险的。
因为你在 range-for 遍历一个容器时,又去修改这个容器,容易触发未定义行为。
错误 4:把整个 unordered_map 当成单个元素用
我写过这样的代码:
pq.push(idx.second);
但 idx 是整个哈希表,不是单个键值对元素。
.second 只存在于:
pair<const int, int>
也就是 map 里的单个元素上。
正确写法应该是:
for (auto& i : idx) {
pq.push(i.second);
}
错误 5:把 unordered_map 的遍历顺序当成算法依据
unordered_map 是无序容器。
所以你不能指望:
“我顺着它扫一遍,顺序就是稳定且合理的。”
如果你的算法决策依赖 unordered_map 的遍历顺序,那结果很可能不稳定,甚至在不同环境下表现不同。
正确思路
一、先按 (y,z) 分成 4 组
同一个千机扣必须满足:
- 内外功一致
- 攻防类型一致
所以不同 (y,z) 的部件一定不能混合使用。
因此整道题可以拆成 4 个独立子问题,最后答案相加即可。
二、每个子问题只看“词条次数”
对于某一组 (y,z),我们只关心:
- 每个词条
x出现了多少次
例如:
x=17 -> 2 次
x=36 -> 1 次
x=85 -> 4 次
...
问题就转化成:
已知若干种词条,每种词条有若干个。
每次要选 3 个不同词条,最多能选多少轮?
三、这题真正的贪心:每次优先取“当前次数最多的 3 种词条”
为什么要这么做?
因为高频词条是后续组装的主要资源。
如果你不优先消耗它们,而是随便乱取,就可能让某些低频词条过早用光,导致后面明明还有很多部件,却凑不够 3 个不同词条。
因此,这题正确的贪心策略是:
每次从当前剩余次数最多的 3 个不同词条中,各拿 1 个出来组装。
这个策略可以用 大根堆 来实现。
四、用优先队列实现
步骤如下:
-
统计当前组内每个词条的出现次数;
-
把所有次数放入大根堆;
-
只要堆中至少还有 3 个元素:
- 取出最大的 3 个次数
a,b,c - 各减 1
- 组装数加 1
- 如果减完后还大于 0,就重新放回堆中
- 取出最大的 3 个次数
最终得到当前组能组成的最大千机扣数。
完整正确代码
#include <iostream>
#include <unordered_map>
#include <queue>
using namespace std;
int MaxSum(unordered_map<int, int>& idx) {
priority_queue<int> pq;
for (auto& i : idx) {
pq.push(i.second);
}
int ans = 0;
while (pq.size() >= 3) {
int a = pq.top(); pq.pop();
int b = pq.top(); pq.pop();
int c = pq.top(); pq.pop();
a--;
b--;
c--;
ans++;
if (a > 0) pq.push(a);
if (b > 0) pq.push(b);
if (c > 0) pq.push(c);
}
return ans;
}
int main() {
int a;
while (cin >> a) {
unordered_map<int, int> _xx;
unordered_map<int, int> _xy;
unordered_map<int, int> _yx;
unordered_map<int, int> _yy;
for (int i = 0; i < a; i++) {
int x, y, z;
cin >> x >> y >> z;
if (y == 1 && z == 1) {
_xx[x]++;
} else if (y == 1 && z == 2) {
_xy[x]++;
} else if (y == 2 && z == 1) {
_yx[x]++;
} else {
_yy[x]++;
}
}
int ans = 0;
ans = MaxSum(_xx) + MaxSum(_xy) + MaxSum(_yx) + MaxSum(_yy);
cout << ans << endl;
}
return 0;
}
复杂度分析
设某一组中共有 k 个不同词条。
- 建堆复杂度约为
O(k log k) - 每次组装会进行 3 次弹出和最多 3 次压入
- 假设最终组装了
m个千机扣,则堆操作复杂度约为O(m log k)
整体复杂度可以看作:
O((不同词条数 + 组装次数) * log(不同词条数))
在题目数据范围下是可以通过的。
这题最后提炼出的贪心经验
-
贪心不是“随便选一个当前可行解”,而是“找到不会破坏全局最优的局部策略”。
-
遇到“每轮选若干个不同种类、要求最大轮数”的题目,要高度警惕“次数统计 + 优先队列”模型。
-
unordered_map适合做计数,不适合直接依赖它的遍历顺序来做策略。 -
map / unordered_map 中:
i.first是 keyi.second是 value- 整个容器本身没有
.second
-
range-for 中
auto i是副本,auto& i才是引用。