题目描述
难度分:2100
输入两个长度≤105的字符串s和t,只包含小写英文字母。
删除t的一个最短的连续子串,使得t是s的子序列。
输出删除子串后的t。
如果必须把t全部删除,输出-
(减号)。
输入样例1
hi
bob
输出样例1
-
输入样例2
abca
accepted
输出样例2
ac
输入样例3
abacaba
abcdcba
输出样例3
abcba
算法
双指针+二分答案
令s的长度为n,t的长度为m。我们可以预处理出两个O(m)级别的数组pre和suf,pre[i]表示t的前缀[1,i]出现在s的前缀[1,pre[i]]中,suf[j]表示t的后缀[j,m]出现在s的后缀[suf[j],m]中,那如果我们想要保留t的前缀[1,i]和后缀[j,m],就要删除j−i−1的长度,删除的子段为(i,j)。而这两段前缀和后缀作为s的一个有效的子序列,必须还要满足pre[i]<suf[j],这样前缀和后缀作为的子序列在s中才不会交叠。
因此,就很容易发现单调性,肯定是给的窗口越小,越难做到让t删除这段子串后能够成为s的子序列更难。对于一个给定的删除长度len,我们遍历t上长度为len的所有窗口[l,r],一旦满足pre[l−1]<suf[r+1],说明将t的子串[l,r]删除后,剩余的前后缀拼起来是s的一个子序列,同时记录此时删除段的左右端点:
- 如果对于len,能够找到t的一个有效删除段,就记录左右端点,并往左搜索看看有没有更短的删除段。
- 否则说明len给的太小了,往右搜索更大的删除长度。
要处理出pre和suf两个数组只需要做两次子序列匹配的相向双指针即可,以pre数组为例进行说明。令s的指针i=1,t的指针j=1,i不断右移,j只有在满足s[i]=t[j]时才右移,说明匹配到了t的一个字符。这时候记录pre[j]=i,说明t的前缀[1,j]已经被s的前缀[1,i]匹配完了,t[1…j]是s[1…i]的子序列。
复杂度分析
时间复杂度
两次相向双指针预处理出pre和suf两个数组,时间复杂度为O(n+m)。初始化答案为删除前后缀的情况,时间复杂度为O(m)。在[1,m]上二分答案,每次check需要滑动窗口遍历[w,n](其中w为窗口长度),时间复杂度为mlog2m。
综上,整个算法的时间复杂度为O(n+m+mlog2m)。
空间复杂度
除了输入的两个字符串s和t,空间瓶颈主要在于pre数组和suf数组,它们的空间消耗为O(m)。所以整个算法的额外空间复杂度为O(m)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, m, pre[N], suf[N], len, low, high;
char s[N], t[N];
bool is_sub_seq() {
int i = 1, j = 1;
while(i <= n && j <= m) {
if(s[i] == t[j]) j++;
i++;
}
return j > m;
}
bool check(int len) {
for(int r = len; r <= m; r++) {
int l = r - len + 1;
if(pre[l - 1] < suf[r + 1]) {
low = l, high = r;
return true;
}
}
return false;
}
int main() {
scanf("%s", s + 1);
scanf("%s", t + 1);
n = strlen(s + 1), m = strlen(t + 1);
if(is_sub_seq()) {
// t本来就是s的子序列,不用删除任何子串
puts(t + 1);
return 0;
}
if(n == 1) {
bool ok = false;
for(int i = 1; i <= m; i++) {
if(s[1] == t[i]) {
ok = true;
break;
}
}
puts(ok? s + 1: "-");
return 0;
}
for(int i = 0; i <= m + 1; i++) {
pre[i] = n + 1, suf[i] = -1;
}
int i = 1, j = 1;
pre[0] = 0, suf[m + 1] = n + 1;
while(i <= n && j <= m) {
if(s[i] == t[j]) {
pre[j] = i;
j++;
}
i++;
}
i = n, j = m;
while(i >= 1 && j >= 1) {
if(s[i] == t[j]) {
suf[j] = i;
j--;
}
i--;
}
low = 1, high = m, len = m;
for(int i = 1; i <= m; i++) {
if(pre[i] <= n && m - i < len) {
len = m - i;
low = i + 1, high = m;
}
}
for(int i = m; i >= 1; i--) {
if(suf[i] >= 1 && i - 1 < len) {
len = i - 1;
low = 1, high = i - 1;
}
}
int l = 1, r = m - 1;
while(l < r) {
int mid = l + r >> 1;
if(check(mid)) {
r = mid;
}else {
l = mid + 1;
}
}
if(low == 1 && high == m) {
puts("-");
}else {
for(int i = 1; i <= m; i++) {
if(i < low || i > high) {
printf("%c", t[i]);
}
}
}
return 0;
}