leetcode:87. 扰乱字符串

题目来源

题目描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

题目解析

分析题意

题意:[扰乱字符串」是指将 s1 中的某些字符经过一系列翻转,能否得到 s2

  • 给定一个字符串S,按照树结构每次二分成左右两个部分,直到单个字符
  • 在树上某些节点交换左右儿子,可以形成新的字符串
  • 判断一个字符串T是否由S经过这样的变化而成

例子1:

在这里插入图片描述

这里有两个随机:

  • 随机选择下标
    • 随机选择下标的结构就是只能对每种情况进行枚举
    • 使用循环判断每种情况
  • 随机选择是否交换
    • 同样是枚举,但是情况少,只有换与不换
      在这里插入图片描述
class Solution {
public:
    bool isScramble(string s1, string s2) {

    }
};

样本对应模型

暴力递归

(1)大的过滤:str1和str2所有字符的种类和个数必须一样
(2)样本对应模型

  • 定义 f ( s t r 1 , L 1 , R 1 , s t r 2 , L 2 , R 2 ) f(str1, L1, R1, str2, L2, R2) f(str1,L1,R1,str2,L2,R2)
    • str1的 L 1 − R 1 L1-R1 L1R1,对 s t r 2 str2 str2 L 2 − R 2 L2-R2 L2R2长度相同,那么
    • 判断 s t r 1 [ L 1... R 1 ] str1[L1...R1] str1[L1...R1] s t r 2 [ L 2... R 2 ] str2[L2...R2] str2[L2...R2]是不是扰乱字符串
  • base case:
    • 当只有一个字符时,即 L 1 = = R 1 , L 2 = = R 2 L1 == R1,L2==R2 L1==R1L2==R2
    • 那么 s t r 1 [ L 1 ] = = s t r 2 [ L 2 ] str1[L1] == str2[L2] str1[L1]==str2[L2]时,返回true,否则返回false
  • 普通情况:
    • 样本对应模型一般都是根据第一刀的情况来划分
    • 那么可以用枚举的方式枚举出str1的第一刀切哪里了

在这里插入图片描述

class Solution {
    bool sameTypeSameNumber(string s1, string s2){
        std::vector<int> map(256, 0);
        for (int i = 0; i < s1.size(); ++i) {
            map[s1[i]]++;
        }
        for (int i = 0; i < s2.size(); ++i) {
            if(--map[s2[i]] < 0){
                return false;
            }
        }
        return true;
    }

    // str1[L1...R1] str2[L2...R2] 是否互为玄变串
    // 一定保证这两段是等长的!
    bool  process(string &str1, int L1, int R1, string &str2, int L2, int R2){
        if(L1 == R1){
            return str1[L1] == str2[L2];
        }

        for (int leftEnd = L1; leftEnd < R1; ++leftEnd) {
            bool p1 = process(str1, L1, leftEnd, str2, L2, L2 + leftEnd - L1)
                    && process(str1, leftEnd + 1, R1, str2, L2 + leftEnd - L1 + 1, R2);
            bool p2 = process(str1, L1, leftEnd, str2, R2 - (leftEnd - L1), R2)
                         && process(str1, leftEnd + 1, R1, str2, L2, R2 - (leftEnd - L1) - 1);
            if (p1 || p2) {
                return true;
            }
        }
        return false;
    }
public:
    bool isScramble(string str1, string str2) {
        if(str1.empty() && str2.empty() || str1 == str2){
            return true;
        }
        
        if(str1.size() != str2.size()){
            return false;
        }
        
        if(!sameTypeSameNumber(str1, str2)){
            return false;
        }
        
        return process(str1, 0, str1.size() - 1, str2, 0, str2.length() - 1);
    }
};

分析,process(string &str1, int L1, int R1, string &str2, int L2, int R2)有四个参数,我们应该尽可能省掉一些参数,以改成动态规划。

  // str1[L1...R1] str2[L2...R2] 是否互为玄变串
    // 一定保证这两段是等长的!
    bool  process(string &str1, string &str2, int L1, int L2, int size){
        if(size == 1){
            return str1[L1] == str2[L2];
        }

        // 枚举每一种情况,有一个计算出互为旋变就返回true。都算不出来最后返回false
        for (int leftPart = 1; leftPart < size; leftPart++) {
            if (
                // 如果1左对2左,并且1右对2右
                     (process(str1, str2,    L1,               L2, leftPart)
                   && process(str1, str2, L1 + leftPart, L2 + leftPart, size - leftPart)) 
                   ||
                    // 如果1左对2右,并且1右对2左
                    (process(str1, str2, L1, L2 + size - leftPart, leftPart)
                     && process(str1, str2, L1 + leftPart, L2, size - leftPart))) {
                return true;
            }
        }
        return false;
    }

递归

从一个位置将两个字符串分别划分成两个子串,然后递归判断两个字符串的两个子串是否互相为「扰乱字符串」。

(1)递归出口

  • 两个字符串长度不相等,false
  • 两个字符串equal,true
  • 两个字符串包含的字符(不论顺序)不同,false

(2)如何随机选择下标

  • 因为不知道在哪个位置分割字符串,所以直接遍历每个位置进行分割
  • 在判断是否两个子串能否通过翻转变成相等的时候,需要保证传给函数的子串长度是相同的。

(3)考虑随机选择是否交换

  • 分别将s1中交换的与不交换的情况与s2对应的情况分别进行递归

综上,因此分两种情况讨论:

  • s1[0:i]和 s2[0:i]作为左子树,s1[i:N]和 s2[i:N]作为右子树
  • s1[0:i]和 s2[N - i:N]作为左子树,s1[i:N]和 s2[0:N-i]作为右子树

其中左子树的两个字符串的长度都是 i, 右子树的两个字符串的长度都是 N - i。如果上面两种情况有一种能够成立,则 s1 和 s2 是「扰乱字符串」。

class Solution {
    unordered_map<string, unordered_map<string, bool>> memo;
public:
    bool isScramble(string s1, string s2) {
        if(memo.count(s1) && memo[s1].count(s2)){
            return memo[s1][s2];
        }

        // 长度不等,必定不能变换
        if (s1.length() != s2.length()) {
            return memo[s1][s2] = false;
        }
        // 长度相等,先特判下
        if (s1 == s2) {
            return memo[s1][s2] = true;
        }

        // 看一下字符个数是否一致,不同直接return false
        std::map<char, int> map;
        int n = s1.length();
        for (int i = 0; i < n; ++i) {
            char c1 = s1[i], c2 = s2[i];
            map[c1]++;
            map[c2]--;
        }

        for(auto i : map){
            if(i.second != 0){
                return memo[s1][s2] = false;
            }
        }

        // 相同的话,开始判断判断,满足一个就能 return true
        for (int i = 1; i < n; ++i) {
            // / S1 -> T1,S2 -> T2
            bool choose1 = isScramble(s1.substr(0, i), s2.substr(0, i)) &&
                           isScramble(s1.substr(i), s2.substr(i));
            // S1 -> T2,S2 -> T1
            bool choose2 = isScramble(s1.substr(0, i),s2.substr(n - i)) &&
                           isScramble(s1.substr(i), s2.substr(0, n - i));
            if (choose1 || choose2) return memo[s1][s2] = true;
        }

        return memo[s1][s2] = false;
    }
};

动态规划

这是一道区间型状态规划:

  • 给定一个序列或者字符串进行一些操作,从最后一步出发,将序列或者字符串去头、去尾…
  • 区间型dp一般用dp[i][j],i表示左端点,j表示右端点,如果有其他维度可以再添加,如果两个端点之间有联系,则可以再压缩空间

(1)确定状态

给定两个字符串 T和 S

  • 显然,如果T.length != S.length,那么false

  • 如果长度一样,并且我们知道S最上层二分被分为S= S 1 S 2 S_1S_2 S1S2,同样T也有两部分 T = T 1 T 2 T=T_1T_2 T=T1T2,那么一定有:

    • 情况一:没有交换, T 1 T_1 T1 S 1 S_1 S1变换而来的, T 2 T_2 T2是由 S 2 S_2 S2变换而来的
    • 情况二:交换了, T 1 T_1 T1 S 2 S_2 S2变换而来的, T 2 T_2 T2是由 S 1 S_1 S1变换而来的
  • 子问题:

    • 要求T是否由S变化而来
    • 情况一:需要知道 T 1 T_1 T1是否 S 1 S_1 S1变换而来的, T 2 T_2 T2是否由 S 2 S_2 S2变换而来的
    • 情况二:需要知道 T 1 T_1 T1 S 2 S_2 S2变换而来的, T 2 T_2 T2是由 S 1 S_1 S1变换而来的
    • T 1 、 S 1 、 T 2 、 S 2 T_1、S_1、T_2、S_2 T1S1T2S2长度更短

在这里插入图片描述

  • 状态:
    • d p [ i ] [ j ] [ k ] [ h ] dp[i][j][k][h] dp[i][j][k][h]表示 T [ k . . . h ] T[k...h] T[k...h]是否由 S [ i . . . j ] S[i...j] S[i...j]变化而来。
      • 由于变换必须长度是一样的,因此这边有个关系 j - i = h - k,可以把四维数组变为三维,每个串都可以用(起始位置,长度)表示。例如:
        • S 1 S_1 S1长度为5,在S中位置7开始
        • T 1 T_1 T1长度为5,在T中位置0开始
        • 可以用f[7][0][5]=true/false表示 S 1 S_1 S1能够变化为 T 1 T_1 T1
    • 因此:设 f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]表示从字符串 S S S中i开始长度为k的字符串是否能变换为从字符串 T T T中开始的长度为 k k k的字符串

(2)转移方程

在这里插入图片描述

(3)初始条件

  • 对于长度是 1 的子串,只有相等才能变过去,相等为 truetrue,不相等为 falsefalse。

(4)得到答案

class Solution {
public:
    bool isScramble(string s1, string s2) {
        if(s1 == s2) return true;
        if(s1.size() != s2.size()) return false;
        int n = s1.size();
        vector<vector<vector<int>>> f(n,
                        vector<vector<int>>(n,
                        vector<int>(n+1)));

        // 初始化单个字符的情况
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < n; j++) {
                f[i][j][1] = (s1[i] == s2[j]);
            }
        }

        // 枚举区间长度 2~n
        for(int len = 2; len <= n; len++) {
            // 枚举 S 中的起点位置
            for(int i = 0; i <= n - len; i++) {
                // 枚举 T 中的起点位置
                for(int j = 0; j <= n - len; j++) {
                    // 枚举划分位置
                    for(int k = 1; k <= len - 1; k++) {
                        // 第一种情况:S1 -> T1, S2 -> T2
                        if (f[i][j][k] && f[i + k][j + k][len - k]) {
                            f[i][j][len] = true;
                            break;
                        }
                        // 第二种情况:S1 -> T2, S2 -> T1
                        // S1 起点 i,T2 起点 j + 前面那段长度 len-k ,S2 起点 i + 前面长度k
                        if (f[i][j + len - k][k] && f[i + k][j][len - k]) {
                            f[i][j][len] = true;
                            break;
                        }
                    }
                }
            }
        }

        return f[0][0][n];
    }
};

类似题目

题目思路
leetcode:87. 扰乱字符串
leetcode:97. 交错字符串
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值