欢迎访问个人博客:我的博客传送门
单词环
题目描述
核心思路
这题主要是要用到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 3−1=2种可能,也就是说要从【1】引出两条有向边,分别指向【2】和【3】。同理分析可知,如果 1 0 5 10^5 105个字符串都相同,则对于每个字符串来说,都会引出 1 0 5 − 1 10^5-1 105−1条边,为了方便起见,就看作是引出 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条有向边,空间还算好一些。
如下图解释题目中的栗子:
知道了该怎么建图后,我们再来思考题目要求的是什么?题目想要求的是形成环的这个串它的平均长度最大,转换到我们建好的图中,意思就是:边权之和/节点个数 最大。即 ∑ w i ∑ 1 \dfrac {\sum w_i}{\sum 1} ∑1∑wi要最大
要求这个值最大,满足单调性,很明显这就是一个01分数规划问题,那么就可以使用二分算法来二分答案了。我们设某一时刻二分出来的答案为 m i d mid mid(但不保证这就是最终正确想要的最大的那个答案),如果当前这个答案 m i d mid mid不够大,则说明还可以更大,即
∑ w i ∑ 1 > m i d \dfrac {\sum w_i}{\sum 1}>mid ∑1∑wi>mid
⟺ \iff ⟺ ∑ w i > m i d ∗ ∑ 1 \sum w_i>mid*\sum 1 ∑wi>mid∗∑1
⟺ \iff ⟺ ∑ w i − m i d ∗ ∑ 1 > 0 \sum w_i-mid*\sum 1>0 ∑wi−mid∗∑1>0
⟺ \iff ⟺ ∑ w i − ∑ m i d ∗ 1 > 0 \sum w_i-\sum mid*1>0 ∑wi−∑mid∗1>0
⟺ \iff ⟺ ∑ ( w i − m i d ∗ 1 ) \sum (w_i-mid*1) ∑(wi−mid∗1)
那么也就是说,对于每一条边,给它赋权值为 w i − m i d ∗ 1 w_i-mid*1 wi−mid∗1,如果存在正环,也就意味着有一个环的权值和大于0,也就是 ∑ ( w i − m i d × 1 ) > 0 \sum (w_i-mid\times 1)>0 ∑(wi−mid×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} ∑1∑wi式子可知,要想取最大,则分子要最大,分子要最小,由于最多有 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} ∑1∑wi=1051000×105最大为 1000 1000 1000。因此答案区间就是 ( 0 , 1000 ] (0,1000] (0,1000]。
题目说了不一定存在环串,那么我们该怎么知道呢?从 w i − m i d × 1 w_i-mid\times1 wi−mid×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 wi−mid×1≤0,那么 ∑ w i − m i d × 1 ≤ 0 \sum w_i-mid\times 1\leq0 ∑wi−mid×1≤0,因此对于剩下的所有节点,必定是 ∑ w i − m i d × 1 ≤ 0 \sum w_i-mid\times 1\leq0 ∑wi−mid×1≤0,但是这个式子显然与 ∑ ( w i − m i d × 1 ) > 0 \sum (w_i-mid\times 1)>0 ∑(wi−mid×1)>0这个存在正环的式子不矛盾。因此,只要带入 m i d = 0 mid=0 mid=0,验证一下看看这个式子 ∑ ( w i − m i d ∗ 1 ) \sum (w_i-mid*1) ∑(wi−mid∗1)是否满足正环,如果不满足,那么全部都不满足,输出无解,否则说明可以求解。
一般来说,如果题目要求保留 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 (′a′−′a′)×261+(′b′−′a′)×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 (′b′−′a′)×261+(′c′−′a′)×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;
}