LeetCode刷题笔记——LCP 36最多牌组数

59 阅读5分钟

题目大意

给定一个代表麻将牌数值的整数数组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]。我们可以枚举t1t2t3,一共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];
    }
}

此解法的时间复杂度为O(nlogn)O(nlogn)

参考题解

附一个比我的思路更好的题解:题解

对应的代码:

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];

    }
}

这个解法的时间复杂度一致,但是运行时间明显更快。