一开始有一个很直观的想法:
对于每一个单词,如果它能够与另外一个单词相匹配,就从当前单词向另外一个单词连一条长度为当前单词长度的边。
但这样做最坏情况下会有 (10^5) 个点,(10^{10}) 条边,时间和空间都会承受不了。
于是我们考虑如何优化建图。
换一种建图的方式:
对于每一个单词,将它的前两个字符和后两个字符之间连一条长度为当前单词的长度的边。
稍微思考一下就会发现这种建图方式其实是和第一种等价的。
建完图后,这个问题就变成了一个 (01) 分数规划问题,要求的是 (frac{sum 单词长度总和}{sum 单词个数}) 的最大值。
然后就是一些基本操作:二分答案 (+) SPFA 找 正环。
注意 SPFA 时需要加一些优化:
- 记录一下进行过松弛操作的点的数量,如果所有的点都已经做过了,就说明很大可能存在一个正环,直接返回 true;
这样就可以通过此题了。
#include <bits/stdc++.h>
using namespace std;
const int N = 703, M = 200003;
int n, m;
int tot, head[N], ver[M], nxt[M], edge[M];
double dist[N];
int cnt[N];
bool st[N];
char s[1003];
inline void add(int u, int v, int w)
{
ver[++tot] = v, edge[tot] = w, nxt[tot] = head[u], head[u] = tot;
}
inline bool check(double mid)
{
memset(cnt, 0, sizeof cnt);
memset(st, false, sizeof st);
queue <int> q;
for (int i = 0; i <= 675; i+=1)
{
q.push(i); //先将所有点加进队列
st[i] = true;
}
int cntt = 0;
while (!q.empty())
{
int u = q.front(); q.pop();
st[u] = false;
for (int i = head[u]; i; i = nxt[i])
{
int v = ver[i], w = edge[i];
if (dist[v] < dist[u] + w - mid)
{
dist[v] = dist[u] + w - mid;
cnt[v] = cnt[u] + 1;
if (++cntt > 10000) return true; //进行过松弛操作的点的数量
if (cnt[v] >= N) return true; //存在正环
if (!st[v])
{
st[v] = true;
q.push(v);
}
}
}
}
return false; //没有正环
}
int main()
{
while (1)
{
cin >> n;
if (!n) break;
memset(head, 0, sizeof head);
tot = 0;
for (int i = 1; i <= n; i+=1)
{
scanf("%s", s + 1);
int len = strlen(s + 1);
if (len >= 2)
{
int c1 = (s[1] - 'a') * 26 + (s[2] - 'a'), c2 = (s[len - 1] - 'a') * 26 + (s[len] - 'a');
add(c1, c2, len); //建图,注意是单向边
}
}
if (!check(0)) {puts("No solution"); continue;}
double l = 0.0, r = 1003.0;
while (r - l > 1e-4)
{
double mid = (l + r) / 2.0;
if (check(mid)) l = mid;
else r = mid;
}
printf("%.2lf
", l);
}
return 0;
}