LeetCode 1079. Letter Tile Possibilities【哈希表,回溯,动态规划,排列组合】中等

这篇具有很好参考价值的文章主要介绍了LeetCode 1079. Letter Tile Possibilities【哈希表,回溯,动态规划,排列组合】中等。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

你有一套活字字模 tiles,其中每个字模上都刻有一个字母 tiles[i]。返回你可以印出的非空字母序列的数目。

**注意:**本题中,每个活字字模只能使用一次。

示例 1:

输入:"AAB"
输出:8
解释:可能的序列为 "A", "B", "AA", "AB", "BA", "AAB", "ABA", "BAA"

示例 2:

输入:"AAABBC"
输出:188

示例 3:

输入:"V"
输出:1

提示:

  • 1 <= tiles.length <= 7
  • tiles 由大写英文字母组成

解法1 回溯+排列+子集生成

随手写的代码:

class Solution {
private:
    int ans = 0;
    unordered_set<string> rec;
    void dfs(string& s, string& t, int b, int e) {
        if (b >= e) {
            string f = t;
            if (f.empty()) return; 
            sort(f.begin(), f.end()); 
            do { 
                if (rec.find(f) == rec.end()) {
                    ++ans;
                    rec.insert(f);
                }
            } while (next_permutation(f.begin(), f.end()));
            return ;
        } 
        dfs(s, t, b + 1, e);
        t.push_back(s[b]); 
        dfs(s, t, b + 1, e); 
        t.pop_back(); 
    }
public:
    int numTilePossibilities(string tiles) {
        string s;
        dfs(tiles, s, 0, tiles.size());
        return ans;
    }
};  

解法2 动态规划

1. 寻找子问题

t i l e s = AABCC tiles=\texttt{AABCC} tiles=AABCC 为例。先来思考,如何计算长为 5 5 5 的序列的数目?

  • 由于相同字母不作区分,先考虑 2 2 2 C \texttt{C} C 如何放置。这等价于在 5 5 5 个位置中选 2 2 2 个位置放 C \texttt{C} C ,其余位置放 AAB \texttt{AAB} AAB 。这 2 2 2 C \texttt{C} C ( 5 2 ) = 10 \dbinom 5 2=10 (25)=10 种放法——表示从 n n n 个数中选 k k k 个数的方案数,即 n ! k ! ( n − k ) ! \dfrac{n!}{k!(n-k)!} k!(nk)!n!
  • 剩余要解决的问题为,用 AAB \texttt{AAB} AAB 构造长为 3 3 3 的序列的数目。这是一个与原问题相似,且规模更小的子问题。
2. 状态定义与转移

根据上面的讨论,定义 f [ i ] f[i] f[i] 表示用前 i i i 种字符构造长为 j j j 的序列的方案数

设第 i i i 种字符有 cnt \textit{cnt} cnt 个:

  • 如果一个也不选,那么 f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i - 1][j] f[i][j]=f[i1][j]
  • 如果选 k k k 个,那么需要从 j j j 个位置中选 k k k 个放第 i i i 种字符,其余位置就是用前 i − 1 i−1 i1 种字符构造长为 j − k j-k jk 的序列的方案数,所以有 f [ i ] [ j ] = f [ i − 1 ] [ j − k ] ⋅ ( j k ) f[i][j] =f[i-1][j-k]\cdot \dbinom j k f[i][j]=f[i1][jk](kj) 。这里 k ≤ min ⁡ ( j , cnt ) k\le\min(j,\textit{cnt}) kmin(j,cnt) 。特别地,一个也不选相当于 k = 0 k=0 k=0 的情况。

所以,枚举 k = 0 , 1 , ⋯   , min ⁡ ( j , cnt ) k=0,1,\cdots,\min(j,\textit{cnt}) k=0,1,,min(j,cnt) ,把所有方案数相加,就得到了 f [ i ] [ j ] f[i][j] f[i][j] ,对应的状态转移方程为:
f [ i ] [ j ] = ∑ k = 0 min ⁡ ( j , cnt ) f [ i − 1 ] [ j − k ] ⋅ ( j k ) f[i][j] = \sum_{k=0}^{\min(j,\textit{cnt})} f[i-1][j-k]\cdot \binom j k f[i][j]=k=0min(j,cnt)f[i1][jk](kj)

初始值: f [ 0 ] [ 0 ] = 1 f[0][0] = 1 f[0][0]=1 ,构造空序列的方案数为 1 1 1
答案: ∑ j = 1 n f [ m ] [ j ] \sum\limits_{j=1}^{n}f[m][j] j=1nf[m][j] ,其中 m m m tiles \textit{tiles} tiles 中的字母种数。

代码实现时,组合数可以用如下恒等式预处理:
( n k ) = ( n − 1 k − 1 ) + ( n − 1 k ) \binom {n}{k} = \binom{n-1}{k-1}+\binom{n-1}{k} (kn)=(k1n1)+(kn1)
这个式子本质是考虑第 n n n 个数「选或不选」。如果选,那么问题变成从 n − 1 n−1 n1 个数中选 k − 1 k-1 k1 个数的方案数;如果不选,那么问题变成从 n − 1 n−1 n1 个数中选 k k k 个数的方案数。二者相加即为从 n n n 个数中选 k k k 个数的方案数。

class Solution {
    private static final int MX = 8;
    private static final int[][] c = new int[MX][MX];
    static {
        for (int i = 0; i < MX; ++i) {
            c[i][0] = c[i][i] = 1;
            for (int j = 1; j < i; ++j) {
                c[i][j] = c[i - 1][j - 1] + c[i - 1][j]; // 预处理组合数
            }
        }
    } 
    public int numTilePossibilities(String tiles) {
        var counts = new HashMap<Character, Integer>(); // 统计每个字母的出现次数
        for (var c : tiles.toCharArray())
            counts.merge(c, 1, Integer::sum); // counts[c]++
        int m = counts.size(), n = tiles.length();
        var f = new int[m + 1][n + 1]; // 以前i种字母构造j长序列的方案数
        f[0][0] = 1; // 构造空序列的方案数
        int i = 1; 
        for (var cnt : counts.values()) { // 枚举第i种字母
            for (int j = 0; j <= n; ++j) // 枚举序列长度
                for (int k = 0; k <= j && k <= cnt; ++k) // 枚举第i种字母选了k个
                    f[i][j] += f[i - 1][j - k] * c[j][k];
                
            ++i;
        }
        int ans = 0;
        for (int j = 1; j <= n; ++j) ans += f[m][j];
        return ans;
    }
}

复杂度分析:

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2) ,其中 n n n t i l e s tiles tiles 的长度。虽然写了个三重循环,但换个角度,对于一个固定的 j j j ,最内层的循环次数之和,约为所有字母的出现次数之和,即 O ( n ) O(n) O(n) 。相当于最外层和最内层合起来是一个 O ( n ) O(n) O(n) 的循环。所以三重循环的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2) 。忽略预处理组合数的时间和空间。
  • 注:也可以只预处理阶乘,用公式计算组合数。
3. 空间优化

由于 f [ i ] f[i] f[i] 只从 f [ i − 1 ] f[i−1] f[i1] 转移过来,我们可以去掉第一个维度,只用一个一维数组。

和0-1背包问题一样,如果 j j j 从小到大遍历,那么 f [ i − 1 ] [ j ] f[i−1][j] f[i1][j] 保存的数据会被 f [ i ] [ j ] f[i][j] f[i][j] 覆盖,但是计算右边的 f [ i ] [ j ′ ] f[i][j'] f[i][j] 时,又需要 f [ i − 1 ] [ j ] f[i−1][j] f[i1][j] 。倒序遍历 j j j 即可解决此问题。

此外,可以累加 c n t cnt cnt记作 n n n ,作为第二层循环的初始值,因为就算全部都选,前 i i i 种字母的长度之和也不会超过 n n n ,计算比 n n n 更大的状态是没有意义的。

class Solution {
    private static final int MX = 8;
    private static final int[][] c = new int[MX][MX];
    static {
        for (int i = 0; i < MX; ++i) {
            c[i][0] = c[i][i] = 1;
            for (int j = 1; j < i; ++j)
                c[i][j] = c[i - 1][j - 1] + c[i - 1][j]; // 预处理组合数
        }
    } 
    public int numTilePossibilities(String tiles) {
        // 改成 int[26] 统计可能会快一点点,感兴趣可以试试(下面 DP 跳过 cnt=0 的情况)
        var counts = new HashMap<Character, Integer>(); // 统计每个字母的出现次数
        for (var c : tiles.toCharArray())
            counts.merge(c, 1, Integer::sum); // counts[c]++
        var f = new int[tiles.length() + 1];
        f[0] = 1; // 构造空序列的方案数
        int n = 0;
        for (var cnt : counts.values()) { // 枚举第i种字母
            n += cnt; // 常数优化:相比从 tiles.length() 开始要更快
            for (int j = n; j > 0; --j)  // 枚举序列长度j
                // 枚举第i种字母选了k个,注意k=0时的方案数已经在f[j]中了
                for (int k = 1; k <= j && k <= cnt; ++k)
                    f[j] += f[j - k] * c[j][k];
        }
        int ans = 0;
        for (int j = 1; j <= n; ++j) ans += f[j];
        return ans;
    }
}

复杂度分析:文章来源地址https://www.toymoban.com/news/detail-498452.html

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2) ,其中 n n n t i l e s tiles tiles 的长度。虽然写了个三重循环,但换个角度,对于一个固定的 j j j ,最内层的循环次数之和,约为所有字母的出现次数之和,即 O ( n ) O(n) O(n) 。相当于最外层和最内层合起来是一个 O ( n ) O(n) O(n) 的循环。所以三重循环的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n ) O(n) O(n) 。忽略预处理组合数的时间和空间。

到了这里,关于LeetCode 1079. Letter Tile Possibilities【哈希表,回溯,动态规划,排列组合】中等的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【LeetCode题解】2645. 构造有效字符串的最少插入数(计算组数+动态规划+考虑相邻字母)+2085. 统计出现过一次的公共字符串(哈希表)+2807. 在链表中插入最大公约数

    2645. 构造有效字符串的最少插入数 方法一:计算组数 1.用count统计,能构成几组abc 2.如果当前字符大于之前字符,说明还在组内,不更新 3.如果当前字符小于等于之前字符,说明不是同一组的abc,组数更新 4.最终返回值:组数*3,再减去原本的字符数,就是要插入的次数 方法二

    2024年04月12日
    浏览(65)
  • 回溯理论基础及leetcode

    学习参考 与递归相辅相成;回溯是递归的副产品,只要有递归就会有回溯。 回溯函数也就是递归函数,指的都是一个函数。 纯暴力搜索 解决的问题 组合问题:N个数里面按一定规则找出k个数的集合 切割问题:一个字符串按一定规则有几种切割方式 子集问题:一个N个数的集

    2023年04月13日
    浏览(36)
  • LeetCode——回溯篇(二)

    刷题顺序及思路来源于代码随想录,网站地址:https://programmercarl.com 目录 131. 分割回文串 93. 复原 IP 地址 78. 子集 90. 子集 II 491. 递增子序列 给你一个字符串  s ,请你将   s   分割成一些子串,使每个子串都是  回文串  。返回  s  所有可能的分割方案。 回文串  是正

    2024年02月11日
    浏览(26)
  • LeetCode——回溯篇(三)

    刷题顺序及思路来源于代码随想录,网站地址:https://programmercarl.com 目录 46. 全排列 47. 全排列 II 332. 重新安排行程 51. N 皇后 37. 解数独   给定一个不含重复数字的数组  nums  ,返回其  所有可能的全排列  。你可以  按任意顺序  返回答案。 给定一个可包含重复数字的

    2024年02月10日
    浏览(30)
  • 回溯理论基础及leetcode例题

    学习参考 与递归相辅相成;回溯是递归的副产品,只要有递归就会有回溯。 回溯函数也就是递归函数,指的都是一个函数。 纯暴力搜索 解决的问题 组合问题:N个数里面按一定规则找出k个数的集合 切割问题:一个字符串按一定规则有几种切割方式 子集问题:一个N个数的集

    2023年04月13日
    浏览(66)
  • Leetcode刷题第八天-回溯

    22:括号生成 链接:22. 括号生成 - 力扣(LeetCode) 括号是一对,所以每一次递归结束条件是字符串长度=2*n 有效括号判断:\\\'(\\\'个数==\\\')\\\'个数时,当前必须是\\\'(\\\',\\\'(\\\'个数==n时,必须是\\\')\\\',其他情况当前位置遍历两边,既可以是\\\'(\\\'又可以是\\\')\\\' generateParenthesis 89:格雷编码 链接:89.

    2024年02月19日
    浏览(37)
  • LeetCode 39. 组合总和(回溯+剪枝)

    链接:LeetCode 39. 组合总和 难度:中等 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数字可以 无限制重复被选

    2024年02月14日
    浏览(45)
  • LeetCode46全排列(回溯入门)

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 难度:中等 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案 示例 1 示例 2 示例 3 在很多刷题文章和书籍中,此题都被用做回溯算法的第一题,可见该题

    2024年02月10日
    浏览(36)
  • leetcode77. 组合(回溯算法-java)

    来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/combinations 给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。 示例 1: 输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ] 示例 2: 输入:n = 1, k = 1 输出:[[1]] 提示:

    2024年02月11日
    浏览(54)
  • LeetCode.46. 全排列(回溯法入门)

    写在前面: 题目链接:LeetCode.46. 全排列 编程语言:C++ 题目难度:中等 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1: 输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] 示例2: 输入:nums = [0,1] 输出:

    2024年02月06日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包