判断2个字符串s1和s2是否互为玄变字符串(扰乱字符串)

判断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即互为扰乱字符,互为玄变字符串
图1
别忘了,切刀的位置,可以任意,只要保证左右都有字符即可
往下,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互为玄变【必须两边同时满足才行】,则算互为玄变
图2
——如果交换,则L1–L1与R2–R2互为玄变,且,L1+1–R1与L2–R2-1互为玄变,则算互为玄变
图3
如果上面所有情况都不行,那返回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;
        }

这里要明白的就是扣清楚边界,非常烦人,但是要搞清楚,才能解决
图4
暴力递归的主函数:

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)多练,多见就熟悉了,想清楚算法宏观调度,一切就都好办了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值