套路回溯动态规划题
87. 扰乱字符串
难度困难
使用下面描述的算法可以扰乱字符串 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 。
示例 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.length1 <= s1.length <= 30s1和s2由小写英文字母组成
第一次一看就懵逼的每日一题。
看到题目提到递归就想到回溯(暴搜法):
class Solution {
public boolean isScramble(String s1, String s2) {
return backroll(s1,s2);
}
boolean backroll(String temp1,String temp2){//判断s1的[0,i),[i,n)是否分别与s2的[0,n-i),[i,n)或者[0,n-i),[n-i,n)匹配
if(temp1.equals(temp2)){//截止条件很难判断,匹配到只有一个元素的时候不相等就是不匹配
return true;
}
int len = temp1.length();
for(int i = 1;i < len;i++){
String a = temp1.substring(0,i),b = temp1.substring(i);
String c = temp2.substring(0,i),d = temp2.substring(i);
if(backroll(a,c) && backroll(b,d)){
return true;
}
String e = temp2.substring(0,len-i),f = temp2.substring(len-i);
if(backroll(a,f) && backroll(b,e)){
return true;
}
}
return false;//遍历了s1[0,]1~len]都没有一个true返回,说明没有一个字符串是和s2对应的
}
}
但是发现样例一大就超时,需要剪枝。
对两个字符串进行词频分析,因为只要是字符一样而数量不一致就说明不是置换过来的,可以剪枝,于是:
class Solution {
public boolean isScramble(String s1, String s2) {
return backroll(s1,s2);
}
boolean backroll(String temp1,String temp2){//判断s1的[0,i),[i,n)是否分别与s2的[0,n-i),[i,n)或者[0,n-i),[n-i,n)匹配
if(temp1.equals(temp2)){//截止条件很难判断,匹配到只有一个元素的时候不相等就是不匹配
return true;
}
if(check(temp1,temp2) == false){
return false;
}
int len = temp1.length();
for(int i = 1;i < len;i++){
String a = temp1.substring(0,i),b = temp1.substring(i);
String c = temp2.substring(0,i),d = temp2.substring(i);
if(backroll(a,c) && backroll(b,d)){
return true;
}
String e = temp2.substring(0,len-i),f = temp2.substring(len-i);
if(backroll(a,f) && backroll(b,e)){
return true;
}
}
return false;//遍历了s1[0,]1~len]都没有一个true返回,说明没有一个字符串是和s2对应的
}
boolean check(String temp1,String temp2){
int len1 = temp1.length();
int len2 = temp2.length();
if(len1 != len2){
return false;
}
int[] res1 = new int[27];
int[] res2 = new int[27];
//桶排序比较两个字符串的词频是否一样
for(int i = 0;i < len1;i++){
res1[temp1.charAt(i)-'a']++;
res2[temp2.charAt(i)-'a']++;
}
for(int i = 0;i < 27;i++){
if(res1[i] != res2[i]) {
return false;
}
}
return true;
}
}
结果在最后两个测试用例的时候倒下:
状态:超出时间限制
最后执行的输入:
"eebaacbcbcadaaedceaaacadccd"
"eadcaacabaddaceacbceaabeccd"
加了我以为的记忆化搜索,还是会超时:(答案还是卡在第286个用例,也就是上面那个,所以属于压根优化不到点子上)
class Solution {
public boolean isScramble(String s1, String s2) {
int sumlen = s1.length();
int[][][] dp = new int[sumlen+1][sumlen+1][sumlen+1];
return backroll(0,0,sumlen,s1,s2,dp);
}
boolean backroll(int s1start,int s2start,int length,String s1,String s2,int[][][] dp){//判断s1的[0,i),[i,n)是否分别与s2的[0,i),[i,n)或者[0,n-i),[n-i,n)匹配
if(length == 1 && s1.substring(s1start,s1start+1).equals(s2.substring(s2start,s2start+1))){//截止条件很难判断,匹配到只有一个元素的时候不相等就是不匹配
return true;
}
if(check(s1.substring(s1start,s1start+length),s2.substring(s2start,s2start+length)) == false){
return false;
}
for(int i = 1;i < length;i++){
if((dp[s1start][s2start][i] == 1 && dp[s1start][s2start+i][length-i] == 1) || backroll(s1start,s2start,i,s1,s2,dp) && backroll(s1start+i,s2start+i,length-i,s1,s2,dp)){
dp[s1start][s2start][length] = 1;
return true;
}
if((dp[s1start][s2start+length-i][i] == 1 && dp[s1start+i][s2start][length-i] == 1) || backroll(s1start,s2start+length-i,i,s1,s2,dp) && backroll(s1start+i,s2start,length-i,s1,s2,dp)){
dp[s1start][s2start][length] = 1;
return true;
}
}
return false;//遍历了s1[0,1~len]都没有一个true返回,说明没有一个字符串是和s2对应的
}
boolean check(String temp1,String temp2){
int len1 = temp1.length();
int len2 = temp2.length();
if(len1 != len2){
return false;
}
int[] res1 = new int[27];
int[] res2 = new int[27];
//桶排序比较两个字符串的词频是否一样
for(int i = 0;i < len1;i++){
res1[temp1.charAt(i)-'a']++;
res2[temp2.charAt(i)-'a']++;
}
for(int i = 0;i < 27;i++){
if(res1[i] != res2[i]) {
return false;
}
}
return true;
}
}
最后才发现是dp用的不熟,没有注意哪些地方很耗时导致超时,而且也没有在一开始就利用dp,而是后面循环判断才利用dp判值,dp要尽量一开始就判断,统一利用,减少查表和递归后续代码。
class Solution {
public boolean isScramble(String s1, String s2) {
int sumlen = s1.length();
int[][][] dp = new int[sumlen+1][sumlen+1][sumlen+1];
boolean res = backroll(0,0,sumlen,s1,s2,dp);
// for(int i = 0;i <= sumlen;i++){
// for(int j = 0;j <= sumlen;j++){
// for(int z = 0;z <= sumlen;z++){
// System.out.print(dp[i][j][z]+" ");
// }
// }
// }
return res;
}
boolean backroll(int s1start,int s2start,int length,String s1,String s2,int[][][] dp){//判断s1的[0,i),[i,n)是否分别与s2的[0,i),[i,n)或者[0,n-i),[n-i,n)匹配
if(dp[s1start][s2start][length] != 0){//截止条件很难判断,匹配到只有一个元素的时候不相等就是不匹配,一定要有-1的区别,毕竟不匹配就是-1,而不是没有匹配过的0
// System.out.println("=="+s1start+" "+s2start+"==");
return dp[s1start][s2start][length] == 1;
}
String a = s1.substring(s1start,s1start+length),b = s2.substring(s2start,s2start+length);
if(a.equals(b)){
dp[s1start][s2start][length] = 1;
return true;
}
if(check(a,b) == false){
dp[s1start][s2start][length] = -1;
return false;
}
for(int i = 1;i < length;i++){ //匹配s1[s1start,s1start+length]和s2[s2start,s2start+length],切割点从1到length-1。注意length是每次递归的长度,不是固定s1的长度。
if(backroll(s1start,s2start,i,s1,s2,dp) && backroll(s1start+i,s2start+i,length-i,s1,s2,dp)){
dp[s1start][s2start][length] = 1;
return true;
}
if(backroll(s1start,s2start+length-i,i,s1,s2,dp) && backroll(s1start+i,s2start,length-i,s1,s2,dp)){
dp[s1start][s2start][length] = 1;
return true;
}
}
dp[s1start][s2start][length] = -1;
return false;//遍历了s1[0,1~len]都没有一个true返回,说明没有一个字符串是和s2对应的
}
boolean check(String temp1,String temp2){
int len1 = temp1.length();
int len2 = temp2.length();
if(len1 != len2){
return false;
}
int[] res1 = new int[27];
int[] res2 = new int[27];
//桶排序比较两个字符串的词频是否一样
for(int i = 0;i < len1;i++){
res1[temp1.charAt(i)-'a']++;
res2[temp2.charAt(i)-'a']++;
}
for(int i = 0;i < 27;i++){
if(res1[i] != res2[i]) {
return false;
}
}
return true;
}
}
总结:看到题目提到递归或者让你做很多选择的时候,就要考虑回溯,超时了再考虑记忆化搜索,而记忆化搜索就是接近动态规划了。一般没有回溯+记忆化搜索解决不了的题。

浙公网安备 33010602011771号