算法34:贴纸拼词(力扣691题)

news2024/11/17 3:53:11

题目:

我们有 n 种不同的贴纸。每个贴纸上都有一个小写的英文单词。

您想要拼写出给定的字符串 target ,方法是从收集的贴纸中切割单个字母并重新排列它们。如果你愿意,你可以多次使用每个贴纸,每个贴纸的数量是无限的。

返回你需要拼出 target 的最小贴纸数量。如果任务不可能,则返回 -1 。

注意:在所有的测试用例中,所有的单词都是从 1000 个最常见的美国英语单词中随机选择的,并且 target 被选择为两个随机单词的连接。

题解:

示例 1:

输入: stickers = ["with","example","science"], target = "thehat"
输出:3
解释:
我们可以使用 2 个 "with" 贴纸,和 1 个 "example" 贴纸。
把贴纸上的字母剪下来并重新排列后,就可以形成目标 “thehat“ 了。
此外,这是形成目标字符串所需的最小贴纸数量。

示例 2:

输入:stickers = ["notice","possible"], target = "basicbasic"
输出:-1
解释:我们不能通过剪切给定贴纸的字母来形成目标“basicbasic”。

分析:

1. 上面的两个式例正好都是前面两个符合条件的单词,如果符合条件的单词中间间隔比较大,该如何去处理?我个人觉得,肯定是需要遍历stickers数组的,否则无法判定那个是最优解。

2. 假设数组为 {abc, ab, bc, b, c} 而 target= abcabc. 那么遍历数组第一个元素拼出了abc, 此时target=abc;  第一个元素处理完以后,应该再次使用数组的第一个元素来拼词才更合理。题目也说了,张数无限。 否则的话至少需要3张才能拼出原始的target=abcabc.  而使用数组第一个元素abc只需要2张。此处,就要考虑使用递归了

3. 如果 假设1 和 假设2 都成立,那么也就是说根据abc拼了2次,存在重复消费abc的情况,是不是需要尝试添加缓存?

4. 因为贴纸是可以剪碎掉的,哪怕第一个元素是adqbec也是可以的。因为它还是包含abc子元素的。如何去统计abc子元素与target存在共同子序列,而且是不分先后顺序的。样本模型肯定是不可以的,它强调可以删减、但是不可以改变元素顺序。而此题是可以剪碎,完全打乱的。只能先去尝试写递归

5. 根据业务去分析,随便假设一个稍微简单的数组和target,满足条件即可。从简单到复杂,如果你随便假设测试数据都成立,那大概率就是成立的。

假设,数组为 {aa, b, bc}, target为aabc. 目测就是2张可以拼词完成 :

递归代码:

package code03.动态规划_07.lesson5;


/**
 * 力扣691 : 贴纸拼词
 * https://leetcode.cn/problems/stickers-to-spell-word/description/
 *
 */
public class Stickers_01 {

    public int minStickers(String[] stickers, String target)
    {
        //边界值
        if (stickers == null
                || stickers.length == 0
                || target == null
                || target.isEmpty()) {
            return -1;
        }

        int ans = process (stickers, target);
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }


    public int process (String[] stickers, String target)
    {
        //如果target为空,说明在上一轮已经拼接完毕
        if (target.isEmpty()) {
            return 0;
        }

        //每一轮递归的最终返回值
        int ans = Integer.MAX_VALUE;
        //讨论每一个单词是否能够参与target的拼接
        for (String word : stickers) {
            //返回target拼词以后剩余字符串
            String res = splice(word, target);
            //如果res与target长度相等,说明word并没有参与进拼词过程
            if (res.length() != target.length()) {
                //最少贴纸数,每一轮递归都要取最小值
                ans = Math.min(ans, process (stickers, res));
            }
        }
        //如果ans不是无效值,说明此轮递归参与到拼词过程中。张数对应的要增加一次
        return ans != Integer.MAX_VALUE ? ans + 1 : ans;
    }

    public String splice(String s, String target)
    {
        char[] ss = s.toCharArray();
        char[] tt = target.toCharArray();

        //26个小写字母,题目给定的
        int[] count = new int[26];

        //target字符串词频统计
        for (char tChar : tt) {
            count[tChar - 'a']++;
        }

        //根据当前单词到target中去减少对应的字符出现次数
        for(char sChar : ss) {
            count[sChar - 'a']--;
        }

        StringBuffer sb = new StringBuffer();
        //统计target还剩下哪些字符,并且把这些字符拼成字符串
        for (int i = 0; i < 26; i++) {
            //count[i]对应出现的次数,而 i 才是对应的字符
            if (count[i] > 0) {
                for (int times = 0; times < count[i]; times++) {
                    sb.append((char)(i + 'a'));
                }
            }
        }
        return sb.toString();
    }


    public static void main(String[] args) {
        Stickers_01 s = new Stickers_01();
        String[] ss = {"aa", "b", "bc"};
        String target = "aabc";

        System.out.println(s.minStickers(ss, target));
    }
}

本地测试是ok的,但是到力扣上测试,发现直接超时了:

好消息是测试了30多个例子没有报错。既然超时,说明执行时间满,代码的时间复杂度有问题。那就进尝试优化,还是看图:

1. 最后一列,aa不参与返回max出现了很多次。而aa参与拼词的情况下也出现了很多次,说明存在重复消费的代码。

2. 遍历aa的时候,可以找到 bc。 这是最优解; 而遍历到bc的时候,可以找到aa, 这也是最优解。

因此,加缓存(记忆化搜索),势在必行。

递归 + 记忆化搜索:

package code03.动态规划_07.lesson5;


import java.util.HashMap;

/**
 * 力扣691 : 贴纸拼词
 * https://leetcode.cn/problems/stickers-to-spell-word/description/
 *
 */
public class Stickers_01_opt {

    public int minStickers(String[] stickers, String target)
    {
        //边界值
        if (stickers == null
                || stickers.length == 0
                || target == null
                || target.isEmpty()) {
            return -1;
        }

        HashMap<String, Integer> map = new HashMap<>();
        //优化之前,target为空直接返回0. 因此,此处必须保持逻辑一直
        map.put("",0);

        int ans = process (stickers, target, map);
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }


    public int process (String[] stickers, String target, HashMap<String, Integer> map )
    {
        if (map.get(target) != null) {
            return map.get(target);
        }
        //如果target为空,说明在上一轮已经拼接完毕
        if (target.isEmpty()) {
            return 0;
        }

        //每一轮递归的最终返回值
        int ans = Integer.MAX_VALUE;
        //讨论每一个单词是否能够参与target的拼接
        for (String word : stickers) {
            //返回target拼词以后剩余字符串
            String res = splice(word, target);
            //如果res与target长度相等,说明word并没有参与进拼词过程
            if (res.length() != target.length()) {
                //最少贴纸数,每一轮递归都要取最小值
                ans = Math.min(ans, process (stickers, res, map));
            }
        }
        //如果ans不是无效值,说明此轮递归参与到拼词过程中。张数对应的要增加一次
        ans = ans != Integer.MAX_VALUE ? ans + 1 : ans;
        map.put(target, ans);
        return ans;
    }

    public String splice(String s, String target)
    {
        char[] ss = s.toCharArray();
        char[] tt = target.toCharArray();

        //26个小写字母,题目给定的
        int[] count = new int[26];

        //target字符串词频统计
        for (char tChar : tt) {
            count[tChar - 'a']++;
        }

        //根据当前单词到target中去减少对应的字符出现次数
        for(char sChar : ss) {
            count[sChar - 'a']--;
        }

        StringBuffer sb = new StringBuffer();
        //统计target还剩下哪些字符,并且把这些字符拼成字符串
        for (int i = 0; i < 26; i++) {
            //count[i]对应出现的次数,而 i 才是对应的字符
            if (count[i] > 0) {
                for (int times = 0; times < count[i]; times++) {
                    sb.append((char)(i + 'a'));
                }
            }
        }
        return sb.toString();
    }


    public static void main(String[] args) {
        Stickers_01_opt s = new Stickers_01_opt();
        String[] ss = {"aa", "b", "bc"};
        String target = "aabc";

        System.out.println(s.minStickers(ss, target));
    }
}

虽然力扣是通过了,但是 5.****%的胜率,实在是有些牵强了。 

再次看图分析:

1.  遍历数组的时候,不管能不能参与到target的拼词过程中去,所有数组都会遍历一遍,这有点说不过去了。因为每遍历一个数组元素,都要进入递归的,这样反反复复,浪费性能。

解决:不去遍历全部数组,而是根据target字符串中的第一个字符(或者其他位置的字符)去过滤。把过滤出来的数组进行遍历,这样可以快很多。

2. 词频统计是放在递归里面生成的,每遍历数组元素一次,就递归调用一次,相当麻烦。而且我们需要对数组元素进行过滤,目前的词频统计位置就不合理了。

解决:把词频统计提前批量完成,这样才方便过滤

优化版本:

package code03.动态规划_07.lesson5;


/**
 * 力扣691 : 贴纸拼词
 * https://leetcode.cn/problems/stickers-to-spell-word/description/
 *
 */
public class Stickers_02 {

    public int minStickers(String[] stickers, String target)
    {
        //边界值
        if (stickers == null
                || stickers.length == 0
                || target == null
                || target.isEmpty()) {
            return -1;
        }

        int[][] dp = new int[stickers.length][26];
        //词频统计,去除掉第一版同一个单词反反复复的词频统计
        for (int i = 0; i < stickers.length; i++) {
            char[] chars = stickers[i].toCharArray();
            for (char sChar : chars) {
                dp[i][sChar - 'a']++;
            }
        }

        int ans = process (dp, target);
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }


    public int process (int[][] arr, String target)
    {
        //如果target为空,说明在上一轮已经拼接完毕
        if (target.isEmpty()) {
            return 0;
        }

       int[] count = new int[26];
       char[] tt = target.toCharArray();
       //每一轮递归target都不一样,因此每一轮都需用统计target
       for (char tChar : tt) {
           count[tChar - 'a']++;
       }

       int result = Integer.MAX_VALUE;
       for (int i = 0; i < arr.length; i++) {

           int[] sticker = arr[i];
           /**
            *
            * tt是原target字符数组, 那么tt[0]就是target第一个字符
            * tt[0] - 'a' 就是第一个字符对应的下标。比如字符 'b' 下标就为1,'a' 对应0. 'c'对应2
            *
            * count[0] 对应的是 a 字符。 target可以以26个字母任意一个开头,此处不可以使用count[0]
            *
            * sticker[tt[0] - 'a'] 对应的是字符出现的次数。如果当前字符串含有target首字符,就考虑;
            * 否则,放弃。 剪枝:去除了不包含的情况
            */
           if (sticker[tt[0] - 'a'] > 0) {
               StringBuilder sb = new StringBuilder();
               for (int j = 0; j < 26; j++) {
                   //target词频需要根据当前数组单词全部剪掉。如果大于0,
                   //则代表target在数组的当前单词参与拼词以后,还存在没有
                   //拼接完的部分,需要记录下来用数组中别的单词再来拼
                   for (int m = 0; m <count[j] - sticker[j]; m++) {
                       //j代表字符的下标, count[j] 代表单签字符出现的次数
                       sb.append((char) (j + 'a'));
                   }
               }

               //递归,把剩余的target部分继续交个arr[][]去拼词
               result = Math.min(result, process(arr, sb.toString()));
           }
       }

       return result != Integer.MAX_VALUE ? result + 1 : result;
    }

    public static void main(String[] args) {
        Stickers_02 s = new Stickers_02();
        String[] ss = {"aa", "b", "bc"};
        String target = "aabc";

        System.out.println(s.minStickers(ss, target));
    }
}

明明代码已经优化了,为啥还超时呢?超时,说明代码应该没有逻辑错误,那就是时间复杂度的问题了。而时间复杂度是和计算有关的,加上缓存,俗称记忆化搜索试试:

public int minStickers(String[] stickers, String target)
    {
        //边界值
        if (stickers == null
                || stickers.length == 0
                || target == null
                || target.isEmpty()) {
            return -1;
        }

        int[][] dp = new int[stickers.length][26];
        //词频统计,去除掉第一版同一个单词反反复复的词频统计
        for (int i = 0; i < stickers.length; i++) {
            char[] chars = stickers[i].toCharArray();
            for (char sChar : chars) {
                dp[i][sChar - 'a']++;
            }
        }

        HashMap<String, Integer> map = new HashMap<>();
        //优化之前,target为空直接返回0. 因此,此处必须保持逻辑一直
        map.put("",0);
        int ans = process (dp, target, map);
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }


    public int process (int[][] arr, String target, HashMap<String, Integer> map)
    {
        if (map.get(target) != null) {
            return map.get(target);
        }

        //如果target为空,说明在上一轮已经拼接完毕
        if (target.isEmpty()) {
            return 0;
        }

       int[] count = new int[26];
       char[] tt = target.toCharArray();
       //每一轮递归target都不一样,因此每一轮都需用统计target
       for (char tChar : tt) {
           count[tChar - 'a']++;
       }

       int result = Integer.MAX_VALUE;
       for (int i = 0; i < arr.length; i++) {

           int[] sticker = arr[i];
           /**
            *
            * tt是原target字符数组, 那么tt[0]就是target第一个字符
            * tt[0] - 'a' 就是第一个字符对应的下标。比如字符 'b' 下标就为1,'a' 对应0. 'c'对应2
            *
            * count[0] 对应的是 a 字符。 target可以以26个字母任意一个开头,此处不可以使用count[0]
            *
            * sticker[tt[0] - 'a'] 对应的是字符出现的次数。如果当前字符串含有target首字符,就考虑;
            * 否则,放弃。 剪枝:去除了不包含的情况
            */
           if (sticker[tt[0] - 'a'] > 0) {
               StringBuilder sb = new StringBuilder();
               for (int j = 0; j < 26; j++) {
                   //target词频需要根据当前数组单词全部剪掉。如果大于0,
                   //则代表target在数组的当前单词参与拼词以后,还存在没有
                   //拼接完的部分,需要记录下来用数组中别的单词再来拼
                   for (int m = 0; m <count[j] - sticker[j]; m++) {
                       //j代表字符的下标, count[j] 代表单签字符出现的次数
                       sb.append((char) (j + 'a'));
                   }
               }

               //递归,把剩余的target部分继续交个arr[][]去拼词
               result = Math.min(result, process(arr, sb.toString(), map));
           }
       }

       result = result != Integer.MAX_VALUE ? result + 1 : result;
       map.put(target, result);
       return result;
    }

加上缓存以后,果然快了很多!

我们之前说递归、递归+记忆化搜索、动态规划。 对于非严格依赖表结构,递归+记忆化搜索有时候也是等价于动态规划的。 

严格依赖表结构的动态规划,则优化的空间更大。以后,还会基于动态规划,进行更为复杂的算法优化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1376550.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在linux中 centos7 连接xhell

网卡配置 仅主机要对应仅主机模式&#xff0c;NAT模式要对应NAT模式 一、在linux中centos7 连接xhell 实验&#xff1a;NAT模式对应NAT模式 以192.168.246.0段为例 1.进入虚拟机: 2.去真机修改&#xff1a; 3.然后去虚拟机里&#xff1a; 4.进入xhell修改&#xff1a; 再输…

【深度学习】Anaconda3 + PyCharm 的环境配置 1:手把手带你安装 PyTorch 并创建 PyCharm 项目

前言 文章性质&#xff1a;实操记录 &#x1f4bb; 主要内容&#xff1a;这篇文章记录了 PyTorch 的安装过程&#xff0c;包括&#xff1a; 1. 创建并激活新的虚拟环境&#xff1b; 2. 查看电脑是否支持 CUDA 以及 CUDA 的版本&#xff1b; 3. 根据 CUDA 的版本安装 PyTorch&am…

企业网络出口部署案例

知识改变命运&#xff0c;技术就是要分享&#xff0c;有问题随时联系&#xff0c;免费答疑&#xff0c;欢迎联系&#xff01; 厦门微思网络​​​​​​ https://www.xmws.cn 华为认证\华为HCIA-Datacom\华为HCIP-Datacom\华为HCIE-Datacom Linux\RHCE\RHCE 9.0\RHCA\ Oracle O…

uniapp运行自定义底座到真机没反应

同步资源失败&#xff0c;未得到同步资源的授权&#xff0c;请停止运行后重新运行&#xff0c;并注意手机上的授权提示。 如果此时手机没有任何反应&#xff0c;请检查自定义基座是否正确;如果是离线制作的自定义基座包&#xff0c; 请检查离线包制作是否正确。 网上各种查找报…

移动通信系统关键技术多址接入MIMO学习(8)

1.Multiple-antenna Techniques多天线技术MIMO&#xff0c;从SISO到SIMO到MISO到如今的MIMO&#xff1b; 2.SIMO单发多收&#xff0c;分为选择合并、增益合并&#xff1b;SIMO&#xff0c;基站通过两路路径将信号发送到终端&#xff0c;因为终端接收到的两路信号都是来自同一天…

【算法与数据结构】63、LeetCode不同路径 II

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;参考【算法与数据结构】62、LeetCode不同路径的题目&#xff0c;可以发现本题仅仅是多了障碍物。我们还…

Kubernetes(K8S)云服务器实操TKE

一、 Kubernetes(K8S)简介 Kubernetes源于希腊语,意为舵手,因为首尾字母中间正好有8个字母,简称为K8S。Kubernetes是当今最流行的开源容器管理平台,是 Google 发起并维护的基于 Docker 的开源容器集群管理系统。它是大名鼎鼎的Google Borg的开源版本。 K8s构建在 Docker …

计算机网络系统结构-2020期末考试解析

【前言】 不知道为什么计算机网络一门课这么多兄弟&#xff0c;这份看着也像我们的学科&#xff0c;所以也做了。 一&#xff0e; 单选题&#xff08;每题 2 分&#xff0c;共 20 题&#xff0c;合计 40 分&#xff09; 1 、当数据由主机 A 发送到主机 B &#xff0c;不参…

回顾2023,展望未来

回顾2023 重拾博客 CSDN博客创建和写作&#xff0c;几乎是和我正式开始学习编程开始&#xff0c;至今已经6年。刚上编程课的时候&#xff0c;刚上C语言课的时候&#xff0c;老师说可以通过写技术博客来帮助自己更好学习&#xff0c;于是我就开始自己的技术博客编写之旅。 我…

架构02 - 架构的基础: 特点,本质...

软件架构简介&#xff1a; 架构是对系统中各个实体以及它们之间关系的抽象描述&#xff0c;是对功能和形式元素之间对应关系的分配&#xff0c;也是对元素之间关系及与周边环境关系的定义。软件架构的核心价值在于控制系统的复杂性&#xff0c;实现核心业务逻辑和技术细节的解耦…

C++I/O流——(1)I/O流的概念

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 勤奋&#xff0c;机会&#xff0c;乐观…

小红书私信组件功能解读,商家如何使用

今年八月&#xff0c;小红书私信组件上新了两大新功能。新功能的出现&#xff0c;无疑为商家与消费者的沟通建联&#xff0c;提供了新的可能。今天我们来针对小红书私信组件功能解读&#xff01; 一、小红书私信组件新功能 这次小红书私信组件上新的两大功能分别是&#xff0c;…

SQL-分页查询and语句执行顺序

&#x1f389;欢迎您来到我的MySQL基础复习专栏 ☆* o(≧▽≦)o *☆哈喽~我是小小恶斯法克&#x1f379; ✨博客主页&#xff1a;小小恶斯法克的博客 &#x1f388;该系列文章专栏&#xff1a;重拾MySQL &#x1f379;文章作者技术和水平很有限&#xff0c;如果文中出现错误&am…

Inis博客系统本地部署结合内网穿透实现远程访问本地站点

文章目录 前言1. Inis博客网站搭建1.1. Inis博客网站下载和安装1.2 Inis博客网站测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 3. 公网访问测试总…

011集:复制txt文件(编码:ANSI复制到UTF-8模式)—python基础入门实例

下面给出一个文本文件复制示例。 代码如下&#xff1a; f_name rD:\mytest2.txt with open(f_name, r, encodinggbk) as f:lines f.readlines()copyfile rD:\copytest2.txtwith open(copyfile, w, encodingutf-8) as copy_f:copy_f.writelines(lines)print(文件复制成功) …

YOLOv8改进 | 二次创新篇 | 在Dyhead检测头的基础上替换DCNv3 (全网独家首发)

一、本文介绍 本文给大家带来的改进机制是在DynamicHead上替换DCNv3模块,其中DynamicHead的核心为DCNv2,但是今年新更新了DCNv3其作为v2的升级版效果肯定是更好的,所以我将其中的核心机制替换为DCNv3给Dyhead相当于做了一个升级,效果也比之前的普通版本要好,这个机制我认…

【2023年度总结与2024展望】---23年故事不长,且听我来讲

文章目录 前言一、学习方面1.1 攥写博客1.2 学习内容1.3 参加比赛获得证书 二、生活方面2.1写周报记录生活 三、运动方面四、CSDN的鼓励五、24年展望总结 前言 时光飞逝&#xff0c;又是新的一年&#xff0c;遥想去年2023年我也同样在这个时间段参加了CSDN举办的年度总结活动&a…

数据库期末复习重点总结

数据库期末复习重点总结 本文为总结&#xff0c;如有不对的地方请指针 第2章 关系模型的介绍 名称符号选择σ投影∏笛卡儿积连接并∪集差-交∩赋值<-更名ρ 除操作 设R和S除运算的结果为T&#xff0c;则T包含所有在R中但不在S中的属性和值&#xff0c;且T的元组与S的元…

数据结构(三)堆和哈希表

目录 哈希表和堆什么是哈希表 &#xff1f;什么是堆 &#xff1f;什么是图 &#xff1f;案例一&#xff1a;使用python实现最小堆案例二 &#xff1a; 如何用Python通过哈希表的方式完成商品库存管理闯关题 &#xff08;包含案例三&#xff1a;python实现哈希表&#xff09; 本…

highlight.js 实现搜索关键词高亮效果

先看效果&#xff1a; 折腾了老半天&#xff0c;记录一下 注意事项都写注释了 代码&#xff1a; <template><div class"absolute-lt wh-full overflow-hidden p-10"><div style"width: 200px"><el-input v-model"keyword"…