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

题目:

我们有 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都不一样,因此每一轮都需用统计targetfor (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都不一样,因此每一轮都需用统计targetfor (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.mzph.cn/news/615456.shtml

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

相关文章

vivado IP Revision Control

2020.2 只需要git 管理 prj.xpr 和 prj.srcs/ https://china.xilinx.com/video/hardware/ip-revision-control.html https://www.xilinx.com/video/hardware/vivado-design-suite-revision-control.html

每日一题:LeetCode-LCR 007. 三数之和

每日一题系列&#xff08;day 18&#xff09; 前言&#xff1a; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f50e…

解决chromebook kabylake安装linux没有声音问题

chromebook kabylake安装arch没有声音&#xff0c;好长时间没有解决&#xff0c;一直用的蓝牙耳机。 今天搜搜帖子解决了&#xff0c; 分享供参考 git clone https://github.com/eupnea-project/chromebook-linux-audiocd chromebook-linux-audio ./setup-audio提示 I Underst…

Shell编程--数组

数组 1.定义数组1.1.普通数组1.2.关联数组1.3.数组常用定义方法 2.使用数组shell数组中"*" 和 "" 区别 1.定义数组 数组也是一种变量&#xff0c;常规变量只能保存一个值&#xff0c;数组可以保存多个值 关联数组跟普通数组的区别&#xff1a; 普通数组&a…

linux高级篇基础理论十一(GlusterFS)

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a; 小刘主页 ♥️不能因为人生的道路坎坷,就使自己的身躯变得弯曲;不能因为生活的历程漫长,就使求索的 脚步迟缓。 ♥️学习两年总结出的运维经验&#xff0c;以及思科模拟器全套网络实验教程。专栏&#xff1a;云计算技…

VS中动态库的创建和调用

VS中动态库的创建和调用 库 ​ 库是写好的现有的&#xff0c;成熟的&#xff0c;可以复用的代码。库的存在形式本质上来说库是一种可执行代码的二进制。 ​ 库有两种&#xff1a;静态库&#xff08;.a、.lib&#xff09;和动态库&#xff08;.so、.dll&#xff09;。所谓静态…

一键操作完整的部署项目流程

目录 一、常见的搭配 二、完整的部署项目流程具体步骤 2.1、安装jdk 安装jdk &#xff1a; 配置环境&#xff1a; 检查是否成功&#xff1a; 2.2、配置tomcat外部访问 下载解压软件 安装tomcat 测试tomcat安装是否成功 2.3、安装MySQL 安装vcc环境 命令输入步骤 安…

应用在热能表领域中的数字温度传感芯片

热能表&#xff0c;是适用于测量在热交换环路中&#xff0c;被称作载热液体的液体所吸收或转换热能的仪器&#xff0c;它由流量传感器、温度传感器和热能积算仪三部分组成。热量表&#xff08;热表&#xff09;又称热能表、热能积算仪&#xff0c;既能测量供热系统的供热量又能…

多模态推荐系统综述:二、特征交互 Fusion

二、Fusion 融合不同的多模态信息&#xff0c;与bridge相比&#xff0c;融合更关注项目之间的多模态内部关系。 它可以灵活地融合不同权重和焦点的多模态信息。 注意机制是应用最为广泛的特征融合。 2.1 粗粒度注意力。 一些模型应用注意力机制在粗粒度级别融合来自多种模式…

ES6笔记总结

首先我们需要了解一下什么是 ECMA&#xff1a; ECMA&#xff08;European Computer Manufacturers Association&#xff09;中文名称为欧洲计算机制造商协会&#xff0c;这 个组织的目标是评估、开发和认可电信和计算机标准。1994 年后该组织改名为 Ecma 国际 什么是 ECMAScr…

美易官方《美股一哥争夺战白热化:微软市值一度超越苹果》

随着科技股的持续走高&#xff0c;美股一哥争夺战也愈发激烈。微软和苹果这两家科技巨头之间的市值争夺战更是吸引了全球市场的关注。上周苹果接连被两家华尔街机构下调评级后&#xff0c;微软的市值已经在逼近苹果&#xff0c;挑战苹果的美股最高市值公司地位。 微软和苹果都…

[深度学习]Open Vocabulary Object Detection 部署开放域目标检测模型使用感受

一、Open Vocabulary Object Detection介绍 Open Vocabulary Object Detection (OpenVOD) 是一种新型的目标检测方法&#xff0c;它使用开放词汇的概念来识别和检测图像中的对象。与传统的目标检测方法相比&#xff0c;OpenVOD具有更高的灵活性和可扩展性&#xff0c;因为它允…

K8S集群重新初始化--详细过程

K8S集群重新初始化 0、当前环境1、master节点1.1、在master节点执行下面reset命令&#xff1a;1.2、手动清除配置信息&#xff0c;这一步很关键&#xff1a;1.3、重新引导集群1.4、创建配置目录&#xff0c;并复制权限配置文件到用户目录下&#xff1a;1.5 查看集群状态1.6 安装…

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

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

Wargames与bash知识10

Wargames与bash知识10 Bandit Level 17 关卡提示&#xff1a; 主目录中有两个文件&#xff1a;passwords.old和passwords.new。下一级的密码在passwords.new中&#xff0c;是在password.old和password.new之间唯一被更改的行 注意&#xff1a;如果你已经解决了这个级别&…

GEE查看SMAP的L3级土壤水分产品并导出为TIFF

SMAP的L3级产品&#xff0c;时间分辨率为每日&#xff0c;空间分辨率为9KM&#xff0c;到2023年12月2日停止提供。 查看逐日的土壤水分变化 // 设置感兴趣区域&#xff08;Region of Interest&#xff09; var roi ee.FeatureCollection(projects/a-flyllf0313/assets/dacha…

【Spring Cloud】微服务架构演变及微服务架构介绍

文章目录 系统架构演变单体应用架构垂直应用架构分布式架构SOA 架构微服务架构 微服务架构介绍微服务架构的常见问题微服务架构的常见概念服务治理服务调用服务网关服务容错链路追踪 微服务架构的常见解决方案ServiceCombSpringCloudSpring Cloud Alibaba 总结 欢迎来到阿Q社区…

C++ 类访问修饰符

数据封装是面向对象编程的一个重要特点&#xff0c;它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。关键字 public、private、protected 称为访问修饰符。 一个类可以有多个 public、protected…

探索web技术与低代码开发的融合应用

随着物联网、云计算和人工智能等技术的迅猛发展&#xff0c;现代软件开发正面临着日益增长的需求和复杂性。为了应对这一挑战&#xff0c;一种被称为低代码开发的快速、可视化开发方法逐渐崭露头角。本文将探讨低代码开发与web技术的融合应用&#xff0c;以及这种趋势对软件开发…

答题小程序源码系统:自带流量主广告位+视频激励广告 带完整的代码安装包以及搭建教程

随着互联网的迅速发展&#xff0c;各种应用程序层出不穷&#xff0c;而答题类小程序由于其独特的互动性和吸引力&#xff0c;成为了当前最热门的应用之一。答题小程序源码系统是一款基于微信小程序开发的源代码系统&#xff0c;它具有丰富的功能和灵活的定制性&#xff0c;可以…