最長連續三字符子串問題

注:本文中python代碼均未經過運行測試,因此可能會有錯誤的地方。



"對於有拖延症的人,在你想起一件事情卻又因爲懶惰不願意做的時候,最好還是先去做三分鐘試試看。"


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------


昨天去面試一個公司,被問了幾道題,結果沒有回答很好,今天想了想,差不多有思路,我就寫一下。


面試官給我筆和紙,先問我數組二分查找怎麼寫,我寫了個遞歸的,然後他說寫個非遞歸的,我想了好久沒想出來,他說這個很簡單的啊,他說控制住邊界寫不就可以了,我這纔想起用while寫,寫出來了就開始下一個問題了,也就是這裏要說的問題。


問題描述:給定長度爲n的字符串s,求s中的最長連續子串,要求子串中只包含三個不同的字符。


我一聽就興奮了,我這兩天剛好在看算法呢,這個感覺不難,想想怎麼做。


他說你就說你一聽到這個想起來用什麼方法了,就能完成就好,不用想太複雜的。


我說最簡單的就是O(n2)的了,從s[0]開始,往後面搜索,一直到不滿足條件,再去搜索s[1],就這樣一直搜。

#python

s = input()
temp = ""
maxlen = 0 
for i in range(len(s)):
    for j in range(i,len(s)):
        if(s[j] not in temp):
            temp = temp + s[j]
        if len(temp) > 3:
            if( j - i + 1 > best ):
                best = j - i + 1
            break # python 裏面有 break?
print (best) 


然後他說還有別的嗎,我說還有就是O(n*logn)的分治了,他說你說一下。


我說就是把s分成兩部分s[0 :k],s[(k+1) : (n-1)],s中最長子串只能是在s[0:k] 或者  s[ k+1 : n -1 ]中 或者是 包含s[k],s[k+1]在內的子串。
求 s[0:k] , s[k+1 : n-1]可以遞歸求解,求包含s[k] , s[k+1] 在內的子串有點複雜,但是可以在O(n)內做到。因此時間就是O(n*logn) 。


他想了想說你這個方法不行,說讓我實際做一下,他給了個十幾個字母的字符串,讓我實際試試。我說先分兩份,然後繼續分,分到最後了,得到一個只有2個字母的字符串,我不知道接下做什麼,我有點心慌,他說接下來怎麼做,我說我沒想清楚,我說做不下去了,他說你最後分了以後得到的不是一個完整的字符串了,他說你這個方法對這個題目怎麼能適用呢,我說是我想錯了。


後來我回來的時候認真想了想,我的分治法是可以的,這是後話,下文再說爲什麼可以。


他說還有別的方法嗎,我說還可以動態規劃,他說怎麼動態規劃,我說我還沒想好動態規劃方程式是什麼,他問動態規劃問題特性是什麼,我說重複的子問題,他說這個有嗎,我說這個沒有,他說那怎麼用動態規劃求呢,我說似乎是。


今天我看了看書,想了想,我的動態規劃也是可行的,可惜那時候沒有書在身邊,沒靈感,也是自己讀書不認真,那時候沒有想通。具體解法後文說。


然後他說你對這個問題有沒有可以改進的地方,我說我想想,我說改進哪個?他說就你給出的那個方法吧,我說是O(n2)嗎?他說是的。


我思考怎麼改進。


我沒想出來怎麼會有太大的改進。我覺得這個方法的O(n2)是不可以避免的。我說能改嗎,他說你看看你這個方法最耗時的操作是什麼,我想了想,我找不到。


現在我想他說的那個改進應該就是改成了動態規劃了,不過那個可以說是另一種思路了,不算是對這個方法的改進,不過人家那時候秒想出來動態規劃,也是厲害。


我繼續說。


我說您看看這麼改行嗎,他說怎樣,我說加上剪枝的條件判斷,如果剩餘長度小於已知的best,就不用繼續循環了,因爲肯定得不到更好的。他說你這樣的話複雜度不還是O(n2),他說這基本沒有改進,我說是的。


我又想了想,沒有想到別的改進。


我說我想不到別的改進了,他說時間差不多了,他說今天就這樣吧,他說你先回去吧等我們給你回覆,我說嗯。


我沒有被錄用,我心情不太好。


我回去的時候想這個怎麼改進,想到了先對s進行一次預處理,把相同元素壓縮;或者把s中元素按照出現次數排序,按照次數從多到少進行搜索或者怎樣。但是都覺得不好。


然後,昨天的事情就成了昨天的事情了。




我說一下後來我想到的分治法以及動態規劃法。


分治法思路:
還是上面說的那樣,不過遇到了字符串長度少於3的,就可以直接求出最長的部分,然後直接返回1,2或者3的。中間s[k]向兩邊搜索可以向左多搜索一個,再向右多搜索一個,知道不滿足條件,然後跟左右兩部分的比較,取最長的。

#python 33
s = 0
s = input()
def divide( a , b ):
    global s
    if (b - a <= 3):
        return (b - a)
    k1 = (b - a) / 2 + a
    k2 = k1 + 1
    leftlen = divide(i , k1)
    rightlen = divide(k2 , j)
    midlen = getMidLen(a , b)  # 獲取包含k1,k2的最長子串,現在還沒想好具體方法,感覺O(b - a)的時間是可以的。
    return max(lefilen , rightlen , midlen)


divide( 0 , len(s) - 1)

        
我現在想到了獲取包含中間的最長子串(該子串記爲ms)長度求法了。之前忽然卡殼是因爲ms向左邊搜索,不止一種情況可能是最長的(即不能用貪心法獲得),舉例示意:......ab|cd......假設從”|“的地方劃分,左邊b爲s[k1],右邊c爲s[k2],那麼該子串可能是 "......abc"  .也可能是  "bcd.....",當然這裏就不會是左右都能延伸的了,但是程序運行的時候沒辦法判斷。所以我當時沒有一個解題方向。
思路如下:
觀察得到ms肯定包含s[k1],s[k2],所以只有三種情況:s[k1]左邊有除s[k1],s[k2]外的0個字符,1個字符,2個字符(只能有這三種情況,左邊的情況確定後,右邊的也隨之確定)。

#python33

def getMidLen(a , b):
    global s
    k1 = (b - a)/2 + a
    k2 = k1 +1
    ms = s[k1] + s[k2]
    best = 0
    for i in range(3):
        temp = 0
        while k1 >= a:
            if(s[k1] in ms):
                temp = temp + 1
            elif(len(ms) < i + 1):
                ms = ms + s[k1]
                temp = temp + 1
            else:
                break
            k1 = k1 - 1
        #左邊的i個字符都搜索到了,下面是右邊的
        while k2<=b :
            if(s[k2] in ms):
                temp = temp + 1
            elif (len(ms) <= 3):
                ms = ms + s[k2]
                temp = temp + 1
            else:
                break
            k2 = k2 + 1
        if temp > best:
            best = temp
    return best


上面getMidLen(a,b)的時間複雜度爲O(3*(b - a)) = O(n),
所以分治長度爲n的字符串的時間爲 T(n) = 2*T(n/2) + O(n)。    得T(n) = O(n * logn)。(具體求法就是不斷帶入,這裏不多說了)。



動態規劃思路:
b是一個結構, 表示s中的一個(不一定是最長的)只包含三個字符的字符串。

struct _b
{
    int len;
    char cs[3] = {0};
    int locs[3] = {-1 , -1 , -1}; int loce[3] = {-1 , -1 ,-1};
};
typedef struct _b b;

設b[ i ].len = s[0 :i]中最長的子串長度,並且b[i]表示的這個子串是包含s[ i ]的。b[i].c包含三個字符,b[i].locs和 b[i].loce分別表示三個字符對應的在s[0 :i] 的最長子串中,第一次和最後一次出現的位置。   
舉例示意:如果s = ”abccd“,那麼b[3]的c = [ 'a' , 'b' ,'c '] , loce = [ 0 , 1 , 3] ,locs = [0 , 1 , 2]。
b的結構之所以這樣設置,是因爲在利用b[ i ]求b[ i  + 1 ]時會用到b[ i ]中最長子串的三個字符以及其最後一次出現的位置。
這樣s最長子串就是 b[0].len , b[1].len ,....b[n-1].len 中的max值。


求b[i]時:
1 :若s[i]在b[i - 1].c中,那麼  b[i].len = b[i - 1 ].len + 1 , b[i].c = b[i - 1].c 。b[i].locs = b[i-1].locs , b[i].loce = b[i-1].loce (”=“此處爲賦值,實際代碼中不是這樣的。此處簡寫。),再  修改s[i]對應的那個字符的loce爲i(最後出現的位置爲i);
2:若s[i] 不在  b[i - 1].c 中 ,b[i]應該包含  b[i - 1].c中三個字符的後兩個出現的字符,加上s[i] 。b[i].locs和b[i].loce爲其各自對應的出現的位置。b[i].len = i - b[i].lens中最小的值。(b[i].len 即b[i]對應的字符串的長度,再次強調,b[i]對應的字符串是包含s[i]在內的)。
1操作時間複雜度爲O(1),2操作時間複雜度爲O(1)。
最重要的動態規劃的方程式就是:
b[ i ].len = b [ i ] .len + 1 ,s[i]在b[i].c中;
或者是修改完b[i].c ,b[i].locs ,b[i].loce後 , b[i].len = i - min(b[i].locs)。


代碼就不打了,有點複雜。但是思路應該是沒問題的。只需要for循環一次算出b[0 : n-1],並且在計算過程中一直維護一個int best的值爲最大的b[i].len就可以了。






就是分治的那個 從中間開始往兩邊搜索的函數還沒想好具體什麼樣子,想一想明天可以的話就加上去。

-----今天我加上去了:)






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