题目地址:
https://leetcode.com/problems/minimum-window-subsequence/
给定两个字符串 s 1 s_1 s1和 s 2 s_2 s2,求 s 1 s_1 s1中长度最短的子串,使 s 2 s_2 s2是它的子序列,返回该子串。若有若干长度相同的,则返回起始位置最左边的那个。
思路是序列自动机(动态规划解法参考https://blog.csdn.net/qq_46105170/article/details/119351720)。先构造 s 1 s_1 s1的序列自动机,然后枚举 s 2 [ 0 ] s_2[0] s2[0]在 s 1 s_1 s1中的位置(找 s 2 [ 0 ] s_2[0] s2[0]的位置本身可以通过序列自动机来找),对于每个位置,都能通过序列自动机找到最短的以 s 2 s_2 s2为子序列的子串的终点位置(跳转 s 2 s_2 s2长度那么多次即可),如果能走到终点位置(即序列自动机判定子序列成功),则尝试更新答案,否则继续找 s 2 [ 0 ] s_2[0] s2[0]在 s 1 s_1 s1中出现的下一个位置继续枚举。代码如下:
public class Solution {
public String minWindow(String s1, String s2) {
int[][] dfa = buildDFA(s1);
String res = "";
// idx表示从哪个位置开始向后找s2[0],end表示找到的子序列的末尾在s1中的位置,下标都从1开始
int idx = 0, end = 0;
// 如果还能在idx之后找到s2[0],则开始枚举该位置起步能找到的子序列
while ((end = compute(idx, dfa, s2)) != 0) {
// 子串开始位置是s2[0]在s1里出现的位置,下标从1开始
int begin = dfa[idx][s2.charAt(0) - 'a'];
// 尝试更新答案,左端点的真实下标是begin - 1,右端点的真实下标是end - 1
if (res.isEmpty() || end - (begin - 1) < res.length()) {
res = s1.substring(begin - 1, end);
}
// 把枚举的位置向后移动
idx = begin;
}
return res;
}
// 从idx的位置开始找以s为子序列的最短子串的右端点,下标都从1开始
private int compute(int idx, int[][] dfa, String s) {
for (int i = 0; i < s.length(); i++) {
int x = s.charAt(i) - 'a';
// s[i]找不到了,说明不存在这样的子串,返回0
if (dfa[idx][x] == 0) {
return 0;
}
// 否则向后跳
idx = dfa[idx][x];
}
// 返回最后一个字符出现的位置,下标从1开始
return idx;
}
// 构造序列自动机
private int[][] buildDFA(String s) {
int[][] dfa = new int[s.length() + 1][26];
for (int i = s.length() - 1; i >= 0; i--) {
for (int j = 0; j < 26; j++) {
dfa[i][j] = dfa[i + 1][j];
}
dfa[i][s.charAt(i) - 'a'] = i + 1;
}
return dfa;
}
}
时间复杂度 O ( l s 1 l s 2 ) O(l_{s_1}l_{s_2}) O(ls1ls2),空间 O ( l s 1 ) O(l_{s_1}) O(ls1)。