回溯算法part3 | ● 39. 组合总和 ● 40.组合总和II ● 131.分割回文串

news/2024/11/9 5:08:43/

文章目录

  • 39. 组合总和
    • 思路
    • 思路代码
    • 困难
  • 40.组合总和II
    • 思路
    • 官方题解
    • 代码
    • 困难
  • 131.分割回文串
    • 思路
    • 思路代码
    • 官方题解
    • 代码
    • 困难
  • 今日收获


39. 组合总和

39.组合总和

思路

回溯的时候不需要从下一个数开始了,从当前数开始即可。

思路代码

func combinationSum(candidates []int, target int) [][]int {res:=[][]int{}path:=[]int{}sum:=0var backtrack func(startindex int)backtrack = func(startindex int){if sum>=target{if sum==target{temp:=make([]int,len(path))copy(temp,path)res=append(res,temp)}return}for i:=startindex;i<len(candidates);i++{path=append(path,candidates[i])sum+=candidates[i]backtrack(i)path=path[:len(path)-1]sum-=candidates[i]}}backtrack(0)return res
}

困难

注意并不是

for i:=startindex;i<len(candidates);i++{path=append(path,candidates[i])sum+=candidates[i]backtrack(i)backtrack(i+1)path=path[:len(path)-1]sum-=candidates[i]}

不需要backtrack(i+1),本身在for循环中就已经满足了递归所有组合的语义了,再多一个backtrack(i+1)就会导致重复。


40.组合总和II

40.组合总和II

思路

发现要进行去重,想的是map去重,不可行
需要对同一树层上的去重,而同一树枝不用去重,使用used数组。

官方题解

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

那么问题来了,我们是要同一树层上使用过,还是同一树枝上使用过呢?

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

所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
强调一下,树层去重的话,需要对数组排序!
单层搜索的逻辑
这里与39.组合总和 (opens new window)最大的不同就是要去重了。

前面我们提到:要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。

如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。

此时for循环里就应该做continue的操作。

代码

func combinationSum2(candidates []int, target int) [][]int {used:=make([]bool,len(candidates))res:=[][]int{}path:=[]int{}sum:=0sort.Ints(candidates)var backtrack func(startindex int)backtrack = func(startindex int){if sum>=target{if sum==target{temp:=make([]int,len(path))copy(temp,path)res=append(res,temp)}return}for i:=startindex;i<len(candidates);i++{if sum+candidates[i]>target{break}if i>0&&candidates[i]==candidates[i-1]&&used[i-1]==false{continue}path=append(path,candidates[i])sum+=candidates[i]used[i]=truebacktrack(i+1)path=path[:len(path)-1]sum-=candidates[i]used[i]=false}}backtrack(0)return res
}

困难

排序为了剪枝和去重
used数组为了区分是同一树层还是同一数枝


131.分割回文串

131.分割回文串

思路

确定从哪里开始(子串的开始位置),然后回溯for循环确定切割位置;
终止条件是开始位置变成了len(s),也就是到了字符串的末尾。

思路代码

1.确定从哪里开始(子串的开始位置),然后回溯for循环确定切割位置

func partition(s string) [][]string {res:=[][]string{}path:=[]string{}var backtrack func(startindex int)backtrack = func(startindex int){if startindex==len(s){temp:=make([]string,len(path))copy(temp,path)res=append(res,temp)return}for i:=startindex;i<len(s);i++{sl:=s[startindex:i+1]if check(sl){path=append(path,sl)backtrack(i+1)path=path[:len(path)-1]}}}backtrack(0)return res
}func check(s string)bool{l,r:=0,len(s)-1for l<r{if s[l]!=s[r]{return false}l++r--}return true
}

2.startindex表示开始切割的位置,i表示第二次切割的位置,语义虽然有很大变化,但代码改动不大,仅仅是for循环i的加1还是不加的区别

func partition(s string) [][]string {res:=[][]string{}path:=[]string{}var backtrack func(startindex int)backtrack = func(startindex int){if startindex==len(s){temp:=make([]string,len(path))copy(temp,path)res=append(res,temp)return}for i:=startindex+1;i<=len(s);i++{if i>0{sl:=s[startindex:i]if check(sl){path=append(path,sl)backtrack(i)path=path[:len(path)-1]}}}}backtrack(0)return res
}func check(s string)bool{l,r:=0,len(s)-1for l<r{if s[l]!=s[r]{return false}l++r--}return true
}

官方题解

本题这涉及到两个关键问题:

切割问题,有不同的切割方式
判断回文
相信这里不同的切割方式可以搞懵很多同学了。

这种题目,想用for循环暴力解法,可能都不那么容易写出来,所以要换一种暴力的方式,就是回溯。

一些同学可能想不清楚 回溯究竟是如何切割字符串呢?

我们来分析一下切割,其实切割问题类似组合问题。

例如对于字符串abcdef:

组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…。
感受出来了不?

所以切割问题,也可以抽象为一棵树形结构,如图:

131.分割回文串

递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。

此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的。

代码

var (path []string  // 放已经回文的子串res  [][]string
)
func partition(s string) [][]string {path, res = make([]string, 0), make([][]string, 0)dfs(s, 0)return res
}func dfs(s string, start int) {if start == len(s) { // 如果起始位置等于s的大小,说明已经找到了一组分割方案了tmp := make([]string, len(path))copy(tmp, path)res = append(res, tmp)return }for i := start; i < len(s); i++ {str := s[start : i+1]if isPalindrome(str) {   // 是回文子串path = append(path, str)dfs(s, i+1)         // 寻找i+1为起始位置的子串path = path[:len(path)-1]  // 回溯过程,弹出本次已经填在的子串}}
}func isPalindrome(s string) bool {for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {if s[i] != s[j] {return false}}return true
}

困难

思考清楚startindex和切割位置的语义,一个是判断子串范围,一个是切割位置,二者必须有一致性(类似循环不变量)。


今日收获

回溯使用used数组去重同一树层。
回溯算法使用场景组合,分割。


http://www.ppmy.cn/news/234552.html

相关文章

【SpinalHDL快速入门】SpinalHDL知识卡片

文章目录 新建工程各个工具版本SpinalHDL入门例子Scala 易忘知识点返回值为空的函数ApplyObject入口函数mainclassCase class继承(Inheritance)饱和运算计数器Demodemo1demo2(支持reset信号异步复位,低电平有效)demo3(一个文件,支持异步复位)新建工程各个工具版本 配置…

c语言中字符串比较的库函数是什么

说起比较运算&#xff0c;肯定第一时间想到了C语言中关于比较的相关运算符 “>、<、&#xff01;、>、<、”&#xff0c;那么要比较两个字符串是否相等是不是直接用“”比较就行了。下面就来看看这种方法行不行&#xff1f; 先看一个例子 void main( void ) {cha…

久戴不痛的蓝牙耳机有哪些?公认佩戴最舒适的无线蓝牙耳机推荐

现在的蓝牙耳机有着各种各样的性能&#xff0c;佩戴舒适也成为了人们选择蓝牙耳机的重要性能之一。戴着舒服&#xff0c;久戴不痛的蓝牙耳机有哪些&#xff1f;符合人体工学设计&#xff0c;轻便小巧&#xff0c;佩戴稳固的蓝牙耳机受到了不少人的欢迎。下面&#xff0c;我来给…

啥牌子的蓝牙耳机好?南卡、华为和漫步者蓝牙耳机

蓝牙耳机是人们不可缺的数码用品&#xff0c;出门在外或是玩游戏都离不开&#xff0c;随着国产TWS耳机的崛起&#xff0c;越来越多的人优先选择国产蓝牙耳机品牌&#xff0c;下面找来了当前市面上三款热门真无线蓝牙耳机&#xff0c;分别有南卡小音舱、华为FreeBuds 4E、漫步者…

2022年千元以下有哪些值得购买的蓝牙耳机?平价耳机深度测评,漫步者、南卡、Vivo、oppo、小米、三星、华为哪款最值得买?

许多朋友挑选无线蓝牙耳机的时候&#xff0c;并不知道自己到底需要什么样的无线蓝牙耳机&#xff0c;一心只想着性能最强可以节省时间的&#xff0c;但如若要找一款适合自己的蓝牙耳机&#xff0c;必要的参数对比是不能省的&#xff0c;所以今天我来一期蓝牙耳机测评&#xff0…

SpringMVC-【回顾】

回顾MVC架构 什么是mvc&#xff1a;模型、视图、控制器 -----软件设计规范 回顾servlet maven项目导入依赖&#xff08;webmvc,servlet-api,jsp-api,jstl,junit&#xff09;创建子模块&#xff0c;在子模块中添加框架支持&#xff08;在子模块中导入依赖jsp、servlet【因为父…

css常用的样式属性

CSS&#xff08;层叠样式表&#xff09;是用于网页排版的标记语言&#xff0c; 利用 CSS 可以控制网页中各个元素的布局、颜色、字体、边框等各种样式&#xff0c;下面是一些常用的 CSS 样式属性&#xff1a; background-color&#xff1a;设置背景颜色color&#xff1a;设置文…

Airpods左右耳音量不一样

戴耳机的场景有限&#xff0c;突然某一天连上mac后&#xff0c;总感觉左边没右边音量大&#xff0c;几次确认没听错&#xff0c;就开始搜索&#xff0c;搜出来一堆乱七八糟不靠谱的帖子 有说是耳机老化坏了的&#xff0c;这个可以换个设备连上看看是不是依然两边音量不一样大如…