【洛谷5319】[BJOI2019] 奥术神杖(分数规划+AC自动机上DP)
- 给定一个长度为\(n\)的数字串,你需要在其中残缺的位置中填入数字。
- 给定\(m\)个咒语串,每个咒语有一个魔力。规定同一咒语串重复出现将计算多次,设咒语串出现总次数为\(c\),魔力总乘积为\(V\),要求最大化\(\sqrt[c]V\)。
- \(n,m,\sum|s_i|\le1501\)
分数规划
\(\sqrt[c]V\)相当于是求\(v\)的乘法平均值,那么只要取个对数就变成加法平均值了。
因此我们只要二分答案\(x\),就相当于要让\(\frac1c\log \prod v_i\ge x\Leftrightarrow\sum(\log v_i-x)\ge 0\)。
\(AC\)自动机上\(DP\)
对咒语串建出\(AC\)自动机,每次二分答案后先把所有咒语串的价值(\(\log v_i-x\))表示到对应节点上,然后按照\(BFS\)序遍历一遍,给每个节点加上父节点权值,使得它的权值变成到根路径上所有节点初始权值总和。(即让一个串的权值变成它的所有后缀初始权值总和)
那么如果我们某一时刻处于点\(x\)上,所获得的价值就是\(x\)的权值了。
然后就是对于这种问题套路地\(AC\)自动机上\(DP\),设\(f_{i,j}\)表示填了第\(i\)个字符后走到节点\(j\)的最大权值。
转移如果这位字符指定就直接转移到对应儿子,否则枚举每个儿子转移。
最后要输出方案只要在\(DP\)时记录一下是谁转移过来的就好了。
P.S. 由于这道题要输出方案,我第三个样例都懒得去测了,抱着侥幸心理直接一交没想到居然过了!
代码:\(O(10nslogV)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 1501
#define DB double
using namespace std;
int n,m,id[N+5],v[N+5];char s[N+5],st[N+5][N+5];
namespace AC//AC自动机
{
int Nt=1;struct node {int F,S[10];DB V;}O[N+5];
I int Ins(char* s)//插入字符串
{
RI x=1;for(RI i=1,l=strlen(s+1),t;i<=l;++i)
!O[x].S[t=s[i]&15]&&(O[x].S[t]=++Nt),x=O[x].S[t];return x;
}
int q[N+5];I void Build()//建AC自动机
{
RI i,k,H=1,T=0;for(i=0;i<=9;++i) (O[1].S[i]?O[q[++T]=O[1].S[i]].F:O[1].S[i])=1;
W(H<=T) for(k=q[H++],i=0;i<=9;++i) (O[k].S[i]?O[q[++T]=O[k].S[i]].F:O[k].S[i])=O[O[k].F].S[i];
}
I void Cl() {for(RI i=1;i<=Nt;++i) O[i].V=0;}//清空
I void PD() {for(RI i=2;i<=Nt;++i) O[q[i]].V+=O[O[q[i]].F].V;}//加上父节点权值
DB ans,f[N+5][N+5];int id,g[N+5][N+5],h[N+5][N+5];I void DP()//AC自动机上DP
{
RI i,j,k;for(i=0;i<=n;++i) for(j=1;j<=Nt;++j) f[i][j]=-1e9;f[0][1]=0;//初始化
#define T(x,c) (x&&f[i-1][j]+O[x].V>f[i][x]&&(f[i][x]=f[i-1][j]+O[x].V,g[i][x]=j,h[i][x]=c))//由f[i-1][j]填上字符c转移到f[i][x]
for(i=1;i<=n;++i) for(j=1;j<=Nt;++j) if(s[i]=='.')
for(k=0;k<=9;++k) T(O[j].S[k],k);else T(O[j].S[s[i]&15],s[i]&15);//没给定枚举儿子转移;给定直接转移
ans=-1e9;for(j=1;j<=Nt;++j) f[n][j]>ans&&(ans=f[n][j],id=j);//统计最优答案
}
I void Print(CI x,CI y) {x&&(Print(x-1,g[x][y]),putchar(48|h[x][y]),0);}//输出方案
}
I bool Check(Con DB& mid)//检验答案
{
RI i;for(AC::Cl(),i=1;i<=n;++i) AC::O[id[i]].V=log(v[i])-mid;AC::PD(),AC::DP();return AC::ans>0;//判断最大值是否大于0(注意不选任何咒语串时答案等于0,因此要大于)
}
int main()
{
RI i;for(scanf("%d%d%s",&n,&m,s+1),i=1;i<=m;++i) scanf("%s%d",st[i]+1,v+i),id[i]=AC::Ins(st[i]);//记录每个咒语串插到的位置
AC::Build();DB l=0,r=30,mid;W(r-l>1e-6) Check(mid=(l+r)/2)?l=mid:r=mid;return Check(l),AC::Print(n,AC::id),0;//分数规划
}
待到再迷茫时回头望,所有脚印会发出光芒