Leetcode:Scramble String 解題報告


Scramble String

Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrings recursively.

Below is one possible representation of s1 = "great":

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

To scramble the string, we may choose any non-leaf node and swap its two children.

For example, if we choose the node "gr" and swap its two children, it produces a scrambled string "rgeat".

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

We say that "rgeat" is a scrambled string of "great".

Similarly, if we continue to swap the children of nodes "eat" and "at", it produces a scrambled string "rgtae".

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

We say that "rgtae" is a scrambled string of "great".

Given two strings s1 and s2 of the same length, determine if s2 is a scrambled string of s1.

解答:
1. Brute Force 遞歸。
基本的思想就是:在S1上找到一個切割點,左邊長度為i, 右邊長為len - i。 有2種情況表明它們是IsScramble
(1). S1的左邊和S2的左邊是IsScramble, S1的右邊和S2的右邊是IsScramble
(2). S1的左邊和S2的右邊是IsScramble, S1的右邊和S2的左邊是IsScramble (實際上是交換了S1的左右子樹)

而i的取值可以是1  ~  len-1。 基於這個思想,我們可以寫出以下的遞歸Brute Force 解:

引自stellari對復雜度的解釋:

stellari

看了你的不少文章,感覺收獲良多!只是有點小問題想請教:按照我的理解,那個遞歸算法在最差情況下應該是O(3^n),而非O(n^2)。理由是:假設函數運行時間為f(n),那么由於在每次函數調用中都要考慮1~n之間的所有長度,並且正反都要檢查,所以有
f(n) = 2[f(1) + f(n-1)] +2[f(2) + f(n-2)] … + 2[f(n/2) + f(n/2+1)]. 易推得f(n+1) = 3(fn), 故f(n) = O(3^n)。當然這是最差情況下的時間復雜度。那么你提到的O(n^2),是否是通過其他數學方法得到的更tight的上限?歡迎探討!

這一個解是不能通過LeetCode的檢查的,復雜度是 3^N

 1 public static boolean isScramble1(String s1, String s2) {
 2         if (s1 == null || s2 == null) {
 3             return false;
 4         }
 5 
 6         int len1 = s1.length();
 7         int len2 = s2.length();
 8 
 9         // the two strings should be the same length.
10         if (len1 != len2) {
11             return false;
12         }
13 
14         return rec(s1, s2);
15     }
16 
17     // Solution 1: The recursion version.
18     public static boolean rec1(String s1, String s2) {
19         int len = s1.length();
20 
21         // the base case.
22         if (len == 1) {
23             return s1.equals(s2);
24         }
25 
26         // 鍒掑垎2涓瓧絎︿覆
27         for (int i = 1; i < len; i++) {
28             // we have two situation;
29             // the left-left right-right & left-right right-left
30             if (rec1(s1.substring(0, i), s2.substring(0, i))
31                     && rec1(s1.substring(i, len), s2.substring(i, len))) {
32                 return true;
33             }
34 
35             if (rec1(s1.substring(0, i), s2.substring(len - i, len))
36                     && rec1(s1.substring(i, len), s2.substring(0, len - i))) {
37                 return true;
38             }
39         }
40 
41         return false;
42     }
View Code


2. 遞歸加剪枝
感謝unieagle的提示,我們可以在遞歸中加適當的剪枝,也就是說在進入遞歸前,先把2個字符串排序,再比較,如果不相同,則直接退出掉。這樣也能有效地減少復雜度,具體多少算不清。但能通過leetcode的檢查。

 1 // Solution 2: The recursion version with sorting.
 2     // 鎺掑簭涔嬪悗鐨勫壀鏋濆彲浠ラ�榪嘗eetCode鐨勬鏌�
 3     public static boolean rec(String s1, String s2) {
 4         int len = s1.length();
 5 
 6         // the base case.
 7         if (len == 1) {
 8             return s1.equals(s2);
 9         }
10 
11         // sort to speed up.
12         char[] s1ch = s1.toCharArray();
13         Arrays.sort(s1ch);
14         String s1Sort = new String(s1ch);
15 
16         char[] s2ch = s2.toCharArray();
17         Arrays.sort(s2ch);
18         String s2Sort = new String(s2ch);
19 
20         if (!s1Sort.equals(s2Sort)) {
21             return false;
22         }
23 
24         // 鍒掑垎2涓瓧絎︿覆
25         for (int i = 1; i < len; i++) {
26             // we have two situation;
27             // the left-left right-right & left-right right-left
28             if (rec(s1.substring(0, i), s2.substring(0, i))
29                     && rec(s1.substring(i, len), s2.substring(i, len))) {
30                 return true;
31             }
32 
33             if (rec(s1.substring(0, i), s2.substring(len - i, len))
34                     && rec(s1.substring(i, len), s2.substring(0, len - i))) {
35                 return true;
36             }
37         }
38 
39         return false;
40     }
View Code

 

3. 遞歸加Memory

我們在遞歸中加上記憶矩陣,也可以減少重復運算,但是我們現在就改一下之前遞歸的結構以方便加上記憶矩陣,我們用index1記憶S1起始地址,index2記憶S2起始地址,len 表示字符串的長度。這樣我們可以用一個三維數組來記錄計算過的值,同樣可以通過leetcode的檢查。這個三維數組一個是N^3的復雜度,在每一個遞歸中,要從1-len地計算一次所有的子串,所以一共的復雜度是N^4

 1 // Solution 3: The recursion version with memory.
 2     // 閫氳繃璁板繂鐭╅樀鏉ュ噺灝戣綆楅噺
 3     public static boolean isScramble3(String s1, String s2) {
 4         if (s1 == null || s2 == null) {
 5             return false;
 6         }
 7 
 8         int len1 = s1.length();
 9         int len2 = s2.length();
10 
11         // the two strings should be the same length.
12         if (len1 != len2) {
13             return false;
14         }
15 
16         int[][][] mem = new int[len1][len1][len1];
17         for (int i = 0; i < len1; i++) {
18             for (int j = 0; j < len1; j++) {
19                 for (int k = 0; k < len1; k++) {
20                     // -1 means unseted.
21                     mem[i][j][k] = -1;
22                 }
23             }
24         }
25 
26         return recMem(s1, 0, s2, 0, len1, mem);
27     }
28 
29     // Solution 3: The recursion version with memory.
30     // 閫氳繃璁板繂鐭╅樀鏉ュ噺灝戣綆楅噺
31     public static boolean recMem(String s1, int index1, String s2, int index2,
32             int len, int[][][] mem) {
33         // the base case.
34         if (len == 1) {
35             return s1.charAt(index1) == s2.charAt(index2);
36         }
37 
38         // LEN: 1 - totalLen-1
39         int ret = mem[index1][index2][len - 1];
40         if (ret != -1) {
41             return ret == 1 ? true : false;
42         }
43 
44         // 鍒濆鍖栦負false
45         ret = 0;
46 
47         // 鍒掑垎2涓瓧絎︿覆. i means the length of the left side in S1
48         for (int i = 1; i < len; i++) {
49             // we have two situation;
50             // the left-left right-right & left-right right-left
51             if (recMem(s1, index1, s2, index2, i, mem)
52                     && recMem(s1, index1 + i, s2, index2 + i, len - i, mem)) {
53                 ret = 1;
54                 break;
55             }
56 
57             if (recMem(s1, index1, s2, index2 + len - i, i, mem)
58                     && recMem(s1, index1 + i, s2, index2, len - i, mem)) {
59                 ret = 1;
60                 break;
61             }
62         }
63 
64         mem[index1][index2][len - 1] = ret;
65         return ret == 1 ? true : false;
66     }
View Code

 


4. 動態規划。

其實如果寫出了3,動態規划也就好寫了。

三維動態規划題目:

我們提出維護量res[i][j][n],其中i是s1的起始字符,j是s2的起始字符,而n是當前的字符串長度,res[i][j][len]表示的是以i和j分別為s1和s2起點的長度為len的字符串是不是互為scramble。
有了維護量我們接下來看看遞推式,也就是怎么根據歷史信息來得到res[i][j][len]。判斷這個是不是滿足,其實我們首先是把當前s1[i...i+len-1]字符串劈一刀分成兩部分,然后分兩種情況:第一種是左邊和s2[j...j+len-1]左邊部分是不是scramble,以及右邊和s2[j...j+len-1]右邊部分是不是scramble;第二種情況是左邊和s2[j...j+len-1]右邊部分是不是scramble,以及右邊和s2[j...j+len-1]左邊部分是不是scramble。如果以上兩種情況有一種成立,說明s1[i...i+len-1]和s2[j...j+len-1]是scramble的。而對於判斷這些左右部分是不是scramble我們是有歷史信息的,因為長度小於n的所有情況我們都在前面求解過了(也就是長度是最外層循環)。
上面說的是劈一刀的情況,對於s1[i...i+len-1]我們有len-1種劈法,在這些劈法中只要有一種成立,那么兩個串就是scramble的。
總結起來遞推式是res[i][j][len] = || (res[i][j][k]&&res[i+k][j+k][len-k] || res[i][j+len-k][k]&&res[i+k][j][len-k]) 對於所有1<=k
如此總時間復雜度因為是三維動態規划,需要三層循環,加上每一步需要線行時間求解遞推式,所以是O(n^4)。雖然已經比較高了,但是至少不是指數量級的,動態規划還是相當有用的,空間復雜度是O(n^3)。代碼如下:

 

注:事實上這里最大的難點,是你怎么安排這三個循環。仔細看一下,計算len對應的解時,要用到一堆len-1的解。所以我們應該len 從0到1地這要子計算(三維啊都沒辦法通過畫圖來推導動態規划的遞增關系了!)

 1 /*
 2      * Solution 4: The DP Version.
 3      */
 4     public static boolean isScramble4(String s1, String s2) {
 5         if (s1 == null || s2 == null) {
 6             return false;
 7         }
 8 
 9         int len1 = s1.length();
10         int len2 = s2.length();
11 
12         // the two strings should be the same length.
13         if (len1 != len2) {
14             return false;
15         }
16 
17         /*
18          * i: The index of string 1. j: The index of string 2. k: The length of
19          * the two string. 1 ~ len1
20          * 
21          * D[i][j][k] =
22          */
23         boolean[][][] D = new boolean[len1][len1][len1 + 1];
24         for (int subLen = 1; subLen <= len1; subLen++) {
25             for (int i1 = 0; i1 <= len1 - subLen; i1++) {
26                 for (int i2 = 0; i2 <= len1 - subLen; i2++) {
27                     if (subLen == 1) {
28                         D[i1][i2][subLen] = s1.charAt(i1) == s2.charAt(i2);
29                         continue;
30                     } 
31                     
32                     D[i1][i2][subLen] = false;
33                     for (int l = 1; l < subLen; l++) {
34                         if (D[i1][i2][l] && D[i1 + l][i2 + l][subLen - l]
35                                 || D[i1][i2 + subLen - l][l] && D[i1 + l][i2][subLen - l]
36                                 ) {
37                             D[i1][i2][subLen] = true;
38                             break;
39                         }
40                     }
41                 }
42             }
43         }
44 
45         return D[0][0][len1];
46     }
47     
48     /*
49      * Solution 4: The DP Version. REDO
50      */
51     public static boolean isScramble(String s1, String s2) {
52         if (s1 == null || s2 == null) {
53             return false;
54         }
55         
56         int len = s1.length();
57         
58         if (s2.length() != len) {
59             return false;
60         }
61 
62         boolean[][][] D = new boolean[len][len][len + 1];
63         
64         // D[i][j][k] = D[i][]
65         for (int k = 1; k <= len; k++) {
66             // 注意這里的邊界選取。 如果選的不對,就會發生越界的情況.. orz..
67             // attention: should use "<="
68             for (int i = 0; i <= len - k; i++) {
69                 for (int j = 0; j <= len - k; j++) {
70                     if (k == 1) {
71                         D[i][j][k] = s1.charAt(i) == s2.charAt(j);
72                         continue;
73                     }
74                     
75                     D[i][j][k] = false;
76                     for (int l = 1; l <= k - 1; l++) {
77                         if (D[i][j][l] && D[i + l][j + l][k - l] 
78                             || D[i][j + k - l][l] && D[i + l][j][k - l] ) {
79                             D[i][j][k] = true;
80                             break;
81                         }
82                     }
83                 }
84             }
85         }
86         
87         return D[0][0][len];
88     }
View Code

 

GITHUB:

https://github.com/yuzhangcmu/LeetCode_algorithm/blob/9241a5148ba94d79c7dfcb3dbbbd3ad5474bdcf1/dp/IsScramble.java


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM