#P3078. 最左侧冗余覆盖子串(100分)
-
1000ms
Tried: 212
Accepted: 40
Difficulty: 5
所属公司 :
华为od
-
算法标签>双指针
最左侧冗余覆盖子串(100分)
题解
题目解释
给定两个字符串 s1 和 s2,以及一个正整数 k。要求在字符串 s2 中找到一个长度为 len(s1) + k 的子串,使得:
- 该子串包含
s1中的所有字母; - 每个字母在子串中的出现次数不少于其在
s1中的出现次数。
需要找出满足上述条件的最左侧的子串的起始下标,如果不存在这样的子串,则输出 -1。
思路分析
题目的本质是在字符串 s2 中找到长度为 len(s1) + k 的最左侧子串,该子串的字母频次数满足 s1 的要求。
考虑以下几点:
-
窗口长度固定:由于子串长度固定为
len(s1) + k,可以使用滑动窗口的方法遍历s2中所有可能的子串。 -
字母频次要求:需要检查每个窗口内的字母频次是否满足
s1的要求。
为此,我们可以:
- 预处理
s1中每个字母的需要次数,存储在数组need中。 - 维护一个滑动窗口,窗口大小为
len(s1) + k,窗口内的字母频次存储在数组counts中。
在滑动窗口移动的过程中,我们需要高效地判断当前窗口是否满足条件。为此,使用一个变量 deficit 来表示当前窗口中缺失的字母种类数。
具体步骤
-
初始化:
- 计算
s1中每个字母的需要次数,存储在need数组中。 - 初始化滑动窗口,窗口大小为
len(s1) + k,计算窗口内的字母频次,存储在counts数组中。 - 初始化
deficit,统计在当前窗口中,哪些字母的出现次数少于need中的要求。
- 计算
-
滑动窗口遍历:
- 检查当前窗口:如果
deficit == 0,说明当前窗口满足条件,输出起始下标。 - 移动窗口:
- 移除左端字符,更新
counts和deficit:- 如果移除的字符在
need中有要求,且移除前字符的出现次数等于要求次数,那么移除后,该字符的出现次数将少于要求次数,因此deficit加 1。 - 无论如何,都要减少该字符在
counts中的计数。
- 如果移除的字符在
- 添加右端字符,更新
counts和deficit:- 增加该字符在
counts中的计数。 - 如果添加的字符在
need中有要求,且添加后字符的出现次数等于要求次数,那么deficit减 1。
- 增加该字符在
- 移除左端字符,更新
- 检查当前窗口:如果
-
结束条件:如果遍历完所有可能的窗口仍未找到满足条件的子串,输出
-1。
时间复杂度分析
-
预处理阶段:
- 计算
s1中的need数组,时间复杂度为 O(len(s1))。
- 计算
-
滑动窗口遍历:
- 窗口大小固定为
len(s1) + k,滑动次数为len(s2) - windowSize + 1,即 O(len(s2))。
- 窗口大小固定为
-
窗口内操作:
- 每次滑动窗口时,更新
counts和deficit,这些操作都是 O(1) 的。
- 每次滑动窗口时,更新
-
总时间复杂度:O(len(s1) + len(s2)),由于
len(s1)可能接近 1,000,000,len(s2)可能接近 20,000,000,因此需要使用高效的线性算法。
c++
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
string s1, s2;
int k;
// 输入
cin >> s1 >> s2 >> k;
int n1 = s1.length();
int n2 = s2.length();
// 如果需要的子串长度超过s2的长度,直接输出-1
if (n1 + k > n2) {
cout << -1 << endl;
return 0;
}
vector<int> need(26, 0); // s1中需要的字符计数
vector<int> counts(26, 0); // 当前窗口的字符计数
// 统计s1中每个字符的需要次数
for (char c : s1) {
need[c - 'a']++;
}
int windowSize = n1 + k;
// 初始化窗口内字符计数
for (int i = 0; i < windowSize; i++) {
counts[s2[i] - 'a']++;
}
// 初始化缺失字符计数
int deficit = 0;
for (int i = 0; i < 26; i++) {
if (counts[i] < need[i]) {
deficit++;
}
}
// 滑动窗口
for (int i = 0; i <= n2 - windowSize; i++) {
// 如果没有缺失的字符,输出起始下标
if (deficit == 0) {
cout << i << endl;
return 0;
}
// 移动窗口,更新字符计数和缺失计数
if (i + windowSize < n2) {
int leftChar = s2[i] - 'a';
int rightChar = s2[i + windowSize] - 'a';
// 移除左边字符
if (counts[leftChar] == need[leftChar]) {
deficit++;
}
counts[leftChar]--;
// 添加右边字符
counts[rightChar]++;
if (counts[rightChar] == need[rightChar]) {
deficit--;
}
} else {
// 已经到达最后一个窗口,无需再移动
break;
}
}
// 如果没有找到符合条件的子串,输出-1
cout << -1 << endl;
return 0;
}
python
def find_substring(s1, s2, k):
n1 = len(s1)
n2 = len(s2)
# 如果需要的子串长度超过s2的长度,直接输出-1
if n1 + k > n2:
return -1
need = [0] * 26 # s1中需要的字符计数
counts = [0] * 26 # 当前窗口的字符计数
# 统计s1中每个字符的需要次数
for c in s1:
need[ord(c) - ord('a')] += 1
window_size = n1 + k
# 初始化窗口内字符计数
for i in range(window_size):
counts[ord(s2[i]) - ord('a')] += 1
# 初始化缺失字符计数
deficit = sum(1 for i in range(26) if counts[i] < need[i])
# 滑动窗口
for i in range(n2 - window_size + 1):
# 如果没有缺失的字符,输出起始下标
if deficit == 0:
return i
# 移动窗口,更新字符计数和缺失计数
if i + window_size < n2:
left_char = ord(s2[i]) - ord('a')
right_char = ord(s2[i + window_size]) - ord('a')
# 移除左边字符
if counts[left_char] == need[left_char]:
deficit += 1
counts[left_char] -= 1
# 添加右边字符
counts[right_char] += 1
if counts[right_char] == need[right_char]:
deficit -= 1
# 如果没有找到符合条件的子串,输出-1
return -1
# 输入
s1 = input().strip()
s2 = input().strip()
k = int(input().strip())
# 输出结果
result = find_substring(s1, s2, k)
print(result)
java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String s1 = scanner.next();
String s2 = scanner.next();
int k = scanner.nextInt();
int n1 = s1.length();
int n2 = s2.length();
// 如果需要的子串长度超过s2的长度,直接输出-1
if (n1 + k > n2) {
System.out.println(-1);
return;
}
int[] need = new int[26]; // s1中需要的字符计数
int[] counts = new int[26]; // 当前窗口的字符计数
// 统计s1中每个字符的需要次数
for (char c : s1.toCharArray()) {
need[c - 'a']++;
}
int windowSize = n1 + k;
// 初始化窗口内字符计数
for (int i = 0; i < windowSize; i++) {
counts[s2.charAt(i) - 'a']++;
}
// 初始化缺失字符计数
int deficit = 0;
for (int i = 0; i < 26; i++) {
if (counts[i] < need[i]) {
deficit++;
}
}
// 滑动窗口
for (int i = 0; i <= n2 - windowSize; i++) {
// 如果没有缺失的字符,输出起始下标
if (deficit == 0) {
System.out.println(i);
return;
}
// 移动窗口,更新字符计数和缺失计数
if (i + windowSize < n2) {
int leftChar = s2.charAt(i) - 'a';
int rightChar = s2.charAt(i + windowSize) - 'a';
// 移除左边字符
if (counts[leftChar] == need[leftChar]) {
deficit++;
}
counts[leftChar]--;
// 添加右边字符
counts[rightChar]++;
if (counts[rightChar] == need[rightChar]) {
deficit--;
}
} else {
// 已经到达最后一个窗口,无需再移动
break;
}
}
// 如果没有找到符合条件的子串,输出-1
System.out.println(-1);
}
}
题目内容
给定两个字符串 s1 和 s2 和正整数 k,其中 s1 长度为 n1,s2 长度为 n2。
在 s2 中选一个子串,若满足下面条件,则称 s2 以长度 k 冗余覆盖 s1
- 该子串长度为 n1+k
- 该子串中包含 s1 中全部字母
- 该子串每个字母出现次数不小于 s1 中对应的字母
给定 s1,s2,k,求最左侧的 s2 以长度 k 冗余覆盖 s1 的子串的首个元素的下标,如果没有返回−1。
举例:
s1 = "ab"
s2 = "aabcd"
k=1
则子串 “aab” 和 "abc" 均满足此条件,由于 "aab" 在 "abc" 的左侧,"aab" 的第一个元素下标为 0,因此输出 0
输入描述
输入三行,第一行为 s1,第二行为 s2,第三行为 k
- s1 和 s2 只包含小写字母
输出描述
最左侧的 s2 以长度 k 冗余覆盖 s1 的子串首个元素下标,如果没有返回 −1。
备注
- 0≤len(s1)≤1000000
- 0≤len(s2)≤20000000
- 0≤k≤1000
样例1
输入
ab
aabcd
1
输出
0
说明
子串aab和abc符合要求,由于aab在abc的左侧,因此输出aab的下标:0
样例2
输入
abc
dfs
10
输出
-1
说明
s2无法覆盖s1,输出 −1