每周题解:单词环

题目链接

单词环

题目描述

我们有 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 22÷3≈7.33 22÷37.33

输入格式

本题有多组数据。

每组数据的第一行,一个整数 n n n,表示字符串数量;

接下来 n n n 行,每行一个长度小于等于 1000 1000 1000 的字符串。

读入以 n = 0 n=0 n=0 结束。

输出格式

若不存在环串,输出No solution,否则输出最长的环串的平均长度,答案保留两位小数。

样例 #1

样例输入 #1

3
intercommunicational
alkylbenzenesulfonate
tetraiodophenolphthalein
0

样例输出 #1

21.66

提示

【数据范围】
1 ≤ n ≤ 1 0 5 1≤n≤10^5 1n105

算法思想

根据题目描述,要求是否存一个首尾相连的环串,如果存在的话,求最长的环串的平均长度。
不妨设环中每个字符串的长度为 W i W_i Wi,环中字符串的个数为 S S S,要求的是 ∑ W i S \frac{\sum W_i}{S} SWi的最大值。这种求“最优比率” 的问题,可以参考博主的另一个文章:每周算法:01分数规划

建图

在图中最长的环串的平均长度首先考虑如何建图。一种直观的建图方式是将每个单词作为一个节点,如果这两个单词能够首尾匹配,则在这两个单词之间连接一条有向边,此时最多有 1 0 5 10^5 105个节点,在最坏情况下(字母都一样)要建 1 0 10 10^{10} 1010条边,显然不可行。

考虑一种等价的建图方式,将一个单词看作一条边:将开头两个字符和结尾两个字符作为节点,单词长度作为边权,连接一条边,如下图所示:
在这里插入图片描述
这样建图,节点数最多只有 26 × 26 = 676 26\times26=676 26×26=676个,边数即单词数= 1 0 5 10^5 105

01分数规划

本题求的是 ∑ W i S \frac{\sum W_i}{S} SWi的最大值,其中 ∑ W i \sum W_i Wi表示环串种单词长度之和, S S S表示单词个数。
最优比率问题(即01分数规划)可以用二分求解。即求是否存在 m i d mid mid使环串上各单词长度之和除以单词个数大于 m i d mid mid,即 ∑ W i S > m i d \frac{\sum W_i}{S}>mid SWi>mid,进一步整理可得: ∑ ( W i − m i d × 1 ) > 0 \sum (W_i-mid\times 1)>0 (Wimid×1)>0

那么,要判断在包含环的图中是否存在 m i d mid mid,使环串上各单词长度之和除以单词个数大于 m i d mid mid,就等价于判断当图中边权为 W i − m i d W_i-mid Wimid时,是否存在正权环。而判断是否存在正权环可以使用「SFPA」算法,基本思想与判断负环稍有不同。判断正权环时:

  • 要求最长路,即 d [ v ] < d [ u ] + w d[v] < d[u] + w d[v]<d[u]+w时,更新 v v v点到起点的最长路;
  • v v v点到起点的最长路被更新时,累加最长路中包含的边数,即 c n t [ v ] = c n t [ u ] + 1 cnt[v]=cnt[u]+1 cnt[v]=cnt[u]+1
  • 如果边数 c n t [ v ] > = n cnt[v]>=n cnt[v]>=n,说明图中存在正权环。

算法实现

综合上述,求 ∑ W i S \frac{\sum W_i}{S} SWi的最大值算法实现如下:

  • [ L , R ] [L,R] [L,R]范围内通过二分查找答案,对于中间结果 m i d mid mid
    • 使用SPFA算法判断当边权为 p i − m i d × w i p_i-mid\times w_i pimid×wi时,图中是否存在正权环
  • 如果存在正权环,则在 [ m i d , R ] [mid,R] [mid,R]范围继续查找答案
  • 否则不存在正权环,则在 [ L , m i d ] [L,mid] [L,mid]范围继续查找答案

最后,如何确定二分查找的范围:

  • 由于单词长度 W i > 0 W_i>0 Wi>0,那么 ∑ W i S > 0 \frac{\sum W_i}{S}>0 SWi>0;
  • 最多有 1 0 5 10^5 105个单词,每个单词的长度不超过 1000 1000 1000,因此 ∑ W i S ≤ 1000 \frac{\sum W_i}{S}\le1000 SWi1000

因此, ∑ W i S ∈ ( 0 , 1000 ] \frac{\sum W_i}{S}\in(0,1000] SWi(0,1000]

代码实现

#include <bits/stdc++.h>
using namespace std;
//有26*26个点
const int N = 700, M = 100005;
int n;
int h[N], e[M], w[M], ne[M], idx;
bool st[N];
double d[N];
void add(int a, int b, int c)  // 添加一条边a->b,边权为c
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool dfs(int u, double mid) //使用dfs实现SPFA,判断以u为起点是否存在正权环
{
    if(st[u]) return true; //走到访问过的点,存在环
    st[u] = true;
    bool flag = false;
    for(int i = h[u]; ~ i ; i = ne[i])
    {
        int  v = e[i];
        if(d[v] < d[u] + w[i] - mid)
        {
            d[v] = d[u] + w[i] - mid;
            flag = dfs(v, mid);
            if(flag) break;
        }
    }
    st[u] = false; //恢复现场
    return flag;
}
bool check(double mid)
{
    memset(d, 0, sizeof d);
    for(int i = 0; i < N; i ++) //枚举所有点作为起点,判读是否存在正权环
        if(dfs(i, mid)) return true;
    return false;
}
int main()
{
    string s;
    while(cin >> n, n)
    {
        idx = 0; //有多组测试样例
        memset(h, -1, sizeof h);
        for(int i = 0; i < n; i ++)
        {
            cin >> s;
            int c = s.size();
            if(c >= 2)
            {
                //将前两和后两个个字符转为26进制数
                int a = (s[0] - 'a') * 26 + s[1] - 'a';
                int b = (s[c - 2] - 'a') * 26 + s[c - 1] - 'a';
                add(a, b, c); //建一条a->b边权为c的边
                
            }
        }
        if(!check(0)) //当mid取0都不存在正权环时,说明无解
        { 
            puts("No solution"); continue;
        }
        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);
    }
}

  • 28
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值