「BJOI2019」奥术神杖
知识点:01 分数规划,ACAM。
没写 build 函数调一天哈哈
简述
给定一只由数字和\(\texttt{.}\)构成的字符串 \(s\)。给定 \(m\) 个特殊串 \(t_{1}\sim t_{m}\),\(t_i\) 的权值为 \(v_i\)。
需要在 \(s\) 中为\(\texttt{.}\)的位置上填入数字,一种填入方案的价值定义为:\[\sqrt[c]{\prod_{i=1}^{c} w_i} \]其中 \(w\) 表示在该填入方案中,出现过的特殊串的价值的可重集合,其大小为 \(c\)。
每个位置填入的数字任意,最大化填入方案的价值,并输出任意一个方案。
\(1\le m,|s|,\sum|t_i|\le 1501\),\(1\le v_i\le 10^9\)。
1S,512MB。
分析
对于两种填入方案,我们只关心它们价值的相对大小。带着根号不易比较大小,套路地取个对数,之后化下式子:
这是一个显然的 01 分数规划的形态,考虑二分答案。存在一种填入方案价值不小于 \(mid\) 的充要条件为:
考虑 DP 检查二分量 \(mid\) 是否合法。
具体地,先将特殊串 \(t_i\) 的权值设为 \(\log v_i - mid\),更新 ACAM 上各状态的权值,之后在 ACAM 上模拟匹配过程套路 DP。
设 \(f_{i,j}\) 表示长度为 \(i\),在 ACAM 上匹配的结束状态为 \(j\) 的串的最大价值。
初始化 \(f_{0,0} = 0\),转移时枚举串长,状态,转移函数。注意某一位不为\(\texttt{.}\)时转移函数只能为串中的字符,则有:
注意记录转移时的前驱与转移函数,根据前驱还原出方案即可。
总复杂度 \(O(\left(10|s|\cdot\sum |t_i|\right)\log w)\) 级别,\(\log w\) 为二分次数。
代码
//知识点:ACAM,分数规划
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
#define DB double
const int kN = 3e3 + 10;
const DB kInf = 1e10;
const DB eps = 1e-6;
//=============================================================
int n, m;
char origin[kN], s[kN], ans[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace ACAM {
int node_num = 0, tr[kN][10], fail[kN], cnt[kN], from[kN][kN];
DB sum[kN], val[kN], f[kN][kN];
char ch[kN][kN];
void Insert(char *s_, int val_) {
int u_ = 0, lth = strlen(s_ + 1);
for (int i = 1; i <= lth; ++ i) {
if (! tr[u_][s_[i] - '0']) tr[u_][s_[i] - '0'] = ++ node_num;
u_ = tr[u_][s_[i] - '0'];
}
sum[u_] += log(val_);
cnt[u_] ++;
}
void Build() {
std::queue <int> q;
for (int i = 0; i < 10; ++ i) {
if (tr[0][i]) q.push(tr[0][i]);
}
while (! q.empty()) {
int u_ = q.front(); q.pop();
for (int i = 0; i < 10; ++ i) {
int v_ = tr[u_][i];
if (v_) {
fail[v_] = tr[fail[u_]][i];
sum[v_] += sum[fail[v_]];
cnt[v_] += cnt[fail[v_]];
q.push(v_);
} else {
tr[u_][i] = tr[fail[u_]][i];
}
}
}
}
bool DP(DB mid_) {
//初始化
for (int i = 0; i <= node_num; ++ i) val[i] = sum[i] - cnt[i] * mid_;
for (int i = 0; i <= n; ++ i) {
for (int j = 0; j <= node_num; ++ j) {
f[i][j] = -kInf;
}
}
f[0][0] = 0;
//DP
for (int i = 0; i < n; ++ i) {
for (int j = 0; j <= node_num; ++ j) {
if (f[i][j] == -kInf) continue;
if (origin[i + 1] == '.') {
for (int k = 0; k < 10; ++ k) {
int v_ = tr[j][k];
if (f[i + 1][v_] < f[i][j] + val[v_]) {
f[i + 1][v_] = f[i][j] + val[v_];
from[i + 1][v_] = j;
ch[i + 1][v_] = k + '0';
}
}
} else {
int v_ = tr[j][origin[i + 1] - '0'];
if (f[i + 1][v_] < f[i][j] + val[v_]) {
f[i + 1][v_] = f[i][j] + val[v_];
from[i + 1][v_] = j;
ch[i + 1][v_] = origin[i + 1];
}
}
}
}
//寻找最优解
int pos = 0;
for (int i = 0; i <= node_num; ++ i) {
if (f[n][i] > f[n][pos]) pos = i;
}
if (f[n][pos] <= 0) return false;
for (int i = n, j = pos; i; -- i) {
ans[i] = ch[i][j];
j = from[i][j];
}
return true;
}
}
//=============================================================
int main() {
n = read(), m = read();
scanf("%s", origin + 1);
for (int i = 1; i <= m; ++ i) {
scanf("%s", s + 1);
int val = read();
ACAM::Insert(s, val);
}
ACAM::Build();
for (DB l = 0, r = log(kInf); r - l >= eps; ) {
DB mid = (l + r) / 2.0;
if (ACAM::DP(mid)) {
l = mid;
} else {
r = mid;
}
}
printf("%s", ans + 1);
return 0;
}