LeetCode刷題 -- 97. 交錯字符串

  今天這道題是困難難度的,二狗很努力的嘗試,還是失敗了。但是感覺雖然沒通過全部的測試用例,思考的過程還是有很多地方挺有趣的,記錄一下。

 

97. 交錯字符串

給定三個字符串 s1, s2, s3, 驗證 s3 是否是由 s1 和 s2 交錯組成的。

示例 1:

輸入: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
輸出: true
示例 2:

輸入: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
輸出: false

  先來看看我的想法以及做法吧:

  這道題目很值得紀念一下。對官方給出的解答十分佩服,另外雖然自己做的過程 99/101 沒有完全通過,但是思考過程也覺得有很多可以留意的地方。

 


    初見這道題,兩個字符串混合成第三個字符串。第一反應就是利用雙指針,分別與第三個字符串進行比較.進一步思考發現也沒有那麼簡單,比如遇到三個指針指向的字母現在是同一個的時候,比如都是'a'
  應該選擇哪個呢?順着這個思路聯想到了 回溯算法 沒關係,現在兩條路看起來都是正確的,那麼我就去嘗試,如果失敗了我就回溯到選擇的地方,選擇另一條路徑繼續進行嘗試。
  除開簡單的邊界值考慮,上面這種雙指針+回溯的算法其實很簡單。需要考慮的就只有三種情況。

  1. s1,s2當前應該計算的字符與s3的應當計算的字符都不相等。 這種可以直接判斷是不滿足條件的,應當返回false.
  2. s1, s2與s3中當前對應應比較的字符有一個相等,這種其實只有一條路徑,更新下次我們應該計算的索引即可。
  3.  s1,s2與s3中當前對應應比較的字符全部都相等,這種其實就對應我們的回溯,應該選擇一條路去嘗試,不行就嘗試另一條。

  照着這個思路,前面的測試用例都通過了,直到遇到了測試用例99:
  s1 "bbbbbabbbbabaababaaaabbababbaaabbabbaaabaaaaababbbababbbbbabbbbababbabaabababbbaabababababbbaaababaa"
  s2 "babaaaabbababbbabbbbaabaabbaabbbbaabaaabaababaaaabaaabbaaabaaaabaabaabbbbbbbbbbbabaaabbababbabbabaab"
  s3 "babbbabbbaaabbababbbbababaabbabaabaaabbbbabbbaaabbbaaaaabbbbaabbaaabababbaaaaaabababbababaababbababbbababbbbaaaabaabbabbaaaaabbabbaaaabbbaabaaabaababaababbaaabbbbbabbbbaabbabaabbbbabaaabbababbabbabbab"

  像是爲上面的方法量身定製的天敵一樣,這組測試用例足夠長,並且內容足夠“簡單”,只有a,b兩個字符,因此按照上面的算法,遇到3的情況就會特別頻繁。也就會花費大量的時間計算錯誤的路徑,最後的結果當然是超時了。
  但我其實不是很願意輕易就放棄寫好的算法,打算優化一下。首先作爲回溯算法來說,合理的剪枝可以減少不必要的計算。但觀察題目,我也沒有想到特別好的剪枝的辦法,因爲實在不容易根據已計算的字符判斷後續哪條路徑是不正確的。
  回憶起當初看的算法教程關於字符串匹配KMP算法的內容,其中一個核心思想是每次儘可能多的匹配字符。於是我也嘗試這樣做。
  對於上述的2的這種情況,比如s1的當前索引i1對應的字符與s3當前索引i3對應的字符相等,s2的索引i2對應的字符不相等,下次遞歸我就會計算 i1+1,i2,i3+1 這三個字符的關係。就從這裏下手,這種情況下,我們可以多計算一些偏移量。
  具體來說就是如果i1+1,i2,i3+1仍滿足這個條件,我們就繼續向後偏移,直到不滿足這種條件了我們才進行下一次的遞歸。結合代碼如下:

if (ca1[i1] == ca3[i3] && ca2[i2] != ca3[i3])
{
    int offset = 0;

    while (i1 + offset < ca1.Count() && ca1[i1 + offset] == ca3[i3 + offset] && ca2[i2] != ca3[i3 + offset])
    {
        offset++;
    }

    if (ca2[i2] != ca3[i3 + offset])
        return false;

    return huisu(ca1, i1 + offset, ca2, i2, ca3, i3+ offset);
}        

 

  這樣一番改造後自信滿滿的去測試,仍舊超時。其實分析一下就明白了,上面的優化主要集中在2的那種情況,而98更多的其實會命中3的那種情況。
  綜上,完敗,雖然有點不甘心,但還是服氣的。完整代碼在下面:

        public bool IsInterleave(string s1, string s2, string s3)
        {
            if (s1.Length + s2.Length != s3.Length)
            {
                return false;
            }

            if (string.IsNullOrEmpty(s1) || string.IsNullOrEmpty(s2))
            {
                return s1 + s2 == s3;
            }

            return huisu(s1.ToCharArray(), 0, s2.ToCharArray(), 0, s3.ToCharArray(), 0);
        }

        public bool huisu(char[] ca1, int i1, char[] ca2, int i2, char[] ca3, int i3)
        {
            if (i1 == ca1.Count() && i2 == ca2.Count() && i3 == ca3.Count())
            {
                return true;
            }

            if (i1 == ca1.Count())
            {
                for (int i = 0; i < ca3.Count() - i3 - 1; i++)
                {
                    if (ca2[i2 + i] != ca3[i3 + i])
                    {
                        return false;
                    }
                }

                return true;
            }

            if (i2 == ca2.Count())
            {
                for (int i = 0; i < ca3.Count() - i3 - 1; i++)
                {
                    if (ca1[i1 + i] != ca3[i3 + i])
                    {
                        return false;
                    }
                }

                return true;
            }

            if (ca1[i1] == ca3[i3] && ca2[i2] != ca3[i3])
            {
                int offset = 0;

                while (i1 + offset < ca1.Count() && ca1[i1 + offset] == ca3[i3 + offset] && ca2[i2] != ca3[i3 + offset])
                {
                    offset++;
                }

                if (ca2[i2] != ca3[i3 + offset])
                    return false;

                return huisu(ca1, i1 + offset, ca2, i2, ca3, i3 + offset);
            }
            else if (ca1[i1] != ca3[i3] && ca2[i2] == ca3[i3])
            {
                int offset = 0;

                while (i2 + offset < ca2.Count() && ca1[i1] != ca3[i3 + offset] && ca2[i2 + offset] == ca3[i3 + offset])
                {
                    offset++;
                }

                if (ca1[i1] != ca3[i3 + offset])
                    return false;

                return huisu(ca1, i1, ca2, i2 + offset, ca3, i3 + offset);
            }
            else if (ca1[i1] != ca3[i3] && ca2[i2] != ca3[i3])
            {
                return false;
            }
            else
            {
                return huisu(ca1, i1 + 1, ca2, i2, ca3, i3 + 1) || huisu(ca1, i1, ca2, i2 + 1, ca3, i3 + 1);
            }

        }

 

  

 下面是官方給出的動態規劃的解法,讓人眼前一亮。
  解題過程中也有想過是否可以使用動態規劃的思路去做,但沒想到一個合適的狀態轉移方程,就沒有繼續思考下去。
  來看看官方給出的狀態轉移的思路吧:傳送門.
  簡單來說:關鍵是要找到當前的狀態和上一個狀態的關係,在這道題目中是 如果當前s1或s2的字符與s3相等,如果滿足條件,那麼上一個字符比如按也是滿足的.C#版代碼如下:

        public bool IsInterleave(string s1, string s2, string s3)
        {
            if (s1.Length + s2.Length != s3.Length)
            {
                return false;
            }

            bool[,] dp = new bool[s1.Length + 1, s2.Length + 1];

            dp[0, 0] = true;

            for (int i = 0; i <= s1.Length; i++)
            {
                for (int j = 0; j <= s2.Length; j++)
                {
                    int p = i + j - 1;

                    if (i > 0)
                    {
                        dp[i, j] = dp[i, j] || (dp[i - 1, j] && s1[i - 1] == s3[p]);
                    }

                    if (j > 0)
                    {
                        dp[i, j] = dp[i, j] || (dp[i, j-1] && s2[j - 1] == s3[p]);
                    }
                }
            }

            return dp[s1.Length, s2.Length];
        }

 

  雖然自己寫的很爽,但是估計沒有人會看的吧,如果你看到這裏了,二狗子在這兒先謝謝您哈,哈哈。

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