leetcode
leetcode 2701 ~ 2750
将数组分成三个子数组的方案数

将数组分成三个子数组的方案数

难度:

标签:

题目描述

A split of an integer array is good if:

  • The array is split into three non-empty contiguous subarrays - named left, mid, right respectively from left to right.
  • The sum of the elements in left is less than or equal to the sum of the elements in mid, and the sum of the elements in mid is less than or equal to the sum of the elements in right.

Given nums, an array of non-negative integers, return the number of good ways to split nums. As the number may be too large, return it modulo 109 + 7.

 

Example 1:

Input: nums = [1,1,1]
Output: 1
Explanation: The only good way to split nums is [1] [1] [1].

Example 2:

Input: nums = [1,2,2,2,5,0]
Output: 3
Explanation: There are three good ways of splitting nums:
[1] [2] [2,2,5,0]
[1] [2,2] [2,5,0]
[1,2] [2,2] [5,0]

Example 3:

Input: nums = [3,2,1]
Output: 0
Explanation: There is no good way to split nums.

 

Constraints:

  • 3 <= nums.length <= 105
  • 0 <= nums[i] <= 104

代码结果

运行时间: 124 ms, 内存: 27.2 MB


/*
 * Problem description:
 * We call a split of an integer array good if:
 * - The array is split into three non-empty continuous subarrays named left, mid, and right from left to right.
 * - The sum of elements in left is less than or equal to the sum of elements in mid, and the sum of elements in mid is less than or equal to the sum of elements in right.
 * Given a non-negative integer array nums, return the number of good ways to split nums. Since the answer may be large, return it modulo 10^9 + 7.
 */

import java.util.*;
import java.util.stream.*;

public class GoodSplitsStream {
    public int waysToSplit(int[] nums) {
        int MOD = 1_000_000_007;
        int n = nums.length;
        int[] prefixSum = new int[n + 1];
        for (int i = 0; i < n; i++) {
            prefixSum[i + 1] = prefixSum[i] + nums[i];
        }
        return IntStream.range(1, n).parallel().map(i -> {
            int leftSum = prefixSum[i];
            int left = binarySearch(prefixSum, i + 1, n, leftSum * 2);
            int right = binarySearch(prefixSum, i + 1, n, leftSum + (prefixSum[n] - leftSum) / 2 + 1);
            return right - left;
        }).reduce(0, (a, b) -> (a + b) % MOD);
    }

    private int binarySearch(int[] prefixSum, int low, int high, int target) {
        while (low < high) {
            int mid = (low + high) / 2;
            if (prefixSum[mid] < target) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }
}

解释

方法:

本题解采用前缀和和双指针策略来解决问题。首先计算数组的总和total,然后遍历数组以确定合适的right分界点。对于每一个right,使用两个指针left_min和left_max来确定可能的左区间的范围。left_min是使得left区间的和最小但仍满足left区间的和小于等于mid区间的和的位置,left_max则是left区间和可以达到的最大位置,同时满足left区间的和小于等于mid区间的和的条件。如果left_min到left_max之间有合法的位置,那么这些位置都可以作为合法的分割方案。最后,所有满足条件的分割方法的数量需要对109+7取模。

时间复杂度:

O(n)

空间复杂度:

O(1)

代码细节讲解

🦆
在确定right分界点的循环中,为什么选择`if 3 * sum_right > 2 * total`作为终止条件?这个条件是如何保证不遗漏任何可能的分割方案的?
这个条件是基于数组总和分配的逻辑。在分割数组时,为了确保每个子数组至少有元素,每个子数组的和需要满足一定的条件。特别是,当我们考虑到第三个子数组(即从right到数组末尾的部分)时,这部分的和至少应该不小于第二个子数组的和。如果`3 * sum_right > 2 * total`,这意味着sum_right的三倍已经超过了total的两倍,从而使得第三部分的和超过了前两部分的和的一半,这使得不可能再均等地将剩余部分分配给第二和第三个子数组。因此,这个条件作为循环的终止条件,可以有效地避免不必要的计算,同时确保不遗漏可能的有效分割方案。
🦆
在调整left_min和left_max时,你是如何保证在每一步迭代中都满足left区间和小于等于mid区间和的条件的?
在算法中,left_min和left_max的调整是通过两个循环来实现的,确保了每次调整后left区间的和都满足与中间区间和的比较条件。具体而言,对于left_min的调整,通过增加left_min的索引直到`sum_left_min >= 2 * sum_right - total`,这保证了left区间的和不会过大,从而满足left区间和小于等于mid区间和的条件。对于left_max的调整,则是增加left_max的索引,直到`2 * sum_left_max <= sum_right`,确保left区间的和始终小于或等于mid区间的和的一半。这两个循环的控制条件都是精心设计的,确保了在每一步迭代中都不会违反left区间和小于等于mid区间和的原则。
🦆
为什么在进行left_max的调整时使用的条件是`2 * sum_left_max <= sum_right`,而不是直接比较sum_left_max和sum_right的大小?
使用`2 * sum_left_max <= sum_right`作为条件,是为了确保left区间的和是mid区间和的一半或更少。这是因为,为了满足题目的要求,确保每个分割后的三个子数组的和都应该相近,左边区间的和不应该大于中间区间。如果使用sum_left_max直接和sum_right比较,可能会导致left区间的和超过mid区间的和,从而不满足题目条件。而`2 * sum_left_max <= sum_right`这个条件则是一个更为严格的限制,可以更安全地保证每个子数组的和的平衡。

相关问题