本题同样是考察​\(01\)分数规划​,只不过难度较上题有所提升。首先需要考虑的是​如何建图​,如果常规的用每个字符串作为图中的节点,最多会有\(10w\)个节点,边数也可能高达\(100\)亿级别,显然难以承受,而且以字符串作为节点,还需要挨个比较每个字符串的末尾两个字符与其它字符串的开头两个字符是否相等,这也将耗费大量的时间。

我们知道,有小写字母构成的两个字符最多有\(26 * 26 = 676\)种可能,而这十万字符串中间的内容并不重要,我们只关心每个字符串的​开始两个字符​、​末尾两个字符​以及​字符串的长度​。这样我们读入一个字符串时,将其开头长度为\(2\)的字符串作为一个节点,末尾长度为\(2\)的字符串作为另一个节点,两个节点间有向边的长度就是该字符串的长度。这种巧妙的建图方式不仅使得节点数骤减,边数不超过限制\(10w\),更方便的是每条边都象征着存在一个字符串开头字符串是\(a\),末尾字符串是\(b\),字符串长度是\(c\)。如果\(b\)指向另一个点\(d\),\(abd\)就自然的连接起来了,并且连接后字符串的长度就是边权之和,非常方便。既然节点最多有\(26*26\)种可能性,那么就可以将长度为\(2\)的字符串进行哈希映射到\(0\)到\(675\)的区间中,比如\(aa\)就映射到编号为\(0\)的节点,这样本题的图就建好了。

第二步就是按照\(01\)分数规划的解题思路推公式,环串的平均长度最大,等价于\(∑s_i / ∑1\)最大,其中\(s_i\)表示环中各边的长度,\(∑1\)就是环上的​边数​,要判断对于某个\(mid\)是否有\(∑s_i / ∑1 >= mid\),只需要\(∑s_i >= ∑1*mid\),即\(∑(mid - s_i) <= 0\)即可,即边权为\(mid - s_i\)的图中存在​负权回路​,问题就进一步化为了\(spfa\)求负权回路问题了。

最后本题时间卡得很紧,如果要等待出现一条路径涉及边的条数达到\(676\)再确定存在负环,就会超时,这里就要用到玄学优化了,一般经验值​取节点数的两倍​,这里边比较多就取\(100000\),即被松弛的总次数达到\(100000\)时就按照经验判定为存在负环,结束\(spfa\)算法。也可以将\(spfa\)中的队列换成栈,在实现上相当于每次都从队尾取元素,这样一来,一旦存在环,就会很快的去松弛环上的下一个节点,更快的结束算法。另外,对于不存在负权的情况,只需要判断下\(mid = 0\)的时候是否合法即可,\(∑s_i / ∑1 >= mid = 0\),等价于\(∑s >= 0\),也就是存在一个单词环满足题目要求即可,无解的情况就是无法构造出环。总的代码如下:

一、玄学优化法

#include <bits/stdc++.h>
using namespace std;

// n=26 26*26=676 这里N设为700
const int N = 700, M = 100010;
int m; //边的数量
int cnt[N], q[N];
double dist[N];
bool st[N];
//邻接表
int idx, h[N], e[M], w[M], ne[M];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool check(double mid) {
memset(dist, 0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, false, sizeof st);
queue<int> q;
for (int i = 0; i < 676; i++) {
q.push(i);
st[i] = true;
}
//整体入队列次数,676*10表示所有点都入队10次了,还没有找到解
int count = 0;
while (q.size()) {
int u = q.front();
q.pop();
st[u] = false;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] < dist[u] + w[i] - mid) {
dist[j] = dist[u] + w[i] - mid;
cnt[j] = cnt[u] + 1;
if (++count > 10000 || cnt[j] >= 676) return true;
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main() {
string s;
while (cin >> m, m) {
//多组测试数据,清空邻接表
memset(h, -1, sizeof h);
idx = 0;
//读入每条边
for (int i = 0; i < m; i++) {
cin >> s;
//不够长,没用
if (s.size() < 2) continue;
//模拟节点号
int a = (s[0] - 'a') * 26 + s[1] - 'a';
int b = (s[s.size() - 2] - 'a') * 26 + s[s.size() - 1] - 'a';
//建图,有向图
add(a, b, s.size());
}
if (!check(0))
puts("No solution");
else {
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);
}
}
return 0;
}

二、SLF优化模板法

#include <bits/stdc++.h>
using namespace std;

// n=26 26*26=676 这里N设为700
const int N = 700, M = 100010;
int m; //边的数量
int cnt[N], q[N];
double dist[N];
bool st[N];
//邻接表
int idx, h[N], e[M], w[M], ne[M];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool check(double mid) {
memset(dist, 0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, false, sizeof st);
//双端队列优化的SPFA
deque<int> q;
for (int i = 0; i < 676; i++) {
q.push_back(i);
st[i] = true;
}
while (q.size()) {
int u = q.front();
q.pop_front();
st[u] = false;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[u] + mid - w[i]) {
dist[j] = dist[u] + mid - w[i];
cnt[j] = cnt[u] + 1;
if (cnt[j] >= 676) return true;
if (!st[j]) {
// SLF优化
if (q.size() && dist[q.front()] > dist[j])
q.push_front(j);
else
q.push_back(j);
st[j] = true;
}
}
}
}
return false;
}
int main() {
string s;
while (cin >> m, m) {
//多组测试数据,清空邻接表
memset(h, -1, sizeof h);
idx = 0;
//读入每条边
for (int i = 0; i < m; i++) {
cin >> s;
//不够长,没用
if (s.size() < 2) continue;
//模拟节点号
int a = (s[0] - 'a') * 26 + s[1] - 'a';
int b = (s[s.size() - 2] - 'a') * 26 + s[s.size() - 1] - 'a';
//建图,有向图
add(a, b, s.size());
}
if (!check(0))
puts("No solution");
else {
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);
}
}
return 0;
}