## [$AcWing$ $1165$ 单词环](https://www.acwing.com/problem/content/1167/) ### 一、题目描述 我们有 $n$ 个字符串,每个字符串都是由 $a$∼$z$ 的 **小写英文字母** 组成的。 如果字符串 $A$ 的结尾两个字符刚好与字符串 $B$ 的 **开头两个字符相匹配**,那么我们称 $A$ 与 $B$ 能够相连(注意:$A$ 能与 $B$ 相连不代表 $B$ 能与 $A$ 相连)。 我们希望从给定的字符串中找出一些,使得它们首尾相连形成一个 **环串**(**一个串首尾相连也算**),我们想要使这个环串的 **平均长度最大** 。 如下例: ```c++ ababc bckjaca caahoynaab ``` 第一个串能与第二个串相连,第二个串能与第三个串相连,第三个串能与第一个串相连,我们按照此顺序相连,便形成了一个环串,长度为 $5+7+10=22$(重复部分算两次),总共使用了 $3$ 个串,所以平均长度是 $\frac{22}{3}≈7.33$。 **输入格式** 本题有多组数据。 每组数据的第一行,一个整数 $n$,表示字符串数量; 接下来 $n$ 行,每行一个长度小于等于 $1000$ 的字符串。 读入以 $n=0$ 结束。 **输出格式** 若不存在环串,输出 `No solution`,否则输出最长的环串的平均长度。 只要答案与标准答案的差不超过 `0.01`,就视为答案正确。 **数据范围** $1≤n≤10^5$ **输入样例**: ```cpp {.line-numbers} 3 intercommunicational alkylbenzenesulfonate tetraiodophenolphthalein 0 ``` **输出样例**: ```cpp {.line-numbers} 21.66 ``` ### 二、题目解析 #### 1、建图技巧 本题同样是考察 $01$分数规划,只不过难度较上题有所提升。首先需要考虑的是 **如何建图**,如果常规的用每个字符串作为图中的节点,最多会有$1e5$个节点,边数也可能高达$1e5 \times 1e5=1e10=100亿$级别,显然难以承受,而且以字符串作为节点,还需要挨个比较每个字符串的末尾两个字符与其它字符串的开头两个字符是否相等,这也将耗费大量的时间。 我们知道,有小写字母构成的两个字符最多有$26 * 26 = 676$种可能,而这十万字符串中间的内容并不重要,我们只关心每个字符串的 **开始两个字符**、**末尾两个字符** 以及 **字符串的长度**。这样我们读入一个字符串时,将其开头长度为$2$的字符串作为一个节点,末尾长度为$2$的字符串作为另一个节点,两个节点间有向边的长度就是该字符串的长度。这种 **巧妙** 的建图方式不仅使得节点数骤减,边数不超过限制$676 \times 676 = 456976$,更方便的是每条边都象征着存在一个字符串开头字符串是$a$,末尾字符串是$b$,字符串长度是$c$。如果$b$指向另一个点$d$,$abd$就自然的连接起来了,并且连接后字符串的长度就是边权之和,非常方便。既然节点最多有$26*26$种可能性,那么就可以将长度为$2$的字符串进行哈希映射到$0$到$675$的区间中,比如$aa$就映射到编号为$0$的节点,这样本题的图就建好了。 #### 2、$01$分数规划 第二步就是按照$01$分数规划的解题思路推公式,**环串的平均长度最大**,等价于$\displaystyle \large \frac{\sum s_i}{\sum 1}$最大,其中 * $\large \sum s_i$表示环中各边的长度累加和 * $\large \sum 1$就是环上的 **边数** 累加和 要判断对于某个$mid$是否有$\large \displaystyle \frac{\sum s_i}{\sum 1} >= mid$,只需要$\displaystyle \sum s_i >= \sum 1*mid$,即$\sum(mid - s_i) <= 0$即可,即边权为$mid - s_i$的图中存在**负权回路**,问题就进一步转化为了$spfa$求负权回路问题了。 >**注意**:此题的数据卡了$SPFA$的$bfs$,可以用$SPFA$的$dfs$方法判断是否存在负环。 #### $Code$ ```cpp {.line-numbers} #include using namespace std; const int N = 1e3 + 10; const int M = 1e5 + 10; const int U = 26 * 26; int n; int h[N], e[M], w[M], ne[M], idx; double dist[N]; bool st[N]; // 如果当前这个点 u 可以松弛的点 v 是被访问过的,那么就说明一定存在负环。 char str[N]; // 黄海替换为string,结果执行时间由70ms--> 400ms!!! const double eps = 1e-3; void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } bool dfs(int u, double mid) { if (st[u]) return 1; // 如果又见u,说明有环 bool flag = 0; // 我的后代们是不是有环? st[u] = 1; // u我出现过一次了~ for (int i = h[u]; ~i; i = ne[i]) { int v = e[i]; // 更新最小值,判负环 if (dist[v] > dist[u] - w[i] + mid) { dist[v] = dist[u] - w[i] + mid; // 检查一下我的下一个节点j,它要是有负环检查到,我也汇报 flag = dfs(v, mid); if (flag) break; } } st[u] = 0; // 回溯 return flag; } bool check(double mid) { memset(dist, 0, sizeof dist); for (int i = 0; i < U; i++) if (dfs(i, mid)) return 1; return 0; } void solve() { memset(h, -1, sizeof h); idx = 0; while (n--) { scanf("%s", str); int len = strlen(str); if (len < 2) continue; int a = (str[0] - 'a') * 26 + str[1] - 'a'; int b = (str[len - 2] - 'a') * 26 + str[len - 1] - 'a'; add(a, b, len); } double l = 0, r = 1e3; while (r - l > eps) { double mid = (l + r) / 2; if (check(mid)) l = mid; else r = mid; } if (l < eps) puts("No solution"); else printf("%.2lf\n", r); } int main() { // 多组测试数组,喜欢单开一个solve()进行处理,确实看着舒服 while (scanf("%d", &n), n) solve(); return 0; } ```