[luogu6396] 要有光 题解
可以先给一份形式化题意:
给定一个初始串 \(S_0\) 和参数 \(m\),你可以进行一下 \(5\) 种操作:
- 对于一个串 \(S\),构造串 \(T\) 满足 \(T\) 为 \(S\) 的最长回文后缀,并将 \(S\) 赋值为 \(T\),代价为 \(A\)。
- 对于一个回文串 \(S\),构造串 \(T\) 满足 \(S\) 为 \(T\) 的最长回文后缀且 \(T\) 为 \(S_0\) 的子串,并将 \(S\) 赋值为 \(T\),代价为 \(B\)。
- 对于一个回文串 \(S\),构造串 \(T\) 满足可以找到一个长度至多为 \(m\) 的串 \(a\),使得 \(S=a+T+a^R\),并将 \(S\) 赋值为 \(T\),代价为 \(C\),其中 \(a^R\) 表示 \(a\) 的反串。
- 对于一个回文串 \(S\),构造串 \(T\) 满足可以找到一个串 \(a\),使得 \(T=a+S+a^R\) 且 \(T\) 为 \(S_0\) 的子串,并将 \(S\) 赋值为 \(T\),代价为 \(D\)。
- 对于一个串 \(S\),构造串 \(T\) 满足可以找到一个字符 \(a\),使得 \(T=a+S\) 且 \(T\) 为 \(S_0\) 的子串,并将 \(S\) 赋值为 \(T\),代价为 \(E\),特别的,进行过 \(5\) 操作后不能再进行其他操作。
每次给定一组 \(l,r\),令 \(T\) 为 \(S_0\) 的 \([l,r]\) 范围内的字符取出形成的新串,询问最少需要多少代价能使 \(S_0\) 变为 \(T\)。
首先发现 \(5\) 操作独立于其他操作之外,其它四个操作都基于初始串的回文子串,这提示我们使用一种数据结构维护 \(S_0\) 的所有回文子串,很明显可以想到回文自动机。
先将 \(S_0\) 建成回文自动机,现在我们思考五个操作在回文自动机上对应的意义。
首先就像上文所说的,\(5\) 操作独立于别的操作之外,大概也可以想象是怎么用的,就是在生成目标串的回文后缀以后在前面直接接上,那么我们就考虑别的操作在回文自动机上的意义了。
首先看到最长回文后缀应该能很自然地想到回文自动机的失配指针,\(1\) 操作对应边 \(\langle p,\mathrm{fail}_p\rangle\),\(2\) 操作对应 \(\langle\mathrm{fail}_p,p\rangle\),然后另外两个操作其实也很明显,在串左右两边同时操作正好对应回文自动机的转移边,因为 \(m\) 很小,所以直接对应向父亲暴力连边最多也没几条,但是其中最棘手的就是 \(4\) 操作,这等价于向回文自动机中的子树里的每个点连边,在极限情况下显然是会被卡的,所以得想点办法。
其实难度并不是很高,这种往整个集合连边经典套路是建虚点,有些特殊情况不如分块优化建图、线段树优化建图之类的,这题不用这么麻烦,可以直接给每个点建一个虚点,如果两点在回文自动机上有连边,那么对应的虚点从父亲往儿子连边,边权为 \(0\),从原点向虚点连有边权的边,虚点向原点连边权为 \(0\),这样原来到子树中的边就可以转化为进入虚点后走到目标点对应的虚点再走回来,这两个操作是等价的。
接下来就是先从整串的状态进入回文自动机(如果整个串不是回文串还要用一次 \(1\) 操作),再跑到目标串对应的一个回文后缀,再用 \(5\) 操作变成目标串。
同时有一个注意点,注意到最后是跑到目标串对应的一个回文后缀而不是最长回文后缀,因为如果 \(2\) 代价较高而 \(5\) 代价较低的话也许最后用 \(5\) 代替 \(2\) 也不是不行,所以为了减少询问的复杂度,预处理 \(d_i\) 表示回文自动机上从最后状态到编号为 \(i\) 的节点只用前 \(4\) 种操作所需要的最小花费,\(f_i\) 表示回文自动机上从最后状态到编号为 \(i\) 的节点所需要的最小花费,\(d_i\) 可以用 Dijkstra 直接处理,\(f_i\) 可以 dp 解决,转移方程为 \(f_i=\min\{d_i,f_{\mathrm{fail}_p}+\operatorname{len}(p)-\operatorname{len}(\textrm{fail}_p)\}\)。
最后就是找到区间 \([l,r]\) 对应字符串的最长回文后缀,可以利用倍增的思想倍增跳 \(\textrm{fail}\) 指针,找到以 \(r\) 结尾的长度不超过 \(r-l+1\) 的最长的回文串。
整体是一道非常综合的题目,具体实现细节较多,可以看代码。
代码
#include<bits/stdc++.h>
using namespace std;
#define Reimu inline void // 灵梦赛高
#define Marisa inline int // 魔理沙赛高
#define Sanae inline bool // 早苗赛高
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int, int> Pii;
typedef tuple<int, int, int> Tiii;
#define fi first
#define se second
template<typename Ty>
Reimu clear(Ty &x) { Ty _; x.swap(_); }
const int N = 100010;
int n, m, A, B, C, D, E;
char s[N];
LL d[N << 1], dp[N];
vector<Pii> G[N << 1];
Marisa get(char c) { return c < 'a' ? c - 'A' : c - 'a' + 26; }
struct PAM {
int sz, las, tot;
int len[N], ch[N][52], fail[20][N], id[N], fa[N];
PAM() { len[fail[0][0] = sz = las = 1] = -1; }
Reimu operator+=(char c) {
s[++tot] = c;
int x = las, o = get(c);
while (s[tot - len[x] - 1] ^ c) x = fail[0][x];
if (!ch[x][o]) {
int y = fail[0][x];
while (s[tot - len[y] - 1] ^ c) y = fail[0][y];
fail[0][++sz] = ch[y][o];
len[ch[x][o] = sz] = len[x] + 2;
}
fa[id[tot] = las = ch[x][o]] = x;
}
Reimu operator~() {
for (int i = 2; i <= sz; ++i) {
G[i].emplace_back(fail[0][i], A); G[fail[0][i]].emplace_back(i, B);
for (int j = 1, k = fa[i]; j <= m && k > 1; ++j, k = fa[k]) G[i].emplace_back(k, C);
G[i].emplace_back(i + sz, D); G[i + sz].emplace_back(i, 0);
for (int j = 0; j < 52; ++j) if (ch[i][j]) G[i + sz].emplace_back(ch[i][j] + sz, 0);
for (int j = 1; fail[j][i] = fail[j - 1][fail[j - 1][i]]; ++j);
}
}
} pam;
Reimu Dij() {
memset(d, 0x3f, sizeof(LL) * (pam.sz << 1 | 1));
priority_queue<pair<LL, int>, vector<pair<LL, int>>, greater<>> Q;
Q.emplace(d[pam.las] = 0, pam.las);
while (!Q.empty()) {
auto [dis, x] = Q.top(); Q.pop();
if (dis ^ d[x]) continue;
for (auto [y, v]: G[x]) if (dis + v < d[y]) Q.emplace(d[y] = dis + v, y);
}
}
Marisa getId(int p, int len) {
if (pam.len[p] <= len) return p;
for (int i = __lg(n); ~i; --i) if (pam.len[pam.fail[i][p]] > len) p = pam.fail[i][p];
return pam.fail[0][p];
}
inline LL solve(int l, int r) {
if (l == 1 && r == n) return 0;
int p = getId(pam.id[r], r - l + 1);
return dp[p] + 1LL * (r - l + 1 - pam.len[p]) * E + (pam.len[pam.las] != n) * A;
}
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> s + 1 >> m >> A >> B >> C >> D >> E; n = strlen(s + 1);
for (int i = 1; i <= n; ++i) pam += s[i];
~pam; Dij(); dp[0] = d[0];
for (int i = 2; i <= pam.sz; ++i) {
if (pam.fail[0][i]) dp[i] = min(d[i], dp[pam.fail[0][i]] + 1LL * (pam.len[i] - pam.len[pam.fail[0][i]]) * E);
else dp[i] = d[i];
}
int q; cin >> q; while (q--) {
int l, r; cin >> l >> r;
cout << solve(l, r) << '\n';
}
return 0;
}