使用下面描述的算法可以扰乱字符串s得到字符串t:
- 如果字符串的长度为 1 ,算法停止
- 如果字符串的长度 > 1 ,执行下述步骤:
- 在一个随机下标处将字符串分割成两个非空的子字符串。即,如果已知字符串
s,则可以将其分成两个子字符串x和y,且满足s = x + y。 - 随机 决定是要「交换两个子字符串」还是要「保持这两个子字符串的顺序不变」。即,在执行这一步骤之后,
s可能是s = x + y或者s = y + x。 - 在
x和y这两个子字符串上继续从步骤 1 开始递归执行此算法。
给你两个 长度相等 的字符串s1和 s2,判断 s2 是否是 s1 的扰乱字符串。如果是,返回 true ;否则,返回false。
提示:
s1.length == s2.length1 <= s1.length <= 30s1和s2由小写英文字母组成
示例 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
题解:
/**
* @description: 递归回溯 TC:O(2^n) SC:O(1)
* @author: JunLiangWang
* @param {*} s1 给定字符串s1
* @param {*} s2 给定字符串s2
* @return {*}
*/
function recursionBacktracking(s1, s2) {
/**
* 该方案使用递归回溯模拟生成扰乱字符串过程
*/
// 如果两字符串相等,s2则为s1的扰乱字符串
if (s1 == s2) return true;
// 如果两字符串长度不相等,s2则不为s1的扰乱字符串
if (s1.length != s2.length) return false;
// 从索引1(由于索引0的情况s1==s2已判断)遍历两字符
// 串(s1,s2长度相等,因此使用s1的长度即可)
for (let i = 1; i < s1.length; i++) {
// 从i处分割字符串s1,preString为前缀
// lastString为后缀
const preString = s1.substr(0, i),
lastString = s1.substr(i),
// 从i处分割字符串s2,comparePreString为前缀
// compareLastString为后缀
comparePreString = s2.substr(0, i),
compareLastString = s2.substr(i);
// 判断无交换的情况,即:s2=preString+lastString,继续
// 递归判断preString是否comparePreString的扰乱字符串
// lastString是否compareLastString的扰乱字符串,如果是
// 证明s2为s1的扰乱字符串,此时返回true
if (recursionBacktracking(preString, comparePreString) && recursionBacktracking(lastString, compareLastString)) return true;
// 否则判断有交换的情况,即:s2=lastString+preString,
// 从后到前以i位置分割字符串,获得交换后的preString,lastString
const exchangeComparePreString = s2.substr(0, s2.length - i),
exchangeCompareLastString = s2.substr(s2.length - i);
// 继续递归判断preString是否exchangeCompareLastString的扰乱字符串
// lastString是否exchangeComparePreString的扰乱字符串,如果是
// 证明s2为s1的扰乱字符串,此时返回true
if (recursionBacktracking(preString, exchangeCompareLastString) && recursionBacktracking(lastString, exchangeComparePreString)) return true
}
// 如果遍历完成仍无,则返回false
return false;
}
/**
* @description: 记忆搜索 TC:O(2^n) SC:O(n)
* @author: JunLiangWang
* @param {*} s1 给定字符串s1
* @param {*} s2 给定字符串s2
* @return {*}
*/
function memorySearch(s1, s2) {
/**
* 我们可以对上述递归回溯方式进行优化,加入记忆搜索。
* 对于每次递归回溯我们将其结果添加到map中,到后面
* 有重复递归内容,我们可直接查询map获取结果,而无
* 需递归,从而优化时间复杂度。
*/
// 定义记录递归结果的map
let cache = new Map();
/**
* @description: 递归回溯 TC:O(2^n) SC:O(1)
* @author: JunLiangWang
* @param {*} str1 给定字符串str1
* @param {*} str2 给定字符串str2
* @return {*}
*/
function recursionBacktracking(str1, str2) {
// 生成map中的key
let key = str1 + '+' + str2;
// 如果两字符串相等,str2则为str1的扰乱字符串
// 并向map添加递归结果
if (str1 == str2) {
cache.set(key, true);
return true;
}
// 如果两字符串长度不相等,s2则不为s1的扰乱字符串
// 并向map添加递归结果
if (str1.length != str2.length) {
cache.set(key, false);
return false;
}
// 查询map中有无记录的结果,有则直接返回其递归结果。
let result = cache.get(key)
if (result != undefined) return result;
// 从索引1(由于索引0的情况s1==s2已判断)遍历两字符
// 串(s1,s2长度相等,因此使用s1的长度即可)
for (let i = 1; i < str1.length; i++) {
// 从i处分割字符串s1,preString为前缀
// lastString为后缀
const preString = str1.substr(0, i),
lastString = str1.substr(i),
// 从i处分割字符串s2,comparePreString为前缀
// compareLastString为后缀
comparePreString = str2.substr(0, i),
compareLastString = str2.substr(i);
// 判断无交换的情况,即:s2=preString+lastString,继续
// 递归判断preString是否comparePreString的扰乱字符串
// lastString是否compareLastString的扰乱字符串,如果是
// 证明s2为s1的扰乱字符串,此时返回true,并向map添加递归结果
if (recursionBacktracking(preString, comparePreString) && recursionBacktracking(lastString, compareLastString)) {
cache.set(key, true);
return true;
}
// 否则判断有交换的情况,即:s2=lastString+preString,
// 从后到前以i位置分割字符串,获得交换后的preString,lastString
const exchangeComparePreString = str2.substr(0, str2.length - i),
exchangeCompareLastString = str2.substr(str2.length - i);
// 继续递归判断preString是否exchangeCompareLastString的扰乱字符串
// lastString是否exchangeComparePreString的扰乱字符串,如果是
// 证明s2为s1的扰乱字符串,此时返回true,并向map添加递归结果
if (recursionBacktracking(preString, exchangeCompareLastString) && recursionBacktracking(lastString, exchangeComparePreString)) {
cache.set(key, true);
return true;
}
}
// 如果遍历完成仍无,则返回false,并向map添加递归结果
cache.set(key, false);
return false;
}
// 执行递归,返回结果
return recursionBacktracking(s1, s2);
}
/**
* @description: 动态规划 TC:O(n^4) SC:O(n^3)
* @author: JunLiangWang
* @param {*} s1 给定字符串s1
* @param {*} s2 给定字符串s2
* @return {*}
*/
function dp(s1,s2){
/**
* 该方式使用动态规划的方式,给定两字符串S与T,
* 假设T是S的扰乱字符串则有以下情况:
*
* 1.T==S
* 2.S能够分割字符串为S1,S2;T同样能够分割字符串为T1,T2,
* 此时会出现两种情况:
* 情况一:无需交换,S1==T1,S2==T2
* 情况二:需要交换,S1==T2,S2==T1
*
* 此时即可将一个复杂问题划分若干子问题,因此我们可以定义一个
* 三维数组(DPArray),第一个维度索引代表S的起点位置,第二个维
* 度索引代表T的起点位置,第三个维度代表长度,DPArray[i][j][k]
* 则表示了S从i开始k长度字符串是否为T从j开始k长度字符串的扰乱字符
*
*/
// 如果两字符串相等,s2则为s1的扰乱字符串
if(s1===s2)return true;
// 定义DP数组
let DPArray=new Array(s1.length).fill(0)
.map(()=>new Array(s1.length).fill(0)
.map(()=>new Array(s1.length+1).fill(false)))
// 初始化单个字符的情况
for(let i=0;i<DPArray.length;i++){
for(let j=0;j<DPArray.length;j++){
DPArray[i][j][1]=s1[i]==s2[j]
}
}
// 枚举区间长度 2~字符串长度
for(let len=2;len<=s1.length;len++){
// 枚举 s1 中的起点位置
for(let i=0;i<=s1.length-len;i++){
// 枚举 s2 中的起点位置
for(let j=0;j<=s1.length-len;j++){
// 枚举划分位置
for(let k=1;k<len;k++){
// 第一种情况:S1 == T1, S2 == T2
if(DPArray[i][j][k]&&DPArray[i + k][j + k][len - k]){
DPArray[i][j][len]=true
break;
}
// 第一种情况:S1 == T2, S2 == T1
// S1 起点 i,T2 起点 j + 前面那段长度 len-k ,S2 起点 i + 前面长度k
if(DPArray[i][j+len-k][k]&&DPArray[i+k][j][len-k]){
DPArray[i][j][len]=true
break;
}
}
}
}
}
// 返回结果
return DPArray[0][0][s1.length];
}
来源:力扣(LeetCode)