题目

给你两个字符串 s 和 t ,请你找出 s 中的非空子串的数目,这些子串满足替换 一个不同字符 以后,是 t 串的子串。换言之,请你找到 s 和 t 串中 恰好 只有一个字符不同的子字符串对的数目。

比方说, “computer” 和 “computation” 加粗部分只有一个字符不同: ‘e’/‘a’ ,所以这一对子字符串会给答案加 1 。

请你返回满足上述条件的不同子字符串对数目。

一个 子字符串 是一个字符串中连续的字符。

示例 1:

输入:s = “aba”, t = “baba”
输出:6
解释:以下为只相差 1 个字符的 s 和 t 串的子字符串对:
(“aba”, “baba”)
(“aba”, “baba”)
(“aba”, “baba”)
(“aba”, “baba”)
(“aba”, “baba”)
(“aba”, “baba”)
加粗部分分别表示 s 和 t 串选出来的子字符串。

示例 2:
输入:s = “ab”, t = “bb”
输出:3
解释:以下为只相差 1 个字符的 s 和 t 串的子字符串对:
(“ab”, “bb”)
(“ab”, “bb”)
(“ab”, “bb”)
加粗部分分别表示 s 和 t 串选出来的子字符串。

示例 3:
输入:s = “a”, t = “a”
输出:0
示例 4:

输入:s = “abe”, t = “bbc”
输出:10

提示:

1 <= s.length, t.length <= 100
s 和 t 都只包含小写英文字母。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/count-substrings-that-differ-by-one-character
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

可以暴力枚举1638. 统计只差一个字符的子串数目 - 图3子串和1638. 统计只差一个字符的子串数目 - 图4子串的开始下标,然后各自往后遍历,找到只差一位时停止。时间复杂度为1638. 统计只差一个字符的子串数目 - 图5)#card=math&code=O%28m%2An%2Amin%28m%2Cn%29%29&id=IMJmL)。

考虑对暴力方法进行优化,我们枚举子串的结束下标,涉及连续子串一般都可以考虑“以当前位置结束”,从而使用动态规划。

1638. 统计只差一个字符的子串数目 - 图6表示1638. 统计只差一个字符的子串数目 - 图71638. 统计只差一个字符的子串数目 - 图8分别以1638. 统计只差一个字符的子串数目 - 图91638. 统计只差一个字符的子串数目 - 图10结尾的满足条件的子串对数,那1638. 统计只差一个字符的子串数目 - 图11可以从什么状态转移过来呢?

1638. 统计只差一个字符的子串数目 - 图12时,可以想到有1638. 统计只差一个字符的子串数目 - 图13

1638. 统计只差一个字符的子串数目 - 图14时,因为这两个字符不相同,所以前面的字符必须都相同了,前面相同的长度加一就是满足条件的对数。那么需要求1638. 统计只差一个字符的子串数目 - 图151638. 统计只差一个字符的子串数目 - 图16的最长公共后缀的长度,记为1638. 统计只差一个字符的子串数目 - 图17,那么有1638. 统计只差一个字符的子串数目 - 图18。最后返回的答案就是所有1638. 统计只差一个字符的子串数目 - 图19的和。

这道题目可以学到很多东西,从暴力到DP,再到DP的空间也可以优化,具体细节见代码注释。

代码

暴力 1638. 统计只差一个字符的子串数目 - 图20)#card=math&code=O%28m%2An%2Amin%28m%2Cn%29%29&id=GavcE)

  1. class Solution {
  2. public int countSubstrings(String s, String t) {
  3. int n = s.length();
  4. int m = t.length();
  5. int ans = 0;
  6. for (int i = 0; i < n; ++i) {
  7. for (int j = 0; j < m; ++j) {
  8. int diff = 0;
  9. for (int k = 0; i + k < n && j + k < m; ++k) {
  10. if (s.charAt(i + k) != t.charAt(j + k)) {
  11. diff++;
  12. }
  13. if (diff > 1) {
  14. break;
  15. }
  16. if (diff == 1) {
  17. ans++;
  18. }
  19. }
  20. }
  21. }
  22. return ans;
  23. }
  24. }

二维DP 1638. 统计只差一个字符的子串数目 - 图21#card=math&code=O%28m%2An%29&id=TNBsO)

  1. class Solution {
  2. public int countSubstrings(String s, String t) {
  3. int n = s.length();
  4. int m = t.length();
  5. // g[i][j]表示s[0,i]和t[0,j]的最长公共后缀的长度
  6. // 声明的矩阵大小行和列都增加1,省去了边界讨论,下面f同理
  7. int[][] g = new int[n + 1][m + 1];
  8. for (int i = 1; i <= n; i++) {
  9. for (int j = 1; j <= m; j++) {
  10. g[i][j] = s.charAt(i - 1) == t.charAt(j - 1) ? g[i - 1][j - 1] + 1 : 0;
  11. }
  12. }
  13. int ans = 0;
  14. // f[i][j]表示s和t分别以s[i]和t[j]结尾的满足条件的子串对数
  15. int[][] f = new int[n + 1][m + 1];
  16. for (int i = 1; i <= n; i++) {
  17. for (int j = 1; j <= m; j++) {
  18. f[i][j] = s.charAt(i - 1) == t.charAt(j - 1) ? f[i - 1][j - 1] : g[i - 1][j - 1] + 1;
  19. ans += f[i][j];
  20. }
  21. }
  22. return ans;
  23. }
  24. }

注意到上面代码的两个for循环可以合并,得到如下代码

  1. class Solution {
  2. public int countSubstrings(String s, String t) {
  3. int n = s.length();
  4. int m = t.length();
  5. int ans = 0;
  6. int[][] g = new int[n + 1][m + 1];
  7. int[][] f = new int[n + 1][m + 1];
  8. for (int i = 1; i <= n; i++) {
  9. for (int j = 1; j <= m; j++) {
  10. g[i][j] = s.charAt(i - 1) == t.charAt(j - 1) ? g[i - 1][j - 1] + 1 : 0;
  11. f[i][j] = s.charAt(i - 1) == t.charAt(j - 1) ? f[i - 1][j - 1] : g[i - 1][j - 1] + 1;
  12. ans += f[i][j];
  13. }
  14. }
  15. return ans;
  16. }
  17. }

DP O(1)空间

观察以上DP代码,发现1638. 统计只差一个字符的子串数目 - 图221638. 统计只差一个字符的子串数目 - 图23都只和左上角的值有关。因此如果按对角线遍历矩阵,只需要维护两个变量即可,而不需要真实地开辟矩阵空间。

下面遍历矩阵对角线的写法参考「零神」的代码。真的优雅,需要牢记。

  1. class Solution {
  2. public int countSubstrings(String s, String t) {
  3. int n = s.length();
  4. int m = t.length();
  5. int ans = 0;
  6. // 从右上角开始,按对角线方向进行遍历
  7. for (int delta = -(m - 1); delta <= n - 1; delta++) {
  8. int i = 0;
  9. int j = 0;
  10. // delta为负表示列,为正表示行
  11. if (delta < 0) {
  12. j = -delta;
  13. } else {
  14. i = delta;
  15. }
  16. int g = 0;
  17. int f = 0;
  18. for (; i < n && j < m; ++i, ++j) {
  19. // 这里f的值依赖g,因此需要先更新f再更新g
  20. f = s.charAt(i) == t.charAt(j) ? f : g + 1;
  21. g = s.charAt(i) == t.charAt(j) ? g + 1 : 0;
  22. ans += f;
  23. }
  24. }
  25. return ans;
  26. }
  27. }