vlambda博客
学习文章列表

动态规划+贪心算法(六)

一、例题

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

进阶:

如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

示例 1:
输入:s = "abc", t = "ahbgdc"
输出:true
示例 2:
输入:s = "axc", t = "ahbgdc"
输出:false

思考

1.1 双指针+贪心解法

看到这个题目第一反应是通过双指针来解决问题,一个指针指向s,一个指针指向t,通过移动指针来判断是否s是t的子串。

|
a b c
a b a b c
|

我们每次移动指针 i,j,分别指向s和t的初始位置,每次贪心的匹配,匹配成功则i和j同时右移,匹配失败j 右移动,尝试用t的下一个字符匹配s,最终i移动到s的末尾,说明s是t的子序列。

JAVA版本代码

 /***
* 贪心算法+双指针 判断是不是子串
* @param s 待检测的串
* @param t 目标串
* @return
*/
private boolean greedyAlgorithmWithDoublePointer(String s, String t) {
// 获取待检测串的长度
final int m = s.length();
// 获取母串的长度
final int n = t.length();
// 开始的索引
int i = 0;
// 开始的索引
int j = 0;
// 双指针满足小于目标长度,则继续循环
while (i < m && j < n) {
// 如果待检测串匹配到母串,则待检测串右移
if (s.charAt(i) == t.charAt(j)) {
i++;
}
j++;
}
return i == m;
}

1.2 动态规划解法

进阶:如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

如何优化加速匹配速度呢?那就是把t串的位置进行预处理。我们可以使用动态规划的方法实现预处理。

灵魂拷问,是不是动态规划问题?

本次状态是否可以依赖上一个状态,如何转成当前串的匹配依赖上一个状态?

原问题和子问题的确定

原问题就是确定最终串s和串t的最后是否匹配,

子问题就是串i位置是否和t的j位置是否匹配

状态确定

设f(i,j)表示t中位置i开始往后在字符j第一次出现的位置

状态转移方程

如果t中的字符就是j,那么f(i,j)=i,否则j出现在位置i+1开始往后,即f(i,j)=f(i+1,j) 状态转移方程:f(i,j)={ i, f(i+1,j)}

JAVA示例代码:

 /***
* 动态规划解决串匹配问题
* @param s 待检测的串
* @param t 目标串
* @return
*/
private boolean checkIsSubsequenceWithDP(String s, String t) {
// 获取待检测串的长度
final int n = s.length();
// 获取母串的长度
final int m = t.length();
int[][] dp = new int[m + 1][26];
for (int i = 0; i < 26; i++) {
Arrays.fill();
dp[m][i] = -1;
}

// 初始化动态表,预先初始化动态表
//如果大规模的检测,dp数组就是缓存,存储了字符串位置,后面直接跳表检测即可
for (int i = m - 1; i >= 0; i--) {
for (int j = 0; j < 26; j++) {
if (t.charAt(i) == j + 'a') {
dp[i][j] = i;
} else {
dp[i][j] = dp[i + 1][j];
}
}
}
int lastIndex = 0;
for (int i = 0; i < n; i++) {
if (dp[lastIndex][s.charAt(i) - 'a'] == -1) {
return false;
}
lastIndex = dp[lastIndex][s.charAt(i) - 'a'] + 1;
}
return true;
}

1.3 Hash+二分查找算法

思路:

思路就是把字符映射到hash桶中,相同字符串放到数组中,然后寻找上一个匹配位置小于数组中第一个大于匹配位置那么就算匹配成功。寻找过程中是一个二分查找算法。

JAVA代码:

  /***
* 使用hash表+二分查找
* @param s 待匹配的字符串
* @param t 目标字符串
* @return
*/
private boolean checkIsSubsequenceWithHash(String s, String t) {
Map<Character, List<Integer>> characterListMap = new HashMap<>();
// 存储各个字符串的位置
for (int i = 0; i < t.length(); i++) {
if (characterListMap.containsKey(t.charAt(i))) {
characterListMap.get(t.charAt(i)).add(i);
} else {
List<Integer> lists = new ArrayList<>();
lists.add(i);
characterListMap.put(t.charAt(i), lists);
}
}
// 设置默认索引位置-1
int lastIndex = -1;
for (int i = 0; i < s.length(); i++) {
if (characterListMap.containsKey(s.charAt(i))) {
List<Integer> list = characterListMap.get(s.charAt(i));
int left = 0;
int right = list.size();
// 二分查找第一个大于lastIndex的位置
while (left < right) {
int mid = left + (right - left) / 2;
if (list.get(mid) > lastIndex) {
right = mid;
} else {
left = mid + 1;
}
}
if (left == list.size()) {
return false;
}
lastIndex = list.get(left);
} else {
return false;
}
}
return true;
}