1. 问题描述:
我们有 n 个字符串,每个字符串都是由 a∼z 的小写英文字母组成的。如果字符串 A 的结尾两个字符刚好与字符串 B 的开头两个字符相匹配,那么我们称 A 与 B 能够相连(注意:A 能与 B 相连不代表 B 能与 A 相连)。我们希望从给定的字符串中找出一些,使得它们首尾相连形成一个环串(一个串首尾相连也算),我们想要使这个环串的平均长度最大。
如下例:
ababc
bckjaca
caahoynaab
第一个串能与第二个串相连,第二个串能与第三个串相连,第三个串能与第一个串相连,我们按照此顺序相连,便形成了一个环串,长度为 5+7+10=22(重复部分算两次),总共使用了 3 个串,所以平均长度是 22 / 3≈7.33。
输入格式
本题有多组数据。每组数据的第一行,一个整数 n,表示字符串数量;接下来 n 行,每行一个长度小于等于 1000 的字符串。读入以 n=0 结束。
输出格式
若不存在环串,输出"No solution",否则输出最长的环串的平均长度。只要答案与标准答案的差不超过 0.01,就视为答案正确。
数据范围
1 ≤ n ≤ 10 ^ 5
输入样例:
3
intercommunicational
alkylbenzenesulfonate
tetraiodophenolphthalein
0
输出样例:
21.66
来源:https://www.acwing.com/problem/content/1167/
2. 思路分析:
分析题目可以知道这道题目类似于361题,属于01分数规划问题:一堆的和除以一堆的和,因为我们需要将原问题转换为图论的问题,所以第一个需要解决的问题是如何进行建图,这里有一个比较巧妙的方法是将每一个字符串看成是一条边,例如对于题目中的例子:
ababc
bckjaca
caahoynaab
我们可以取出每一个字符串的开始与结束连续的两个字符,对于第一个字符串来说建立一条边:ab --> bc,对于第二个字符串bc --> ca,第三个字符串ca -->ab,这样可以所有的字符串表示成一个有向图,每一个连续的开始与结束的两个字符可以看成是图的顶点,字符串的长度看成是边的权重,在存储边的信息的时候将两个字符看成是一个26进制的数字,这样最多有26 * 26 + 26个节点,因为单词个数最多为10 ^ 5所以边的数量最多为10 ^ 5,对于01分数规划问题一般使用二分来解决,因为边权都是大于0的,而且因为字符串长度最大是1000,使得每一个字符串都取到最大那么最终可以得到答案最大为1000,所以二分的范围为(0,1000],由题目可知我们需要使得满足下图中的等式:
根据二分的结果判断出答案在mid的左边还是右边,当右边界与左边界的差在一个范围之内就可以退出二分的循环了。对于无解的情况我们可以使得mid = 0,如果mid = 0都不存在负环那么肯定是无解的,因为mid越大结果是越大的,特判一下即可,这道题目的另外一个需要解决的问题是如果直接这样提交会超时,所以这里使用到的一个小技巧是当迭代超过一定的次数的时候就认为有向图中存在负环,直接返回True。
3. 代码如下:
import collections
from typing import List
class Solution:
# 对等式进行变形可以发现其实将对应的式子移到一边找的可以是负环也可以是正环, 下面找的是负环
# 根据自己变形的得到的等式大于0和小于0决定是找正环还是负环
def check(self, mid: int, g: List[List[int]]):
dis = [0] * 710
vis = [0] * 710
count = [0] * 710
q = collections.deque()
# 注意0也是合法的节点
for i in range(710):
q.append(i)
vis[i] = 1
cnt = 0
while q:
p = q.popleft()
vis[p] = 0
for next in g[p]:
# 寻找负环
if dis[next[0]] > dis[p] + mid - next[1]:
dis[next[0]] = dis[p] + mid - next[1]
count[next[0]] = count[p] + 1
cnt += 1
# 提前退出算法我们认为图中存在负环
if cnt >= 10000: return True
if count[next[0]] >= 710: return True
if vis[next[0]] == 0:
q.append(next[0])
vis[next[0]] = 1
return False
def process(self):
# 题目中有多组测试数据
while True:
n = int(input())
if n == 0: break
# 将每一个包含两个字母的字符串看成是一个26进制的数字
g = [list() for i in range(710)]
for i in range(n):
s = input()
if len(s) >= 2:
# 把最开始的两个字符与最后面的两个字符看成是一个二十六进制的数字
c1, d1 = ord(s[0]) - ord("a"), ord(s[1]) - ord("a")
c2, d2 = ord(s[-2]) - ord("a"), ord(s[-1]) - ord("a")
# 注意是有向边
g[c1 * 26 + d1].append((c2 * 26 + d2, len(s)))
# 二分的范围为(0, 1000]
l, r, eps = 0.00, 1000.00, 10 ** -4
if not self.check(0, g): print("No solution")
else:
while r - l > eps:
mid = (l + r) / 2
if self.check(mid, g):
l = mid
else:
r = mid
print(r)
if __name__ == '__main__':
Solution().process()