#P2876. 第2题-删除字符
-
1000ms
Tried: 18
Accepted: 8
Difficulty: 6
所属公司 :
米哈游
时间 :2025年4月19日
-
算法标签>二分算法
第2题-删除字符
题解
题面描述
给定两个字符串 s(长度 n)和 t(长度 m),为了数据安全,需要删除 s 中的字符,使得 t 不再作为 s 的子串(即连续片段)出现。删除顺序由长度为 n 的排列 a 给出,表示第 i 次删除位置为 ai(删除后用空白字符替代,不会合并剩余字符)。
请问最少需要删除多少个字符,才能保证删除后的 s 中不包含 t 作为子串?
思路
- 二分答案:设尝试删除前 k 次(位置 a1,…,ak),检查此时剩余的 s 是否包含 t 作为子串。
- 判断子串:维护布尔数组 del[1…n],标记被删除的位置。然后对每个可能的子串起始位置 i(1≤i≤n−m+1)执行:
- 若 del[i+j]=false 且 s[i+j]=t[j+1] 对所有 0≤j<m 均成立,则存在子串。
- 否则继续下一个 i。
- 若任意 i 满足上述条件,则说明目前 s 仍包含 t,需要增大 k;否则可尝试更小的 k。
- 在区间 [0,n] 上二分,找最小的 k 使得“删除前 k 次后 s 不包含 t”。
单次判断复杂度 O(nm),m≤50,n≤105,故 O(nmlogn) 可接受。
C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
string s, t;
cin >> s >> t;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
--a[i]; // 转为 0-based
}
vector<bool> deleted(n, false);
// 检查删除前 k 次后,s 中是否仍包含 t
auto contains_substring = [&](int k) {
fill(deleted.begin(), deleted.end(), false);
for (int i = 0; i < k; i++) deleted[a[i]] = true;
// 构建前缀和,方便快速判断区间内是否有空隙
vector<int> pre(n+1, 0);
for (int i = 0; i < n; i++)
pre[i+1] = pre[i] + (deleted[i] ? 1 : 0);
for (int i = 0; i + m <= n; i++) {
// 如果区间 [i,i+m) 中无删除
if (pre[i+m] - pre[i] == 0) {
bool ok = true;
for (int j = 0; j < m; j++) {
if (s[i+j] != t[j]) { ok = false; break; }
}
if (ok) return true;
}
}
return false;
};
int l = 0, r = n, ans = n;
while (l <= r) {
int mid = l + (r - l) / 2;
if (contains_substring(mid)) {
// 仍包含,需要更多删除
l = mid + 1;
} else {
ans = mid;
r = mid - 1;
}
}
cout << ans << '\n';
return 0;
}
Python
import sys
input = sys.stdin.readline
def main():
n, m = map(int, input().split())
s = input().strip()
t = input().strip()
a = list(map(lambda x: int(x)-1, input().split()))
deleted = [False] * n
def contains_substring(k):
# 标记删除前 k 次的位置
for i in range(n): deleted[i] = False
for i in range(k): deleted[a[i]] = True
# 前缀和统计删除数
pre = [0] * (n+1)
for i in range(n): pre[i+1] = pre[i] + (1 if deleted[i] else 0)
# 检查每个窗口
for i in range(0, n-m+1):
if pre[i+m] - pre[i] == 0:
# 连续窗口中无删除
if s[i:i+m] == t:
return True
return False
# 二分查找最小 k
l, r, ans = 0, n, n
while l <= r:
mid = (l + r) // 2
if contains_substring(mid):
l = mid + 1
else:
ans = mid
r = mid - 1
print(ans)
if __name__ == '__main__':
main()
Java
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
String s = br.readLine();
String t = br.readLine();
int[] a = new int[n];
st = new StringTokenizer(br.readLine());
for (int i = 0; i < n; i++) a[i] = Integer.parseInt(st.nextToken()) - 1;
boolean[] deleted = new boolean[n];
int[] pre = new int[n+1];
// 检查删除前 k 次后是否仍包含 t
Function<Integer, Boolean> contains_substring = (k) -> {
Arrays.fill(deleted, false);
for (int i = 0; i < k; i++) deleted[a[i]] = true;
pre[0] = 0;
for (int i = 0; i < n; i++) pre[i+1] = pre[i] + (deleted[i] ? 1 : 0);
for (int i = 0; i + m <= n; i++) {
if (pre[i+m] - pre[i] == 0) {
boolean ok = true;
for (int j = 0; j < m; j++) {
if (s.charAt(i+j) != t.charAt(j)) { ok = false; break; }
}
if (ok) return true;
}
}
return false;
};
int l = 0, r = n, ans = n;
while (l <= r) {
int mid = l + (r - l) / 2;
if (contains_substring.apply(mid)) {
l = mid + 1;
} else {
ans = mid;
r = mid - 1;
}
}
System.out.println(ans);
}
}
题目内容
公司里有两个字符串,分别是 s 和 t 。由于数据安全的原因,字符串 t 不允许作为 s 的子串存在。为了满足数据规范,米小游需要按照一定的顺序删除字符串 s 中的一些字符,确保 s 中不再包含 t 。注意删除字符 si 后,不会自动将前后字符串合并,你可以认为使用一个空白字符代替 si 。
米小游想知道,在保证 s 不包含 t 的前提下,她最少需要删除多少个字符?
删除顺序可以描述为一个排列,ai 表示删除 sai 字符。
输入描述
第一行输入两个整数 n 和 m ,表示字符串 s 和 t 的长度。
第二行输入一个长度为 n 的字符串 s ,仅包含小写字母。
第三行输入一个长度为 m 的字符串 t ,仅包含小写字母。
第四行输入一个长度为 n 的排列 a,表示第 i 次删除 sai 字符。
1≤n≤105
1≤m≤50
1≤ai≤n
输出描述
输出一个整数,表示最少需要删除的字符个数。
样例1
输入
5 2
ababa
ab
3 2 5 4 1
输出
2
说明
第一次删除 S3 ,s 变为 ab a ba。
第二次删除 S2 ,s 变为 a ba ba。
此时 s 不包含 t ,最少需要删除 2 个字符。