经过解决“构造包含全部给定子串的最短字符串”问题思考算法优化

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