1 条题解
-
0
思路与算法
花色不影响牌型,先统计3、4、…、K、A、2、小王、大王的张数。2和双王不参加顺子。把出牌分为两类:单顺(至少5个连续点)、双顺(至少3个连续点)、三顺(至少2个连续点);以及图表列出的全部非顺子牌型。 任意一套最优出牌的各手可以交换顺序而不改变合法性,所以可把所有顺子移到前面打。对当前各点张数,枚举每一种顺子的起点、终点和重复次数1/2/3,扣除后递归;以完整张数编码记忆化。每个状态也尝试直接用最优非顺子方案清掉残牌。 非顺子与点数之间的大小、邻接无关,只需(h1,h2,h3,h4,j):有h_i个普通点恰剩i张,j个大小王。枚举每次从某类普通点取1、2、3、4张;3张可再带1张单牌或一个普通对子,4张可带任意两张单牌或任意两对普通对子,双王可以单独组成火箭。每次扣牌后相应修改直方图并记忆化。带牌允许拆开原有对子、三张、四张,不能只从恰好1张或2张的组中选。
正确性说明
一手牌型的合法性只取决于它包含的牌,因此不同出牌次序只要分配的牌不重叠就等价。每个合法分拆均可先打其中的全部顺子,其顺子在递归中必被枚举。剩余非顺子分拆正是尾部DP的目标。 对非顺子,任意交换两个普通点的剩余张数都不改变后续可选的非顺子组合,所以直方图是充分状态;大小王另记,是因为它们可一起构成火箭,却不是同点对子。尾部DP穷尽图表中的基本手型和合法带牌,并且每条转移都实际减少相应牌数,故可按剩余张数归纳得到最少手数。四带二的两对按图表“任意两对牌”理解,同一普通点有4张时可拆成两对;两王不能充当普通对子。三顺带单牌、三顺带对子不在题图规则中,程序不添加这些牌型。综合顺子枚举和尾部精确DP,所有合法分拆均有对应路径,每条路径也都是合法分拆,取最小值正确。
复杂度
令R为一手牌在扣除顺子后可达的完整张数状态数,H为总牌数不超过23的直方图状态数。完整状态每次枚举常数12个顺子点上的O(12²)区间并更新至多12张计数,尾部DP每状态枚举至多常数种组别和附带组合。用有序映射记忆化,总时间O(R·12³log R+H·4⁴),空间O(R+H)。初始化统计牌O(n),共享尾部数组初始化O(2^18),各测试组复用尾部结果。完整扣牌状态数不超过∏(a_i+1),15个点23张且王各至多1的范围内至多839808;实际只访问顺子可达子集。
易错点
A在顺子中接在K后面;2和王不能进入顺子。顺子最短长度分别5、3、2。不要照搬普通斗地主的飞机带翅膀规则。双王是火箭而非同点对子,但可分别当单牌。三带一与四带二都可拆已有组提供附带牌。
参考代码(C++11)
#include <bits/stdc++.h> using namespace std; int histogramMemo[1 << 18]; int encode(const array<int, 5> &h, int j) { return h[1] | (h[2] << 4) | (h[3] << 8) | (h[4] << 12) | (j << 16); } vector<pair<array<int, 5>, int>> removeGroups(const array<int, 5> &h, int j, int amount) { vector<pair<array<int, 5>, int>> out; for (int size = amount; size <= 4; size++) if (h[size]) { auto z = h; z[size]--; if (size > amount) z[size - amount]++; out.push_back({z, j}); } if (amount == 1 && j) out.push_back({h, j - 1}); return out; } int tailSolve(const array<int, 5> &h, int j) { int key = encode(h, j); int &memo = histogramMemo[key]; if (memo >= 0) return memo; if (!key) return memo = 0; int best = h[1] + h[2] + h[3] + h[4] + (j ? 1 : 0); if (j == 2) best = min(best, 1 + tailSolve(h, 0)); for (int core = 1; core <= 4; core++) for (auto base : removeGroups(h, j, core)) { best = min(best, 1 + tailSolve(base.first, base.second)); if (core == 3) { for (int amount = 1; amount <= 2; amount++) for (auto z : removeGroups(base.first, base.second, amount)) best = min(best, 1 + tailSolve(z.first, z.second)); } if (core == 4) { for (int amount = 1; amount <= 2; amount++) for (auto a : removeGroups(base.first, base.second, amount)) for (auto b : removeGroups(a.first, a.second, amount)) best = min(best, 1 + tailSolve(b.first, b.second)); } } return memo = best; } struct Solver { array<int, 15> a; map<unsigned long long, int> memo; unsigned long long code() { unsigned long long k = 0; for (int i = 0; i < 15; i++) k |= (unsigned long long)a[i] << (3 * i); return k; } int solve() { auto key = code(); auto old = memo.find(key); if (old != memo.end()) return old->second; array<int, 5> h{}; for (int i = 0; i < 13; i++) if (a[i]) h[a[i]]++; int best = tailSolve(h, a[13] + a[14]); if (best <= 1) return memo[key] = best; for (int count = 1; count <= 3; count++) { int minimum = (count == 1 ? 5 : count == 2 ? 3 : 2); for (int l = 0; l < 12; l++) { int r = l; while (r < 12 && a[r] >= count) r++; for (int end = l + minimum; end <= r; end++) { for (int i = l; i < end; i++) a[i] -= count; best = min(best, 1 + solve()); for (int i = l; i < end; i++) a[i] += count; } } } return memo[key] = best; } }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); fill(histogramMemo, histogramMemo + (1 << 18), -1); int T, n; cin >> T >> n; while (T--) { Solver s; s.a.fill(0); for (int i = 0, x, color; i < n; i++) { cin >> x >> color; int rank = x == 0 ? 12 + color : x == 1 ? 11 : x == 2 ? 12 : x - 3; s.a[rank]++; } cout << s.solve() << '\n'; } }
- 1