【滑动窗口】应用 II

目录

1. 题目列表

题目列表:

序号 题目 难度
1 643. 子数组最大平均数 I 简单
2 3. 无重复字符的最长子串 中等
3 159. 至多包含两个不同字符的最长子串 中等
4 209. 长度最小的子数组 中等
5 1695. 删除子数组的最大得分 中等
6 438. 找到字符串中所有字母异位词 中等
7 487. 最大连续1的个数 II 中等
8 1208. 尽可能使字符串相等 中等
9 1052. 爱生气的书店老板 中等
10 1423. 可获得的最大点数 中等
11 1151. 最少交换次数来组合所有的 1 中等
12 718. 最长重复子数组 中等
13 220. 存在重复元素 III 中等
14 713. 乘积小于 K 的子数组 中等

2. 应用

需要注意,滑动窗口的指针移动策略:

  • 如果窗口中的长度是固定的长度,那么,当窗口内的元素个数等于目标长度时,就要开始移动左指针,并且左指针跟随右指针移动;

  • 如果窗口中的长度是变化的长度,那么,当窗口内的元素满足条件时,就要需要多次移动左指针;

2.1. Leetcode 643. 子数组最大平均数 I

2.1.1. 题目

643. 子数组最大平均数 I

给你一个由 n 个元素组成的整数数组 nums 和一个整数 k 。
请你找出平均数最大且 长度为 k 的连续子数组,并输出该最大平均数。
任何误差小于 10-5 的答案都将被视为正确答案。

示例 1:

输入:nums = [1,12,-5,-6,50,3], k = 4
输出:12.75
解释:最大平均数 (12-5-6+50)/4 = 51/4 = 12.75

2.1.2. 解题思路

对于数组的区间求和,可以使用前缀和的思路求解。

对于区间长度固定的子数组,我们只需要枚举区间的右侧端点即可。

2.1.3. 代码实现

class Solution {
    public double findMaxAverage(int[] nums, int k) {
        int n = nums.length;
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            sum[i] = sum[i - 1] + nums[i - 1];
        }

        int right = k;
        int windowSum = sum[k];
        while (right <= n) { // 注意:这里前缀和的范围是[1, n]
            windowSum = Math.max(windowSum, sum[right] - sum[right - k]);
            right++;
        }
        return windowSum * 1.0 / k;
    }
}

2.2. Leetcode 3. 无重复字符的最长子串

2.2.1. 题目

3. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

2.2.2. 解题思路

使用滑动窗口的思路,使用哈希表 \(window\) 记录窗口中字符的数量,每一个右指针移动的时候,对应的字符个数就增加 \(1\),左指针移动时,就将移除窗口的字符数量减 \(1\),字符数量减小到 \(0\)时,就将哈希表中对应字符的 \(key\) 删除。

2.2.3. 代码实现

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length();
        Map<Character, Integer> window = new HashMap<>();
        int maxLength = 0;
        int left = 0, right = 0;
        while (right < n) {
            Character tail = s.charAt(right);
            window.put(tail, window.getOrDefault(tail, 0) + 1);
            if (window.size() == right - left + 1) {
                maxLength = Math.max(maxLength, right - left + 1);
            }

            while (right - left + 1 > window.size()) {
                Character head = s.charAt(left);
                window.put(head, window.get(head) - 1);
                if (window.get(head).equals(0)) {
                    window.remove(head);
                }
                left++;
            }
            right++;
        }
        return maxLength;
    }
}

类似的题目有:

2.3. Leetcode 159. 至多包含两个不同字符的最长子串

2.3.1. 题目

159. 至多包含两个不同字符的最长子串

给你一个字符串 s ,请你找出 至多 包含 两个不同字符 的最长子串,并返回该子串的长度。

示例 1:

输入:s = "eceba"
输出:3
解释:满足题目要求的子串是 "ece" ,长度为 3 。

2.3.2. 解题思路

至多包含两个不同的字符,表示子串可能只有一种字符或者只有两种字符。

使用一个哈希表 \(window\) 记录窗口中每个字符出现的次数,假设字符串 \(s\) 的长度为 \(n\),维护两个指针 \(left\)\(right\),在移动右指针 \(right\) 的过程中,如果窗口中字符种类小于等于 \(2\) 时,就更新最大的窗口长度。

同时,只要窗口中的字符种类大于 \(2\) 就缩小左侧区间。

2.3.3. 代码实现

class Solution {
    private static final int MAX = 2;

    public int lengthOfLongestSubstringTwoDistinct(String s) {
        int n = s.length();
        Map<Character, Integer> window = new HashMap<>();
        int left = 0, right = 0;
        int maxWindLength = 0;
        while (right < n) {
            Character tail = s.charAt(right);
            window.put(tail, window.getOrDefault(tail, 0) + 1);
            if (window.size() <= MAX) {
                maxWindLength = Math.max(maxWindLength, right - left + 1);
            }

            while (window.size() > MAX) {
                Character head = s.charAt(left);
                window.put(head, window.get(head) - 1);
                if (window.get(head).equals(0)) {
                    window.remove(head);
                }
                left++;
            }
            right++;
        }
        return maxWindLength;
    }
}

类似的题目有:

2.4. Leetcode 209. 长度最小的子数组

2.4.1. 题目

209. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

2.4.2. 解题思路

这里,我们可以考虑使用滑动窗口的思路求解,使用一个整形变量 \(window\) 记录窗口内的所有元素之和。

当窗口的元素之和大于等于 \(target\) 时,就尝试不断移动左指针缩小窗口,同时,更新最小窗口长度。

2.4.3. 代码实现

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        int window = 0;
        int left = 0, right = 0;
        int minSize = n + 1; // 初始化为无效值
        while (right < n) {
            window += nums[right];
            while (window >= target) {
                minSize = Math.min(minSize, right - left + 1);
                window -= nums[left];
                left++;
            }
            right++;
        }
        return minSize == n + 1 ? 0 : minSize;
    }
}

2.5. Leetcode 1695. 删除子数组的最大得分

2.5.1. 题目

1695. 删除子数组的最大得分

给你一个正整数数组 nums ,请你从中删除一个含有 若干不同元素 的子数组。删除子数组的 得分 就是子数组各元素之 和 。
返回 只删除一个 子数组可获得的 最大得分 。
如果数组 b 是数组 a 的一个连续子序列,即如果它等于 a[l],a[l+1],...,a[r] ,那么它就是 a 的一个子数组。

示例 1:

输入:nums = [4,2,4,5,6]
输出:17
解释:最优子数组是 [2,4,5,6]

2.5.2. 解题思路

题目等价求数组 \(nums\) 的最长的连续子数组的和。

这里,我们可以考虑使用滑动窗口的思路求解,使用哈希表 \(window\) 记录窗口内的数字出现的次数,同时,使用一个整形变量 \(sum\) 记录窗口内的所有元素之和,如果窗口的长度大于哈希表中的 \(key\) 的个数,则说明窗口中存在重复的数字

当窗口内的元素个数等于窗口长度时,就更新最大窗口长度。

当窗口中出现重复字符时,即窗口中的数字个数小于窗口长度时,就尝试不断移动左指针缩小窗口。

2.5.3. 代码实现

class Solution {
    public int maximumUniqueSubarray(int[] nums) {
        int n = nums.length;
        Map<Integer, Integer> window = new HashMap<>();
        int left = 0, right = 0;
        int result = 0;
        int sum = 0;
        while (right < n) {
            window.put(nums[right], window.getOrDefault(nums[right], 0) + 1);
            sum += nums[right];
            if (window.size() == right - left + 1) {
                result = Math.max(result, sum);
            }

            while (left <= right && window.size() < right - left + 1) {
                window.put(nums[left], window.get(nums[left]) - 1);
                if (window.get(nums[left]).equals(0)) {
                    window.remove(nums[left]);
                }
                sum -= nums[left];
                left++;
            }
            right++;
        }
        return result;
    }
}

2.6. Leetcode 438. 找到字符串中所有字母异位词

2.6.1. 题目

438. 找到字符串中所有字母异位词

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:

输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

2.6.2. 解题思路

使用一个哈希表 \(window\) 记录窗口中每个数字出现的次数,如果窗口的长度大于哈希表中的 \(key\) 的个数,则说明窗口中存在重复的数字

2.6.3. 代码实现

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        Map<Character, Integer> window = new HashMap<>();
        Map<Character, Integer> need = new HashMap<>();
        for (int i = 0; i < p.length(); i++) {
            need.put(p.charAt(i), need.getOrDefault(p.charAt(i), 0) + 1);
        }

        int left = 0, right = 0;
        List<Integer> result = new ArrayList<>();
        while (right < s.length()) {
            Character tail = s.charAt(right);
            window.put(tail, window.getOrDefault(tail, 0) + 1);
            if (need.equals(window)) {
                result.add(left);
            }

            // 窗口中子串长度等于目标字符串时,开始移动左指针
            if (right + 1>= p.length()) {
                Character head = s.charAt(left);
                window.put(head, window.get(head) - 1);
                if (window.get(head).equals(0)) {
                    window.remove(head);
                }

                left++;
            }
            right++;
        }
        return result;
    }
}

2.7. Leetcode 487. 最大连续1的个数 II

2.7.1. 题目

487. 最大连续1的个数 II

给定一个二进制数组 nums ,如果最多可以翻转一个 0 ,则返回数组中连续 1 的最大个数。

示例 1:

输入:nums = [1,0,1,1,0]
输出:4
解释:翻转第一个 0 可以得到最长的连续 1。
当翻转以后,最大连续 1 的个数为 4。

进阶:如果输入的数字是作为 无限流 逐个输入如何处理?换句话说,内存不能存储下所有从流中输入的数字。您可以有效地解决吗?

2.7.2. 解题思路

2.7.2.1. 方法一:滑动窗口

题目可以等价于求解至多包含一个 \(0\) 的最长连续子序列的长度。

因此,我们可以使用滑动窗口求解,使用一个变量 \(count\) 记录窗口中的 \(0\) 出现的次数。

2.7.2.2. 方法二:动态规划

\(dp[i][0]\) 表示以 \(nums[i]\) 结尾,且未使用翻转操作,序列 \(nums[0 \cdots i]\) 中最大连续 \(1\) 的长度;

\(dp[i][1]\) 表示以 \(nums[i]\) 结尾,且使用了一次翻转操作,序列 \(nums[0 \cdots i]\) 中最大连续 \(1\) 的长度。

2.7.2.2.1. 边界条件
  • 对于 \(dp[0][0]\),若 \(nums[0] = 0\)\(dp[0][0] = 0\);若 \(nums[0] = 1\)\(dp[0][0] = 1\)

  • 对于 \(dp[0][1]\),不管 \(nums[0]\) 的值为 \(0\) 还是 \(1\),经过一次翻转一定能得到序列 \([1]\),因此,\(dp[0][1] = 1\)

因此,边界条件为:

\[\begin{aligned} dp[0][0] &= nums[0] \\ dp[0][1] &= 1 \end{aligned} \]

2.7.2.2.2. 状态转移

\(i \ge 1\) 时,有

  • 对于 \(dp[i][0]\),由于不能翻转子数组 \(nums[0 \cdots i]\) 中的任何 \(0\),因此

    • \(nums[i] = 0\) 时,\(dp[i][0] = 0\)

    • \(nums[i] = 1\) 时,\(dp[i][0] = dp[i - 1][0] + 1\)

    所以,当 \(i \ge 1\) 时,\(dp[i][0]\) 的状态转移方程为

    \[dp[i][0]= \begin{cases} 0, &nums[i] = 0\\ dp[i - 1][0] + 1,&nums[i] = 1 \end{cases} \]

  • 对于 \(dp[i][1]\),至多可以翻转子数组 \(nums[0 \cdots i]\) 中的一个 \(0\),因此

    • \(nums[i] = 0\) 时,需要使用一次翻转机会,因此,\(dp[i][1] = dp[i - 1][0] + 1\)

    • \(nums[i] = 1\) 时,不需要使用一次翻转机会,因此,\(dp[i][1] = dp[i - 1][1] + 1\)

    所以,当 \(i \ge 1\) 时,\(dp[i][1]\) 状态转移方程为

    \[dp[i][1]= \begin{cases} dp[i - 1][0] + 1, &nums[i] = 0\\ dp[i - 1][1] + 1, &nums[i] = 1 \end{cases} \]

因此,综上所述,状态转移方程为:

\[\begin{cases} dp[i][0]= 0, &nums[i] = 0\\ dp[i][1]=dp[i - 1][0] + 1, &nums[i] = 0 \end{cases} \]

\[\begin{cases} dp[i][0]=dp[i - 1][0] + 1,&nums[i] = 1\\ dp[i][1]=dp[i - 1][1] + 1, &nums[i] = 1 \end{cases} \]

可以看出,后项仅与前项相关,因此,可以通过压缩状态的方式,将状态转移方程压缩为:

\[\begin{cases} dp[0]= 0, &nums[i] = 0\\ dp[1]=dp[0] + 1, &nums[i] = 0 \end{cases} \]

\[\begin{cases} dp[0]=dp[0] + 1,&nums[i] = 1\\ dp[1]=dp[1]+ 1, &nums[i] = 1 \end{cases} \]

因此,我们就可以通过压缩状态后的状态转移方程,求解题目的进阶问题。

2.7.3. 代码实现

2.7.3.1. 方法一:滑动窗口

class Solution {
    private static int MAX = 1;

    public int findMaxConsecutiveOnes(int[] nums) {
        int left = 0, right = 0;
        int count = 0;
        int maxSize = 0;
        while (right < nums.length) {
            if (nums[right] == 0) {
                count++;
            }

            while (count > MAX) {
                if (nums[left] == 0) {
                    count--;
                }
                left++;
            }
            maxSize = Math.max(maxSize, right - left + 1);
            right++;
        }
        return maxSize;
    }
}

复杂度:

  • 时间复杂度:\(O(n)\)

  • 空间复杂度:\(O(1)\)

2.7.3.2. 方法二:动态规划

class Solution {
    public int findMaxConsecutiveOnes(int[] nums) {
        int[] dp = new int[2];
        dp[0] = nums[0];
        dp[1] = 1;
        int maxSize = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] == 0) {
                // 由于dp[1]依赖dp[0],所以,需要先更新dp[1]
                dp[1] = dp[0] + 1;
                dp[0] = 0;
            } else if (nums[i] == 1) {
                dp[0] = dp[0] + 1;
                dp[1] = dp[1] + 1;
            }
            maxSize = Math.max(maxSize, dp[1]);
        }
        return maxSize;
    }
}

复杂度:

  • 时间复杂度:\(O(n)\)

  • 空间复杂度:\(O(1)\)

2.7.4. 扩展

类似的题目有:

2.8. Leetcode 1208. 尽可能使字符串相等

2.8.1. 题目

1208. 尽可能使字符串相等

给你两个长度相同的字符串,s 和 t。
将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。
用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。
如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。
如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。

示例 1:

输入:s = "abcd", t = "bcdf", maxCost = 3
输出:3
解释:s 中的 "abc" 可以变为 "bcd"。开销为 3,所以最大长度为 3。

2.8.2. 解题思路

这里,我们可以先对两个字符串预处理一下,使用数组 \(diff\) 记录两个字符的每一个位置的字符的差值。然后,再使用滑动窗口的思路,计算窗口中的字符差值之和,只要满足条件就更新结果。

2.8.3. 代码实现

class Solution {
    public int equalSubstring(String s, String t, int maxCost) {
        int n = s.length();
        int[] diff = new int[n];
        for (int i = 0; i < n; i++) {
            diff[i] = Math.abs((int) s.charAt(i) - t.charAt(i));
        }

        int left = 0, right = 0;
        int maxSize = 0;
        int cost = 0;
        while (right < n) {
            cost += diff[right];
            while (cost > maxCost) {
                cost -= diff[left];
                left++;
            }
            maxSize = Math.max(maxSize, right - left + 1);
            right++;
        }
        return maxSize;
    }
}

2.9. Leetcode 1052. 爱生气的书店老板

2.9.1. 题目

1052. 爱生气的书店老板

有一个书店老板,他的书店开了 n 分钟。每分钟都有一些顾客进入这家商店。给定一个长度为 n 的整数数组 customers ,其中 customers[i] 是在第 i 分钟开始时进入商店的顾客数量,所有这些顾客在第 i 分钟结束后离开。
在某些时候,书店老板会生气。 如果书店老板在第 i 分钟生气,那么 grumpy[i] = 1,否则 grumpy[i] = 0。
当书店老板生气时,那一分钟的顾客就会不满意,若老板不生气则顾客是满意的。
书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 minutes 分钟不生气,但却只能使用一次。
请你返回 这一天营业下来,最多有多少客户能够感到满意 。

示例 1:

输入:customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], minutes = 3
输出:16
解释:书店老板在最后 3 分钟保持冷静。
感到满意的最大客户数量 = 1 + 1 + 1 + 1 + 7 + 5 = 16.

2.9.2. 解题思路

这里,我们可以分两部分统计:我们先将所有老板不生气的时刻,所对应的顾客数量统计出来,然后,再使用滑动窗口计算生气的时间段内顾客的数量,将两者相加,就是最后的答案。

2.9.3. 代码实现

class Solution {
    public int maxSatisfied(int[] customers, int[] grumpy, int minutes) {
        int n = customers.length;
        int result = 0;
        // 先统计不生气的时刻,顾客的数量
        for (int i = 0; i < n; i++) {
            if (grumpy[i] == 0) {
                result += customers[i];
            }
        }
        // 再统计生气的时刻,顾客的数量
        int left = 0, right = 0;
        // 记录窗口中生气时刻的顾客数量
        int count = 0;
        int maxCount = 0;
        while (right < n) {
            // 只统计窗口内生气的时刻所对应的顾客数量
            if (grumpy[right] == 1) {
                count += customers[right];
            }
            // 记录窗口内生气时,对应的最大顾客数量
            if (right - left + 1 == minutes) {
                maxCount = Math.max(maxCount, count);
            }

            while (right - left + 1 >= minutes) {
                // 左指针移动的时候,就将生气时刻的顾客移出窗口
                if (grumpy[left] == 1) {
                    count -= customers[left];
                }
                left++;
            }
            right++;
        }
        // 生气时刻对应的顾客 + 不生气时刻对应的顾客
        return result + maxCount;
    }
}

2.10. Leetcode 1423. 可获得的最大点数

2.10.1. 题目

1423. 可获得的最大点数

几张卡牌 排成一行,每张卡牌都有一个对应的点数。点数由整数数组 cardPoints 给出。
每次行动,你可以从行的开头或者末尾拿一张卡牌,最终你必须正好拿 k 张卡牌。
你的点数就是你拿到手中的所有卡牌的点数之和。
给你一个整数数组 cardPoints 和整数 k,请你返回可以获得的最大点数。

示例 1:

输入:cardPoints = [1,2,3,4,5,6,1], k = 3
输出:12
解释:第一次行动,不管拿哪张牌,你的点数总是 1 。但是,先拿最右边的卡牌将会最大化你的可获得点数。最优策略是拿右边的三张牌,最终点数为 1 + 6 + 5 = 12 。

2.10.2. 解题思路

比较直观的思路是使用动态规划的思路求解,进行枚举所有的子问题,但是动态规划的复杂度太高,会导致用例超时。

这里我们可以考虑逆向思考,假设数组 \(cardPoints\) 的长度为 \(n\),由于只能从开头和末尾取 \(k\) 张卡牌,所以,最后必然剩下连续的 \(n - k\) 张卡牌。

这样,就可以将原始问题等价求通过抽取卡牌,使得剩余点数之和最小的方式。因此,我们就可以使用滑动窗口求连续子数组的最小和。

2.10.3. 代码实现

class Solution {
    public int maxScore(int[] cardPoints, int k) {
        int n = cardPoints.length;
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            sum[i] = sum[i - 1] + cardPoints[i - 1];
        }
        int windowSize = n - k;
        int minSum = Integer.MAX_VALUE;
        for (int i = windowSize; i <= n; i++) {
            minSum = Math.min(minSum, sum[i] - sum[i - windowSize]);
        }
        return sum[n] - minSum;
    }
}

2.11. Leetcode 1151. 最少交换次数来组合所有的 1

2.11.1. 题目

1151. 最少交换次数来组合所有的 1

给出一个二进制数组 data,你需要通过交换位置,将数组中 任何位置 上的 1 组合到一起,并返回所有可能中所需 最少的交换次数。

示例 1:

输入: data = [1,0,1,0,1]
输出: 1
解释:
有三种可能的方法可以把所有的 1 组合在一起:
[1,1,1,0,0],交换 1 次;
[0,1,1,1,0],交换 2 次;
[0,0,1,1,1],交换 1 次。
所以最少的交换次数为 1。

2.11.2. 解题思路

我们可以先计算出数组 \(data\) 中数字 \(1\) 的个数,记为 \(count\),那么,通过交换后,全为 \(1\) 的子数组长度就是 \(count\)

那么,我们可以将该题等价于,寻找一个长度为 \(count\) 的连续子数组,并且,它包含的数字 \(0\) 最少,这样的子数组就是交换次数最少的目标子数组。

而求解一个包含数字 \(0\) 最少,且长度为 \(count\) 的子数组,我们就可以使用滑动窗口的思路求解。

2.11.3. 代码实现

class Solution {
    public int minSwaps(int[] data) {
        int n = data.length;
        int count = 0; // 数组中数字1的个数
        for (int num : data) {
            if (num == 1) {
                count++;
            }
        }

        int left = 0, right = 0;
        int result = count;
        int window = 0; // 窗口里面数字0的个数
        while (right < n) {
            if (data[right] == 0) {
                window++;
            }

            if (right + 1 - left == count) {
                result = Math.min(result, window);
            }

            if (right + 1 >= count) {
                if (data[left] == 0) {
                    window--;
                }
                left++;
            }
            right++;
        }
        return result;
    }
}

2.12. Leetcode 718. 最长重复子数组

2.12.1. 题目

718. 最长重复子数组

给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。

示例 1:

输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。

2.12.2. 解题思路

注意,这里的子数组是连续的子数组。

2.12.2.1. 方法一:动态规划(最长公共子串)

这里直接按照求两个字符串的最长公共子串的思路求解即可。

这里,我们假设数组 \(num1\) 和数组 \(num2\) 的长度分别为 \(m\)\(n\)

同时,假设 \(dp[i][j]\) 表示子数组 \(num1[0 \cdots i - 1]\) 和子数组 \(num2[0 \cdots j - 1]\) 的最长公共前缀。

  • 初始条件

    显然,当其中一个数组长度为零时,它们的公共前缀为零,因此,初始条件为:

\[\begin{aligned} dp[i][0] &= 0, \quad 0 \le i \le m\\ dp[0][j] &= 0, \quad 0 \le j \le n \end{aligned} \]

  • 状态转移

    当字符长度大于零时,我们可以考虑枚举其中一个数组的前缀,这里假设枚举前缀 \(num1[0 \cdots i - 1]\),那么,此时,我们只需要枚举另一个数组的前缀即可,只要某一个字符不相等,下一个位置的前缀就要从 \(0\) 开始计算。

    即对于子数组 \(num1[0 \cdots i - 1]\) 和子数组 \(num2[0 \cdots j - 1]\) 的最后一个数字,有:

    • 如果 \(num1[i - 1] = num2[j - 1]\),那么,它们显然可以构成更长的数组前缀;

    • 如果 \(num1[i - 1] \ne num2[j - 1]\),那么,它们的最长公共前缀就为零。

    因此,状态转移方程为:

\[dp[i][j]= \begin{cases} dp[i - 1][j - 1] + 1, &num1[i - 1] = num2[j - 1],1 \le i \le m, 1 \le i \le n\\ 0, & num1[i - 1] \ne num2[j - 1],1 \le i \le m, 1 \le i \le n \end{cases} \]

  • 复杂度

    时间复杂度: \(O(n \times m)\)

    空间复杂度: \(O(n \times m)\)

2.12.2.2. 方法二:滑动窗口

我们考虑使用滑动窗口的思路求解,我们以其中一个数组为基准,枚举它的起始位置,并求解它与另一个数组的最长公共前缀。

  • 复杂度

    时间复杂度: \(O((m + n) \times min(m, n))\)

    空间复杂度: \(O(1)\)

2.12.3. 代码实现

  • 方法一
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int m = nums1.length, n = nums2.length;
        int[][] dp = new int[m + 1][n + 1];
        int result = 0;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = 0;
                }
                result = Math.max(result, dp[i][j]);
            }
        }
        return result;
    }
}
  • 方法二
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
            int n = nums1.length, m = nums2.length;
            int result = 0;
            for (int i = 0; i < n; i++) {
                int length = Math.min(m, n - i);
                int maxLength = getMaxLength(nums1, nums2, i, 0, length);
                result = Math.max(result, maxLength);
            }

            for (int i = 0; i < m; i++) {
                int length = Math.min(n, m - i);
                int maxLength = getMaxLength(nums1, nums2, 0, i, length);
                result = Math.max(result, maxLength);
            }
            return result;
        }

    private int getMaxLength(int[] a, int[] b, int i, int j, int length) {
        int result = 0, count = 0;
        for (int k = 0; k < length; k++) {
            if (a[i + k] == b[j + k]) {
                count++;
            } else {
                count = 0;
            }
            result = Math.max(result, count);
        }
        return result;
    }
}

2.13. Leetcode 220. 存在重复元素 III

2.13.1. 题目

220. 存在重复元素 III

给你一个整数数组 nums 和两个整数 indexDiff 和 valueDiff 。
找出满足下述条件的下标对 (i, j):

  • i != j,
  • abs(i - j) <= indexDiff
  • abs(nums[i] - nums[j]) <= valueDiff
    如果存在,返回 true ;否则,返回 false 。

示例 1:

输入:nums = [1,2,3,1], indexDiff = 3, valueDiff = 0
输出:true
解释:可以找出 (i, j) = (0, 3) 。
满足下述 3 个条件:

  • i != j --> 0 != 3
  • abs(i - j) <= indexDiff --> abs(0 - 3) <= 3
  • abs(nums[i] - nums[j]) <= valueDiff --> abs(1 - 1) <= 0

提示:

  • \(2 <= nums.length <= 10^5\)
  • \(-10^9 <= nums[i] <= 10^9\)
  • \(1 <= indexDiff <= nums.length\)
  • \(0 <= valueDiff <= 10^9\)

2.13.2. 解题思路

可以看出,题目中的数组元素数量级范围在十万级别,如果直接使用暴力求解,复杂度为 \(O(n^2)\)

这里,我们可以考虑使用滑动窗口的思路求解,并且使用一个能保持窗口中的数据有序的有序集合维护窗口中的数据,这样可以使复杂度降低。

在 Java 中,可以使用 TreeSet 维护有序集合,TreeSet 是通过 TreeMap 实现的数据结构,TreeMap 是基于红黑树实现的,天然支持排序,默认情况下会基于 Key 值自然顺序排序,查找、插入、删除的复杂度都是 \(O(logn)\)

因此,我们可以使用一个 \(TreeSet\) 数据结构维护窗口中的数据,比较直观的思路是,遍历数组 \(nums\) 中的所有元素,同时,维护一个长度不大于 \(indexDiff\) 的窗口,对于数组中的任意一个元素 \(nums[i]\),找到窗口中最接近 \(nums[i]\) 的元素,判断其差值的是否小于 \(valueDiff\),即

  • 找到窗口内大于等于 \(nums[i]\) 的最小元素 \(right\),如果它存在,并且 \(right - nums[i] \le valueDiff\),则返回 \(true\)

  • 找到窗口内小于等于 \(nums[i]\) 的最大元素 \(left\),如果它存在,并且 \(nums[i] - left \le valueDiff\),则返回 \(true\)

2.13.3. 代码实现

class Solution {
    public boolean containsNearbyAlmostDuplicate(int[] nums, int indexDiff, int valueDiff) {
        int n = nums.length;
        TreeSet<Long> window = new TreeSet<>();
        for (int i = 0; i < n; i++) {
            // 返回在集合中大于或者等于给定元素的最小元素,如果不存在这样的元素,返回null
            Long right = window.ceiling((long) nums[i]);
            if (right != null && right - nums[i] <= valueDiff) {
                return true;
            }
            // 返回在集合中小于或者等于给定元素的最大元素,如果不存在这样的元素,返回null
            Long left = window.floor((long) nums[i]);
            if (left != null && nums[i] - left <= valueDiff) {
                return true;
            }

            window.add((long)nums[i]);
            if (i >= indexDiff) {
                window.remove((long) nums[i - indexDiff]);
            }
        }
        return false;
    }
}

复杂度:

  • 时间复杂度:\(O(n \times log(min(n, k)))\)

  • 空间复杂度:\(O(min(n, k))\)

2.14. Leetcode 713. 乘积小于 K 的子数组

2.14.1. 题目

713. 乘积小于 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你返回子数组内所有元素的乘积严格小于 k 的连续子数组的数目。

示例 1:

输入:nums = [10,5,2,6], k = 100
输出:8
解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2],、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。

2.14.2. 解题思路

我们使用一个整形变量 \(window\) 来记录窗口中所有元素的乘积,当窗口内的所有元素之积大于 \(k\),就移除左侧的元素。

我们使用 \(count\) 来记录满足条件的窗口中的子数组个数,对于窗口最右侧的元素 \(nums[i]\),如果窗口内的元素之积小于 \(k\),那么,窗口内所有以 \(nums[i]\) 结尾的连续子数组都是符合条件的子数组,这样的连续子数组个数恰好等于窗口的长度

这里,我们以用例 \(nums = [10,5,2,6]\), \(k = 100\) 为例,来介绍求解过程:

  • 初始状态,窗口中的元素为空,即 \(window = []\)

  • 窗口扩大,移入一个元素 \(10\),此时,窗口内的元素为 \(window = [10]\)

    此时,窗口内的元素之积满足条件,此时的子数组长度为 \(1\),因此,记录 \(count = 1\);

  • 窗口继续扩大,移入一个元素 \(5\),此时,窗口内的元素为 \(window = [10,5]\)

    此时,窗口内的元素之积也满足条件,此时的子数组长度为 \(2\),因此,记录 \(count = 1 + 2 = 3\);

  • 窗口继续扩大,移入一个元素 \(2\),此时,窗口内的元素为 \(window = [10,5,2]\)

    • 这时,窗口内的元素之积大于 \(100\),移出窗口最左侧的元素 \(10\),此时,窗口内的元素为 \(window = [5,2]\)

      移出元素后,窗口内的元素之积满足条件,此时的子数组长度为 \(2\),因此,记录 \(count = 3 + 2 = 5\);

  • 窗口继续扩大,移入一个元素 \(6\),此时,窗口内的元素为 \(window = [5,2,6]\)

    此时,窗口内的元素之积也满足条件,此时的子数组长度为 \(3\),因此,记录 \(count = 5 + 3 = 8\);

2.14.3. 代码实现

class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        if (k <= 1) {
            return 0;
        }
        int window = 1;
        int left = 0, right = 0;
        int count = 0;
        while (right < nums.length) {
            window *= nums[right];
            while (left <= right && window >= k) {
                window /= nums[left];
                left++;
            }
            // 窗口的长度就是以nums[i]结尾的连续子数组个数
            count += right + 1 - left;
            right++;
        }

        return count;
    }
}

参考:

posted @ 2024-01-29 17:13  LARRY1024  阅读(10)  评论(0编辑  收藏  举报