洛谷P5319 [BJOI2019] 奥术神杖 题解
题意:
Bezorath 大陆抵抗地灾军团入侵的战争进入了僵持的阶段,世世代代生活在 Bezorath 这片大陆的精灵们开始寻找远古时代诸神遗留的神器,试图借助神器的神秘力量帮助她们战胜地灾军团。
在付出了惨痛的代价后,精灵们从步步凶险的远古战场取回了一件保存尚完好的神杖。但在经历过那场所有史书都视为禁忌的“诸神黄昏之战”后,神杖上镶嵌的奥术宝石已经残缺,神力也几乎消耗殆尽。精灵高层在至高会议中决定以举国之力收集残存至今的奥术宝石,并重金悬赏天下能工巧匠修复这件神杖。
你作为神术一脉第五百零一位传人,接受了这个艰巨而神圣的使命。
神杖上从左到右镶嵌了 $n$ 颗奥术宝石,奥术宝石一共有 $10$ 种,用数字
0123456789
表示。有些位置的宝石已经残缺,用.
表示,你需要用完好的奥术宝石填补每一处残缺的部分(每种奥术宝石个数不限,且不能够更换未残缺的宝石)。古老的魔法书上记载了 $m$ 种咒语 $(S_i,V_i)$ ,其中 $S_i$ 是一个非空数字串,$V_i$ 是这种组合能够激发的神力。神杖的初始神力值 $\mathsf{Magic} = 1$,每当神杖中出现了连续一段宝石与 $S_i$ 相等时,神力值 $\mathsf{Magic}$ 就会乘以 $V_i$。但神杖如果包含了太多咒语就不再纯净导致神力降低:设 $c$ 为神杖包含的咒语个数(若咒语类别相同但出现位置不同视为多次),神杖最终的神力值为 $ \sqrt[c]{\mathsf{Magic}}$。(若 $c = 0$ 则神杖最终神力值为 $1$ )
例如有两种咒语 $(01,3)$ 、$(10,4)$ ,那么神杖
0101
的神力值为 $\sqrt[3]{ 3 \times 4 \times 3}$你需要使修复好的神杖的最终的神力值最大,输出任何一个解即可。
输入格式:
第一行两个正整数 $n,m$ ,表示宝石数和咒语数。
第二行为一个长度为 $n$ 的字符串 $T$ ,表示初始的神杖。
接下来 $m$ 行每行一个非空数字串 $S_i$ 和一个正整数 $V_i$ ,表示每种咒语。
输出格式:
输出最终神杖上从左到右镶嵌的宝石,多解时任意输出一个即可。
数据范围:
$1 \le n,m \le 1501,~1 \le (\sum_{i=1}^{m} |S_i|) \le 1501,~1 \le V_i \le 10^9$ ,保证 $S_i$ 互不相同。
记神力值 $\mathrm{Ans} = \sqrt[c]{\prod_{i=1}^c w_i}$ ,对两侧取对数得
显然这是个01分数规划。考虑二分 $x$ ,则
问题就变成了最大化 $w_i$ 的和。
那么对 $S_i$ 建 AC 自动机,并设 $f(i,j)$ 为考虑 $T$ 的前 $i$ 位且当前在节点 $j$ 时的最大值,则
时间复杂度
其中 $\epsilon=10^{-8}$ 表示精度,$|\Sigma|=10$ 表示字符集大小。
代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
// #define double long double
#define INF 0x3f3f3f3f3f3f3f3f
const double eps = 1e-8;
void up(int &x, int y) { x < y ? x = y : 0; }
void down(int &x, int y) { x > y ? x = y : 0; }
int dcmp(double x) { if(fabs(x) <= eps) return 0; return x > eps ? 1 : -1; }
#define N ((int)(2019))
char t[N], ans[N]; queue<int> q;
double val[N], f[N][N];
int n, tot, ch[N][12], sum[N], fail[N], g[N][N][2];
void insert(char *s, double v)
{
int u = 0;
for(int i = 1, c; s[i]; i++)
{
c = s[i] - '0';
if(!ch[u][c]) ch[u][c] = ++tot;
u = ch[u][c];
}
++sum[u]; val[u] += v;
}
void build()
{
for(int i = 0; i < 10; i++) if(ch[0][i]) q.push(ch[0][i]);
while(!q.empty())
{
int u = q.front(); q.pop();
sum[u] += sum[fail[u]]; val[u] += val[fail[u]];
for(int i = 0; i < 10; i++)
{
if(ch[u][i])
{
fail[ch[u][i]] = ch[fail[u]][i];
q.push(ch[u][i]);
}else ch[u][i] = ch[fail[u]][i];
}
}
}
double check(double mid)
{
for(int i = 0; i <= tot; i++) val[i] -= sum[i] * mid;
for(int i = 0; i <= n; i++)
for(int j = 0; j <= tot; j++) f[i][j] = -1e111;
f[0][0] = 0;
for(int i = 0; i < n; i++)
for(int j = 0; j <= tot; j++) if(f[i][j] > -1e100)
for(int k = 0; k < 10; k++) if(t[i + 1] == '.' || t[i + 1] - '0' == k)
{
int c = ch[j][k];
if(dcmp(f[i][j] + val[c] - f[i + 1][c]) > 0)
{
f[i + 1][c] = f[i][j] + val[c];
g[i + 1][c][0] = j, g[i + 1][c][1] = k;
}
}
for(int i = 0; i <= tot; i++) val[i] += sum[i] * mid;
int pos = 0;
for(int i = 1; i <= tot; i++) if(dcmp(f[n][i] - f[n][pos]) > 0) pos = i;
for(int i = n, now = pos; i; i--) {
ans[i] = g[i][now][1] + '0', now = g[i][now][0];
}
return f[n][pos];
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
// freopen("check.in","r",stdin);
// freopen("check.out","w",stdout);
int m; cin >> n >> m >> (t + 1);
for(int i = 1, v; i <= m; i++)
{
static char s[N];
cin >> (s + 1) >> v; insert(s, log((double)v));
}
build(); double l = 0, r = log((double)1e9);
while(fabs(r - l) > eps)
{
double mid = (l + r) / 2.0;
if(dcmp(check(mid)) > 0) l = mid; else r = mid;
}
check(l); cout << (ans + 1) << '\n';
return 0;
}
参考文献:
[1] https://www.luogu.com.cn/article/5m7wm9eu
[2] https://www.luogu.com.cn/article/dunslmu5
题外话:
这 tag 太长了首页显示会有问题,完整如下:
tag: ["算法","数据结构","字符串","DP","01分数规划"]
AC 自动机怎么不是数据结构?
AC 自动机就是数据结构。AC 自动机就是数据结构。