最近由于工作相对比较忙,需要学习一些新的技术项目,写代码的时间比较少。继续解决百度2017秋招4星的题目,今天要分析的这个题目,是目前我遇到相对其他4星题目算是有一点难度的题目。


今天我们将从一个题目的不同解决方案:超时到快速出解(无法等待结果 -> 10s -> 70ms)的改进过程来讨论算法的优化过程和一些思想。

域名选择


<题目来源:百度2017秋招,http://exercise.acmcoder.com/... >

问题描述

给网站选择一个好的域名是一件令人头痛的事,你希望你的域名在包含给定的一组关键字的同时,最短的长度是多少。

输入与输出:
输入文件的第一行包含一个整数 n,表示关键字的数目。(n<=10)
接下来的n行,每行包含了一个长度小于等于100的字符串,表示一组关键字。

输出一行一个数字,表示最短的长度

样例:
输入
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几乎已经接近极限时间3000ms

产生一个全排列结果后,我们还需要时间去计算两两之间可以重叠多少,每次增加一个单词都在原来的基础上去检查它们能够重叠多少,并更保留合并结果,这样会使得保留的串越来越长,后续在计算重叠的时候(还要考虑子串的情况)计算量越来越大。提交测试后,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 itertoolsdef 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 = Truefor i in range(overlap_len):if wa[len(wa) - overlap_len + i] != wb[i]:match_f = Falsebreakif match_f:return overlap_lendef 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 = 0for i in range(n):ans = max(opt[(1 << n) - 1][i], ans)return ansdef 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] = Truewords_slv = []t_len = 0n_slv = 0for i in range(n):if not sub_f[i]:n_slv += 1t_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_wordsif __name__ == '__main__':main()

通过解决“构造包含所有给定子串的最短字符串”问题思考算法优化相关推荐

  1. python3完美解决在包含中文、中文符号的字符串中匹配手机号码和身份证号码的方案

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言 一.前言 二.完美解决方案 总结 前言 在英文文本中去正则表达式同时去匹配手机号码和身份证号码很简单.但是,如果文本中 ...

  2. 给定一个字符串,判断该字符串中是否包含某个子串.如果包含,求出子串的所有出现位置. 要求:从键盘输入两个字符串,第一个是给定的字符串,第二个是子串。

    给定一个字符串,判断该字符串中是否包含某个子串.如果包含,求出子串的所有出现位置. 要求:从键盘输入两个字符串,第一个是给定的字符串,第二个是子串. 输入 abcd23abc34bcd bc 输出 1 ...

  3. 解决json包含html标签无法显示的问题

    解决json包含html标签无法显示的问题 参考文章: (1)解决json包含html标签无法显示的问题 (2)https://www.cnblogs.com/shitoupi/p/6708331.h ...

  4. numpy ndarray.tostring()(在数组中构造包含原始数据字节的Python字节)(tobytes()函数的兼容性别名)

    from numpy\core\multiarray def tostring(self, order='C'): # real signature unknown; restored from __ ...

  5. python 子串是否在字符串中_python七种方法判断字符串是否包含子串

    1. 使用 in 和 not in in 和 not in 在 Python 中是很常用的关键字,我们将它们归类为 成员运算符. 使用这两个成员运算符,可以很让我们很直观清晰的判断一个对象是否在另一个 ...

  6. Java 判断是否包含指定的子串 contains()

    Java 手册 contains public boolean contains(CharSequence s) 当且仅当此字符串包含指定的 char 值序列时,返回 true. 参数: s - 要搜 ...

  7. json在线去除html标签,解决json包含html标签无法显示的问题

    要是在json中包含html标签的话,在js接收数据的时候就会出现问题,导致接收失败. 所以在java端,对json包含有html标签的句子先进行转义. package com.alibaba.int ...

  8. Hot Chocolate提供了UseFiltering属性来用于构造包含查询过滤的SQL语句,所以我们直接使用就好了

    使用Hot Chocolate和.NET 6构建GraphQL应用(5) -- 实现Query过滤功能 系列导航# 使用Hot Chocolate和.NET 6构建GraphQL应用文章索引 需求# ...

  9. leetcode 5. 最长回文子串 暴力法、中心扩展算法、动态规划,马拉车算法(Manacher Algorithm)

    给定一个字符串 s,找到 s 中最长的回文子串.你可以假设 s 的最大长度为 1000.示例 1: 输入: "babad" 输出: "bab" 注意: &quo ...

最新文章

  1. 忘掉什么鬼并发,先听完这个故事!
  2. opencv-python:win7下,搭建python2.7.5环境,配置opencv3.1.0准备开工-OpenCV步步精深
  3. zookeeper集群配置与配置文件详解
  4. 字节码是java虚拟机的指令组_JVM??
  5. x64版本的OpenGL库配置
  6. twitter批量取消关注_如何在Twitter上取消阻止“潜在敏感内容”
  7. 8051单片机的中断发送
  8. REST(三)Restlet实现REST
  9. 迅为iTOP-IMX6ULL开发板Pinctrl和GPIO子系统实验-修改设备树文件
  10. WinForm中的NotifyIcon控件的使用
  11. 2018年航空概论期末考试
  12. 【100+ python基础入门-26】python修改列表元素方法
  13. 中国城市资本流动问题探索
  14. python学习笔记9——第八章 异常
  15. 网络爬虫-神器fiddler抓取app数据
  16. python sample函数取样_Pytorch各种取样器sample
  17. networking /etc/network/interfaces 笔记221102
  18. 这是什么代码帮我看看
  19. 直方图规定化(直方图匹配)
  20. 对象在内存中的存储基本类型和包装类java类型转换

热门文章

  1. sqoop操作之HIVE导出到ORACLE
  2. 用Node.js 写web框架(番外)
  3. 在WEBSERVICE学习中遇到的问题
  4. 浅谈Activiti Modeler 的扩展
  5. Windbg 教程-调试非托管程序的基本命令下
  6. select 下拉菜单Option对象使用add(elements,index)方法动态添加
  7. Spring Hibernate Mybatis配置详解
  8. Linux基本命令——vi文本编辑器
  9. app.config中增加appSettings节点,conn.open时报初始化错误
  10. 安装mysql和memcached