Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrings recursively.

Below is one possible representation of s1 = "great":

    great
/ \
gr eat
/ \ / \
g r e at
/ \
a t

To scramble the string, we may choose any non-leaf node and swap its two children.

For example, if we choose the node "gr" and swap its two children, it produces a scrambled string "rgeat".

    rgeat
/ \
rg eat
/ \ / \
r g e at
/ \
a t

We say that "rgeat" is a scrambled string of "great".

Similarly, if we continue to swap the children of nodes "eat" and "at", it produces a scrambled string "rgtae".

    rgtae
/ \
rg tae
/ \ / \
r g ta e
/ \
t a

We say that "rgtae" is a scrambled string of "great".

Given two strings s1 and s2 of the same length, determine if s2 is a scrambled string of s1.

Example 1:

Input: s1 = "great", s2 = "rgeat"
Output: true

Example 2:

Input: s1 = "abcde", s2 = "caebd"
Output: false

这道题定义了一种搅乱字符串,就是说假如把一个字符串当做一个二叉树的根,然后它的非空子字符串是它的子节点,然后交换某个子字符串的两个子节点,重新爬行回去形成一个新的字符串,这个新字符串和原来的字符串互为搅乱字符串。这道题可以用递归 Recursion 或是动态规划 Dynamic Programming 来做,我们先来看递归的解法,参见网友 uniEagle 的博客简单的说,就是 s1 和 s2 是 scramble 的话,那么必然存在一个在 s1 上的长度 l1,将 s1 分成 s11 和 s12 两段,同样有 s21 和 s22,那么要么 s11 和 s21 是 scramble 的并且 s12 和 s22 是 scramble 的;要么 s11 和 s22 是 scramble 的并且 s12 和 s21 是 scramble 的。就拿题目中的例子 rgeat 和 great 来说,rgeat 可分成 rg 和 eat 两段, great 可分成 gr 和 eat 两段,rg 和 gr 是 scrambled 的, eat 和 eat 当然是 scrambled。根据这点,我们可以写出代码如下:

解法一:

// Recursion
class Solution {
public:
bool isScramble(string s1, string s2) {
if (s1.size() != s2.size()) return false;
if (s1 == s2) return true;
string str1 = s1, str2 = s2;
sort(str1.begin(), str1.end());
sort(str2.begin(), str2.end());
if (str1 != str2) return false;
for (int i = ; i < s1.size(); ++i) {
string s11 = s1.substr(, i);
string s12 = s1.substr(i);
string s21 = s2.substr(, i);
string s22 = s2.substr(i);
if (isScramble(s11, s21) && isScramble(s12, s22)) return true;
s21 = s2.substr(s1.size() - i);
s22 = s2.substr(, s1.size() - i);
if (isScramble(s11, s21) && isScramble(s12, s22)) return true;
}
return false;
}
};

当然,这道题也可以用动态规划 Dynamic Programming,根据以往的经验来说,根字符串有关的题十有八九可以用 DP 来做,那么难点就在于如何找出状态转移方程。参见网友 Code Ganker 的博客,这其实是一道三维动态规划的题目,使用一个三维数组 dp[i][j][n],其中i是 s1 的起始字符,j是 s2 的起始字符,而n是当前的字符串长度,dp[i][j][len] 表示的是以i和j分别为 s1 和 s2 起点的长度为 len 的字符串是不是互为 scramble。有了 dp 数组接下来看看状态转移方程,也就是怎么根据历史信息来得到 dp[i][j][len]。判断这个是不是满足,首先是把当前 s1[i...i+len-1] 字符串劈一刀分成两部分,然后分两种情况:第一种是左边和 s2[j...j+len-1] 左边部分是不是 scramble,以及右边和 s2[j...j+len-1] 右边部分是不是 scramble;第二种情况是左边和 s2[j...j+len-1] 右边部分是不是 scramble,以及右边和 s2[j...j+len-1] 左边部分是不是 scramble。如果以上两种情况有一种成立,说明 s1[i...i+len-1] 和 s2[j...j+len-1] 是 scramble 的。而对于判断这些左右部分是不是 scramble 是有历史信息的,因为长度小于n的所有情况都在前面求解过了(也就是长度是最外层循环)。上面说的是劈一刀的情况,对于 s1[i...i+len-1] 有 len-1 种劈法,在这些劈法中只要有一种成立,那么两个串就是 scramble 的。总结起来状态转移方程是:

dp[i][j][len] = || (dp[i][j][k] && dp[i+k][j+k][len-k] || dp[i][j+len-k][k] && dp[i+k][j][len-k])

对于所有 1<=k<len,也就是对于所有 len-1 种劈法的结果求或运算。因为信息都是计算过的,对于每种劈法只需要常量操作即可完成,因此求解递推式是需要 O(len)(因为 len-1 种劈法)。如此总时间复杂度因为是三维动态规划,需要三层循环,加上每一步需要线行时间求解递推式,所以是 O(n^4)。虽然已经比较高了,但是至少不是指数量级的,动态规划还是有很大优势的,空间复杂度是 O(n^3)。代码如下:

解法二:

// DP
class Solution {
public:
bool isScramble(string s1, string s2) {
if (s1.size() != s2.size()) return false;
if (s1 == s2) return true;
int n = s1.size();
vector<vector<vector<bool>>> dp (n, vector<vector<bool>>(n, vector<bool>(n + )));
for (int len = ; len <= n; ++len) {
for (int i = ; i <= n - len; ++i) {
for (int j = ; j <= n - len; ++j) {
if (len == ) {
dp[i][j][] = s1[i] == s2[j];
} else {
for (int k = ; k < len; ++k) {
if ((dp[i][j][k] && dp[i + k][j + k][len - k]) || (dp[i + k][j][len - k] && dp[i][j + len - k][k])) {
dp[i][j][len] = true;
}
}
}
}
}
}
return dp[][][n];
}
};

上面的代码的实现过程如下,首先按单个字符比较,判断它们之间是否是 scrambled 的。在更新第二个表中第一个值 (gr 和 rg 是否为 scrambled 的)时,比较了第一个表中的两种构成,一种是 g与r, r与g,另一种是 g与g, r与r,其中后者是真,只要其中一个为真,则将该值赋真。其实这个原理和之前递归的原理很像,在判断某两个字符串是否为 scrambled 时,比较它们所有可能的拆分方法的子字符串是否是 scrambled 的,只要有一个种拆分方法为真,则比较的两个字符串一定是 scrambled 的。比较 rge 和 gre 的实现过程如下所示:

     r    g    e
g x √ x
r √ x x
e x x √ rg ge
gr √ x
re x x rge
gre √

DP 的另一种写法,参考网友加载中..的博客,思路都一样,代码如下:

解法三:

// Still DP
class Solution {
public:
bool isScramble(string s1, string s2) {
if (s1.size() != s2.size()) return false;
if (s1 == s2) return true;
int n = s1.size();
vector<vector<vector<bool>>> dp (n, vector<vector<bool>>(n, vector<bool>(n + )));
for (int i = n - ; i >= ; --i) {
for (int j = n - ; j >= ; --j) {
for (int k = ; k <= n - max(i, j); ++k) {
if (s1.substr(i, k) == s2.substr(j, k)) {
dp[i][j][k] = true;
} else {
for (int t = ; t < k; ++t) {
if ((dp[i][j][t] && dp[i + t][j + t][k - t]) || (dp[i][j + k - t][t] && dp[i + t][j][k - t])) {
dp[i][j][k] = true;
break;
}
}
}
}
}
}
return dp[][][n];
}
};

下面这种解法和第一个解法思路相同,只不过没有用排序算法,而是采用了类似于求异构词的方法,用一个数组来保存每个字母出现的次数,后面判断 Scramble 字符串的方法和之前的没有区别:

解法四:

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(), m[] = {};
for (int i = ; i < n; ++i) {
++m[s1[i] - 'a'];
--m[s2[i] - 'a'];
}
for (int i = ; i < ; ++i) {
if (m[i] != ) return false;
}
for (int i = ; i < n; ++i) {
if ((isScramble(s1.substr(, i), s2.substr(, i)) && isScramble(s1.substr(i), s2.substr(i))) || (isScramble(s1.substr(, i), s2.substr(n - i)) && isScramble(s1.substr(i), s2.substr(, n - i)))) {
return true;
}
}
return false;
}
};

下面这种解法实际上是解法二的递归形式,我们用了 memo 数组来减少了大量的运算,注意这里的 memo 数组一定要有三种状态,初始化为 -1,区域内为 scramble 是1,不是 scramble 是0,这样就避免了已经算过了某个区间,但由于不是 scramble,从而又进行一次计算,从而会 TLE,感谢网友 bambu 的提供的思路,参见代码如下:

解法五:

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>>> memo(n, vector<vector<int>>(n, vector<int>(n + , -)));
return helper(s1, s2, , , n, memo);
}
bool helper(string& s1, string& s2, int idx1, int idx2, int len, vector<vector<vector<int>>>& memo) {
if (len == ) return true;
if (len == ) memo[idx1][idx2][len] = s1[idx1] == s2[idx2];
if (memo[idx1][idx2][len] != -) return memo[idx1][idx2][len];
for (int k = ; k < len; ++k) {
if ((helper(s1, s2, idx1, idx2, k, memo) && helper(s1, s2, idx1 + k, idx2 + k, len - k, memo)) || (helper(s1, s2, idx1, idx2 + len - k, k, memo) && helper(s1, s2, idx1 + k, idx2, len - k, memo))) {
return memo[idx1][idx2][len] = ;
}
}
return memo[idx1][idx2][len] = ;
}
};

Github 同步地址:

https://github.com/grandyang/leetcode/issues/87

参考资料:

https://leetcode.com/problems/scramble-string/

https://leetcode.com/problems/scramble-string/discuss/29387/Accepted-Java-solution

https://leetcode.com/problems/scramble-string/discuss/29392/Share-my-4ms-c%2B%2B-recursive-solution

https://leetcode.com/problems/scramble-string/discuss/29396/Simple-iterative-DP-Java-solution-with-explanation

https://leetcode.com/problems/scramble-string/discuss/29394/My-C%2B%2B-solutions-(recursion-with-cache-DP-recursion-with-cache-and-pruning)-with-explanation-(4ms)

LeetCode All in One 题目讲解汇总(持续更新中...)

[LeetCode] 87. Scramble String 搅乱字符串的更多相关文章

  1. [LeetCode] 87. Scramble String 爬行字符串

    Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrin ...

  2. [leetcode]87. Scramble String字符串树形颠倒匹配

    Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrin ...

  3. [leetcode] 87. Scramble String (Hard)

    题意: 判断两个字符串是否互为Scramble字符串,而互为Scramble字符串的定义: 字符串看作是父节点,从字符串某一处切开,生成的两个子串分别是父串的左右子树,再对切开生成的两个子串继续切开, ...

  4. leetCode 87.Scramble String (拼凑字符串) 解题思路和方法

    Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrin ...

  5. Leetcode#87 Scramble String

    原题地址 两个字符串满足什么条件才称得上是scramble的呢? 如果s1和s2的长度等于1,显然只有s1=s2时才是scramble关系. 如果s1和s2的长度大于1,那么就对s1和s2进行分割,划 ...

  6. leetcode@ [87] Scramble String (Dynamic Programming)

    Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrin ...

  7. [LeetCode] Scramble String 爬行字符串

    Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrin ...

  8. 【一天一道LeetCode】#87. Scramble String

    一天一道LeetCode 本系列文章已全部上传至我的github,地址:ZeeCoder's Github 欢迎大家关注我的新浪微博,我的新浪微博 欢迎转载,转载请注明出处 (一)题目 Given a ...

  9. [LintCode] Scramble String 爬行字符串

    Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrin ...

随机推荐

  1. 一些 Java 和 Android 的参考资料

    1. .net程序员转战android第三篇---登录模块之静态登录 2. .net程序员转战android第二篇---牛刀小试 3. .net程序员转战android第一篇---环境部署 4. 一些 ...

  2. kubernetes 之一些报错

    1.kubelet与docker的Cgroup Driver不一致导致的报错 7月 :: kubeadm-master kubelet[]: W0701 :: watcher.go:] Error w ...

  3. docker命令之link

    1.新建两台容器,第二台(busybox_2)link到第一台(busybox_1) [root@localhost ~]# docker run -d -it --name busybox_1 bu ...

  4. Flink,Storm,SparkStreaming性能对比

    Yahoo 的 Storm 团队曾发表了一篇博客文章 ,并在其中展示了 Storm.Flink 和 Spark Streaming 的性能测试结果.该测试对于业界而言极 具价值,因为它是流处理领域的第 ...

  5. SSM手动实现分页逻辑---非PageHelper方式

    第一种方法:查询出所有数据再分页 分析: 分页时,需要获得前台传来的两个参数,分别为pageNo(第几页数据),pageSize(每页的条数); 根据这两个参数来计算出前端需要的数据是查出数据list ...

  6. java的Class<T>与类型信息

    类型是一个数据符号,代表着数据的内存布局和访问规则. default public <T> T xxxxx(Class<T> xclass) throws Exception ...

  7. 升级 ASP.NET Core 3.0 设置 JSON 返回 PascalCase 格式与 SignalR 问题

    由于一些 JS 组件要求 JSON 格式是 PascalCase 格式,新版本 ASP.NET Core 3.0 中默认移除了 Newtonsoft.Json ,使用了微软自己实现的 System.T ...

  8. 如何判断服务器之间的服务是否可用?ping 还是 telnet?

    1. 背景 机器A需要调用机器B的服务,为此要保证服务的可用性,我们有时候用ping,有时候用telent来验证机器A和B的连通性,但有时候会出现这种情况,A可以ping通B,但A调用B的服务会一直报 ...

  9. 【USTC】雨

    自九月初来到科大,到现在已经一个月.这几天是国庆假期(祖国七十华诞,祝福祖国),我没有回家,白天在实验室,晚上去找小许. 今天下雨了,不大,但是温度降了大约10度,上次下雨还是九月初开学那几日. 9月 ...

  10. StringBuilder.sb.AppendLine();

    StringBuilder sb=new StringBuilder(); sb.AppendLine("The world is fair and wonderful.Everything ...