最近因为工做相对比较忙,须要学习一些新的技术项目,写代码的时间比较少。继续解决百度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()