网站设计好做吗做网站比较好的软件
web/
2025/9/29 9:32:42/
文章来源:
网站设计好做吗,做网站比较好的软件,免费稳定的网站空间,电子商务成功的网站今天是LeetCode专题第20篇文章#xff0c;今天讨论的是数字组合问题。描述给定一个int类型的候选集#xff0c;和一个int类型的target#xff0c;要求返回所有的数字组合#xff0c;使得组合内所有数字的和刚好等于target。注意#xff1a;所有的元素都是正数所有元素没有…今天是LeetCode专题第20篇文章今天讨论的是数字组合问题。描述给定一个int类型的候选集和一个int类型的target要求返回所有的数字组合使得组合内所有数字的和刚好等于target。注意所有的元素都是正数所有元素没有重复答案不能有重复每一个元素可以使用若干次样例 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位所有拿和不拿的状态了。比如说我们当下遍历到的数字是55的二进制表示是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显然会复杂度爆炸。所以这个方法只是理论上可行但是实际上并不具有可操作性我之所以拿出来介绍纯粹是为了引出二进制表示法。搜索解决一切当一个问题明显有很多种情况需要遍历但是我们又很难直接遍历的时候往往都是搜索问题我们可以思考一下能否用搜索问题的方法来解决。这题其实已经非常明显了搜索的条件已经有了搜索的空间也明白了剩下的就是制定搜索策略。我个人认为搜索策略其实就是搜索的顺序和范围合适的搜索顺序以及范围可以大大降低编码和计算的复杂度再穿插合适的剪枝就可以非常漂亮地完成一道搜索问题。我们带着思考来看这道题如果我们用回溯法来写这道题的话代码其实并不复杂。很容易就可以写出来你看只有几行我们每次遍历一个数加在当前的总和x上然后往下递归并且我们还加上了对当前和判断的剪枝。如果当前和已经超过了target那么显然已经不可能构成正解了我们直接跳过。但是我们也都发现了在上面这段代码里我们搜索的区间就是所有的候选值我们没有对这些候选值进行任何的限制。这其实隐藏了一个很大的问题还记得题目的要求当中有一条吗答案不能有重复。也就是说相同元素的不同顺序会被认为是同一个解我们需要去重。举个例子[3, 2, 2]和[2, 2, 3]会被认为是重复的但是在上面的搜索策略当中我们没有对这个情况做任何的控制这就导致了我们在找到所有答案之后还需要进行去重的工作。先找到包含重复的答案再进行去重这显然会消耗大量计算资源所以这个搜索策略虽然简单但远远不是最好的。我们先来分析一下问题究竟什么时候会出现重复呢我想大家列举一下应该都能发现就是当我们顺序错乱的时候。比如说我们有两个数3和4我们先选择3再选择4和先选择4再选择3是一样的。如果我们不对3和4的选择做任何限制那么就会出现重复。换句话说如果我们对3和4的选择确定顺序就可以避免重复如果从一开始就不会出现重复那么我们也就没有必要去重了这就可以节省下大量的时间。所以我们要做的就是确定搜索的时候元素选择的顺序在搜索的时候进行一定的限制从而避免重复。落实在代码当中就体现在我们枚举候选集的时候我们之前没有做任何限制我们现在需要人为加上限制我们只能选择之前选过的元素后面的只能往后拿不能往前拿。所以我们需要在dfs当中传入一个下标标记之前拿过的最大的下标我们只能拿这个下标之后的这样搜索就有了顺序就避免了元素重复和复杂度过高的问题。这一点确定了之后剩下的代码就很简单了。从代码上来看我们并没有做太大的改动所有的细节几乎都体现在搜索和遍历时的边界以及控制条件上。和整个算法以及代码逻辑比起来这些是最无关紧要的但是对于解决问题来说这些才是实实在在的。题目变形今天的题目有一个变种它就是LeetCode的第40题大部分题意都一样只有两个条件发生了变化。第一是40题当中去掉了候选集当中的元素没有重复的限制第二点是不再允许元素重复使用。其他的内容都和这题保持一致。我们想一下就会发现如果我们去掉重复使用的条件好像没什么变化我们是不是只要将递归遍历的条件稍稍改动就好了呢之前我们是从pos位置开始化后遍历现在由于不能重复所以之前取过的pos不能再取我们是不是只要将for循环改成从pos1开始就行了如果候选集的元素中没有重复这当然是可行的。但是很遗憾这个条件也被去掉了。所以候选集当中本身就可能出现重复如果还按照刚才的思路会出现重复的答案。原因也很简单举个例子比如说候选集是[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 pos1 and candidates[i] candidates[i-1]的时候则跳过。其中pos是上次选择的位置在递归的起始时带入的是-1我想这个条件应该大家都能看明白但是它为什么有效可能会一头雾水翻译成大白话这个条件其实是在限制一点在有多个相同元素出现的时候必须选择下标小的也就是前面的。我们分析一下可能触发continue的条件只有两种情况第一种其中pos是上次选择的数字我们假设它是1我们当前的位置在pos3。从上图可以看出来pos1到pos3全都相等。如果我们想要选择pos3而跳过pos1和pos2则会进入continue会跳过。原因也很简单因为前面递归的过程当中已经选过pos和pos1的组合了我们如果选了pos和pos3的组合一定会构成重复。也就是说我们保证了在连续出现的元素当中如果要枚举的话必须要从第一个开始。另一种情况也类似也就是说从pos到pos3都是2都相等这个时候我们跳过pos1和pos2直接选择pos3也会进入continue原因也很简单我们现在枚举的是获取两个2的情况在之前的递归当中已经没举过pos和pos1了我们现在想要跳过pos1和pos2直接获取pos3对应的情况是一样的所以需要跳过。我们将排序和上述的剪枝方法一起使用就解出了本题仔细观察一下会发现这两个方法根本是相辅相成天作之合单独使用哪一个也不管用但是一起作用就可以非常简单地解出题目。理解了这两点之后代码就变得很简单了不知道大家有没有从这个变种当中感受到搜索策略以及剪枝的威力和巧妙我个人还蛮喜欢今天的题目的如果能够把今天的两道题目吃透我想大家对于深度优先搜索和回溯算法的理解一定可以更上一个台阶这也是我将这两个问题合在一起介绍的原因。在明天的LeetCode专题当中我们会来看LeetCode41题查找第一个没有出现的自然数。今天的文章就到这里如果觉得有所收获请顺手点个关注吧你们的举手之劳对我来说很重要。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/83821.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!