Skip to content

子数组子串

通常考虑滑窗

子数组最大和/积

53. 最大子数组和 mid

剑指 Offer 42. 连续子数组的最大和

给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

记录以当前元素为结束元素的子数组的最大和, 即可 DP

DP

java
class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        int[] sum = new int[n];
        int res = sum[0] = nums[0];
        for (int i = 1; i < n; i++) {
            sum[i] = nums[i] + Math.max(sum[i - 1], 0);
            res = Math.max(res, sum[i]);
        }
        return res;
    }
}

压缩

java
class Solution {
    public int maxSubArray(int[] nums) {
        int sum = 0;
        int res = Integer.MIN_VALUE;
        for (int e : nums) {
            sum = e + Math.max(sum, 0);
            res = Math.max(res, sum);
        }
        return res;
    }
}

分治
线段树, 可以求任意区内的最大和, 建树的过程中直接求

java
class Solution {
    static class Node {
        int lSum, rSum, max, sum;

        Node(int lSum, int rSum, int max, int sum) {
            this.lSum = lSum; // 包含左端点最大和
            this.rSum = rSum; // 包含右端点最大和
            this.max = max;   // 最大和
            this.sum = sum;   // 和
        }
    }

    public int maxSubArray(int[] nums) {
        return getSum(nums, 0, nums.length - 1).max;
    }

    public Node getSum(int[] a, int l, int r) {
        if (l == r) return new Node(a[l], a[l], a[l], a[l]);
        int m = l + (r - l >> 1);
        Node lSub = getSum(a, l, m);
        Node rSub = getSum(a, m + 1, r);
        return merge(lSub, rSub);
    }

    public Node merge(Node l, Node r) {
        int sum = l.sum + r.sum;
        int lSum = Math.max(l.lSum, l.sum + r.lSum);
        int rSum = Math.max(r.rSum, r.sum + l.rSum);
        int max = Math.max(Math.max(l.max, r.max), l.rSum + r.lSum);
        return new Node(lSum, rSum, max, sum);
    }
}

152. 乘积最大子数组 mid

给你一个整数数组 nums,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

子数组 是数组的连续子序列。

DP

与 和最大和子数组 类似, 保存以 i 为结尾的最大积和最小积
最大积要在线求 max=max(maxdp[pre]curr,mindp[pre]curr,curr)

java
class Solution {
    public int maxProduct(int[] nums) {
        int n = nums.length;
        int[] maxA = new int[n];
        int[] minA = new int[n];
        maxA[0] = nums[0];
        minA[0] = nums[0];
        int ans = maxA[0];
        for (int i = 1; i < n; ++i) {
            maxA[i] = Math.max(maxA[i - 1] * nums[i], Math.max(nums[i], minA[i - 1] * nums[i]));
            minA[i] = Math.min(minA[i - 1] * nums[i], Math.min(nums[i], maxA[i - 1] * nums[i]));
            ans = Math.max(ans, maxA[i]);
        }
        return ans;
    }
}

最小窗口

76. 最小覆盖子串 hard

LCR 017. 最小覆盖子串

给你一个字符串 s, 一个字符串 t。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""。

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

解法 滑动窗口

java
class Solution {
    public String minWindow(String s, String t) {
        int[] need = new int[128];
        for (char c : t.toCharArray()) {
            need[c]++;
        }
        // gap是需求的字符个数
        int j = 0, gap = t.length(), start = 0, size = Integer.MAX_VALUE;
        char[] arr = s.toCharArray();
        for (int i = 0; i < arr.length; i++) {
            if (need[arr[i]] > 0) gap--;
            need[arr[i]]--;
            // 缩
            if (gap == 0) {
                while (need[arr[j]] < 0) {
                    need[arr[j]]++;
                    j++;
                }
                if (i - j + 1 < size) {
                    size = i - j + 1;
                    start = j;
                }
            }
        }
        return size == Integer.MAX_VALUE ? "" : s.substring(start, start + size);
    }
}

209. 长度最小的子数组 mid

LCR 008. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

解法 滑动窗口

java
class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        int l = 0;
        int sum = 0;
        int res = n + 1;
        for (int r = 0; r < n; r++) {
            sum += nums[r];
            // 缩窗
            while (sum - nums[l] >= target) {
                sum -= nums[l++];
            }
            if (sum >= target) res = Math.min(res, r - l + 1);
        }
        return res == n + 1 ? 0 : res;
    }
}

581. 最短无序连续子数组 mid

给你一个整数数组 nums,你需要找出一个 连续子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。

请你找出符合题意的 最短 子数组,并输出它的长度。

可以暴力排序, 然后按索引比较
需要参与排序的范围, nums[+1]>max(),nums[1]<max()
就可以 从左到右找右边界, 从右到左找左边界

java
class Solution {
    public int findUnsortedSubarray(int[] nums) {
        int n = nums.length;
        int l = -1, r = -2;
        int max = nums[0];
        int min = nums[n - 1];
        for (int i = 0; i < n; i++) {
            // 从左到右, 找右边界
            if (nums[i] < max) r = i;
            else max = nums[i];
            // 从右到左, 找左边界
            if (nums[n - 1 - i] > min) l = n - 1 - i;
            else min = nums[n - 1 - i];
        }
        return r - l + 1;
    }
}

697. 数组的度 easy

给定一个非空且只包含非负数的整数数组 nums,数组的 度 的定义是指数组里任一元素出现频数的最大值。

你的任务是在 nums 中找到与 nums 拥有 相同的度 的最短连续子数组,返回其长度。

找到每个元素的度和左右范围

java
class Solution {
    public int findShortestSubArray(int[] nums) {
        Map<Integer, int[]> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int[] val = map.putIfAbsent(nums[i], new int[]{1, i, i});
            if (val != null) {
                val[0]++;
                val[2] = i;
            }
        }
        int res = 0;
        int max = 0;
        for (Map.Entry<Integer, int[]> e : map.entrySet()) {
            int[] val = e.getValue();
            if (max < val[0]) {
                max = val[0];
                res = val[2] - val[1] + 1;
            } else if (max == val[0]) {
                res = Math.min(res, val[2] - val[1] + 1);
            }
        }
        return res;
    }
}

窗口内最大值

需要一种数据结构, 维护窗口内的最大值
优先考虑单调队列

滑动窗口
开窗可以在滑动中先 while;
缩窗可以在求值前, 也可以在求值后, 如果求值后不需要写 if 就写在求值后(通常需要维护一个 left)

239. 滑动窗口最大值 hard

剑指 Offer 59 - I. 滑动窗口的最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值。

解法 1 优先队列 实时维护(超时)

java
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        PriorityQueue<Integer> que = new PriorityQueue<>((a, b) -> b - a);
        int[] res = new int[n - k + 1];
        for (int l = 0, r = 0; r < n; r++, l++) {
            while (--k > 0) {
                que.add(nums[r++]);
            }
            que.add(nums[r]);
            res[l] = que.element();
            que.remove(nums[l]);
        }
        return res;
    }
}

解法 2 有序集合

java
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        int[] res = new int[n - k + 1];
        TreeMap<Integer, Integer> map = new TreeMap<>();
        for (int l = 0, r = 0; r < n; r++, l++) {
            // 开窗
            while (--k > 0) {
                map.merge(nums[r++], 1, Integer::sum);
            }
            // 滑动
            map.merge(nums[r], 1, Integer::sum);
            // 取值
            res[l] = map.lastKey();
            // 缩窗
            if (map.get(nums[l]) == 1) map.remove(nums[l]);
            else map.merge(nums[l], -1, Integer::sum);
        }
        return res;
    }
}

解法 3 优先队列 惰性维护

java
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        PriorityQueue<int[]> que = new PriorityQueue<>((a, b) -> a[0] != b[0] ? b[0] - a[0] : b[1] - a[1]);
        int[] res = new int[n - k + 1];
        for (int r = 0; r < n; r++) {
            for (; r < k - 1; r++) {
                que.add(new int[]{nums[r], r});
            }
            que.add(new int[]{nums[r], r});
            // 堆顶元素过期才删
            while (r - que.element()[1] >= k) {
                que.remove();
            }
            res[r - k + 1] = que.element()[0];
        }
        return res;
    }
}

解法 4 单调队列

单调队列为什么可行?
单调队列非常适合处理滑窗内的极值, 对于单调减区间[s,e], 移除小于元素 e 的元素不会影响区间的最大值

java
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        int[] res = new int[n - k + 1];
        Deque<Integer> que = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            while (!que.isEmpty() && nums[que.getLast()] <= nums[i]) {
                que.removeLast();
            }
            que.addLast(i);
            // 队首已经离开窗口了
            if (i - que.getFirst() >= k) {
                que.removeFirst();
            }
            if (i >= k - 1) {
                res[i - k + 1] = nums[que.getFirst()];
            }
        }
        return res;
    }
}

其他解法 线段数 稀疏表等

能否由子串组成

139. 单词拆分 mid

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

解法 1 DP

dp[i]: s[0,i] 是否由子串构成

java
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        int n = s.length();
        Set<String> set = new HashSet<>(wordDict);
        boolean[] dp = new boolean[n + 1]; // 右侧开区间
        dp[0] = true;
        for (int i = 0; i < n; i++) { // 右端点
            for (int j = 0; j <= i; j++) { // 左端点
                if (dp[j] && set.contains(s.substring(j, i + 1))) {
                    dp[i + 1] = true;
                    break;
                }
            }
        }
        return dp[n];
    }
}

解法 2 DFS

DFS 搜到一段后递归搜下一段
可以记忆化剪枝, 记录哪些 start 不可用
substring 太慢, startWith 更好

java
class Solution {

    public boolean wordBreak(String s, List<String> wordDict) {
        boolean[] memo = new boolean[s.length() + 1];
        Arrays.fill(memo, true);
        return dfs(s, wordDict, 0, memo);
    }

    boolean dfs(String s, List<String> wordDict, int start, boolean[] memo) {
        if (start == s.length()) return true;
        if (!memo[start]) return false;
        for (String word : wordDict) {
            if (s.startsWith(word, start)) {
                if (dfs(s, wordDict, start + word.length(), memo)) {
                    return true;
                }
            }
        }
        memo[start] = false;
        return false;
    }
}

子数组数量

413. 等差数列划分 mid

如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。

  • 例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。

给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。

子数组 是数组中的一个连续序列。

解法 1 滑动窗口

找到每个最长等差数列, 如果等差数列长度为 n, 那么子数组数量为 1+...+(n-2)
n=3, cnt=1; n=4, cnt=1+2

java
class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        if (nums.length < 3) return 0;
        int res = 0;

        int gap = nums[1] - nums[0];
        int cnt = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] - nums[i - 1] == gap) {
                cnt++;
                continue;
            }
            // 计算前一段
            res += sum(cnt);
            gap = nums[i] - nums[i - 1];
            cnt = 2;
        }
        res += sum(cnt);
        return res;
    }

    private int sum(int n) {
        int sum = 0;
        for (int i = 1; i <= n - 2; i++) {
            sum += i;
        }
        return sum;
    }
}

解法 2 DP

dp[i]: 以 nums[i] 为结尾的新增数列数量

dp[2] = 1
    [0, 1, 2]

dp[3] = dp[2] + 1 = 2
    [0, 1, 2, 3], // [0, 1, 2] 之后加一个 3
    [1, 2, 3]     // 新的递增子区间

dp[4] = dp[3] + 1 = 3
    [0, 1, 2, 3, 4], // [0, 1, 2, 3] 之后加一个 4
    [1, 2, 3, 4],    // [1, 2, 3] 之后加一个 4
    [2, 3, 4]        // 新的递增子区间
java
class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        if (nums.length < 3) return 0;
        int[] dp = new int[nums.length];

        int res = 0;
        for (int i = 2; i < nums.length; i++) {
            if (nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]) {
                dp[i] = dp[i - 1] + 1;
                res += dp[i];
            }
        }
        return res;
    }
}

560. 和为 K 的子数组 mid

LCR 010. 和为 K 的子数组

给你一个整数数组 nums 和一个整数 k,请你统计并返回 该数组中和为 k 的连续子数组的个数。

解法 前缀和

迭代过程中, 只要知道前面跟 sum-target 相等的前缀和的数量, 即可O(n)

java
class Solution {
    public int subarraySum(int[] nums, int k) {
        int sum = 0;
        int res = 0;
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        for (int num : nums) {
            sum += num;
            res += map.getOrDefault(sum - k, 0);
            map.merge(sum, 1, Integer::sum);
        }
        return res;
    }
}

647. 回文子串 mid

LCR 020. 回文子串

给你一个字符串 s,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

解法 1 中心拓展

java
class Solution {
    public int countSubstrings(String s) {
        int sum = 0;
        for (int i = 0; i < s.length(); i++) {
            sum += len(s, i);
        }
        return sum;
    }

    private int len(String s, int idx) {
        int cnt = 0;
        int p1 = idx, p2 = idx;
        while (p1 >= 0 && p2 < s.length()) {
            if (s.charAt(p1--) == s.charAt(p2++)) cnt++;
            else break;
        }
        p1 = idx;
        p2 = idx + 1;
        while (p1 >= 0 && p2 < s.length()) {
            if (s.charAt(p1--) == s.charAt(p2++)) cnt++;
            else break;
        }
        return cnt;
    }
}

解法 2 manacher

java
class Solution {
    public int countSubstrings(String s) {
        if (s.isEmpty()) return 0;
        StringBuilder sb = new StringBuilder("^#");
        for (char c : s.toCharArray()) {
            sb.append(c).append("#");
        }
        sb.append("$");
        char[] arr = sb.toString().toCharArray();
        int n = arr.length;
        int[] arm = new int[n];
        int core = 0, right = 0;
        int res = s.length();
        for (int i = 1; i < n - 1; i++) { // i 是每个串的中心
            int j = 2 * core - i; // i 关于 core 的对称点
            if (i < right) arm[i] = Math.min(right - i, arm[j]); // 镜像 arm[i]
            while (arr[i + arm[i] + 1] == arr[i - arm[i] - 1]) arm[i]++;
            if (i + arm[i] > right) {
                core = i;
                right = i + arm[i];
            }
            res += arm[i] / 2;
        }
        return res;
    }
}

696. 计数二进制子串 easy

给定一个字符串 s,统计并返回具有相同数量 0 和 1 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是成组连续的。

重复出现(不同位置)的子串也要统计它们出现的次数。

解法 1 中心扩展

java
class Solution {
    public int countBinarySubstrings(String s) {
        if (s.length() < 2) return 0;

        int cnt = 0;
        for (int i = 0; i < s.length() - 1; i++) {
            int j = i;
            int k = i + 1;
            char c1 = s.charAt(j);
            char c2 = s.charAt(k);
            if (c1 == c2) continue;
            while (j >= 0 && k < s.length() && s.charAt(j) == c1 && s.charAt(k) == c2) {
                cnt++;
                j--;
                k++;
            }
        }
        return cnt;
    }
}

解法 2 计数

java
class Solution {
    public int countBinarySubstrings(String s) {
        int i = 0, n = s.length();
        int lastCnt = 0;
        int res = 0;
        while (i < n) {
            char c = s.charAt(i);
            int cnt = 0;
            while (i < n && s.charAt(i) == c) {
                i++;
                cnt++;
            }
            res += Math.min(cnt, lastCnt);
            lastCnt = cnt;
        }
        return res;
    }
}

713. 乘积小于 K 的子数组 mid

LCR 009. 乘积小于 K 的子数组

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

解法 滑窗

java
class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        if (k <= 1) return 0;
        int n = nums.length, res = 0, prod = 1, l = 0;
        for (int r = 0; r < n; r++) {
            prod *= nums[r];
            while (prod >= k) {
                prod /= nums[l++];
            }
            res += r - l + 1;
        }
        return res;
    }
}

最大窗口

525. 连续数组 mid

LCR 011. 连续数组

给定一个二进制数组 nums, 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。

解法 前缀和

求和为 0 的最长子数组
找到前缀和所对应的最早索引

java
class Solution {
    public int findMaxLength(int[] nums) {
        int n = nums.length;
        Map<Integer, Integer> map = new HashMap<>(); // sum => idx
        map.put(0, -1);
        int sum = 0;
        int res = 0;
        for (int i = 0; i < n; i++) {
            sum += nums[i] == 0 ? -1 : 1;
            Integer idx = map.get(sum);
            if (idx != null) res = Math.max(res, i - idx);
            else map.put(sum, i);
        }
        return res;
    }
}

窗口中的元素

567. 字符串的排列 mid

LCR 014. 字符串的排列

给你两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true;否则,返回 false。

换句话说,s1 的排列之一是 s2 的 子串。

解法 1 固定窗口滑窗

在 s2 中是否存在大小为 s1 的窗口里面含有 s1 所有的字符

java
class Solution {
    public boolean checkInclusion(String s1, String s2) {
        if (s1.length() > s2.length()) return false;
        int[] a1 = new int[26];
        int[] a2 = new int[26];
        for (int i = 0; i < s1.length(); i++) {
            ++a1[s1.charAt(i) - 'a'];
            ++a2[s2.charAt(i) - 'a'];
        }
        for (int l = 0, r = s1.length() - 1; r < s2.length() - 1; r++, l++) {
            if (Arrays.equals(a1, a2)) return true;
            a2[s2.charAt(l) - 'a']--;
            a2[s2.charAt(r + 1) - 'a']++;
        }
        return Arrays.equals(a1, a2);
    }
}

解法 2 缩小窗口滑窗

java
class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int n = s1.length(), m = s2.length();
        if (n > m) return false;
        int[] arr = new int[26];
        for (int i = 0; i < n; i++) {
            arr[s1.charAt(i) - 'a']--;
        }

        for (int l = 0, r = 0; r < m; r++) {
            int c = s2.charAt(r) - 'a';
            arr[c]++;
            while (arr[c] > 0) {
                // 集合相等, 元素必须严丝合缝
                arr[s2.charAt(l) - 'a']--;
                l++;
            }
            if (r - l + 1 == n) return true;
        }
        return false;
    }
}

划分

763. 划分字母区间 mid

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

解法 贪心

固定左端点, 更新右端点

java
class Solution {
    public List<Integer> partitionLabels(String s) {
        char[] arr = s.toCharArray();
        int[] idxs = new int[26];
        Arrays.fill(idxs, -1);
        for (int i = 0; i < arr.length; i++) {
            idxs[arr[i] - 'a'] = i;
        }
        List<Integer> res = new ArrayList<>();
        int r = 0, l = 0;
        for (int i = 0; i < arr.length; i++) {
            r = Math.max(r, idxs[arr[i] - 'a']);
            if (r == i) {
                res.add(r - l + 1);
                l = i + 1;
            }
        }
        return res;
    }
}

769. 最多能完成排序的块 mid

给定一个长度为 n 的整数数组 arr ,它表示在 [0, n - 1] 范围内的整数的排列。

我们将 arr 分割成若干 块 (即分区),并对每个块单独排序。将它们连接起来后,使得连接的结果和按升序排序后的原数组相同。

返回数组能分成的最多块数量。

注意到值的范围
[0,k] 的最大值应在 k 位置, 只要 max==k, 说明值的分布是连续的, 就可以分割

java
class Solution {
    public int maxChunksToSorted(int[] arr) {
        int res = 0, max = 0;
        for (int i = 0; i < arr.length; i++) {
            max = Math.max(max, arr[i]);
            if (max == i) res++;
        }
        return res;
    }
}