93.复原IP地址
思路
参考分割字符串问题,是一个类型的问题,startindex表示子串从哪里开始。path记录分割的位置,每次判断分割能否成功,只有到分割到了len(s)的位置并且这时len(path)==4,才能终止回溯。
时间复杂度
O
(
3
4
∗
s
)
O(3^4*s)
O(34∗s)
思路代码
func restoreIpAddresses(s string) []string {
res:=[]string{}
path:=[]int{}
var backtrack func(startindex int)
backtrack = func(startindex int){
if startindex==len(s)&&len(path)==4{
temp:=[]byte{}
pre:=0
for _,v:=range path{
temp=append(temp,s[pre:v]...)
temp=append(temp,'.')
pre=v
}
temp=temp[:len(temp)-1]
res=append(res,string(temp))
}
for i:=startindex;i<len(s);i++{
if len(path)>3{
break
}
if check(s[startindex:i+1]){
path=append(path,i+1)
backtrack(i+1)
path=path[:len(path)-1]
}
}
}
backtrack(0)
return res
}
func check(s string)bool{
i,_:=strconv.Atoi(s)
if i>=0&&i<=255{
if len(s)>1&&s[0]=='0'{
return false
}
return true
}
return false
}
官方题解
做这道题目之前,最好先把131.分割回文串 (opens new window)这个做了。
这道题目相信大家刚看的时候,应该会一脸茫然。
其实只要意识到这是切割问题,切割问题就可以使用回溯搜索法把所有可能性搜出来,和刚做过的131.分割回文串 (opens new window)就十分类似了。
切割问题可以抽象为树型结构,如图:
93.复原IP地址
#回溯三部曲
递归参数
在131.分割回文串 (opens new window)中我们就提到切割问题类似组合问题。
startIndex一定是需要的,因为不能重复分割,记录下一层递归分割的起始位置。
本题我们还需要一个变量pointNum,记录添加逗点的数量。
所以代码如下:
vector result;// 记录结果
// startIndex: 搜索的起始位置,pointNum:添加逗点的数量
void backtracking(string& s, int startIndex, int pointNum) {
递归终止条件
终止条件和131.分割回文串 (opens new window)情况就不同了,本题明确要求只会分成4段,所以不能用切割线切到最后作为终止条件,而是分割的段数作为终止条件。
pointNum表示逗点数量,pointNum为3说明字符串分成了4段了。
然后验证一下第四段是否合法,如果合法就加入到结果集里
代码如下:
if (pointNum == 3) { // 逗点数量为3时,分隔结束
// 判断第四段子字符串是否合法,如果合法就放进result中
if (isValid(s, startIndex, s.size() - 1)) {
result.push_back(s);
}
return;
}
单层搜索的逻辑
在131.分割回文串 (opens new window)中已经讲过在循环遍历中如何截取子串。
在for (int i = startIndex; i < s.size(); i++)循环中 [startIndex, i] 这个区间就是截取的子串,需要判断这个子串是否合法。
如果合法就在字符串后面加上符号.表示已经分割。
如果不合法就结束本层循环,如图中剪掉的分支:
93.复原IP地址
然后就是递归和回溯的过程:
递归调用时,下一层递归的startIndex要从i+2开始(因为需要在字符串中加入了分隔符.),同时记录分割符的数量pointNum 要 +1。
回溯的时候,就将刚刚加入的分隔符. 删掉就可以了,pointNum也要-1。
代码如下:
for (int i = startIndex; i < s.size(); i++) {
if (isValid(s, startIndex, i)) { // 判断 [startIndex,i] 这个区间的子串是否合法
s.insert(s.begin() + i + 1 , ‘.’); // 在i的后面插入一个逗点
pointNum++;
backtracking(s, i + 2, pointNum); // 插入逗点之后下一个子串的起始位置为i+2
pointNum–; // 回溯
s.erase(s.begin() + i + 1); // 回溯删掉逗点
} else break; // 不合法,直接结束本层循环
}
代码
var (
path []string
res []string
)
func restoreIpAddresses(s string) []string {
path, res = make([]string, 0, len(s)), make([]string, 0)
dfs(s, 0)
return res
}
func dfs(s string, start int) {
if len(path) == 4 { // 够四段后就不再继续往下递归
if start == len(s) {
str := strings.Join(path, ".")
res = append(res, str)
}
return
}
for i := start; i < len(s); i++ {
if i != start && s[start] == '0' { // 含有前导 0,无效
break
}
str := s[start : i+1]
num, _ := strconv.Atoi(str)
if num >= 0 && num <= 255 {
path = append(path, str) // 符合条件的就进入下一层
dfs(s, i+1)
path = path[:len(path) - 1]
} else { // 如果不满足条件,再往后也不可能满足条件,直接退出
break
}
}
}
困难
startindex语义,表示子串的开始位置。
cheak的时候要考虑012,000这种情况。
最后通过path索引切割s,s[pre,v]… | 官方用的是strings.join
78.子集
78.子集
思路
回溯即可,每次回溯都将path加到结果中,而不是说到达最后才加。
思路代码
func subsets(nums []int) [][]int {
res:=[][]int{}
path:=[]int{}
var backtrack func(stratindex int)
backtrack = func(stratindex int){
temp:=make([]int,len(path))
copy(temp,path)
res=append(res,temp)
for i:=stratindex;i<len(nums);i++{
path=append(path,nums[i])
backtrack(i+1)
path=path[:len(path)-1]
}
}
backtrack(0)
return res
}
官方题解
如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!
有同学问了,什么时候for可以从0开始呢?
求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合,排列问题我们后续的文章就会讲到的。
代码
var (
path []int
res [][]int
)
func subsets(nums []int) [][]int {
res, path = make([][]int, 0), make([]int, 0, len(nums))
dfs(nums, 0)
return res
}
func dfs(nums []int, start int) {
tmp := make([]int, len(path))
copy(tmp, path)
res = append(res, tmp)
for i := start; i < len(nums); i++ {
path = append(path, nums[i])
dfs(nums, i+1)
path = path[:len(path)-1]
}
}
困难
那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
90.子集II
思路
和之前的组合问题去重一样,去的是同一树层的重,先对nums排序,然后used数组记录同一树层的是否被使用过。为used[i-1]为false就是使用过。
思路代码
func subsetsWithDup(nums []int) [][]int {
res:=[][]int{}
path:=[]int{}
used:=make([]bool,len(nums))
sort.Ints(nums)
var backtrack func(startindex int)
backtrack = func(startindex int){
temp:=make([]int,len(path))
copy(temp,path)
res=append(res,temp)
for i:=startindex;i<len(nums);i++{
if i>0&&nums[i]==nums[i-1]&&used[i-1]==false{
continue
}
used[i]=true
path=append(path,nums[i])
backtrack(i+1)
used[i]=false
path=path[:len(path)-1]
}
}
backtrack(0)
return res
}
困难
同一树层去重,used数组([]bool)
今日收获
抽象的问题联想到本质,就变得简单。列出ip地址是切割问题,而切割问题用回溯解决即可。
子集问题,记录每个树节点的值,所以直接在回溯的时候添加到res中即可,不需要等到判断条件成立(比如组合的k个数)