判断2个字符串s1和s2是否互为玄变字符串(扰乱字符串)
提示:这个题是范围上的尝试,因为涉及到字符串的交换,字符串不是永远以0开头
题目
本题是LeetCode 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 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/scramble-string
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
一、审题
示例:abcd
咱在i=位置,切一刀
分为a bcd
咱可以随机决定,这两部分,是交换,还是不交换
不交换即:abcd
交换a与bcd得bcda
abcd与bcda即互为扰乱字符,互为玄变字符串
别忘了,切刀的位置,可以任意,只要保证左右都有字符即可
往下,bcd这部分,怎么切,交换与否,随机的,最次保留1个字符
然后组成很多的字符串,都与s原串互为玄变字符串(扰乱字符串)
二、解题
先过滤非法字符串:
至少,s1和s2他们俩的字符种类要一样,为啥呢?
因为你s1=abc,s2=abd,怎么切s1都不可能是s2,哪里来的互为玄变串
那我们如何知道2字符串都是同类字符,而且数量还一样呢?
先拿s1统计词频,自然用数组当哈希表【之前咱们介绍过,一个长256的数组,就可以统计词频】即可:
然后再拿s2消除词频,在此过程中,一旦返现有哪个词频竟然<0,说明这个字符在s2中是多出来的,失败
public static boolean isValid(char[] str1, char[] str2){
if (str1.length != str2.length) return false;
//由于都是小写字母,好说
int[] map = new int[256];
for (int i = 0; i < str1.length; i++) {
map[str1[i]]++;//统计词频
}
for (int i = 0; i < str2.length; i++) {
map[str2[i]]--;//词频降低
if (map[str2[i]] < 0) return false;//最低不能为0,否则就是多了一个类型
//只要你有不同的字符,另一个字符一定缺,必然小于0
}
return true;
}
解题方法:
根据案例所示,既然已经切刀了,那么,肯定是要枚举刀所切的位置
至少从i=1……N-1,位置,每个地方都要切一刀,然后看看,哪一刀切下去之后,
继续递归两边的子串,直到……子串只剩1个字符,中间交换不交换,分2种情况
其中有一种切分的可能性都行的,return true;
如果所有的情况拼不出来s2,则返回false;
因此我们定义一个函数:f(s1,s2,L1,R1,L2,R2)
为,是否s1的L1–R1通过切分,能找到与s2的L2–R2互为玄变串?
则主函数自然调用f(s1,s2,0,N1-1,0,N2-1)
这里有4个变量,过于复杂了,往往互联网大厂的动态规划题,最多不会超过3个变量的
本题,巧了,既然是互为玄变的显然s1的长度=s2的长度
所以R1-L1=R2-L2=k
因此我们只需要定义k替代R1和R2
R1=L1+k
R2=L2+k
故函数变为:f(s1,s2,L1,L2,k) 主函数调用f(s1,s2,L1,L2,N),总共N长度,都是s1和s2的长,一样长
(1)当k=1时,自然就只剩下一个字符了
就看s1[0]=s2[0]与否?相等必然就是玄变的,不相等就不是
(2)从i=1……k-1每个位置枚举一遍,有一种情况满足即可:
随便切一刀,交换还是不交换左右呢?有下面2种情况,有其中1个满足都行
——如果不交换则L1–L1与L2–L2互为玄变,且,L1+1–R1与L2+1–R2互为玄变【必须两边同时满足才行】,则算互为玄变
——如果交换,则L1–L1与R2–R2互为玄变,且,L1+1–R1与L2–R2-1互为玄变,则算互为玄变
如果上面所有情况都不行,那返回false;
看代码:
//递归
public static boolean f(char[] str1, char[] str2, int L1, int L2, int N){
if (N == 1) return str1[L1] == str2[L2];//base case,就一个字符,两者必须相等,否则扯淡
//枚举N-1刀,有一个行就OK
for (int i = 1; i < N; i++) {
//两种情况,咱不交换,和交换,有一个行就行
boolean noChange = f(str1, str2, L1, L2, i) &&
f(str1, str2, L1 + i, L2 + i, N - i);//s1左边长为i个字符,与s2左边长为i个字符比
boolean exChange = f(str1, str2, L1, L2 + N - i, i) &&
f(str1, str2, L1 + i, L2, N - i);//s1左边长为i个字符,,与s2右边长为i个字符比
if (noChange || exChange) return true;
}
//一种情况都不行
return false;
}
这里要明白的就是扣清楚边界,非常烦人,但是要搞清楚,才能解决
暴力递归的主函数:
public boolean isScramble(String s1, String s2) {
//大流程:
//设定这么一个范围上的尝试
//不妨设Boolean 的函数f,送入str1,str2,L1,L2,N
//str1的L1--L1+N-1这段字符串,与str2的L2--L2+N-1这一段字符串,是否是互为玄变的字符串【扰乱字符串】
//一个字符串咱们第一刀可以这样切
//a|bcde,ab|cde,abc|de,abcd|e---每种切法,有一种能互玄就行
//N==5的话,有N-1==4种切法
//对于每一种刀法,s1和s2是否互为玄变,看以下两种情况,有一种能互为玄变字符串就行
//第一种:切完咱不换,s1左边与s2左边互玄切,s1右边与s2右边互玄就行:比如s1==a|bcde s2==a|bcde
//L1--L1与L2--L2互玄,且L1+1--R1,L2+1--R2互玄就行
//第一种:切完交换,s1左边与s2右边互玄切,s1右边与s2左边互玄就行:比如s1==a|bcde s2==bcde|a
//L1--L1与L2--L2互玄,且L1+1--R1,L2+1--R2互玄就行
//由于俩字符串等长,所以R1,R2可以用一个量N代表就行
if (s1 =="" && s2 == "") return true;
if (s1 =="" && s2 != "") return false;
if (s1 !="" && s2 == "") return false;
if (s1.equals(s2)) return true;
//主函数先过滤那些不合法的,就俩字符类型竟然都不相等这种
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
if (!isValid(str1, str2)) return false;
return f(str1, str2, 0, 0, str1.length);//两者全部去数,看看互玄吗
}
当然,这里三个参数,可以改为傻缓存,暴力递归比较耗时,用dp存一下计算过的结果就可以加速运算:
//DP记忆化搜素
public boolean isScrambleDP(String s1, String s2) {
//大流程:
//设定这么一个范围上的尝试
//不妨设Boolean 的函数f,送入str1,str2,L1,L2,N
//str1的L1--L1+N-1这段字符串,与str2的L2--L2+N-1这一段字符串,是否是互为玄变的字符串【扰乱字符串】
//一个字符串咱们第一刀可以这样切
//a|bcde,ab|cde,abc|de,abcd|e---每种切法,有一种能互玄就行
//N==5的话,有N-1==4种切法
//对于每一种刀法,s1和s2是否互为玄变,看以下两种情况,有一种能互为玄变字符串就行
//第一种:切完咱不换,s1左边与s2左边互玄切,s1右边与s2右边互玄就行:比如s1==a|bcde s2==a|bcde
//L1--L1与L2--L2互玄,且L1+1--R1,L2+1--R2互玄就行
//第一种:切完交换,s1左边与s2右边互玄切,s1右边与s2左边互玄就行:比如s1==a|bcde s2==bcde|a
//L1--L1与L2--L2互玄,且L1+1--R1,L2+1--R2互玄就行
//由于俩字符串等长,所以R1,R2可以用一个量N代表就行
if (s1 =="" && s2 == "") return true;
if (s1 =="" && s2 != "") return false;
if (s1 !="" && s2 == "") return false;
if (s1.equals(s2)) return true;
//主函数先过滤那些不合法的,就俩字符类型竟然都不相等这种
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
if (!isValid(str1, str2)) return false;
//L1,L2取值0--N-1,长度也是1--N个
int N = str1.length;
int[][][] dp = new int[N][N][N+1];
//搞int类型,dpijk==-1,没有求过
//0==false
//1==true,这样规定好办了就
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
for (int k = 0; k <= N; k++) {
dp[i][j][k] = -1;
}
}
}
return fDP(str1, str2, 0, 0, N, dp);//两者全部去数,看看互玄吗
}
//递归
public static boolean fDP(char[] str1, char[] str2, int L1, int L2, int N, int[][][] dp){
if (dp[L1][L2][N] != -1) return dp[L1][L2][N] == 1;//真的是1就是true,否则就是F
if (N == 1) {
dp[L1][L2][N] = str1[L1] == str2[L2] ? 1 : 0;
return dp[L1][L2][N] == 1;//真的是1就是true,否则F
}//base case,就一个字符,两者必须相等,否则扯淡
//枚举N-1刀,有一个行就OK
for (int i = 1; i < N; i++) {
//两种情况,咱不交换,和交换,有一个行就行
boolean noChange = fDP(str1, str2, L1, L2, i, dp) &&
fDP(str1, str2, L1 + i, L2 + i, N - i, dp);//s1左边长为i个字符,与s2左边长为i个字符比
boolean exChange = fDP(str1, str2, L1, L2 + N - i, i, dp) &&
fDP(str1, str2, L1 + i, L2, N - i, dp);//s1左边长为i个字符,,与s2右边长为i个字符比
if (noChange || exChange) {
dp[L1][L2][N] = 1;
return dp[L1][L2][N] == 1;
}
}
//一种情况都不行
dp[L1][L2][N] = 0;
return dp[L1][L2][N] == 1;//这里返回false
}
测试代码:
public static void test(){
Solution solution = new Solution();
String s1 = "great";
String s2 = "rgeat";
String s3 = "tgrea";
//都是
System.out.println(solution.isScramble(s1, s2));
System.out.println(solution.isScrambleDP(s1, s2));
System.out.println(solution.isScramble(s1, s3));
System.out.println(solution.isScrambleDP(s1, s3));
}
public static void main(String[] args) {
test();
}
总结
提示:重要经验:
1)本题,2个串,平时的话,经常是样本位置对应模型,但是往往,样本位置对应是考虑以i开头或者结尾的字符情况如何,这里显然涉及交换,没法,并不是永远以0位置开头,而是有一个范围的,故直接用范围上的尝试模型
2)多练,多见就熟悉了,想清楚算法宏观调度,一切就都好办了。