做题随笔: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)\)

细节

这题思路比较清晰,但是细节处理令人相思。本蒟蒻爆改一整天,总结一些问题如下:

  1. 本题不怎么卡精度(讨论区不知道是怎么),大概 \([0,20]\) 之间二分 \(20\) 就可以过(\(eps=1\times10^{-5}\));
  2. fail 树是在 trie 图整个建完之后才完整的,insert 的时候不要处理 \(w\)\(dep\)
  3. 二分结束后获取路径时,要传入 \(l\) 而不是 \(r\),否则误差巨大(精度提到最后几个点 T 了都还有 WA),可见这里
  4. chk 判断是否可行时,条件要写 \(max>0\),不要写 \(max>-eps\),否则结果同 3;
  5. 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 卡精度应该都会很好(笑)。

如果觉得有用,点个赞吧!

posted @ 2025-09-04 20:53  Tenil  阅读(6)  评论(0)    收藏  举报