LeetCode 每日一题 87. 扰乱字符串

87. 扰乱字符串

使用下面描述的算法可以扰乱字符串 s 得到字符串 t

  1. 如果字符串的长度为 1 ,算法停止
  2. 如果字符串的长度 > 1 ,执行下述步骤:
    • 在一个随机下标处将字符串分割成两个非空的子字符串。即,如果已知字符串 s ,则可以将其分成两个子字符串 xy ,且满足 s = x + y
    • 随机 决定是要「交换两个子字符串」还是要「保持这两个子字符串的顺序不变」。即,在执行这一步骤之后,s 可能是 s = x + y 或者 s = y + x
    • xy 这两个子字符串上继续从步骤 1 开始递归执行此算法。

给你两个 长度相等 的字符串 s1s2,判断 s2 是否是 s1 的扰乱字符串。如果是,返回 true ;否则,返回 false

示例 1:

输入:s1 = "great", s2 = "rgeat"
输出:true
解释:s1 上可能发生的一种情形是:
"great" --> "gr/eat" // 在一个随机下标处分割得到两个子字符串
"gr/eat" --> "gr/eat" // 随机决定:「保持这两个子字符串的顺序不变」
"gr/eat" --> "g/r / e/at" // 在子字符串上递归执行此算法。两个子字符串分别在随机下标处进行一轮分割
"g/r / e/at" --> "r/g / e/at" // 随机决定:第一组「交换两个子字符串」,第二组「保持这两个子字符串的顺序不变」
"r/g / e/at" --> "r/g / e/ a/t" // 继续递归执行此算法,将 "at" 分割得到 "a/t"
"r/g / e/ a/t" --> "r/g / e/ a/t" // 随机决定:「保持这两个子字符串的顺序不变」
算法终止,结果字符串和 s2 相同,都是 "rgeat"
这是一种能够扰乱 s1 得到 s2 的情形,可以认为 s2 是 s1 的扰乱字符串,返回 true

示例 2:

输入:s1 = "abcde", s2 = "caebd"
输出:false

示例 3:

输入:s1 = "a", s2 = "a"
输出:true

提示:

  • s1.length == s2.length
  • 1 <= s1.length <= 30
  • s1s2 由小写英文字母组成

方法一:暴力递归

一个暴力的做法,直接根据题目的「扰乱规则」来匹配字符串。

假设字符串长度为 n,当前分割的长度为 i,有 不交换交换 两种匹配方式:

  1. 不交换 相等条件:s1[0, i) == s2[0, i)s1[i, n) == s2[i, n)
  2. 交换 相等条件: s1[0, i) == s2[n - i, n)s1[i, n) == s2[0, n - i)

递归执行函数,直到字符串长度 n = 1 即可,代码如下:

public boolean isScramble(String s1, String s2) {
    int n = s1.length();
    for (int i = 1; i < n; i++) {
        String x1 = s1.substring(0, i), y1 = s1.substring(i);
        String x2 = s2.substring(0, i), y2 = s2.substring(i);
        // 不交换
        if (isScramble(x1, x2) && isScramble(y1, y2)) { 
            return true;
        }
        String x3 = s2.substring(n - i, n), y3 = s2.substring(0, n - i);
        // 交换
        if (isScramble(x1, x3) && isScramble(y1, y3)) {
            return true;
        }
    }
    return s1.equals(s2);
}

结果必然超时,卡在 195 个测试用例,如下图所示:
在这里插入图片描述
上面的方法没有进行任何 剪枝,能够想到的 剪枝 方法有:

  1. 在方法入口先判断两个字符串是否相等,相等直接返回 true
  2. 判断两个字符串每个字母的出现频率是否相等,不相等直接返回 false

剪枝 的代码如下:

public boolean isScramble(String s1, String s2) {
    // 剪枝1:先判断字符串是否相等
    if (s1.equals(s2)) {
        return true;
    }
    // 剪枝2:判断单词出现频率是否相同
    if (!checkFreq(s1, s2)) {
        return false;
    }
    int n = s1.length();
    for (int i = 1; i < n; i++) {
        String x1 = s1.substring(0, i), y1 = s1.substring(i);
        String x2 = s2.substring(0, i), y2 = s2.substring(i);
        if (isScramble(x1, x2) && isScramble(y1, y2)) {
            return true;
        }
        String x3 = s2.substring(n - i, n), y3 = s2.substring(0, n - i);
        if (isScramble(x1, x3) && isScramble(y1, y3)) {
            return true;
        }
    }
    return false;
}

public boolean checkFreq(String s1, String s2) {
    int[] count1 = new int[26];
    int[] count2 = new int[26];
    for (int i = 0; i < s1.length(); i++) {
        count1[s1.charAt(i) - 'a']++;
        count2[s2.charAt(i) - 'a']++;
    }
    for (int i = 0; i < 26; i++) {
        if (count1[i] != count2[i]) {
            return false;
        }
    }
    return true;
}

虽然 剪枝 优化了一些匹配路径,但并不会使时间复杂度下降一个数量级。

结果依然超时,卡在 286 个测试用例,如下图所示:
在这里插入图片描述

方法二:记忆化递归

暴力递归 超时的原因很简单,即我们做了很多重复的判断,我们可以使用一个记忆数组 memory 保存之前的处理结果。

memory[i][j][len] 表示 s1 从 i 开始长度为 len 的子串s2 从 j 开始长度为 len 的字串,是否已经处理过和对应的处理结果,这里可以使用包装类 Boolean

  1. memory[i][j][len] == null,表示没处理过
  2. memory[i][j][len] == true,表示处理过 且 两个子串处理后可以相等
  3. memory[i][j][len] == false,表示处理过 且 两个子串处理后也无法相等

参考代码

String str1, str2;
Boolean[][][] memory;

public boolean isScramble(String s1, String s2) {
    int n = s1.length();
    str1 = s1; str2 = s2;
    memory = new Boolean[n][n][n + 1];
    return dfs(0, 0, n);
}

public boolean dfs(int i, int j, int len) {
    if (memory[i][j][len] != null) {
        return memory[i][j][len];
    }
    String x = str1.substring(i, i + len), y = str2.substring(j, j + len);
    if (x.equals(y)) {
        return true;
    }
    if (checkFreq(x, y)) {
        for (int k = 1; k < len; k++) {
            if (dfs(i, j, k) && dfs(i + k, j + k, len - k) ||
                    dfs(i, len + j - k, k) && dfs(i + k, j, len - k)) {
                return memory[i][j][len] = true;
            }
        }
    }
    return memory[i][j][len] = false;
}

public boolean checkFreq(String s1, String s2) {
    int[] count1 = new int[26];
    int[] count2 = new int[26];
    for (int i = 0; i < s1.length(); i++) {
        count1[s1.charAt(i) - 'a']++;
        count2[s2.charAt(i) - 'a']++;
    }
    for (int i = 0; i < 26; i++) {
        if (count1[i] != count2[i]) {
            return false;
        }
    }
    return true;
}

执行结果
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值