§ Desc.
你有两个字符串 和 , 初始为空. 每次你可以进行以下三种操作中的一种:
- 选定小写字母 , 令 ;
- 令 , 令 ;
- 令 .
三种操作分别花费 , 现要求你将 转化为目标串 , 求最小花费.
, .
§ Sol.
今天真调了一天 RE, 人麻了. 😅
考虑区间 DP, 设 表示达成 的最小花费. 操作 1 的转移很简单:
其中 表示更新, 即取最小值. 可以发现操作 2 比较有性质, 因为每次执行操作 2 都可以「刷新」当前字符串的状态. 设当前这次操作 2 后, 变成了 , 那么后面若干次操作中, 我们会使用操作 2&3, 来使 变成类似 的形状, 其中省略号代表操作 1 插入的字母. 所以如果考虑从 转移到 (), 那么有 . 并且这三个部分无交. 我们可以这样钦定 的两端一定等于 的原因是, 上述形式两边多出来的字母可以通过第一种操作转移到.
具体转移的路径是从 转移到 , 其中 且 . 令 表示 中最多可选出的子串个数, 使得这些子串都等于 且互相不交, 则转移的方程可以写为:
这样的复杂度最坏可以达到 . 接下来引出一个优化复杂度的重要性质:
- 若存在两个下标 和 , 满足可以从 转移到 和 , 并且 (即有交), 那么我们只需要转移到 即可!
为什么? 因为 可以被 以同样的代价更新 (注意从 到 的更新能且仅能使用操作 1)! 比如 , 取最右边的 作为当前的模式串, 花费 到 和从 花费 都可以以同样的代价到达 .
那么我们每次转移就只剩下 次了, 加起来就是调和级数. 预处理 表示 中最后出现的 位置即可.
int main() {
ios::sync_with_stdio(0);
cin.tie(nullptr);
const ull BASE = 1331;
int n; cin >> n;
vector<ull> pw(n+1), hs(n+1);
pw[0] = 1;
for (int i=1;i<=n;++i) pw[i] = pw[i-1]*BASE;
string s; cin >> s;
ll A, B, C; cin >> A >> B >> C;
for (int i=0;i<n;++i) hs[i+1] = hs[i]*BASE+s[i]-'a'+1;
const auto get_hash = [&](int l, int r) { return hs[r]-hs[l]*pw[r-l]; };
vector pos(n, vi(n+1)); // pos[l][r] = Latest @i such that i+r-l <= l, s[i, i+r-l) == s[l, r)
unordered_map<ull, int> latest;
for (int d=1;d<=n;++d) {
latest.clear();
for (int r=d;r<=n;++r) {
int l = r-d;
if (l-d >= 0) latest[get_hash(l-d, l)] = l-d;
auto h = get_hash(l, r);
if (latest.find(h) != latest.end()) pos[l][r] = latest[h];
else pos[l][r] = -1;
}
}
const ll INF = 1.01e18;
vector dp(n, vector(n+1, INF));
for (int i=0;i<n;++i) dp.at(i).at(i+1) = A;
for (int d=1;d<n;++d) {
for (int l=0,r=d;r<=n;++l,++r) {
if (l > 0) chkmin(dp.at(l-1).at(r), dp.at(l).at(r)+A);
if (r < n) chkmin(dp.at(l).at(r+1), dp.at(l).at(r)+A);
int p = pos[l][r], cnt = 2;
while (p >= 0) {
chkmin(dp.at(p).at(r), dp.at(l).at(r)+B+cnt*C+(r-p-cnt*(r-l))*A);
cnt++;
p = pos[p][p+r-l];
}
}
}
cout << dp.at(0).at(n) << "\n";
}
/ With a handshake /
/ Or an embrace /
/ Or a kiss on the cheek /
/ Possibly, all three /