正睿1595 「20联赛集训day1」打字机(区间编辑距离查询问题)
题目大意
给定两个串 (S), (T)。(q) 次询问,每次查询 (S) 的一个子串 (S[l,r]) 和 (T) 的编辑距离。
两串 (A,B) 的编辑距离定义为,通过进行插入、删除、替换的操作(每次操作选择其中一种),将 (A) 变成 (B) 所需的最小操作次数。
数据范围:(1leq |S|,qleq 10^5),(1leq |T|leq 20)。
本题题解
朴素 DP
抛开多次询问。只考虑对两个串 (A), (B),求他们的编辑距离。
可以做一个朴素 DP。设 ( ext{dp}_0(i,j)) 表示 (A[1,i]) 和 (B[1,j]) 的编辑距离(( ext{dp}_0) 是为了和后面的其他 DP 区分开来)。则转移有三种:
一次 DP 的复杂度是 (O(|A||B|))。
在本题里,总复杂度是 (O(q|S||T|))。
正解
先讨论一个一般性的问题。对于一个串 (A),考虑它所有后缀与另一个串 (B) 的编辑距离。设 (A) 的长度为 (x) 的后缀与 (B) 的编辑距离为 (h(x))。我们发现,函数 (F(x) = x-h(x)) 有很好的性质。
- 首先,(F(x) = x - h(x)) 单调不降。这很好证明,因为当 (x) 加 (1) 时(后缀的长度加 (1),即在开头增加一个字符),(h(x)) 最多加 (1)(最坏情况就是直接把新加的字符删掉,用 (1) 次操作)。
- 其次,(F(x) = x - h(x)) 的值域是很有限的。因为 (h(x)leq x + |B|),且 (h(x)geq x-|B|),所以 (-|B|leq F(x)leq |B|)。
回到本题。(S) 的每个子串都可以表述为 (S) 的一个前缀的后缀。所以为了回答询问,我们要对 (S) 的每个前缀,预处理出关于它所有后缀的信息。即,我们要对所有:(A) 是 (S) 的一个前缀,(B) 是 (T),的情况,维护出 (F_{A,B}(x))。
那么朴素的做法是,设 ( ext{dp}_1(i,j,x)) 表示 (F_{S[1,i],T[1,j]}(x))。仿照上述的朴素 DP,可以 (O(1)) 转移。最后对于询问 (l,r),答案就是:(r - l + 1 - ext{dp}_1(r,|T|,r - l + 1))。时间复杂度 (O(|S|^2|T|+q))。
继续优化,前面说过,(F(x)) 有一个很好的性质,就是值域很小。所以考虑将 DP 的下标与值域互换。重新定义状态:设 ( ext{dp}_2(i,j,k)) 表示使得 (F_{S[1,i],S[1,j]}(x)leq k) 的最大的 (x)。
转移还是仿照朴素 DP 中的三种情况:
你可以直接按照编辑距离来理解(和朴素 DP 是类似的)。也可以按照分段函数的合并来理解。在朴素合并分段函数时(也就是 ( ext{dp}_1)),函数值是对能转移到它的三种值取 (max)。那新的 DP 中,对于同一个函数值,我们希望它出现的位置越早越好,所以 ( ext{dp}_2) 的转移应该是取 (min)。
DP 的边界也比较重要。考虑 (F_{A,B}(x)) 函数,(x) 的定义域应该是 ([0,|A|])。但是为了方便 DP,我们定义 (F(-1)=-infty)。这样 DP 的边界就是:(forall i,j:forall k in[-m-1,-j): ext{dp}_2(i,j,k)=-1)。( ext{dp}_2(0,0,0)=0)。其他 ( ext{dp}_2(i,j,k) = infty)。
DP 的时间复杂度 (O(|S||T|^2))。空间复杂度,用滚动数组优化后,可以做到 (O(|T|^2))。
对于询问 (l,r)。我们从小到大枚举 (k),找到第一个使得 ( ext{dp}_2(r,|T|,k)geq r - l + 1) 的 (k),则答案就是 (r-l+1-k)。单次查询的时间复杂度 (O(|T|))。显然可以通过二分查找优化到 (O(log |T|)),不过意义不大。
总时间复杂度 (O(|S||T|^2+q|T|))。
参考代码
本题可能需要使用读入、输出优化,详见本博客公告。
// problem: ZR1595
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 1e5, MAXM = 20;
const int INF = 1e9;
int n, m, q;
char s[MAXN + 5], t[MAXM + 5];
int dp[2][MAXM + 5][MAXM * 2 + 5];
int g[MAXN + 5][MAXM * 2 + 5];
int main() {
cin >> (s + 1) >> (t + 1) >> q;
n = strlen(s + 1);
m = strlen(t + 1);
int B = m + 1;
for (int j = 0; j <= m; ++j) {
for (int k = -j; k <= m; ++k) {
dp[0][j][k + B] = INF;
}
for (int k = -m - 1; k < -j; ++k) {
dp[0][j][k + B] = -1;
}
}
dp[0][0][B] = 0;
for (int i = 0; i <= n; ++i) {
int cur = (i & 1);
int nxt = (cur ^ 1);
for (int j = 0; j <= m; ++j) {
for (int k = -j; k <= m; ++k) {
dp[nxt][j][k + B] = INF;
}
for (int k = -m - 1; k < -j; ++k) {
dp[nxt][j][k + B] = -1;
}
}
for (int j = 0; j <= m; ++j) {
for (int k = -m - 1; k <= m; ++k) {
// cerr << "** " << i << " " << j << " " << k << " " << dp[i][j][k + m] << endl;
if (i < n)
ckmin(dp[nxt][j][k + B], dp[cur][j][k + B] + 1);
if (j < m)
ckmin(dp[cur][j + 1][k - 1 + B], dp[cur][j][k + B]);
if (i < n && j < m)
ckmin(dp[nxt][j + 1][k + (s[i + 1] == t[j + 1]) + B], dp[cur][j][k + B] + 1);
}
}
for (int k = -m; k <= m; ++k) {
g[i][k + B] = dp[cur][m][k + B];
}
}
for (int tq = 1; tq <= q; ++ tq) {
int l, r;
cin >> l >> r;
int len = r - l + 1;
for (int k = -m; k <= m; ++k) {
if (g[r][k + B] >= len) {
cout << len - k << endl;
break;
}
}
}
return 0;
}