做题随笔:P5319
Solution
题意
给定字符串 \(T\),其中有若干空位可以填充。同时给出 \(m\) 个模式串对应价值 \(V_i\) 组成。
总价值定义为所有出现模式串价值乘积的 \(c\) 次方根(\(c\) 为出现咒语次数),需要最大化这个价值,并输出任意一个填充方案。
分析
推柿子
先看柿子——注意力惊人的你一定可以发现:
\(\ln \left( \sqrt[c]{\prod V_i} \right) = \frac{1}{c} \sum \ln V_i\)
(本蒟蒻当时听机房巨佬讲思路的时候惊为天人)
这样,问题转化为最大化 \(\frac{1}{c} \sum \ln V_i\),这是一个典型的0/1 分数规划问题。
分数规划的做法都应该很清楚,这里只简单讲讲:
二分答案,设答案大于等于 \(mid\),有:
\(\frac{1}{c} \sum \ln V_i \geq mid\)
简单变换可得:
\(\sum (\ln V_i-c \times mid) \geq 0\)
因此,我们只需判断最大值与 0 的大小关系。一般来说是跑 01 背包,但是本题就不一样了。仔细看看:多模式串匹配?AC 自动机,启动!
dp
关于 AC 自动机上的 dp,基于无后效性原则,我们从拓扑排序优化的匹配方式来考虑(不知道的先看看这个):
进行匹配时,我们对模式串逐位跳儿子,到哪个点就给它的 \(ans\) 加 \(1\);统计时,按照 fail 内向树的拓扑序,处理完自己的答案后,把答案向父亲累加。这么看,其实就是到哪个点就把它到根的路径上的所有点全部加一。
很直观,但是为什么呢?
让我们回到朴素的匹配方式:走到某个点,对这个点一直跳 fail,直到跳到一个跳过的点或者根上为止。这些途经点对应的模式串都成功匹配一次(跳到的位置一定匹配是 fail 指针自己的性质)。
由于 fail 边成一棵内向树,一直跳就是一直往上跳(跳链),“跳过的点”其实就是深度为一的结点从根继承的指向自己的 fail 指针。所以其实在一个字符上的匹配就是从对应点跳到它深度为一的祖先(如果自己深度就是一,不会有自环,就会跳到根节点)。
由于 trie 的根节点没有实义,跳到深度为一的祖先和跳到根没有区别,合并起来方便一些。
综上,停在某个 trie 图的节点,则 fail 树上其到根的所有对应模式串都被匹配,单步贡献为 \(\sum \ln V_i-mid\times dep[u]\)。\(dep[u]\) 表示 \(u\) 的深度(根的深度为 \(0\))。
于是我们轻松写出状态转移方程:
设 \(dp[i][j]\) 表示处理到字符串 \(T\) 的前 \(i\) 个字符,当前位于 AC自动机状态 \(j\) 时,\(\sum (\ln V_i - mid)\) 的最大值。状态转移如下:
- 如果 \(T[i]\) 是数字,则直接转移到对应的状态 \(k = \text{tr}[j][c]\)。
- 如果 \(T[i]\) 是点号,则需要枚举所有数字 \(0\)-\(9\) 进行转移。
转移方程:
\(dp[i+1][k] = \max \{ dp[i][j] + v[k] - mid \cdot cnt[k] \}\)
同时,需要记录路径以便最后输出方案。
复杂度 \(O(ns\log V)\)。
细节
这题思路比较清晰,但是细节处理令人相思。本蒟蒻爆改一整天,总结一些问题如下:
- 本题不怎么卡精度(讨论区不知道是怎么),大概 \([0,20]\) 之间二分 \(20\) 就可以过(\(eps=1\times10^{-5}\));
- fail 树是在 trie 图整个建完之后才完整的,insert 的时候不要处理 \(w\) 和 \(dep\);
- 二分结束后获取路径时,要传入 \(l\) 而不是 \(r\),否则误差巨大(精度提到最后几个点 T 了都还有 WA),可见这里;
- chk 判断是否可行时,条件要写 \(max>0\),不要写 \(max>-eps\),否则结果同 3;
- string 用 scanf 的话会 RE,原因是 scanf 要求足够的缓冲,而 string 是动态的。
Code
#include <iostream>
#include <cctype>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <queue>
#include <algorithm>
const int maxs = 1505;
const double eps = 1e-8;
const double inf = 1e18;
struct node {
int son[10];
int fail;
double w;
int dep;
} tr[maxs];
int tot;
void insert(std::string s, double w) {
int u = 0;
for (char c : s) {
int idx = c - '0';
if (!tr[u].son[idx]) {
tr[u].son[idx] = ++tot;
memset(tr[tot].son, 0, sizeof(tr[tot].son));
tr[tot].fail = 0;
tr[tot].w = 0;
tr[tot].dep = 0;
}
u = tr[u].son[idx];
}
tr[u].w += w;
tr[u].dep++;
}
int q[maxs], st, ed;
void build() {
st = 0, ed = 0;
for (int i = 0; i < 10; i++) {
if (tr[0].son[i]) {
q[++ed] = tr[0].son[i];
}
}
while (st < ed) {
int u = q[++st];
for (int i = 0; i < 10; i++) {
if (tr[u].son[i]) {
int v = tr[u].son[i];
tr[v].fail = tr[tr[u].fail].son[i];
q[++ed] = v;
} else {
tr[u].son[i] = tr[tr[u].fail].son[i];
}
}
}
for (int i = 1; i <= ed; i++) {
int u = q[i];
tr[u].w += tr[tr[u].fail].w;
tr[u].dep += tr[tr[u].fail].dep;
}
}
int n, m;
std::string t;
double dp[maxs][maxs];
int pre[maxs][maxs];
int choice[maxs][maxs];
bool chk(double mid, bool record = false) {
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= tot; j++) {
dp[i][j] = -inf;
}
}
dp[0][0] = 0;
if (record) {
memset(pre, -1, sizeof(pre));
memset(choice, -1, sizeof(choice));
}
for (int i = 0; i < n; i++) {
for (int j = 0; j <= tot; j++) {
if (dp[i][j] == -inf) continue;
int l = 0, r = 9;
if (t[i] != '.') {
l = r = t[i] - '0';
}
for (int c = l; c <= r; c++) {
int k = tr[j].son[c];
double val = dp[i][j] + tr[k].w - mid * tr[k].dep;
if (val > dp[i+1][k]) {
dp[i+1][k] = val;
if (record) {
pre[i+1][k] = j;
choice[i+1][k] = c;
}
}
}
}
}
if (record) {
return true;
} else {
for (int j = 0; j <= tot; j++) {
if (dp[n][j] > 0) return true;
}
return false;
}
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(0);
std::cin >> n >> m;
std::cin >> t;
for (int i = 0; i < m; i++) {
std::string s;
int v;
std::cin >> s >> v;
insert(s, log(v));
}
build();
double l = 0, r = 20;
for (int iter = 0; iter < 20; iter++) {
double mid = (l + r) / 2;
if (chk(mid)) {
l = mid;
} else {
r = mid;
}
}
chk(l, true);
int pos = 0;
for (int j = 1; j <= tot; j++) {
if (dp[n][j] > dp[n][pos]) pos = j;
}
std::string ans;
int sta = pos;
for (int i = n; i >= 1; i--) {
ans += char(choice[i][sta] + '0');
sta = pre[i][sta];
}
reverse(ans.begin(), ans.end());
std::cout << ans << std::endl;
return 0;
}
一些闲话
题解区的其他巨佬写得已经很好了,本蒟蒻写这篇题解主要是发泄改题改一天的怨气,以及希望其他人不会出现这些问题。如果这题都过了的话,至少我觉得你的 AC 自动机和 SA 卡精度应该都会很好(笑)。
如果觉得有用,点个赞吧!