每日一題——擾亂字符串

菜雞每日一題系列打卡87

每天一道算法題目 

小夥伴們一起留言打卡

堅持就是勝利,我們一起努力!

題目描述(引自LeetCode)

給定一個字符串s1,我們可以把它遞歸地分割成兩個非空子字符串,從而將其表示爲二叉樹。

下圖是字符串s1 = "great"的一種可能的表示形式。

    great
   /    \
  gr    eat
 / \    /  \
g   r  e   at
           / \
          a   t

在擾亂這個字符串的過程中,我們可以挑選任何一個非葉節點,然後交換它的兩個子節點。

例如,如果我們挑選非葉節點"gr",交換它的兩個子節點,將會產生擾亂字符串"rgeat"。

    rgeat
   /    \
  rg    eat
 / \    /  \
r   g  e   at
           / \
          a   t

我們將"rgeat"稱作"great"的一個擾亂字符串。

同樣地,如果我們繼續交換節點"eat"和"at"的子節點,將會產生另一個新的擾亂字符串"rgtae"。

    rgtae
   /    \
  rg    tae
 / \    /  \
r   g  ta  e
       / \
      t   a

我們將"rgtae"稱作"great"的一個擾亂字符串。

給出兩個長度相等的字符串s1和s2,判斷s2是否是s1的擾亂字符串。

示例 1:
輸入: s1 = "great", s2 = "rgeat"
輸出: true
示例 2:
輸入: s1 = "abcde", s2 = "caebd"
輸出: false

題目分析

這道題比較容易想到的方式是遞歸,基於leetcode測試集提供的s1和s2均由字母組成,且長度比較小時,遞歸是可行的,而且相對比較容易實現;但當組成s1和s2的字符集變大時,或者長度達到一定規模時,就需要考慮採用動態規劃。

在這裏,菜雞給出了兩種解法,供大家參考。話不多說,上代碼!

代碼實現

// 遞歸
class Solution {


    public boolean isScramble(String s1, String s2) {
        // 特殊情況處理
        if (s1 == null || s2 == null) return s1 == s2;
        if (s1.length() != s2.length()) return false;


        // dfs
        return dfs(s1, s2, 0, 0, s1.length());
    }
   
    private boolean dfs(String s1, String s2, int i, int j, int l){
        // 遞歸終止條件
        if (l == 1) return s1.charAt(i) == s2.charAt(j);


        // 剪枝
        int[] tmp = new int[26];
        for (int k = 0; k < l; k++) {
            tmp[s1.charAt(i + k) - 'a']++;
            tmp[s2.charAt(j + k) - 'a']--;
        }
        for (int c : tmp) if (c != 0) return false;


        // 遞歸
        for (int k = 1; k < l; k++) {
            if (dfs(s1, s2, i, j, k) && dfs(s1, s2, i + k, j + k, l - k)) return true;
            if (dfs(s1, s2, i, j + l - k, k) && dfs(s1, s2, i + k, j, l - k)) return true;
        }


        // 返回
        return false;
    }


}
// 動態規劃
class Solution {


    public boolean isScramble(String s1, String s2) {
        // 特殊情況處理
        if (s1 == null || s2 == null) return s1 == s2;
        int n = s1.length();
        if (n != s2.length()) return false;
        
        // 初始化dp數組
        boolean[][][] dp = new boolean[n][n][n + 1];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                dp[i][j][1] = s1.charAt(i) == s2.charAt(j);
            }
        }


        // 計算dp數組
        for (int l = 2; l <= n; l++) {
            for (int i = 0; i <= n - l; i++) {
                for (int j = 0; j <= n - l; j++) {
                    for (int k = 1; k <= l - 1; k++) {
                        if (dp[i][j][k] && dp[i + k][j + k][l - k]) {
                            dp[i][j][l] = true;
                            break;
                        }
                        if (dp[i][j + l - k][k] && dp[i + k][j][l - k]) {
                            dp[i][j][l] = true;
                            break;
                        }
                    }
                }
            }
        }


        // 返回結果
        return dp[0][0][n];
    }


}

代碼分析

對代碼進行分析,遞歸方式的時間複雜度爲階乘級別;遞歸調用佔用了大量棧空間,空間複雜度爲階乘級別。

動態規劃方式,計算dp數組需要四層for循環,時間複雜度爲O(n^4);同時,空間上需要三維數組進行狀態存儲,空間複雜度爲O(n^3)。

執行結果

遞歸方式的執行結果

動態規劃方式的執行結果

注意:上述執行結果的差異受leetcode測試集的限制,並不能客觀表現遞歸方式和動態規劃方式之間的差異。

學習 | 工作 | 分享

????長按關注“有理想的菜雞

只有你想不到,沒有你學不到

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章