You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 1165 单词环

一、题目描述

我们有 n 个字符串,每个字符串都是由 az小写英文字母 组成的。

如果字符串 A 的结尾两个字符刚好与字符串 B开头两个字符相匹配,那么我们称 AB 能够相连(注意:A 能与 B 相连不代表 B 能与 A 相连)。

我们希望从给定的字符串中找出一些,使得它们首尾相连形成一个 环串一个串首尾相连也算),我们想要使这个环串的 平均长度最大

如下例:

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

输入样例

3
intercommunicational
alkylbenzenesulfonate
tetraiodophenolphthalein
0

输出样例

21.66

二、题目解析

1、建图技巧

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

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

注意:此题的数据卡了SPFAbfs,可以用SPFAdfs方法判断是否存在负环。

Code

#include <bits/stdc++.h>

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;
}