LeetCode每日签到题目(2021-04-16)《扰乱字符串》官方答案抄袭

题目描述

使用下面描述的算法可以扰乱字符串 s 得到字符串 t :

  1. 如果字符串的长度为 1 ,算法停止
  2. 如果字符串的长度 > 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.length
  • 1 <= s1.length <= 30
  • s1 和 s2 由小写英文字母组成

官方解答

思路与算法

显然「扰乱字符串」的关系是具有对称性的,即如果 s 1 s_1 s1 s 2 s_2 s2 的扰乱字符串,那么 s 2 s_2 s2 也是 s 1 s_1 s1 的扰乱字符串。为了叙述方便,我们称这种情况下, s 1 s_1 s1 s 2 s_2 s2 是「和谐」的。

那么如何判断 s 1 s_1 s1 s 2 s_2 s2 是否「和谐」呢?我们首先可以想到几个简单的判断方法:

  • 如果 s 1 = s 2 s_1 = s_2 s1=s2,那么它们是「和谐」的;
  • 如果 s 1 s_1 s1 s 2 s_2 s2 的长度不同,那么它们一定不是「和谐」的;
  • 如果 s 1 s_1 s1 中某个字符 c c c 出现了 x 1 x_1 x1 次,而 c c c s 2 s_2 s2 中出现了 x 2 x_2 x2 次,且 x 1 ≠ x 2 x_1 \neq x_2 x1=x2,那么它们一定不是「和谐」的。这是因为任意操作都不会改变一个字符串中的字符种类以及数量。

那么对于剩下的情况,我们该如何判断呢?我们可以从 s 1 s_1 s1 的分割方法入手。假设 s 1 s_1 s1 作为根节点时被分割成了 l ( s 1 ) l(s_1) l(s1) 以及 r ( s 1 ) r(s_1) r(s1) 两个子串,那么:

  • 如果 l ( s 1 ) l(s_1) l(s1) r ( s 1 ) r(s_1) r(s1) 没有被交换,那么 s 2 s_2 s2 需要存在一种分割方法 s 2 = l ( s 2 ) + r ( s 2 ) s_2 = l(s_2) + r(s_2) s2=l(s2)+r(s2),使得 l ( s 1 ) l(s_1) l(s1) l ( s 2 ) l(s_2) l(s2) 是「和谐」的,并且 r ( s 1 ) r(s_1) r(s1) r ( s 2 ) r(s_2) r(s2) 也是「和谐」的;
  • 如果 l ( s 1 ) l(s_1) l(s1) r ( s 1 ) r(s_1) r(s1) 被交换了,那么 s 2 s_2 s2 需要存在一种分割方法 s 2 = l ( s 2 ) + r ( s 2 ) s_2 = l(s_2) + r(s_2) s2=l(s2)+r(s2),使得 l ( s 1 ) l(s_1) l(s1) r ( s 2 ) r(s_2) r(s2) 是「和谐」的,并且 r ( s 1 ) r(s_1) r(s1) l ( s 2 ) l(s_2) l(s2) 也是「和谐」的。

这样一来,我们就把原本需要解决的问题划分成了两个本质相同,但规模更小的子问题,因此可以考虑使用动态规划解决。

f ( s 1 , s 2 ) f(s_1, s_2) f(s1,s2) 表示 s 1 s_1 s1 s 2 s_2 s2 是否「和谐」,那么我们可以写出状态转移方程:

f ( s 1 , s 2 ) = { True , s 1 = s 2 False , 存 在 某 个 字 符   c , 它 在   s 1   和   s 2   中 的 出 现 次 数 不 同 f(s_1, s_2) = \begin{cases} \text{True}, & \quad s_1=s_2 \\ \text{False}, & \quad 存在某个字符~c,它在~s_1~和~s_2~中的出现次数不同 \\ \end{cases} f(s1,s2)={True,False,s1=s2 c s1  s2 

因为题目保证给定的原始字符串的长度相同,因此我们只需要判断上面的两种情况。如果 s 1 s_1 s1 s 2 s_2 s2 不符合这两种情况,那么我们需要枚举分割点。

s 1 s_1 s1 s 2 s_2 s2 的长度为 n n n,我们用 s 1 ( x , y ) s_1(x, y) s1(x,y) 表示从 s 1 s_1 s1 从第 x x x 个字符(从 0 0 0 开始编号)开始,长度为 y y y 的子串。由于分割出的两个字符串不能为空串,那么其中一个字符串就是 s 1 ( 0 , i ) s_1(0, i) s1(0,i),另一个字符串是 s 1 ( i , n − i ) s_1(i, n-i) s1(i,ni)

  • 对于 l ( s 1 ) l(s_1) l(s1) r ( s 1 ) r(s_1) r(s1) 没有被交换的情况, s 2 s_2 s2 同样需要被分为 s 2 ( 0 , i ) s_2(0, i) s2(0,i) 以及 s 2 ( i , n − i ) s_2(i, n-i) s2(i,ni),否则长度不同的字符串是不可能「和谐」的。因此我们可以写出状态转移方程:

    f ( s 1 , s 2 ) = ⋁ i = 1 n − 1 ( f ( s 1 ( 0 , i ) , s 2 ( 0 , i ) ) ∧ f ( s 1 ( i , n − i ) , s 2 ( i , n − i ) ) ) f(s_1, s_2) = \bigvee_{i=1}^{n-1} \big( f(s_1(0, i), s_2(0, i)) \wedge f(s_1(i, n-i), s_2(i, n-i)) \big) f(s1,s2)=i=1n1(f(s1(0,i),s2(0,i))f(s1(i,ni),s2(i,ni)))

    其中 ∧ \wedge 表示与运算,即 s 1 s_1 s1 s 2 s_2 s2 分割出的两对字符串都要是「和谐」的; ∨ \vee
    表示或运算,即只要有一种满足要求的分割方法, s 1 s_1 s1 s 2 s_2 s2 就是和谐的。

  • 对于 l ( s 1 ) l(s_1) l(s1) r ( s 1 ) r(s_1) r(s1) 被交换的情况, s 2 s_2 s2 需要被分为 s 2 ( 0 , n − i ) s_2(0, n-i) s2(0,ni) 以及 s 2 ( n − i , i ) s_2(n-i, i) s2(ni,i),这样对应的长度才会相同。因此我们可以写出状态转移方程:

    f ( s 1 , s 2 ) = ⋁ i = 1 n − 1 ( f ( s 1 ( 0 , i ) , s 2 ( n − i , i ) ) ∧ f ( s 1 ( i , n − i ) , s 2 ( 0 , n − i ) ) ) f(s_1, s_2) = \bigvee_{i=1}^{n-1} \big( f(s_1(0, i), s_2(n-i, i)) \wedge f(s_1(i, n-i), s_2(0, n-i)) \big) f(s1,s2)=i=1n1(f(s1(0,i),s2(ni,i))f(s1(i,ni),s2(0,ni)))

    我们将上面两种状态转移方程用 \vee∨ 或运算拼在一起,即可得到最终的状态转移方程。

细节

细节部分比较长,希望读者仔细阅读,否则写出来的代码可能会较为复杂,或者使用较多不必要的空间。

  1. 在进行状态转移时,我们需要先计算出较短的字符串对应的 f f f 值,再去转移计算出较长的字符串对应的 f f f 值,这是因为我们需要保证在计算 f ( s 1 , s 2 ) f(s_1, s_2) f(s1,s2) 时,所有它们的子串对应的状态都需要被计算过。因此,如果我们使用常规的动态规划方法编写代码,可能会受到计算顺序的困扰,使得代码冗长。
    而我们可以考虑使用「记忆化搜索」自顶向下地进行动态规划,这样我们只需要用题目中给定的两个原始字符串开始,递归地计算所有的 f f f 值,而无需考虑计算顺序。

  2. 由于我们使用记忆化搜索,因此我们需要把 s 1 s_1 s1 s 2 s_2 s2 作为参数传入记忆化搜索使用的递归函数。这样一来,在递归传递参数的过程中,会使用到大量字符串的切片、拷贝等操作,使得时空复杂度不那么优。本题中,由于给定原始字符串的长度不超过 30 30 30,因此不会产生太大的影响,但我们还是要尽可能对代码进行优化。

    一种通用的优化方法是,我们将状态变更为 f ( i 1 , i 2 , length ) f(i_1, i_2, \textit{length}) f(i1,i2,length),表示第一个字符串是原始字符串从第 i 1 i_1 i1 个字符开始,长度为 length \textit{length} length 的子串,第二个字符串是原始字符串从第 i 2 i_2 i2 个字符开始,长度为 length \textit{length} length 的子串。可以发现,我们只是改变了表达 s 1 s_1 s1 s 2 s_2 s2 的方式,但此时我们只需要在递归时传递三个整数类型的变量,省去了字符串的操作;

代码

class Solution {
private:
    // 记忆化搜索存储状态的数组
    // -1 表示 false,1 表示 true,0 表示未计算
    int memo[30][30][31];
    string s1, s2;

public:
    bool checkIfSimilar(int i1, int i2, int length) {
        unordered_map<int, int> freq;
        for (int i = i1; i < i1 + length; ++i) {
            ++freq[s1[i]];
        }
        for (int i = i2; i < i2 + length; ++i) {
            --freq[s2[i]];
        }
        if (any_of(freq.begin(), freq.end(), [](const auto& entry) {return entry.second != 0;})) {
            return false;
        }
        return true;
    }

    // 第一个字符串从 i1 开始,第二个字符串从 i2 开始,子串的长度为 length,是否和谐
    bool dfs(int i1, int i2, int length) {
        if (memo[i1][i2][length]) {
            return memo[i1][i2][length] == 1;
        }

        // 判断两个子串是否相等
        if (s1.substr(i1, length) == s2.substr(i2, length)) {
            memo[i1][i2][length] = 1;
            return true;
        }

        // 判断是否存在字符 c 在两个子串中出现的次数不同
        if (!checkIfSimilar(i1, i2, length)) {
            memo[i1][i2][length] = -1;
            return false;
        }
        
        // 枚举分割位置
        for (int i = 1; i < length; ++i) {
            // 不交换的情况
            if (dfs(i1, i2, i) && dfs(i1 + i, i2 + i, length - i)) {
                memo[i1][i2][length] = 1;
                return true;
            }
            // 交换的情况
            if (dfs(i1, i2 + length - i, i) && dfs(i1 + i, i2, length - i)) {
                memo[i1][i2][length] = 1;
                return true;
            }
        }

        memo[i1][i2][length] = -1;
        return false;
    }

    bool isScramble(string s1, string s2) {
        memset(memo, 0, sizeof(memo));
        this->s1 = s1;
        this->s2 = s2;
        return dfs(0, 0, s1.size());
    }
};

Java

class Solution {
    // 记忆化搜索存储状态的数组
    // -1 表示 false,1 表示 true,0 表示未计算
    int[][][] memo;
    String s1, s2;

    public boolean isScramble(String s1, String s2) {
        int length = s1.length();
        this.memo = new int[length][length][length + 1];
        this.s1 = s1;
        this.s2 = s2;
        return dfs(0, 0, length);
    }

    // 第一个字符串从 i1 开始,第二个字符串从 i2 开始,子串的长度为 length,是否和谐
    public boolean dfs(int i1, int i2, int length) {
        if (memo[i1][i2][length] != 0) {
            return memo[i1][i2][length] == 1;
        }

        // 判断两个子串是否相等
        if (s1.substring(i1, i1 + length).equals(s2.substring(i2, i2 + length))) {
            memo[i1][i2][length] = 1;
            return true;
        }

        // 判断是否存在字符 c 在两个子串中出现的次数不同
        if (!checkIfSimilar(i1, i2, length)) {
            memo[i1][i2][length] = -1;
            return false;
        }
        
        // 枚举分割位置
        for (int i = 1; i < length; ++i) {
            // 不交换的情况
            if (dfs(i1, i2, i) && dfs(i1 + i, i2 + i, length - i)) {
                memo[i1][i2][length] = 1;
                return true;
            }
            // 交换的情况
            if (dfs(i1, i2 + length - i, i) && dfs(i1 + i, i2, length - i)) {
                memo[i1][i2][length] = 1;
                return true;
            }
        }

        memo[i1][i2][length] = -1;
        return false;
    }

    public boolean checkIfSimilar(int i1, int i2, int length) {
        Map<Character, Integer> freq = new HashMap<Character, Integer>();
        for (int i = i1; i < i1 + length; ++i) {
            char c = s1.charAt(i);
            freq.put(c, freq.getOrDefault(c, 0) + 1);
        }
        for (int i = i2; i < i2 + length; ++i) {
            char c = s2.charAt(i);
            freq.put(c, freq.getOrDefault(c, 0) - 1);
        }
        for (Map.Entry<Character, Integer> entry : freq.entrySet()) {
            int value = entry.getValue();
            if (value != 0) {
                return false;
            }
        }
        return true;
    }
}

C

struct HashTable {
    int key;
    int val;
    UT_hash_handle hh;
};

void modifyHashTable(struct HashTable** hashTable, int x, int inc) {
    struct HashTable* tmp;
    HASH_FIND_INT(*hashTable, &x, tmp);
    if (tmp == NULL) {
        tmp = malloc(sizeof(struct HashTable));
        tmp->key = x;
        tmp->val = inc;
        HASH_ADD_INT(*hashTable, key, tmp);
    } else {
        tmp->val += inc;
    }
}

bool checkHashTable(struct HashTable** hashTable) {
    struct HashTable *iter, *tmp;
    HASH_ITER(hh, *hashTable, iter, tmp) {
        if (iter->val) {
            return false;
        }
    }
    return true;
}

void freeHashTable(struct HashTable** hashTable) {
    struct HashTable *iter, *tmp;
    HASH_ITER(hh, *hashTable, iter, tmp) {
        HASH_DEL(*hashTable, iter);
        free(iter);
    }
}

bool equals(char* s1, char* s2, int i1, int i2, int len) {
    for (int i = 0; i < len; i++) {
        if (s1[i + i1] != s2[i + i2]) {
            return false;
        }
    }
    return true;
}

// 记忆化搜索存储状态的数组
// -1 表示 false,1 表示 true,0 表示未计算
int memo[30][30][31];

// 第一个字符串从 i1 开始,第二个字符串从 i2 开始,子串的长度为 length,是否和谐
bool dfs(char* s1, char* s2, int i1, int i2, int length) {
    if (memo[i1][i2][length]) {
        return memo[i1][i2][length] == 1;
    }

    // 判断两个子串是否相等
    if (equals(s1, s2, i1, i2, length)) {
        memo[i1][i2][length] = 1;
        return true;
    }

    // 判断是否存在字符 c 在两个子串中出现的次数不同
    struct HashTable* hashTable = NULL;

    for (int i = i1; i < i1 + length; ++i) {
        modifyHashTable(&hashTable, s1[i], 1);
    }
    for (int i = i2; i < i2 + length; ++i) {
        modifyHashTable(&hashTable, s2[i], -1);
    }
    if (!checkHashTable(&hashTable)) {
        memo[i1][i2][length] = -1;
        return false;
    }
    freeHashTable(&hashTable);

    // 枚举分割位置
    for (int i = 1; i < length; ++i) {
        // 不交换的情况
        if (dfs(s1, s2, i1, i2, i) && dfs(s1, s2, i1 + i, i2 + i, length - i)) {
            memo[i1][i2][length] = 1;
            return true;
        }
        // 交换的情况
        if (dfs(s1, s2, i1, i2 + length - i, i) && dfs(s1, s2, i1 + i, i2, length - i)) {
            memo[i1][i2][length] = 1;
            return true;
        }
    }

    memo[i1][i2][length] = -1;
    return false;
}

bool isScramble(char* s1, char* s2) {
    memset(memo, 0, sizeof(memo));
    return dfs(s1, s2, 0, 0, strlen(s1));
}

Python3

class Solution:
    def isScramble(self, s1: str, s2: str) -> bool:
        @cache
        def dfs(i1: int, i2: int, length: int) -> bool:
            """
            第一个字符串从 i1 开始,第二个字符串从 i2 开始,子串的长度为 length,是否和谐
            """

            # 判断两个子串是否相等
            if s1[i1:i1+length] == s2[i2:i2+length]:
                return True
            
            # 判断是否存在字符 c 在两个子串中出现的次数不同
            if Counter(s1[i1:i1+length]) != Counter(s2[i2:i2+length]):
                return False
            
            # 枚举分割位置
            for i in range(1, length):
                # 不交换的情况
                if dfs(i1, i2, i) and dfs(i1 + i, i2 + i, length - i):
                    return True
                # 交换的情况
                if dfs(i1, i2 + length - i, i) and dfs(i1 + i, i2, length - i):
                    return True
        
            return False

        ans = dfs(0, 0, len(s1))
        dfs.cache_clear()
        return ans

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/scramble-string
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值