代码随想录DAY27 |

目录

39. 组合总和

self分析

代码随想录

总结:

40. 组合总和 II

分析

去重详解

去重的两种方法

复杂度分析

131. 分割回文串

分析

切割

判断是否是回文子串?

39. 组合总和

 代码随想录 (programmercarl.com)

self分析

无限重复就以为着不需要剪枝操作,只需要写好终止条件,就是累计和 sum 大于 target

存储一个答案的容器,vector<int>  ,总答案vector<vector<int>>

这样一顿操作后,出现去重问题。——脑子里面只有容器

 那这样就是保存结果容器的问题。——但是这样的结果作为返回值总是觉得不太合适

代码随想录

本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window)216.组合总和III (opens new window)

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合

对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历

总结:

去重只想到容器,但是排序往往可以简单快速重复的问题

排序后,剪枝操作就很有必要。

组合问题的 startIndex 使用问题

剪枝 与 未剪枝 的回溯的区别: 剪枝只是进入递归的数量减少了而已

40. 组合总和 II

分析

排序 + startIndex

在此处我默认的是:排序之后,加上startIndex就不会出现重复的现象,但是排序并不能去重,排序本质上是:按照顺序把相同的放在一起

 上图中的 sum +candidates[i] <= target 放在进入for循环的条件中,这就表示新的树上数值不需要进行,多余的计算,减少回溯的次数。

sum > target 这个条件其实可以省略,因为和在递归单层遍历的时候,会有剪枝的操作,下面会介绍到。

去重详解

我把所有组合求出来,再用set或者map去重,这么做很容易超时!

所以要在搜索的过程中就去掉重复组合。

这个去重为什么很难理解呢,所谓去重,其实就是使用过的元素不能重复选取。 这么一说好像很简单!

都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。

回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。

所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重

去重的两种方法

是否使用过,也就是是否被加入sum中,当回溯弹出的时候,也就是没有被使用过,这是一个很重要的解决方法,很巧妙,举重若轻。 

 

这里直接用startIndex来去重也是可以的, 就不用used数组了

复杂度分析

131. 分割回文串

分析

如何确定是同一个回文子串,原字符串 == reverse后的字符串,此处需要考虑一下reverse的时间复杂度。(以前做过的题忘记完了,真的会谢)

树的结构进行遍历。使用回溯法,每次递归调用函数都是一个 for循环

回溯函数终止条件 - 结果情况的总结:产生了一个回文子串,传入答案,没有产生并且已经执行到字符串末尾,就回溯

问题分解

1. 切割 - 切割点不能重叠  2. 判断回文子串 - 双指针

切割

每次传入一个字符就判断是否是回文子串 ——这种现象是仅仅是start == i  的情况下

当每层树枝的for循环进入的一个元素弹出path之后,当第二个元素进入后,直接每层树枝开始的下标start 和   当前第二个元素的下标 i ,切片字符串,进行判断是否是回文子串。

上面的情况是通过 debug 获得的 ,那么问题就是 切割点究竟在哪里 。

—— 每层的start 与 i 就是 切割块,i 就是切割点。

判断是否是回文子串?

如何更高效的计算一个子字符串是否是回文字串。上述代码isPalindrome函数运用双指针的方法来判定对于一个字符串s, 给定起始下标和终止下标, 截取出的子字符串是否是回文字串。但是其中有一定的重复计算存在:

例如给定字符串"abcde", 在已知"bcd"不是回文字串时, 不再需要去双指针操作"abcde"而可以直接判定它一定不是回文字串。

具体来说, 给定一个字符串s, 长度为n, 它成为回文字串的充分必要条件是s[0] == s[n-1]s[1:n-1]是回文字串。

大家如果熟悉动态规划这种算法的话, 我们可以高效地事先一次性计算出, 针对一个字符串s, 它的任何子串是否是回文字串, 然后在我们的回溯函数中直接查询即可, 省去了双指针移动判定这一步骤

void computePalindrome(const string& s) {
        // isPalindrome[i][j] 代表 s[i:j](双边包括)是否是回文字串 
        isPalindrome.resize(s.size(), vector<bool>(s.size(), false)); // 根据字符串s, 刷新布尔矩阵的大小
        for (int i = s.size() - 1; i >= 0; i--) { 
            // 需要倒序计算, 保证在i行时, i+1行已经计算好了
            for (int j = i; j < s.size(); j++) {
                if (j == i) {isPalindrome[i][j] = true;}
                else if (j - i == 1) {isPalindrome[i][j] = (s[i] == s[j]);}
                else {isPalindrome[i][j] = (s[i] == s[j] && isPalindrome[i+1][j-1]);}
            }
        }
    }

为什么嵌套的for循环里面 j == i  ?

这块好像是动态规划的问题,等到时候看一下

vector容器的 resize函数

resize()的作用是改变vector中元素的数目。

如果n比当前的vector元素数目要小,vector的容量要缩减到resize的第一个参数大小,既n。并移除那些超出n的元素同时销毁他们。

如果n比当前vector元素数目要大,在vector的末尾扩展需要的元素数目,如果第二个参数val指定了,扩展的新元素初始化为val的副本,否则按类型默认初始化。

注意:如果n大于当前的vector的容量(是容量,并非vector的size),将会引起自动内存分配。所以现有的pointer,references,iterators将会失效。关于vector的resize()的理解_ubunfans的博客-CSDN博客_vector resize

resize(),设置大小(size);
reserve(),设置容量(capacity);
size()是分配容器的内存大小,而capacity()只是设置容器容量大小,但并没有真正分配内存。
打个比方:正在建造的一辆公交车,车里面可以设置40个座椅(reserve(40);),这是它的容量,但并不是说它里面就有了40个座椅,只能说明这部车内部空间大小可以放得下40张座椅而已。而车里面安装了40个座椅(resize(40);),这个时候车里面才真正有了40个座椅,这些座椅就可以使用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值