这道题的60分做法是:分答案串<=100和>100讨论。
<=100的部分可以使用算法3解决。枚举一个点l表示现在要统计左端点l~l+99所有前缀字符串的密度的最大值。可以使用ac自动机解决
>100的部分可以二分+ac自动机。
原问题实际上是一个分数规划问题。假设选了i个物品,编号a[1],a[2]...a[i],则平均值是$frac{a[1]+a[2]+....+a[i]}{i}$
如果设一个二分值mid,判定答案是否>=mid,则实际上是判定$a[1]-md+a[2]-md+...+a[i]-md>=0$,实际上就是选一个位置有md的代价,要求最大值。
使用dp解决。设f[i]表示i结尾的串,长度要>100的最大价值。ans[i]表示i-99~i的以i结尾的子串价值的最大值,st[i]表示ac机上以i结尾的串的价值,ans可以使用算法3计算。
f的转移有点像最大子段和。设v表示1~i-100的前缀最大价值,则v=max(v+st[i-100]-md,0),表示枚举是否选i-100这个点。
f[i]=max(f[i],v+ans[i]-100*md)表示使用i-99~i和1~i-100的最大价值组合更新答案。
只需要判定max(f[1~n])是否>=0即可。
核心代码:
int ck(double md){ double r=-1e18,v=0; for(int i=100;i<n;i++){ v=max(v+st[i-100]-md,0.0); r=max(r,v+ans[i]-100.0*md); } return r>=0; } double va=0; for(int i=0;i<n;i++){ int x=0,ss=0; for(int j=i;j<min(i+100,n);j++){ x=a.c[x][s[j]-'a']; ss+=a.f[x]; va=max(va,ss/((double)j-i+1)); } if(i+99<n)ans[i+99]=ss; } int x=0; for(int i=n-1;i;i--){ x=b.c[x][s[i]-'a']; st[i]=b.f[x]; } double l=0,r=1000000000; while(l+1e-6<r){ double md=(l+r)*0.5; if(ck(md))l=md; else r=md; } printf("%.4lf ",max(l,va));
满分做法也是要二分(0/1分数规划)。
将原串在ac机上匹配。
我们假设在匹配的时候匹配到了一个字符c,且当前匹配点没有c的儿子,则跳fail。直到有c位置。
答案就是max(当前匹配串~i 后缀最大贡献,当前匹配串外~i 后缀最大贡献),和60分做法较为相似。
设mx[p]表示ac自动机上,p对应的字符串的所有后缀的贡献的最大值。
f[p]表示ac机上[当前~fail]到i的贡献最大值。
g[p]表示根到p对应的字符串的分数和。
fa[p]表示p的失配指针。
计算f,g时在ac自动机上按照bfs序进行遍历。
设当前bfs到的节点为x。我们接下来要计算x在trie的儿子y的f,g值。
计算g时直接把x+分数和赋值给g[y]即可。
计算f时,我们要跳到一个点,使得这个点存在y对应的字符。
在跳的时候,我们发现原来的值是(las~fail)(las表示这次跳前对应的字符串的首位),跳之后我们对应的是fail~ffail(ffail时现在跳到的点)。发现这2个值恰好可以覆盖整个字符串区间。
所以对沿途的所有f取max即可。
然后把f对节点g的值取max,表示插入当前y对应的字符后,有新的贡献是g[y]前面没有统计到,要取max。
在统计答案时,统计的是现在枚举到的位置i的后缀的最大贡献。
匹配到的部分的后缀最大值就是mx[p],p为当前串在ac机上匹配到的节点。
匹配到的位置前面的部分要使用一个变量pp记录,表示前面的最大分数和。
在跳fail时,我们要计算前面的贡献,所以要将pp对沿途的f[p]取max。
然后在跳完以后,pp加上现在这个节点的分数和。
把答案对max(mx[p],pp)取max即可,就是对max(当前匹配串~i 后缀最大贡献,当前匹配串外~i 后缀最大贡献)取max
然后根据答案是否>=0来判定如何缩小二分区间。
感觉这题的做法十分巧妙。