最近因爲工做相對比較忙,須要學習一些新的技術項目,寫代碼的時間比較少。繼續解決百度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()