搜索重复代码_LeetCode专题——详解搜索算法中的搜索策略和剪枝

今天是LeetCode专题第20篇文章,今天讨论的是数字组合问题。

描述

给定一个int类型的候选集,和一个int类型的target,要求返回所有的数字组合,使得组合内所有数字的和刚好等于target。

注意:

  1. 所有的元素都是正数
  2. 所有元素没有重复
  3. 答案不能有重复
  4. 每一个元素可以使用若干次

样例 1:

Input: candidates = [2,3,6,7], target = 7,A solution set is:[  [7],  [2,2,3]]

样例 2:

Input: candidates = [2,3,5], target = 8,A solution set is:[   [2,2,2,2],  [2,3,3],  [3,5]]

题解

我们拿到这道题还是按照老规矩来思考暴力的解法,但是仔细一想会发现好像没有头绪,没有头绪的原因也很简单,因为题目当中的一个条件:一个元素可以随意使用若干次

我们根本不知道一个元素可以使用多少次,这让我们的暴力枚举有一种无从下手的感觉。如果去掉这个条件就方便多了,因为每个元素只剩下了两个状态,要么拿要么不拿,我们可以用一个二进制的数来表示。这就引出了一个常用的表示状态的方法——二进制表示法

二进制表示法

举个例子,假如当下我们有3个数字,这3个数字都有两个状态选或者不选,我们想要枚举这3个数字的所有状态,应该怎么办?

我们当然可以用递归来实现,在每层递归当中做决策当前元素选或者不选,分别递归。但是可以不用这么麻烦,我们可以用二进制简化这个过程。这个原理非常简单,我们都知道在计算机二进制当中每一个二进制位只有两个状态0或者1,那么我们就用1表示拿,0表示不拿,那么这三个数拿或者不拿的状态其实就对应一个二进制的数字了。3位二进制,对应的数字是0到7,也就是说我们只需要遍历0到7,就可以获得这3位所有拿和不拿的状态了。

比如说我们当下遍历到的数字是5,5的二进制表示是101,我们再把1和0对应拿和不拿两种状态。那么5就可以对应上第一和第三个拿,第二个不拿的状态了。我们可以用位运算很方便地进行计算。比如我们要判断第i位是否拿了,我们可以用(1 << i),<左移n位就相当于乘上了2的n次方。1对应右边起第0位,也就是最低位的二进制位,我们对它做左移i的操作就相当于乘上了 2^i ,那么就得到了第i位了。我们拿到了之后,只需要将它和状态state做一个二进制中的与运算,就可以得到state中第i位究竟是0还是1了。

因为在二进制当中,and运算会将两个数的每一位做与运算,运算的结果也是一个二进制数。由于我们用来进行与运算的数是(1 << i),它只有第i位为1,所以其他位进行与运算的结果必然是0,那么它和state进行与运算之后,如果结果大于0,则说明state的第i位也是1,否则则是0。这样我们就获取了state当中第i位的状态。

由于位运算是指令集的运算,在没有指令集优化的一些语言当中,它的计算要比加减乘除更快。除了快以外它最大的好处是节省空间和计算方便,这两个优点其实是一体的,我们一个一个来说。

首先来说节省空间,有了二进制表示之后,我们可以用一个32位的int来代表32个物体的0和1的所有状态。如果我们用数组来存储的话,显然我们需要一个长度为32的数组,需要的空间要大得多。这一点在单个状态下并不明显,一旦数据量很大会非常显著。尤其是在密集的IO当中,数据越轻量则传输效率越高

第二个优点是计算方便,计算方便的原因也很简单,假如我们要遍历所有的状态,如果用数组或者其他数据结构的话免不了使用递归来遍历,这样会非常麻烦。而使用二进制之后就方便了,由于我们用二进制表示了所有元素0和1的状态,我们只需要在一个整数范围做循环就可以了。就像刚才例子当中,我们要枚举3个元素的状态,我们只需要从0遍历到7即可。如果在多点元素也没问题,如果是N个元素,我们只需要从0遍历到(1 << N) - 1。

但是还有一个问题没解决,你可能会说如果我们用int来表示状态的话,最多只能表示32个物品的状态,如果更多怎么办?一个方法是使用int64,即范围更大的int,如果范围更大的int还是解决不了问题也没关系,还有一些基于同样原理实现的第三方包可以支持。但是老实说我们基本上不会碰到超过64个物品让我们枚举所有状态的情况,因为这个数字已经非常大了,几乎可以说是天荒地老也算不完。

回到问题

我相信关于二进制表示法的使用和原理,大家应该都了解了,但是本题当中元素是可以多次出现的,二进制表示法看起来并不顶用,我们怎么解决这个问题呢?难道这么大的篇幅就白写了?

当然不会白写,针对这种情况也有办法。其实很简单,因为题目当中规定所有的元素都是正数,那么对于每一个元素而言,我们最多取的数量是有限的。举个例子,比如样例当中[2, 3, 6, 7] target是7,对于元素2而言,target是7,即使可以多次使用,也最多能用上3个2。那么我们可以拓充候选集,将1个2拓充成3个,同理,我们可以继续拓充3,最后候选集变成这样:[2, 2, 2, 3, 3, 6, 7],这样我们就可以使用二进制表示法了。

但是显然这个方法不靠谱,因为如果数据当中出现一个1,并且target稍微大一些,那肯定直接gg,显然会复杂度爆炸。所以这个方法只是理论上可行,但是实际上并不具有可操作性,我之所以拿出来介绍,纯粹是为了引出二进制表示法。

搜索解决一切

当一个问题明显有很多种情况需要遍历,但是我们又很难直接遍历的时候,往往都是搜索问题,我们可以思考一下能否用搜索问题的方法来解决。

这题其实已经非常明显了,搜索的条件已经有了,搜索的空间也明白了,剩下的就是制定搜索策略

我个人认为搜索策略其实就是搜索的顺序和范围,合适的搜索顺序以及范围可以大大降低编码和计算的复杂度,再穿插合适的剪枝,就可以非常漂亮地完成一道搜索问题。

我们带着思考来看这道题,如果我们用回溯法来写这道题的话,代码其实并不复杂。很容易就可以写出来:

b3e31564aa1be2eb56b939179ee7c7c5.png

你看只有几行,我们每次遍历一个数加在当前的总和x上然后往下递归,并且我们还加上了对当前和判断的剪枝。如果当前和已经超过了target,那么显然已经不可能构成正解了,我们直接跳过。

但是我们也都发现了,在上面这段代码里,我们搜索的区间就是所有的候选值,我们没有对这些候选值进行任何的限制。这其实隐藏了一个很大的问题,还记得题目的要求当中有一条吗,答案不能有重复。也就是说相同元素的不同顺序会被认为是同一个解,我们需要去重。举个例子,[3, 2, 2]和[2, 2, 3]会被认为是重复的,但是在上面的搜索策略当中,我们没有对这个情况做任何的控制,这就导致了我们在找到所有答案之后还需要进行去重的工作。先找到包含重复的答案,再进行去重,这显然会消耗大量计算资源,所以这个搜索策略虽然简单,但远远不是最好的。

我们先来分析一下问题,究竟什么时候会出现重复呢?

我想大家列举一下应该都能发现,就是当我们顺序错乱的时候。比如说我们有两个数3和4,我们先选择3再选择4和先选择4再选择3是一样的。如果我们不对3和4的选择做任何限制,那么就会出现重复。换句话说如果我们对3和4的选择确定顺序就可以避免重复,如果从一开始就不会出现重复,那么我们也就没有必要去重了,这就可以节省下大量的时间。

所以我们要做的就是确定搜索的时候元素选择的顺序,在搜索的时候进行一定的限制,从而避免重复。落实在代码当中就体现在我们枚举候选集的时候,我们之前没有做任何限制,我们现在需要人为加上限制,我们只能选择之前选过的元素后面的,只能往后拿不能往前拿。所以我们需要在dfs当中传入一个下标,标记之前拿过的最大的下标,我们只能拿这个下标之后的,这样搜索就有了顺序,就避免了元素重复和复杂度过高的问题

这一点确定了之后,剩下的代码就很简单了。

21ddcc8ed487b7e7c1d5597dcf9fca77.png

从代码上来看,我们并没有做太大的改动,所有的细节几乎都体现在搜索和遍历时的边界以及控制条件上。和整个算法以及代码逻辑比起来,这些是最无关紧要的,但是对于解决问题来说,这些才是实实在在的。

题目变形

今天的题目有一个变种,它就是LeetCode的第40题,大部分题意都一样,只有两个条件发生了变化。第一是40题当中去掉了候选集当中的元素没有重复的限制,第二点是不再允许元素重复使用。其他的内容都和这题保持一致。

我们想一下就会发现,如果我们去掉重复使用的条件,好像没什么变化,我们是不是只要将递归遍历的条件稍稍改动就好了呢?之前我们是从pos位置开始化后遍历,现在由于不能重复,所以之前取过的pos不能再取,我们是不是只要将for循环改成从pos+1开始就行了?

如果候选集的元素中没有重复,这当然是可行的。但是很遗憾,这个条件也被去掉了。所以候选集当中本身就可能出现重复,如果还按照刚才的思路会出现重复的答案。

原因也很简单,举个例子,比如说候选集是[1, 2, 3, 2, 2],target是5,如果还用刚才的方法搜索的话,我们的答案当中会出现两个[2, 3]。虽然我们也是每个元素都只用了一次,但是仍然违背了答案不能重复的限制。

你可能会有很多想法,比如可以手动去重,比如我们可以在元素数量上做手脚,将重复的元素去重。很遗憾的是,两者都不是最优解。第一种当然是可行的,找到所有可行解再去重,是一个很朴素的思路。通过优化,可以解决复杂度问题。第二种想法并不可行,因为如果我们把重复的元素去掉,可能会导致某些解丢失。比如[1, 2, 2],也是和等于5,但是如果我们把重复的2去掉了,那么就无法得到这个解了。

要解决问题,我们还是要回到搜索策略上来。手动筛选、加工数据只是逼不得已的时候用的奇淫技巧,搜索策略才是解题的核心

我们整理一下思路,可以归纳出当前需要我们解决的问题有两个,第一个是我们要找到所有解,意味着我们不能删减元素,第二个是我们想要搜索的结果没有重复。这看起来是矛盾的,我们既想要不出现重复,又想重复的元素可以出现,这可能吗?

如果你仔细思考分析了,你会发现是可能的。不过从搜索策略的角度上来说,比较难想到。首先我们要保证元素的聚集性,也就是说相同的元素应该聚集在一起。要做到这点很简单,我们只需要排序就行了。这么做的原因也不难想到,就是为了避免重复。如果数据是分散的,我们是很难去重的,还用刚才的例子,当我们从2开始递归的时候,我们可以找到解[2, 3],当我们从3开始递归的时候,我们仍然可以找到解[3, 2],这两者是一样的。虽然我们限制了遍历的顺序严格地从前到后,但是由于元素分散会使得我们的限制失去作用。为了限制依旧有效,我们需要排序,让相同的元素聚集,这样我们每次搜索的内容其实是由大于等于当前元素的数字组成的答案,这就保证了不在重复。

但是这并没有解决所有的问题,我们再来看一个例子,候选集是[2, 2, 2, 3, 4],target是7,显然[2, 2, 3]是答案,但是我们怎么保证[2, 2, 3]只出现一次呢?因为我们有3个2,但是要选出两个2来,我们需要一个机制,使得只会找到一个答案。这点通过策略已经无能为力了,只能依靠剪枝。我们当然可以引入额外的数据结构解决问题,但会比较麻烦,而我们其实有更简单的做法。

这个做法是一个非常精妙的剪枝,我们在递归当中加入一个判断:当i > pos+1 and candidates[i] == candidates[i-1]的时候,则跳过。其中pos是上次选择的位置,在递归的起始时,带入的是-1,我想这个条件应该大家都能看明白,但是它为什么有效可能会一头雾水,翻译成大白话,这个条件其实是在限制一点:在有多个相同元素出现的时候,必须选择下标小的,也就是前面的。

我们分析一下可能触发continue的条件,只有两种情况,第一种:

7de3af89f338d1b4ccb58fae6fff7208.png

其中pos是上次选择的数字,我们假设它是1,我们当前的位置在pos+3。从上图可以看出来,pos+1到pos+3全都相等。如果我们想要选择pos+3而跳过pos+1和pos+2则会进入continue会跳过。原因也很简单,因为前面递归的过程当中已经选过pos和pos+1的组合了,我们如果选了pos和pos+3的组合一定会构成重复。也就是说我们保证了在连续出现的元素当中,如果要枚举的话,必须要从第一个开始。

另一种情况也类似:

4330d594e58c398aa4b0c50d72b36245.png

也就是说从pos到pos+3都是2,都相等,这个时候我们跳过pos+1和pos+2直接选择pos+3也会进入continue,原因也很简单,我们现在枚举的是获取两个2的情况,在之前的递归当中已经没举过pos和pos+1了,我们现在想要跳过pos+1和pos+2直接获取pos+3,对应的情况是一样的,所以需要跳过。

我们将排序和上述的剪枝方法一起使用就解出了本题,仔细观察一下会发现这两个方法根本是相辅相成,天作之合,单独使用哪一个也不管用,但是一起作用就可以非常简单地解出题目。理解了这两点之后,代码就变得很简单了:

9f0dafd047f768c57bb3070e1d2aedec.png

不知道大家有没有从这个变种当中感受到搜索策略以及剪枝的威力和巧妙,我个人还蛮喜欢今天的题目的,如果能够把今天的两道题目吃透,我想大家对于深度优先搜索和回溯算法的理解一定可以更上一个台阶,这也是我将这两个问题合在一起介绍的原因。在明天的LeetCode专题当中我们会来看LeetCode41题,查找第一个没有出现的自然数。

今天的文章就到这里,如果觉得有所收获,请顺手点个关注吧,你们的举手之劳对我来说很重要。

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

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

相关文章

Unity内实现Android APK版本更新

最近做项目有个需求是要Android应用内强更包体。Google一波直接选了一个android原生开源项目 GitHub - yangchong211/YCUpdateApp: 轻量级版本更新弹窗&#xff0c;弹窗上支持更新进度条&#xff0c;可以设置普通更新或者强制更新。解决8.0以上通知栏不显示问题&#xff0c;解…

a标签隐藏真实地址_家庭影院布线非常杂乱应该怎么补救?A/V电缆管理的7个技巧...

家庭影院应该是给人带来快乐的东西&#xff0c;不管是声音还是外观&#xff0c;但是如果您布的线很杂乱&#xff0c;那么想必整个影院也好看不到哪去&#xff0c;如果你想要整理的话&#xff0c;那么这篇文章应该能帮助到您。有没有整理过家庭影院的电线&#xff0c;观感是完全…

螺钉装弹垫平垫机器人_【经验总结】什么时候用平垫,什么时候用弹垫?

很多人为了节约成本想省了平垫或者弹垫&#xff0c;其实在螺栓使用过程中平垫和弹垫各自起着不可或缺的作用。今天咱们来针对平垫和弹垫给大家介绍一下。左 平 垫 右 弹 垫 平垫&#xff0c;形状一般是一个平垫圈&#xff0c;中间有一个孔&#xff0c;主要是用铁板冲压出来的&a…

生成FaceBook所需的散列哈希值

Hex to base64 converter FaceBook后台发布应用时&#xff0c;需要填入hash值&#xff0c;今天自己用openssl生成的时候只有24位并不正确&#xff0c;与其费劲巴拉的自己折腾openssl&#xff0c;还是感谢国外的大佬们吧&#xff0c;工具网址直接输入hex的sha1值*&#xff08;从…

打开方式中选择默认方式无反映_「Windows」得看,更改文件的默认应用,告别“打开方式”...

前言&#xff1a;你有没有在使用电脑过程中&#xff0c;打开各种各样格式的文件时&#xff0c;电脑有没有很准确的用你想用的那个软件将其打开&#xff1f;还是说你还在用“打开方式”手动选择你想使用的软件打开你想打开的这个文件。如何设置默认应用&#xff1a;步骤&#xf…

Z深度相关知识

渲染中深度信息很重要&#xff0c;但是也很让人迷惑&#xff0c;透视投影是什么&#xff0c;为什么要做透视除法&#xff0c;view空间&#xff0c;clip空间&#xff0c;ndc空间对应的z值又代表什么&#xff0c;这里简单总结下。 一.顶点变换的完整过程 二.View空间下的顶点和Z…

quartus管脚分配后需要保存吗_掉电保存数据到EEPROM

我想在掉电时保存数据(3 个字节)到 EEPROM 中&#xff0c;用 BOD掉电检测&#xff0c;不知怎样使用。望高手指点&#xff1a;1. 在 BOOT 区设置好 BODEN&#xff0c;BODLEVEL&#xff0c;后软件还要怎样设置&#xff1f;2. 掉电中断是否是产生复位&#xff1f;我的写 EEPROM 程…

matlab 最小二乘法拟合_计量与论文串讲:最小二乘法

点为什么要用R计量经济学作为经济大类下一门非常实用的学科有很强的实践意义。尽管内容庞杂但基本的指导思想却很简单&#xff0c;因此通过一门统计编程语言学习、掌握计量经济学有很大的益处。目前&#xff0c;市面上已经有多款统计编程语言和统计软件&#xff0c;实证分析领域…

ZoomBlur 聚焦模糊效果Shader(URP)

修改自这篇文章【Unity】UniversalRPでカスタムポストプロセスを作る【ZoomBlur】 - Qiita 1. VolumeEditor&#xff0c;用于在UnityVolume中控制自己写的后处理效果 using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal;[System.Seriali…

魅蓝s6启动android密码_魅蓝s6怎么恢复出厂设置?忘记密码怎么办

魅蓝s6怎么恢复出厂设置&#xff1f;忘记密码怎么办&#xff1f;手机是现代生活必不可少的工具之一&#xff0c;在使用时我们常常会忘记手机密码。本次就给大家介绍手机密码忘了怎么恢复出厂设置&#xff0c;快来看看吧。我们知道手机恢复出厂设置的注意事项最重要的就是做好手…

漫游飞行_魔兽世界:德拉诺时光周 冲声望解锁德拉诺飞行好时机

虽然德拉诺飞行开放已经是6.22版本的事情了&#xff0c;但是目前还是有些玩家还没有解锁。但是本周德拉诺时光周的开放&#xff0c;获取德拉诺飞行声望将会更加简单。德拉诺飞行声望德拉诺飞行解锁需要先知之手(部落是沃金之锋)、觉醒教派和刃牙追猎者三个声望达到崇敬。而在本…

python怎么解释语言_python是解释型语言吗

Python 是解释型的语言吗&#xff1f;它会被编译吗&#xff1f; 这个问题没有想象中那么好回答。和很多人认识世界一样&#xff0c;习惯以一个简单的模型去评判一些事物。而事实上&#xff0c;里面包含了很多很多的细节。通常的说法&#xff0c;编译代表着将一个高级语言转化为…

第一次失效_神兵小将:净化之力失效地魔兵兽,全靠特殊办法,铁心方式真霸气...

神兵小将&#xff1a;净化之力失效地魔兵兽&#xff0c;全靠特殊办法&#xff0c;铁心方式真霸气。在经典动漫神兵小将中&#xff0c;魔兵兽在漫迷眼中属于比较特殊的存在&#xff0c;很多人气指数超高的神兵兽被魔化后却成为阻碍问天前进的高山。因此魔兵兽在很大意义上讲也是…

Python 每日定时查询数据库生成Excel报表,并群发邮件

最近在做游戏打点数据的一些统计处理&#xff0c;写了个Python脚本完成每日定时自动查询生成Excel报表并群发邮件的小功能。 拆解几个需求点&#xff1a; 一.连接数据库并查询 以下是一个查询总注册人数的示例代码&#xff0c;host port db user password等填入自己数据库的…

com口驱动_Ubuntu 安装Nvidia显卡驱动指南

该文档适用于&#xff1a; Ubuntu 14/16/18 三个版本。Nvidia显卡驱动适用于&#xff1a;RTX2080TI/RTX2080/RTX2070/GTX1080TI/GTX1080/GTX1070以及更低级别显卡。本文档旨在帮助大家解决安装Nvidia显卡遇到的常见问题。Nvidia驱动下载地址&#xff1a;https://www.geforce.co…

Unity URP中根据深度重建世界坐标

通过深度值重建世界坐标&#xff0c;可以做出很多有意思的后处理效果&#xff0c;先实现下度值重建世界坐标这个功能。 一.验证重建效果 首先&#xff0c;得先找到一种证明反推回世界空间位置正确的方法。在相机前摆放几个物体&#xff0c;尽量使之在世界坐标下的位置小于1&a…

dubbo yml配置_Spring boot 的profile功能如何实现多环境配置自动切换

通常服务端应用开发需要经过以下几个流程&#xff1a;开发 -> 测试 -> RC验证 -> 上线这就涉及到四个不同的环境&#xff0c;开发环境、测试环境、RC环境以及生产环境&#xff0c;为了避免不同环境之间相互干扰&#xff0c;通常需要独立部署数据库、缓存服务器等&…

Unity中的SystemInfo.deviceUniqueIdentifier 唯一ID

做游戏时可能经常使用SystemInfo.deviceUniqueIdentifier作为用户的唯一ID进行注册登录&#xff0c; 但是你会发现从谷歌商店上下载的自己游戏&#xff0c;和自己从Unity工程中直接打包出来的游戏账号竟然是不一致的&#xff01; 这个坑还是很坑爹的&#xff0c;纠其原因是Sy…

sketchup生成面域插件_独家教程 | 快速抓取“高精准”场地信息,康石石教你生成不同“体量”地形...

无论建筑设计还是景观设计&#xff0c;同学们的设计项目都必须依托于场地来进行&#xff0c;通过分析场地的区位范围、地形地势&#xff0c;结合场地的局限性与可能性&#xff0c;才能进一步展开项目设计。可以说&#xff0c;获取场地信息是同学们在作品集创作中最重要的环节之…

Unity URP高度雾效果Shader

实现原理 见这篇文章Unity Shader-深度相关知识总结与效果实现&#xff08;LinearDepth&#xff0c;Reverse Z&#xff0c;世界坐标重建&#xff0c;软粒子&#xff0c;高度雾&#xff0c;运动模糊&#xff0c;扫描线效果&#xff09;_puppet_master的专栏-CSDN博客_shader深度…