回溯算法——切割问题

切割问题

一、问题特点

这类问题通常会给出一个数组(或字符串),要求列举出所有满足特定条件的划分。其特点如下:

  • 通常不允许改变原集合中元素的相对顺序
  • 所求答案通常是若干组满足特定条件的划分
  • 划分中的所有元素必须在原集合中连续取值

二、相关例题

(一)分割回文串

131.分割回文串(LeetCode)
题目要求:

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串 。返回 s 所有可能的分割方案。
回文串是正着读和反着读都一样的字符串。

示例:

输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]

提示:

1 <= s.length <= 16
s 仅由小写英文字母组成

解题思路:
切割问题同样是回溯算法的典型应用,其解题思路和组合问题大致相同,同样需要用for循环加递归的方法遍历搜索树查找满足条件的解,又因为切割问题中不允许重复取值,所以仍需要startIndex标明切割的起始位置。和组合问题不同的是,切割问题每次添加到路径中的往往不是单个元素s[i],而是从[startIndex, i]索引范围内的子串。
本题中需要找出所有元素均为回文子串的划分。所以在进行添加操作前,需要先判断该段子串是否为回文串,该步骤需要重复许多次,可以通过设置一个函数bool isPalindrome(string s){}利用双指针法实现判断,但由于问题中每一次取子串都需要检查,故调用次数过多,影响程序运行效率。为了进行优化,可以设一个二维数组vector<vector> isP先将字符串s中所有可能的子串是否为回文串的情况记录下来,当需要判断时查找二维数组即可。后者只需要遍历判断一次,效率更高。

代码示例:
// 优化后的代码

class Solution {
public:
    void isPalindrome(string s) {
        // 判断字符串s中的所有子串是否为回文串并将结果存入isP[][]中
        // 根据s的大小刷新数组大小
        isP.resize(s.size(), vector<bool>(s.size(), false));
        // 子串[i, j]是回文串的充要条件是s[i] = s[j] && [i+1, j-1]是回文串
        for(int i = s.size() - 1; i >= 0; i--) {
            // 反向遍历,确保在判断[i, j]时,[i+1, j-1]已被判断
            for(int j = i; j < s.size(); j++) {
                if(i == j) isP[i][j] = true;
                else if(j - i == 1) isP[i][j] = (s[i] == s[j]);
                else {
                    isP[i][j] = (isP[i+1][j-1] && s[i] == s[j]);
                }
            }
        }
        return;
    }

    void backtracking(const string s, int startIndex) {
        // startIndex是分割线
        // [0, startIndex - 1]是已被分割的部分
        if(startIndex >= s.size()) {
            // 本路径分割完毕,存入结果数组中
            ans.push_back(path);
            return;
        }
        for(int i = startIndex; i < s.size(); ++i) {
            if(!isP[startIndex][i]) continue;
            // 确认[startIndex, i]是回文子串,准备取出
            string temp = s.substr(startIndex, i - startIndex + 1);
            path.push_back(temp);
            backtracking(s, i + 1); // 纵向遍历
            path.pop_back();
        }
        return;
    }
    vector<vector<string>> partition(string s) {
        isPalindrome(s);
        backtracking(s, 0);
        return ans;
    }
private:
    vector<string> path;
    vector<vector<string>> ans;
    vector<vector<bool>> isP;
};

// 未优化的代码
class Solution {
public:
    bool isPalindrome(string s, int m, int n) {
        // 判断字符串s中第i个字符到第j个字符之间的子串是否是回文串
        // 时间复杂度:O(n)
        if(m > n || n >= s.size()) return false;
        for(int i = m; i < n; ++i , --n) {
            if(s[i] != s[n]) return false;
        }
        return true;
    }

    void backtracking(const string s, int startIndex) {
        // startIndex是分割线
        // [0, startIndex - 1]是已被分割的部分
        if(startIndex >= s.size()) {
            // 本路径分割完毕,存入结果数组中
            ans.push_back(path);
            return;
        }
        for(int i = startIndex; i < s.size(); ++i) {
            if(!isPalindrome(s, startIndex, i)) continue;
            // 确认[startIndex, i]是回文子串,准备取出
            string temp = s.substr(startIndex, i - startIndex + 1);
            path.push_back(temp);
            backtracking(s, i + 1); // 纵向遍历
            path.pop_back();
        }
        return;
    }
    vector<vector<string>> partition(string s) {
        backtracking(s, 0);
        return ans;
    }
private:
    vector<string> path;
    vector<vector<string>> ans;
};

(二)复原IP地址

93.复原IP地址(LeetCode)
题目要求:

有效 IP 地址正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你不能重新排序或删除 s 中的任何数字。你可以按任何顺序返回答案。

示例:

输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]

解题思路:

  • 因为不能重复取值,所以需要startIndex计数
  • 因为一定是分成四份,所以需要一个int型参数count用于纪录当前深度,当深度不等于4时结果无效,据此可以直接剪去深度大于4的分支
  • 因为每一次分割出来的子串长度为[1, 3]所以可以剪去长度大于3的分支
  • 要求分割后的子串不能有前导0,范围在[0, 255],可以设置一个函数判断特定子串是否满足条件。
  • 注意除了第一次向path中添加子串,每一次添加前都需要先向path中添加一个"."
  • 注意回溯时需要删除添加的子串和"."

代码示例:

class Solution {
public:
    bool isValid(string num) {
        // 判断数字是否满足有效条件
        if(num.size() > 1 && num[0] == '0') return false;
        int n = stoi(num);
        if(n > 255 || n < 0) return false;
        return true;
    }
    void backtracking(string s, int startIndex, int count) {
        if(startIndex >= s.size()) {
            // 分割完毕,将结果保存到数组ans
            if(count == 4) ans.push_back(path);
            return;
        }else if(count == 4) return;
        // 递归查找的深度一定是4
        for(int i = startIndex; i < s.size(); ++i) {
            // 分割[startIndex, i]
            // 每一个整数的长度一定小于4
            if(i - startIndex >= 3) break;
            string temp = s.substr(startIndex, i - startIndex + 1);
            if(!isValid(temp)) break;

            // 将当前有效字段加入字符串
            if(startIndex != 0) path.append(".");
            path.append(temp);
    
            backtracking(s, i + 1, count + 1);
            
            // 回溯
            if(startIndex == 0) path.erase(path.size() - temp.size(), temp.size());
            else path.erase(path.size() - temp.size() - 1, temp.size() + 1); 
        }
        return;
    }
    vector<string> restoreIpAddresses(string s) {
        backtracking(s, 0, 0);
        return ans;
    }
private:
    vector<string> ans;
    string path = "";
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值