题目链接
单词环
题目描述
我们有 n n n 个字符串,每个字符串都是由 a ∼ z a∼z a∼z 的小写英文字母组成的。
如果字符串 A A A 的结尾两个字符刚好与字符串 B B B 的开头两个字符相匹配,那么我们称 A A A 与 B B B 能够相连(注意: A A A 能与 B B B 相连不代表 B B B 能与 A A A 相连)。
我们希望从给定的字符串中找出一些,使得它们首尾相连形成一个环串(一个串首尾相连也算),我们想要使这个环串的平均长度最大。
如下例:
ababc
bckjaca
caahoynaab
第一个串能与第二个串相连,第二个串能与第三个串相连,第三个串能与第一个串相连,我们按照此顺序相连,便形成了一个环串,长度为 5 + 7 + 10 = 22 5+7+10=22 5+7+10=22(重复部分算两次),总共使用了 3 3 3 个串,所以平均长度是 22 ÷ 3 ≈ 7.33 22÷3≈7.33 22÷3≈7.33。
输入格式
本题有多组数据。
每组数据的第一行,一个整数 n n n,表示字符串数量;
接下来 n n n 行,每行一个长度小于等于 1000 1000 1000 的字符串。
读入以 n = 0 n=0 n=0 结束。
输出格式
若不存在环串,输出No solution
,否则输出最长的环串的平均长度,答案保留两位小数。
样例 #1
样例输入 #1
3
intercommunicational
alkylbenzenesulfonate
tetraiodophenolphthalein
0
样例输出 #1
21.66
提示
【数据范围】
1
≤
n
≤
1
0
5
1≤n≤10^5
1≤n≤105
算法思想
根据题目描述,要求是否存一个首尾相连的环串,如果存在的话,求最长的环串的平均长度。
不妨设环中每个字符串的长度为
W
i
W_i
Wi,环中字符串的个数为
S
S
S,要求的是
∑
W
i
S
\frac{\sum W_i}{S}
S∑Wi的最大值。这种求“最优比率” 的问题,可以参考博主的另一个文章:每周算法:01分数规划。
建图
在图中最长的环串的平均长度首先考虑如何建图。一种直观的建图方式是将每个单词作为一个节点,如果这两个单词能够首尾匹配,则在这两个单词之间连接一条有向边,此时最多有 1 0 5 10^5 105个节点,在最坏情况下(字母都一样)要建 1 0 10 10^{10} 1010条边,显然不可行。
考虑一种等价的建图方式,将一个单词看作一条边:将开头两个字符和结尾两个字符作为节点,单词长度作为边权,连接一条边,如下图所示:
这样建图,节点数最多只有
26
×
26
=
676
26\times26=676
26×26=676个,边数即单词数=
1
0
5
10^5
105。
01分数规划
本题求的是
∑
W
i
S
\frac{\sum W_i}{S}
S∑Wi的最大值,其中
∑
W
i
\sum W_i
∑Wi表示环串种单词长度之和,
S
S
S表示单词个数。
最优比率问题(即01分数规划)可以用二分求解。即求是否存在
m
i
d
mid
mid,使环串上各单词长度之和除以单词个数大于
m
i
d
mid
mid,即
∑
W
i
S
>
m
i
d
\frac{\sum W_i}{S}>mid
S∑Wi>mid,进一步整理可得:
∑
(
W
i
−
m
i
d
×
1
)
>
0
\sum (W_i-mid\times 1)>0
∑(Wi−mid×1)>0
那么,要判断在包含环的图中是否存在 m i d mid mid,使环串上各单词长度之和除以单词个数大于 m i d mid mid,就等价于判断当图中边权为 W i − m i d W_i-mid Wi−mid时,是否存在正权环。而判断是否存在正权环可以使用「SFPA」算法,基本思想与判断负环稍有不同。判断正权环时:
- 要求最长路,即 d [ v ] < d [ u ] + w d[v] < d[u] + w d[v]<d[u]+w时,更新 v v v点到起点的最长路;
- 当 v v v点到起点的最长路被更新时,累加最长路中包含的边数,即 c n t [ v ] = c n t [ u ] + 1 cnt[v]=cnt[u]+1 cnt[v]=cnt[u]+1
- 如果边数 c n t [ v ] > = n cnt[v]>=n cnt[v]>=n,说明图中存在正权环。
算法实现
综合上述,求 ∑ W i S \frac{\sum W_i}{S} S∑Wi的最大值算法实现如下:
- 在
[
L
,
R
]
[L,R]
[L,R]范围内通过二分查找答案,对于中间结果
m
i
d
mid
mid:
- 使用SPFA算法判断当边权为 p i − m i d × w i p_i-mid\times w_i pi−mid×wi时,图中是否存在正权环
- 如果存在正权环,则在 [ m i d , R ] [mid,R] [mid,R]范围继续查找答案
- 否则不存在正权环,则在 [ L , m i d ] [L,mid] [L,mid]范围继续查找答案
最后,如何确定二分查找的范围:
- 由于单词长度 W i > 0 W_i>0 Wi>0,那么 ∑ W i S > 0 \frac{\sum W_i}{S}>0 S∑Wi>0;
- 最多有 1 0 5 10^5 105个单词,每个单词的长度不超过 1000 1000 1000,因此 ∑ W i S ≤ 1000 \frac{\sum W_i}{S}\le1000 S∑Wi≤1000
因此, ∑ W i S ∈ ( 0 , 1000 ] \frac{\sum W_i}{S}\in(0,1000] S∑Wi∈(0,1000]
代码实现
#include <bits/stdc++.h>
using namespace std;
//有26*26个点
const int N = 700, M = 100005;
int n;
int h[N], e[M], w[M], ne[M], idx;
bool st[N];
double d[N];
void add(int a, int b, int c) // 添加一条边a->b,边权为c
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool dfs(int u, double mid) //使用dfs实现SPFA,判断以u为起点是否存在正权环
{
if(st[u]) return true; //走到访问过的点,存在环
st[u] = true;
bool flag = false;
for(int i = h[u]; ~ i ; i = ne[i])
{
int v = e[i];
if(d[v] < d[u] + w[i] - mid)
{
d[v] = d[u] + w[i] - mid;
flag = dfs(v, mid);
if(flag) break;
}
}
st[u] = false; //恢复现场
return flag;
}
bool check(double mid)
{
memset(d, 0, sizeof d);
for(int i = 0; i < N; i ++) //枚举所有点作为起点,判读是否存在正权环
if(dfs(i, mid)) return true;
return false;
}
int main()
{
string s;
while(cin >> n, n)
{
idx = 0; //有多组测试样例
memset(h, -1, sizeof h);
for(int i = 0; i < n; i ++)
{
cin >> s;
int c = s.size();
if(c >= 2)
{
//将前两和后两个个字符转为26进制数
int a = (s[0] - 'a') * 26 + s[1] - 'a';
int b = (s[c - 2] - 'a') * 26 + s[c - 1] - 'a';
add(a, b, c); //建一条a->b边权为c的边
}
}
if(!check(0)) //当mid取0都不存在正权环时,说明无解
{
puts("No solution"); continue;
}
double L = 0, R = 1000;
while(R - L > 1e-4)
{
double mid = (L + R) / 2;
if(check(mid)) L = mid;
else R = mid;
}
printf("%.2lf\n", L);
}
}