【C++】回溯算法——“字符串切割问题”

目录

回溯算法理论基础

1.什么是回溯算法

2.回溯法可以解决的问题

例题一 

 回溯 “三部曲” 如下:

(1)确定递归函数的参数。

(2)确定递归函数的终止条件。

(3)确定单层搜索的逻辑。

例题二

回溯 “三部曲” 如下:

(1)确定递归函数的参数。

(2)确定递归的终止条件。

(3)确定单层搜索的逻辑。


回溯算法理论基础

1.什么是回溯算法

回溯算法也可以叫做回溯搜索算法,简称回溯法,是一种搜索的方式。

回溯是递归的“副产品”,只要有递归的地方就会有对应回溯的过程。在后续内容中,回溯函数就是递归函数,指的都是一个函数。

2.回溯法可以解决的问题

  • 组合问题:如何按照一定规则在N个数中找出k个数的集合
  • 切割问题:一个字符串按照一定的规则切割,有几种切割方式
  • 子集问题:一个N个数的集合中几个符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后、数独等问题。

注:有些同学可能分不清组合和排列,只需要记住组合无序,排列有序即可。

例题一 

 

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

  • 切割问题,即 s 有不同的切割方式。
  • 判断回文。

其实,我们稍加分析就会发现,切割问题和组合问题其实很像。我们也同样可以将切割问题抽象为树形结构,如图所示。递归用来纵向遍历,for循环用来横向遍历,当切割线切割到字符串的的结尾位置时,说明我们找到了一种切割方案。

 回溯 “三部曲” 如下:

(1)确定递归函数的参数。

全局变量数组path用于存放切割后的回文的子串,二维数组 result 用于存放结果集。本题递归函数仍需要startindex,因为切割过的地方不能重复切割。代码如下:

vector<vector<string>> result;
vector<string> path; //存放已经回文的子串
void backtracking(const string& s,int startindex)

(2)确定递归函数的终止条件。

从树形图中可以看出:切割线切割到了字符串最后面,说明找到了一种切割方案,此时本层递归终止。在这里,切割线指的就是startindex,在for循环中startindex表示的就是下一轮遍历递归的起始位置。所以终止条件的代码如下:

// 如果起始位置大于s,说明找到了一组分割方案
    if(startindex>=s.size()){
        result.push_back(path);
        return;
    }

(3)确定单层搜索的逻辑。

在for循环中定义了starindex为起始位置,[startindex,i]为要截取的字串。但是,截取之后需要判断这个字串是不是回文,如果是则加入path中,path用来记录切割过的回文字串。注意:切割过的位置不能重复切割,所以backtracking(s,i+1)传入下一层的起始位置为i+1。代码如下:

for(int i=startindex;i<s.size();i++)
    {
        if(check(s,startindex,i)){ // 是回文子串

            // 获取[startindex,i]在s中的子串
            string str=s.substr(startindex,i-startindex+1);
            path.push_back(str);
        }
        else{  // 如果不是则直接跳过
            continue;
        }
        backtracking(s,i+1); // 寻找i+1为起始位置的子串
        path.pop_back();   // 回溯过程,弹出本次已处理的子串
    }

整体代码如下:

class Solution {
public:
vector<vector<string>> result;
vector<string> path; //存放已经回文的子串
bool check(const string& s,int i,int j){
    // 利用双指针法来判断是否为回文串
    for(int left=i,right=j;left<right;left++,right--)
    {
        if(s[left]!=s[right]){
            return false;
        }
    }
    return true;
}
void backtracking(const string& s,int startindex){
    // 如果起始位置大于s,说明找到了一组分割方案
    if(startindex>=s.size()){
        result.push_back(path);
        return;
    }
    for(int i=startindex;i<s.size();i++)
    {
        if(check(s,startindex,i)){ // 是回文子串

            // 获取[startindex,i]在s中的子串
            string str=s.substr(startindex,i-startindex+1);
            path.push_back(str);
        }
        else{  // 如果不是则直接跳过
            continue;
        }
        backtracking(s,i+1); // 寻找i+1为起始位置的子串
        path.pop_back();   // 回溯过程,弹出本次已处理的子串
    }
}
    vector<vector<string>> partition(string s) {
        backtracking(s,0);
        return result;
    }
};

例题二

只要意识到这是一个切割问题,就可以使用回溯搜索法,我们只需要将问题抽象为树形结构,如下图所示:

 

回溯 “三部曲” 如下:

(1)确定递归函数的参数。

类似于例题一,本题同样不能重复分割,所以一定需要starindex,用于记录下一层递归分割的起始位置。本题还需要添加一个int变量pointnum来记录 “.”的数量。代码如下:

vector<string> result;
void backtracking(string& s,int startindex,int pointnum)

(2)确定递归的终止条件。

本题明确要求字符串只会被分割成 4 段,所以不能用切割线切割到最后为终止条件,而是要将分割的段数作为终止条件。pointnum表示 "." 的数量,pointnum为 3 说明字符串分成了 4 层。然后验证第 4 段是否合法,如果合法就加入结果集。代码如下:

if(pointnum==3){  // "."的数量为3时,分割结束

// 这里时判断第4段是否合法,合法就加入result里
        if(check(s,startindex,s.size()-1)==true){
            result.push_back(s);
        }
        return;
    }

(3)确定单层搜索的逻辑。

在for循环中,[startindex,i]这个区间就是截取的子字符串,需要判断其是否合法。如果合法,就在字符串后面加上“.”表示已经被分割。如果不合法,就结束本层循环。

注意:调用函数递归时,下一层的递归starindex要从 i+2 开始(因为需要在字符串中加入分隔符     “.”),同时记录分隔符的数量pointnum要+1。回溯的时候要删除加入的分隔符,以及pointnum要-1。代码如下:

for(int i=startindex;i<s.size();i++)
    {
        // 判断[starindex,i]这个区间的子字符串是否合法
        if(check(s,startindex,i)==true){
            s.insert(s.begin()+1+i,'.');  // 在i的后面插入一个"."
            pointnum++;
            // 插入"."后,下一个起始位置要i+2
            backtracking(s,i+2,pointnum);
            pointnum--;
            s.erase(s.begin()+i+1);
        }
        else  break;  // 不合法,直接结束本层循环
    }

最后就是判断子字符串是否为合法的,主要考虑如下三点:

  • 子字符串以0开头的数字不合法;
  • 子字符串有非整数字符不合法;
  • 如果有子字符串大于255不合法;

整体代码如下:

class Solution {
public:
vector<string> result;
bool check(const string& s,int begin,int end){
    if(begin>end){
        return false;
    }
    if(s[begin]=='0'&&begin!=end){  // 这里还需要考虑单独为 "0"的情况
        return false;
    }
    int num=0;
    for(int i=begin;i<=end;i++)
    {
        if(s[i]<'0'||s[i]>'9'){
            return false;
        }
        num=num*10+(s[i]-'0');   // 将字符串转化为数字的形式
        if(num>255){
            return false;
        }
    }
    return true;
}
void backtracking(string& s,int startindex,int pointnum){
    if(pointnum==3){  // "."的数量为3时,分割结束
        if(check(s,startindex,s.size()-1)==true){
            result.push_back(s);
        }
        return;
    }
    for(int i=startindex;i<s.size();i++)
    {
        // 判断[starindex,i]这个区间的子字符串是否合法
        if(check(s,startindex,i)==true){
            s.insert(s.begin()+1+i,'.');  // 在i的后面插入一个"."
            pointnum++;
            // 插入"."后,下一个起始位置要i+2
            backtracking(s,i+2,pointnum);
            pointnum--;
            s.erase(s.begin()+i+1);
        }
        else  break;  // 不合法,直接结束本层循环
    }
}
    vector<string> restoreIpAddresses(string s) {
        if(s.size()>12) return result;
        backtracking(s,0,0);
        return result;
    }
};

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值