题目地址:
https://www.acwing.com/problem/content/1167/
我们有 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 \frac{22}{3}≈7.33 322≈7.33。
输入格式:
本题有多组数据。每组数据的第一行,一个整数
n
n
n,表示字符串数量;接下来
n
n
n行,每行一个长度小于等于
1000
1000
1000的字符串。读入以
n
=
0
n=0
n=0结束。
输出格式:
若不存在环串,输出”No solution”
,否则输出最长的环串的平均长度。只要答案与标准答案的差不超过
0.01
0.01
0.01,就视为答案正确。
数据范围:
1
≤
n
≤
1
0
5
1≤n≤10^5
1≤n≤105
这道题与https://blog.csdn.net/qq_46105170/article/details/116115660非常像,也是在寻找某个均值最大的环。我们可以这样建图,将连续的两个字母看成是图里的点,每个字符串看成是一条边,这条边连接的就是其前两个字母和后两个字母,边权就是字符串长度。那么问题就转化为问均值最大的环是多少,可以用二分来做。设某个环上的边权是 c 1 , . . . , c k c_1,...,c_k c1,...,ck,均值即为 ∑ c i k \frac{\sum c_i}{k} k∑ci,设二分点是 x x x,那么 ∑ c i k ≥ x ⇔ ∑ ( c i − x ) ≥ 0 \frac{\sum c_i}{k}\ge x\Leftrightarrow \sum (c_i-x)\ge 0 k∑ci≥x⇔∑(ci−x)≥0即要将边权视为是原边权减去 x x x,再去判断是否存在正环(SPFA可以判断正环)。可以先令 x = 0 x=0 x=0,如果找不到正环,说明原图就不存在环(因为边权都是正的),所以可以判定为无解。否则开始二分,初始搜索范围是 [ 0 , 1000 ] [0,1000] [0,1000],如果分点在 x x x时找到了正环,说明最大平均值至少可以到达 x x x,于是收缩左端点;否则收缩右端点。
这里还需要注意一点,因为SPFA求正环的时候,时间复杂度是很高的,最差的时候会到接近 O ( m n ) O(mn) O(mn)上界的时间复杂度。所以我们可以采取一个经验值使得循环提前退出:如果所有点最短路被更新了多于 5 n 5n 5n次了( n , m n,m n,m分别是图的顶点、边数),那么可以直接认为是存在正环的。
代码如下:
#include <iostream>
#include <cstring>
#include <queue>
#include <unordered_set>
using namespace std;
const int N = 26 * 26, M = 100010;
int n, V;
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
int cnt[N];
bool st[N];
// 用来记录顶点数
unordered_set<int> s;
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
// 返回是否能找到正环
bool check(double mi) {
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
memset(dist, 0, sizeof dist);
V = s.size();
queue<int> q;
// 只把有出边的点入队,因为无出边的点是不可能走到环上的
for (int i = 0; i < N; i++) {
if (h[i] == -1) continue;
q.push(i);
st[i] = true;
}
int count = 0;
while (q.size()) {
int t = q.front(); q.pop();
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] < dist[t] + w[i] - mi) {
dist[j] = dist[t] + w[i] - mi;
cnt[j] = cnt[t] + 1;
// 更新次数太多了,直接判定存在正环
if (++count > 5 * V) return true;
if (cnt[j] >= V) return true;
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main() {
string str;
while (scanf("%d", &n), n) {
memset(h, -1, sizeof h);
idx = 0;
for (int i = 0; i < n; i++) {
cin >> str;
int len = str.size();
if (len >= 2) {
int left = (str[0] - 'a') * 26 + str[1] - 'a';
int right = (str[len - 2] - 'a') * 26 + str[len - 1] - 'a';
s.insert(left), s.insert(right);
add(left, right, len);
}
}
if (!check(0)) cout << "No solution" << endl;
else {
double l = 0, r = 1000;
while (l + 1e-4 < r) {
double mi = (l + r) / 2;
if (check(mi)) l = mi;
else r = mi;
}
printf("%lf\n", l);
}
}
return 0;
}
时间复杂度 O ( m n ) O(mn) O(mn),空间 O ( n ) O(n) O(n), m m m和 n n n分别是图的边数和点数。
此外,另一种比较好的办法是,在SPFA求正环的时候,可以将队列改成栈,这样有助于及早发现正环(这时,可以去掉上面的那个判定的代码,但是运行时间依然不能确保是稳定的)。代码如下:
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 26 * 26, M = 100010;
int n, V;
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
int stk[N], top;
int cnt[N];
bool st[N];
unordered_set<int> s;
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
bool check(double mi) {
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
memset(dist, 0, sizeof dist);
V = s.size();
top = 0;
for (int i = 0; i < N; i++) {
if (h[i] == -1) continue;
stk[top++] = i;
st[i] = true;
}
int count = 0;
while (top) {
int t = stk[--top];
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] < dist[t] + w[i] - mi) {
dist[j] = dist[t] + w[i] - mi;
cnt[j] = cnt[t] + 1;
if (cnt[j] >= V) return true;
if (!st[j]) {
stk[top++] = j;
st[j] = true;
}
}
}
}
return false;
}
int main() {
string str;
while (scanf("%d", &n), n) {
memset(h, -1, sizeof h);
idx = 0;
for (int i = 0; i < n; i++) {
cin >> str;
int len = str.size();
if (len >= 2) {
int left = (str[0] - 'a') * 26 + str[1] - 'a';
int right = (str[len - 2] - 'a') * 26 + str[len - 1] - 'a';
s.insert(left), s.insert(right);
add(left, right, len);
}
}
if (!check(0)) cout << "No solution" << endl;
else {
double l = 0, r = 1000;
while (l + 1e-4 < r) {
double mi = (l + r) / 2;
if (check(mi)) l = mi;
else r = mi;
}
printf("%lf\n", l);
}
}
return 0;
}