回溯算法part4 | ● 93.复原IP地址 ● 78.子集 ● 90.子集II


93.复原IP地址

93.复原IP地址

思路

参考分割字符串问题,是一个类型的问题,startindex表示子串从哪里开始。path记录分割的位置,每次判断分割能否成功,只有到分割到了len(s)的位置并且这时len(path)==4,才能终止回溯。
时间复杂度
O ( 3 4 ∗ s ) O(3^4*s) O34s

思路代码

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

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个数)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值