单词环--

欢迎访问个人博客:我的博客传送门

单词环


题目描述

image-20210712192242017


核心思路

这题主要是要用到spfa判断正环+01分数规划+spfa玄学优化

我们先来思考一下该怎么建图呢?

如果按照常规思路,我们把一个字符串看作一个节点,那么题目最多有 1 0 5 10^5 105个节点,那么会最多会有多少条边呢?我们来考虑最坏情况,这 1 0 5 10^5 105个字符串都是相同的,以3个完全相同的字符串为栗子,比如:

  • 【1】 a a a a a aaaaa aaaaa
  • 【2】 a a a a a aaaaa aaaaa
  • 【3】 a a a a a aaaaa aaaaa

可以发现对于【1】来说,它可以接到【2】和【3】上,那么有 3 − 1 = 2 3-1=2 31=2种可能,也就是说要从【1】引出两条有向边,分别指向【2】和【3】。同理分析可知,如果 1 0 5 10^5 105个字符串都相同,则对于每个字符串来说,都会引出 1 0 5 − 1 10^5-1 1051条边,为了方便起见,就看作是引出 1 0 5 10^5 105条边。有 1 0 5 10^5 105个节点,每个节点会引出 1 0 5 10^5 105条有向边,因此总共有 1 0 5 × 1 0 5 = 1 0 10 10^5\times 10^5=10^{10} 105×105=1010条有向边。很明显,点数为 1 0 5 10^5 105,边数为 1 0 10 10^{10} 1010,存储空间要爆炸。因此,我们需要思考该怎么建图呢?

从题目描述中,我们可以知道,一个字符串除了前面的2个字符和后面的2个字符有用之外,字符串中间的其他所有字符都没有用,因此,我们可以这样建图:

直接hash前面的两个字符和后面的两个字符作为节点,该字符串的长度作为这两个节点之间的边的权值。对于一个字符串来说,它后面的两个字符,每个字符都有26种可能,因此这两个字符产生了 26 × 26 = 676 26\times26=676 26×26=676种可能,也就是最多会有 676 676 676个节点。例如题目中的第一个字符串 a b a b c ababc ababc,它后面的 b c bc bc就确定了图中的一个顶点,第二个字符串 b c k j a c a bckjaca bckjaca,它后面的 c a ca ca就确定了图中的一个顶点,第三个字符串 c a a h o y n a a b caahoynaab caahoynaab,它后面的 a b ab ab就确定了图中的一个顶点。也就是说对于后面两个字符所确定的一种可能,都会对应地确定了图中的一个顶点。那么有 26 × 26 26\times 26 26×26种可能,也就是确定了图中最多会有 676 676 676个顶点。由之前分析可知,每个节点最多很引出 1 0 5 10^5 105条有向边。因此这种建图方式最多会引出 676 × 1 0 5 = 6.76 × 1 0 7 676\times 10^5=6.76\times 10^7 676×105=6.76×107条有向边,空间还算好一些。

如下图解释题目中的栗子:

image-20210712194716858

知道了该怎么建图后,我们再来思考题目要求的是什么?题目想要求的是形成环的这个串它的平均长度最大,转换到我们建好的图中,意思就是:边权之和/节点个数 最大。即 ∑ w i ∑ 1 \dfrac {\sum w_i}{\sum 1} 1wi要最大

要求这个值最大,满足单调性,很明显这就是一个01分数规划问题,那么就可以使用二分算法来二分答案了。我们设某一时刻二分出来的答案为 m i d mid mid(但不保证这就是最终正确想要的最大的那个答案),如果当前这个答案 m i d mid mid不够大,则说明还可以更大,即

∑ w i ∑ 1 > m i d \dfrac {\sum w_i}{\sum 1}>mid 1wi>mid

   ⟺    \iff ∑ w i > m i d ∗ ∑ 1 \sum w_i>mid*\sum 1 wi>mid1

   ⟺    \iff ∑ w i − m i d ∗ ∑ 1 > 0 \sum w_i-mid*\sum 1>0 wimid1>0

   ⟺    \iff ∑ w i − ∑ m i d ∗ 1 > 0 \sum w_i-\sum mid*1>0 wimid1>0

   ⟺    \iff ∑ ( w i − m i d ∗ 1 ) \sum (w_i-mid*1) (wimid1)

那么也就是说,对于每一条边,给它赋权值为 w i − m i d ∗ 1 w_i-mid*1 wimid1,如果存在正环,也就意味着有一个环的权值和大于0,也就是 ∑ ( w i − m i d × 1 ) > 0 \sum (w_i-mid\times 1)>0 (wimid×1)>0,就意味着 m i d mid mid需要更大,由于是单调递增的,那么此时就应该执行 L = m i d L=mid L=mid,否则就是执行 R = m i d R=mid R=mid

这里还要思考一下二分的区间是啥?由于 ∑ w i \sum w_i wi ∑ 1 \sum 1 1都是正数,因此相除的话一定是大于0的,注意这里是浮点数相除哦!因此左范围一定大于0。由 ∑ w i ∑ 1 \dfrac {\sum w_i}{\sum 1} 1wi式子可知,要想取最大,则分子要最大,分子要最小,由于最多有 1 0 5 10^5 105条边,每条边的权值最大是 1000 1000 1000,所以 ∑ w i \sum w_i wi最大是 1000 × 1 0 5 1000\times 10^5 1000×105。由于最多有 1 0 5 10^5 105条边,所以至少有 1 0 5 10^5 105个节点,所以 ∑ 1 \sum 1 1最小为 1 0 5 10^5 105,于是 ∑ w i ∑ 1 = 1000 × 1 0 5 1 0 5 \dfrac {\sum w_i}{\sum 1}=\dfrac {1000\times 10^5}{10^5} 1wi=1051000×105最大为 1000 1000 1000。因此答案区间就是 ( 0 , 1000 ] (0,1000] (0,1000]

题目说了不一定存在环串,那么我们该怎么知道呢?从 w i − m i d × 1 w_i-mid\times1 wimid×1可知,这是一个递减的线性函数,当 m i d mid mid取0时,有最大值,因此我们可以先尝试 m i d = 0 mid=0 mid=0,如果 w i − m i d × 1 ≤ 0 w_i-mid\times 1\leq 0 wimid×10,那么 ∑ w i − m i d × 1 ≤ 0 \sum w_i-mid\times 1\leq0 wimid×10,因此对于剩下的所有节点,必定是 ∑ w i − m i d × 1 ≤ 0 \sum w_i-mid\times 1\leq0 wimid×10,但是这个式子显然与 ∑ ( w i − m i d × 1 ) > 0 \sum (w_i-mid\times 1)>0 (wimid×1)>0这个存在正环的式子不矛盾。因此,只要带入 m i d = 0 mid=0 mid=0,验证一下看看这个式子 ∑ ( w i − m i d ∗ 1 ) \sum (w_i-mid*1) (wimid1)是否满足正环,如果不满足,那么全部都不满足,输出无解,否则说明可以求解。

一般来说,如果题目要求保留 k k k位小数,那么我们一般把精度多控制两位,即精度位 e p s = 1 e − ( k + 2 ) eps=1e^{-(k+2)} eps=1e(k+2)

问题:如何理解一下代码呢?

int left=(s[0]-'a')*26+(s[1]-'a');
int right=(s[len-2]-'a')*26+s[len-1]-'a';

我们这里是以字符串的前面两个字符和后面两个字符来作为节点,那么这个节点该怎么编号呢?其实我们可以利用字符串hash的思想,把这两个字符hash成对应的一个整数值。比如字符串 a b a b c ababc ababc前面的两个字符 a b ab ab其实就是 ( ′ a ′ − ′ a ′ ) × 2 6 1 + ( ′ b ′ − ′ a ′ ) × 2 6 0 = 1 ('a'-'a')\times 26^1+('b'-'a')\times 26^0=1 (aa)×261+(ba)×260=1,所以字符 a b ab ab这个节点的编号就是1;比如字符 a b a b c ababc ababc后面的两个字符 b c bc bc其实就是 ( ′ b ′ − ′ a ′ ) × 2 6 1 + ( ′ c ′ − ′ a ′ ) × 2 6 0 = 28 ('b'-'a')\times 26^1+('c'-'a')\times 26^0=28 (ba)×261+(ca)×260=28,所以字符 b c bc bc这个节点的编号就是28

这里还有一个spfa判环的玄学优化(不太常用):

当图中所有顶点的更新次数(入队次数)count大于图中顶点的个数的2倍时,则认为存在环。如果2倍不行,那就长度3倍、4倍… 这玩意很玄学


代码

#include <iostream>
#include <algorithm>
#include<cstring>
using namespace std;
const int N = 700, M = 1e5+10;
const double eps=1e-4;  //控制精度
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
int q[N], cnt[N];
bool st[N];
//建图
void add(int a, int b, int c)
{
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx ++ ;
}
//spfa判正环
bool check(double mid)
{
    memset(dist,-0x3f,sizeof dist);
    memset(st, 0, sizeof st);
    memset(cnt, 0, sizeof cnt);
    int hh = 0, tt = 1;
    //图中最多有676个顶点,一开始就把原图中的所有顶点都放入队列q中
    //就等效于建立了一个虚拟源点和其他676个节点的新图
    for (int i = 0; i < 676; i ++ )
    {
        q[tt ++ ] = i;
        //初始化该虚拟源点到其他676个节点的权值为0
        dist[i]=0;
        st[i] = true;
    }
    int count = 0;  //统计图中所有节点的更新次数即所有节点的入队次数
    while (hh != tt)
    {
        int t = q[hh ++ ];
        if (hh == N) 
        hh = 0;
        st[t] = false;
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] < dist[t] + w[i] - mid)
            {
                dist[j] = dist[t] + w[i] - mid;
                cnt[j] = cnt[t] + 1;
                count++;    //更新次数+1  即入队次数+1
                //一般经验上来说是count >2N 但是这里点数少,边数实在太多了 
                //那么就3N,4N...一直尝试吧
                if (count> 10*N) 
                return true; // 经验上的trick
                //说明存在正环
                if (cnt[j] >= N) 
                return true;
                if (!st[j])
                {
                    q[tt ++ ] = j;
                    if (tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
    //说明不存在正环
    return false;
}
int main()
{
    int T;
    char str[1010]; //字符串
    while (scanf("%d", &T), T)
    {
        //初始化表头
        memset(h, -1, sizeof h);
        idx = 0;
        //T个字符串
        while(T--)
        {
            scanf("%s", str);
            int len = strlen(str);
            if (len >= 2)
            {
                //该字符串的前面两个字符  hash 成一个整数值 作为 一个节点的编号
                int left = (str[0] - 'a') * 26 + str[1] - 'a';
                //该字符串的后面两个字符  hash 成一个整数值 作为 一个节点的编号
                int right = (str[len - 2] - 'a') * 26 + str[len - 1] - 'a';
                //从left节点想right节点连一条长度为len的有向边
                add(left, right, len);
            }
        }
        //如果把0带进去计算都不能得到正环的话,那么1,2,...,1000就不可能得到正环
        if (!check(0)) 
        puts("No solution");
        //说明还是可以得到正环的
        else
        {
            double l = 0, r = 1000;
            //二分答案
            while (l+eps<r)
            {
                double mid = (l + r) / 2;
                //由于答案是单调递增的 
                //所以当满足性质时  往右侧收缩 寻找答案
                if (check(mid)) 
                l = mid;
                //否则不满足性质  往左侧收缩 寻找答案
                else 
                r = mid;
            }
            //输出最终的答案
            printf("%.2lf\n", r);
        }
    }
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

卷心菜不卷Iris

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

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

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

打赏作者

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

抵扣说明:

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

余额充值