题目大意
给定一个代表麻将牌数值的整数数组tiles
,我们需要计算出使用这些牌能组成的最多牌组数。按照麻将规则,一组牌可以是顺子(三张连续数值的牌)或刻子(三张数值相同的牌)。
题目分析
在本题中,核心挑战在于如何最大化使用给定的牌组成最多的顺子和刻子。
根据题意,牌组的组合需要遵循以下规则:
- 顺子:由三张数值连续的牌组成,例如
[4,5,6]
。 - 刻子:由三张数值相同的牌组成,例如
[7,7,7]
。
在尝试解决问题时,我们首先会考虑使用贪心算法,因为它简单直观,易于实现。
贪心算法的尝试
那么很容易想到,对于排序好的数据,有两种贪心方式:
1. 优先当成顺子,后续计算刻子
尝试列出反例。
我们考虑[1,1,1,2,2,2,3,4,4,4]
这种情况。
优先当成顺子,则有[1,2,3],[4,4,4]
两种情况。
但是显而易见最优解是[1,1,1],[2,2,2],[4,4,4]
。那么这个贪心策略可以排除。
2. 优先排除所有刻子
同样尝试列出反例。
我们考虑[1,1,1,2,2,3,3]
这种情况。
优先排除刻子,则可以得出[1,1,1]
一种情况。
但是最优解是[1,2,3],[1,2,3]
。同理,排除此贪心策略。
根据上述总结,我们可以发现,难以找到合适的贪心策略。但是它给我们提供了一个重要的洞察:我们需要一个能够考虑全局状态的算法,以便在多步决策中找到最优解。这启发我们使用动态规划(DP)来寻找最优解,DP算法可以帮助我们记录中间状态,并在需要时进行回溯,以找到全局最优解。
动态规划的尝试
对于本问题,我们可以尝试设计一个动态规划解法来确定最大的牌组数量。
1. 状态定义
令结果数组表示为ans[i][t1][t2]
考虑前i
个面额,然后有t1
个类型为[i-1,i,i+1]
的三元组,有t2
个类型为[i,i+1,i+2]
的三元组。
2. 状态转移方程
状态转移是动态规划的核心,我们需要考虑当前状态下所有可能的选择,并从中选择最优解。
我们有以下几种情况:
考虑所有可能的类型为[i+1,i+2,i+3]
数量的三元组,然后尝试将第i+1
的面值均考虑为[i+1,i+1,i+1]
并对ans[i+1][t2][t3]
进行转移。
转移时,我们考虑,三个顺子等价于三个刻子,所以ans
数组中,后两维的大小最多为2。因此,t3
的范围也是[0,2]
。我们可以枚举t1
、t2
、t3
,一共27种情况。
那么可以尝试转移了。
令ans[i+1][t2][t3]
代表目标状态,我们有:
ans[i+1][t2][t3] = max(ans[i+1][t2][t3], ans[i][t1][t2] + t3 + (cnt[i+1] - t1 - t2 - t3) / 3);
cnt[i+1]
代表第i+1
个不同牌出现的次数;
能进行这个转移的条件是cnt[i+1] >= t1 + t2 + t3
。
但是我们又遇见了问题。这个方程默认了牌的大小是连续的,但实际上需要离散化。所以,我们可以将牌与牌之间的间隙用一张虚拟的牌进行表示(本例中取n+1
),其对应的cnt=0
即可。在间隙之间的转移必然只会有ans[i][0][0]
会有合法的值,这也是可以用虚拟的牌进行代替的原因。
示例代码
class Solution {
private int[][] getArr() {
int[][] res = new int[3][3];
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
res[i][j] = 0;
}
}
return res;
}
public int maxGroupNumber(int[] tiles) {
TreeMap<Integer, Integer> map = new TreeMap<>();
for (int ele : tiles) {
if (map.containsKey(ele)) {
map.put(ele, (int) map.get(ele) + 1);
if (!map.containsKey(ele + 1)) {
map.put(ele + 1, 0);
}
} else {
map.put(ele, 1);
if (!map.containsKey(ele + 1)) {
map.put(ele + 1, 0);
}
}
}
// 离散化完毕
int[][] prev = getArr();
int[][] next = getArr();
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
int cur = entry.getKey();
int curCnt = entry.getValue();
prev = next;
next = getArr();
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 3; k++) {
if (curCnt - i - j - k >= 0) {
next[j][k] = Math.max(next[j][k], prev[i][j] + k + (curCnt - i - j - k) / 3);
}
}
}
}
}
return next[0][0];
}
}
此解法的时间复杂度为。
参考题解
附一个比我的思路更好的题解:题解。
对应的代码:
class Solution {
private int[][] getArr() {
int[][] res = new int[3][3];
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
res[i][j] = Integer.MIN_VALUE;
}
}
return res;
}
public int maxGroupNumber(int[] tiles) {
Arrays.sort(tiles);
int[] nums = new int[tiles.length];
int[] cnt = new int[tiles.length];
int idx = 0;
for (int i = 0; i < tiles.length; i++) {
if (i == 0 || tiles[i] != tiles[i - 1]) {
nums[idx] = tiles[i];
cnt[idx] = 1;
idx++;
} else {
cnt[idx - 1]++;
}
}
int[][] prev = null;
int[][] next = getArr();
int prevK = -1;
next[0][0] = 0;
for (int i = 0; i < idx; i++) {
prev = next;
next = getArr();
if (prevK + 1 == nums[i]) {
for (int t1 = 0; t1 < 3; t1++) {
for (int t2 = 0; t2 < 3; t2++) {
for (int t3 = 0; t3 < 3; t3++) {
if (t1 + t2 + t3 <= cnt[i]) {
next[t1][t2] = Math.max(next[t1][t2], prev[t3][t1] + t3 + (cnt[i] - t1 - t2 - t3) / 3);
}
}
}
}
} else {
for (int t1 = 0; t1 <= cnt[i] && t1 < 3; t1++) {
next[0][t1] = prev[0][0] + (cnt[i] - t1) / 3;
}
}
prevK = nums[i];
}
return next[0][0];
}
}
这个解法的时间复杂度一致,但是运行时间明显更快。