經過解決「構造包含全部給定子串的最短字符串」問題思考算法優化

最近因爲工做相對比較忙,須要學習一些新的技術項目,寫代碼的時間比較少。繼續解決百度2017秋招4星的題目,今天要分析的這個題目,是目前我遇到相對其餘4星題目算是有一點難度的題目。html


今天咱們將從一個題目的不一樣解決方案:超時到快速出解(沒法等待結果 -> 10s -> 70ms)的改進過程來討論算法的優化過程和一些思想。java

域名選擇


<題目來源:百度2017秋招,http://exercise.acmcoder.com/... >python

問題描述

給網站選擇一個好的域名是一件使人頭痛的事,你但願你的域名在包含給定的一組關鍵字的同時,最短的長度是多少。算法

輸入與輸出:
輸入文件的第一行包含一個整數 n,表示關鍵字的數目。(n<=10)
接下來的n行,每行包含了一個長度小於等於100的字符串,表示一組關鍵字。app

輸出一行一個數字,表示最短的長度學習

樣例:
輸入
3
ABC
CBA
AB
輸出
5測試

題目描述很簡單,你須要構造一個字符串S,使得它給定每一個單詞串s[i]都是這個S的一個子串,而且要求S的長度儘量的短。首先,咱們須要考慮如何去構造知足條件的字符串,分析樣例5是如何得出的:
ABC__
__CBA
AB___
能夠看出最短的串是ABCBA,長度爲5。觀察發現:優化

性質1.若是單詞A和B之間若是存在重疊的部分,那麼將AB構造一個新的字符串增長的長度是len(A)+len(B)-len(A∩B),其中A∩B表示A和B重疊的部分。須要注意到,A和B都必須是新構造出的字符串的一個子串,而不只僅是A和B中的全部字符在構造的串中出現。網站

顯然咱們的目標是用這邊單詞排成一列,使得兩兩之間的重疊部分儘量的多,這樣最後構造出來字符串才能儘量的短。但一時間咱們彷佛沒有特別好的構造方法,觀察數據規模,n<=10,枚舉彷佛可行,時間複雜度O(n!),當n=10,n!=3628800,對於C/C++來講,在1000ms內計算完成應該是能夠,但也不會過輕鬆。對於我所使用的python幾乎已經接近極限時間3000msspa

產生一個全排列結果後,咱們還須要時間去計算兩兩之間能夠重疊多少,每次增長一個單詞都在原來的基礎上去檢查它們可以重疊多少,並更保留合併結果,這樣會使得保留的串愈來愈長,後續在計算重疊的時候(還要考慮子串的狀況)計算量愈來愈大。提交測試後,50%的數據沒法計算出結果。

因而,考慮對算法進行優化:
考慮到每次在計算單詞重疊和合並時的時間太長,先從這裏入手,再次回到題目中,每次計算時是否能夠只考慮當前兩個單詞之間的關係,而不考慮之間已經合併的內容。那麼假如存在下列狀況
a.沒有重疊部分
abcdef
cdgvp
hik
直接按順序合併便可,須要注意到,儘管兩個單詞都含有「cd」,但他們並不能合併
abcdefcdgvphik

b.有重疊部分
abcdef
cdefgh
xyzab
按順序逐一合併後獲得,後面的單詞不能合併到以前的單詞前面
abcdefghxyzab

這並非最優結果,最佳的合併順序是xyzab->abcdef->cdefgh
可是咱們也發現了在合併兩個單詞的時候,咱們只須要考慮兩兩之間的狀況,而不須要考慮以前的,所以儘管當前看起來並非一個最佳的構造方案,可是當咱們枚舉了全部的狀況之後,其實是總能找到一個最佳的構造的方案
例如:(數字代表了重疊的部分的長度)
et->abcetfgh->fgh (最佳構造方案)
2+3
abcetfgh->et->fgh (非最佳)
2+0
abcetfgh->fgh->et (非最佳)
3+0

c.存在子串的狀況
實際上,咱們在最佳合併方案中觀察到,若是存在子串,例以下面的單詞
f
dfg
adfgk
ceadfgkh
abceadfgkhev
按任意順序合併後均可以獲得abceadfgkhev

性質2.若是A⊆B,即單詞A是單詞B的一個子串,那麼A和B構成的最短的字符串就是B,而且這種性質具備傳遞性,例如A⊆B、B⊆C、C⊆D,那麼最終構成的最短字符串是D。

那麼在存在子串的狀況下,咱們必須先將子串和主串合併,這樣纔多是最優的構造。而且實際上在子串與主串合併後,相對於主串並無帶來長度的增長。至此,咱們獲得兩種算法的優化方案。

優化1:以預處理方式計算全部單詞之間兩兩合併後的重疊部分
顯然,咱們在預處理的時候就能夠計算出單詞i與單詞j(i在前j在後,由於咱們還要計算到j在前i後的狀況)的重疊字符數,用overlap[i, j]表示

優化2:預處理同時去掉全部的子串,保留主串計算便可
顯然對於子串,甚至子串的子串都不會對計算產生影響,咱們須要將沒必要要的計算因素去掉,可能咱們只去掉了一個單詞,可是對於計的影響倒是顯著的,例如10!和9!相比,計算量整整少了1個數量級,對效率的提高很大。此外,去掉子串後,也簡化優化1的計算(再也不識別子串,全部的串那麼兩頭部分重疊,要不沒有重疊)

編寫代碼時,咱們先實現優化2的步驟,再來完成優化1,以後,咱們依然是全排列後,對每一組排列都利用預處理獲得的overlap[i, j]進行重疊部分的計算,並記錄重疊部分的總和。最後用全部單詞的總長度-重疊部分的總和,那就是問題的答案了。

提交測試後,極限數據n=10仍然不能在規定的時間內出解,咱們來看看計算量,假設沒有子串存在的狀況,咱們的全排列有n!,對每一個排列還要計算n-1次重疊總和,實際上,咱們計算量是n!*(n - 1),這裏還忽略了全排列生成自己花費的時間。

優化3:已知的模型幷包含高效的算法
其實咱們已經發現,咱們的目標是但願找到一個排列a(1),a(2),...,a(n),這也能夠看作是一個路徑,使得∑overlap[a(i), a(i+1)]最小,若是咱們把一個單詞看作一個頂點(vertex),而且任何兩個頂點(單詞)都有邊,這個邊的權就是overlap[,]中相應的值,例如單詞w(i),w(j)就是兩個頂點,這兩個頂點之間還有一條權重爲overlap[i, j]的邊。顯然這個問題已經轉換一個TSP(旅行商的問題),這個是一個很經典的問題,解法比較多,我選擇一種本身比較熟悉且效率較高的算法:狀態壓縮動態規劃(DP)

因爲這篇文章的重點是在優化思想而不是具體算法自己,而且動態規劃自己是一門比較大的算法類別,狀態壓縮動態規劃也很是靈活。所以咱們簡單介紹TSP的狀態壓縮動態規劃的方法。

因爲最多n個單詞,咱們須要用2進制的每一位去描述當前的單詞是否已經參與構造最終的字符串:
例如00101表示當前有5個單詞,其中第1個和第3個(從低位到高位)已參與經構造,那麼咱們一共有(1 << 5) - 1種狀態,一個都沒得選的時候狀態是00000,所有都參與構造的時候11111也就是(1 << 5) -1了。
那麼動態規劃的要素:咱們按將第i個單詞拿去參與構造字符串做爲階段劃分,狀態已經有了,將第i個單詞拿去參與構造字符串放在以哪一個單詞後面能夠得到最大的overlap做爲決策。

設f[i, j]表示在狀態i的狀況,以單詞j做爲最後一個單詞得到的最大overlap,考慮在單詞k加入後的狀況:
f[i|(1<<k), k] = max{f[i|(1<<k), k], f[i, j] + overlap[j, k]}
其中1 =< i < (1 << n) - 1, 0 <= k < n, 0 <= j < n

注意到一些位操做:(這裏爲了方便描述,最低位是從0開始計數)
a.判斷x的二進制位中的第i位是否爲1
x & (1 << i)

b.將x的二進制位中的第i位置爲1
x | (1 << i)

最後,代碼裏包含兩種方法,包括使用全排列的的方法(註釋掉的部分),附上時間消耗,其中java的兩個代碼同樣,都是dfs,最下面的cpp採用的狀態壓縮的DP,倒數第2個採用的是全排列的方法,其實可見cpp在運行效率上確實仍是大大優於python
圖片描述

*關於全排列算法
儘管幾乎全部的高級語言都帶有相關的庫,但有必要對其中的原理進行理解,而且嘗試編寫代碼。全排列的算法有不少種,其中回溯法編寫比較容易,字典序法效率較高,建議掌握。能夠參加下面的連接:
http://www.cnblogs.com/noworn...

*TSP問題的其餘算法:
a.分支限界
b.貪心
c.含剪枝的深度優先搜索(DFS)
http://blog.csdn.net/q_l_s/ar...

import sys
import itertools


def combine_words(wa, wb):
    max_overlap_len = min(len(wa), len(wb))
    for overlap_len in range(max_overlap_len - 1, -1, -1):
        match_f = True
        for i in range(overlap_len):
            if wa[len(wa) - overlap_len + i] != wb[i]:
                match_f = False
                break

        if match_f:
            return overlap_len


def calc_each_overlap(words_slv, n_slv, overlap):
    for i in range(n_slv):
        for j in range(n_slv):
            if i != j:
                overlap[i][j] = combine_words(words_slv[i], words_slv[j])


def dp(n, overlap):
    opt = [[0 for i in range(n)] for i in range(1 << (n + 1))]

    for i in range(1, 1 << n):
        for j in range(n):
            if i & (1 << j):
                for k in range(n):
                    if (i & (1 << k)) == 0 and opt[i | (1 << k)][k] < opt[i][j] + overlap[j][k]:
                        opt[i | (1 << k)][k] = opt[i][j] + overlap[j][k]

    ans = 0
    for i in range(n):
        ans = max(opt[(1 << n) - 1][i], ans)
    return ans


def main():
    n = map(int, sys.stdin.readline().strip().split())[0]
    words = []
    for i in range(n):
        words.append(map(str, sys.stdin.readline().strip().split())[0])

    sub_f = [False for i in range(n)]
    for i in range(n):
        for j in range(n):
            if i != j and ''.join(words[i]).find(''.join(words[j])) != -1:
                sub_f[j] = True

    words_slv = []
    t_len = 0
    n_slv = 0
    for i in range(n):
        if not sub_f[i]:
            n_slv += 1
            t_len += len(words[i])
            words_slv.append(words[i])

    overlap = [[0 for i in range(n_slv)] for i in range(n_slv)]
    calc_each_overlap(words_slv, n_slv, overlap)

    t_overlap_len = dp(n_slv, overlap)
    print t_len - t_overlap_len

    # per = [i for i in range(n_slv)]
    # shortest_words = sys.maxint
    # for permutation in itertools.permutations(per, n_slv):
    #     total_words_len = 0
    #     total_overlap_len = 0
    #     for i in range(n_slv):
    #         total_words_len += len(words_slv[permutation[i]])
    #         if i + 1 < n_slv:
    #             total_overlap_len += overlap[permutation[i]][permutation[i + 1]]
    #             if total_overlap_len >= shortest_words:
    #                 break
    #
    #     if total_words_len - total_overlap_len < shortest_words:
    #         shortest_words = total_words_len - total_overlap_len
    #
    # print shortest_words


if __name__ == '__main__':
    main()
相關文章
相關標籤/搜索