【ACWing】1165. 单词环

题目地址:

https://www.acwing.com/problem/content/1167/

我们有 n n n个字符串,每个字符串都是由 a ∼ z a∼z az的小写英文字母组成的。如果字符串 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 3227.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 1n105

这道题与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} kci,设二分点是 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 kcix(cix)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;
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值