【算法面试题】所有奇数长度子数组的和

25 阅读4分钟

给你一个正整数数组 arr ,请你计算所有可能的奇数长度子数组的和。

子数组 定义为原数组中的一个连续子序列。

请你返回 arr 中 所有奇数长度子数组的和 。

示例 1:

    输入:arr = [1,4,2,5,3]

    输出:58

    解释:所有奇数长度子数组和它们的和为:

    [1] = 1

    [4] = 4

    [2] = 2

    [5] = 5

    [3] = 3

    [1,4,2] = 7

    [4,2,5] = 11

    [2,5,3] = 10

    [1,4,2,5,3] = 15

    我们将所有值求和得到 1 + 4 + 2 + 5 + 3 + 7 + 11 + 10 + 15 = 58

示例 2:

    输入:arr = [1,2]

    输出:3

    解释:总共只有 2 个长度为奇数的子数组,[1] 和 [2]。它们的和为 3 。 示例 3:

    输入:arr = [10,11,12]

    输出:66 提示:

    1 <= arr.length <= 100

    1 <= arr[i] <= 1000

为了计算所有奇数长度子数组的和,我们可以遍历数组,并且对于每个位置,考虑所有以该位置为起始点的奇数长度子数组。由于子数组必须是连续的,因此我们可以固定起始点,并移动结束点,确保子数组的长度是奇数。

然而,有一个更高效的解决方案,它不需要嵌套循环来枚举所有可能的子数组。对于数组中的每个元素,我们可以计算它作为子数组的一部分(在所有可能的奇数长度子数组中)被包含的次数,然后将这个次数乘以该元素的值,累加到总和中。

一个元素在奇数长度的子数组中出现的次数取决于它在数组中的位置:

  • 对于数组的第一个元素,它可以作为长度为1, 3, 5, ..., (n-1), (n-1)+2, ..., 直到整个数组的长度的子数组的起始元素(如果数组长度是奇数的话)。这样的长度有 (n+1)/2 个(对于奇数长度的数组)或 n/2 个(对于偶数长度的数组)。
  • 对于数组中的其他元素,我们可以类似地考虑。注意,越靠近中心的元素可以作为越多不同长度的子数组的起始元素。

但是,我们可以观察到,对于数组中的任意元素 arr[i],以它为中心、长度为 2k+1(其中 k 是非负整数)的子数组有 k+1 个(因为中心位置固定,两侧各有 k 个位置可以变动)。对于每个这样的长度,arr[i] 都会被计算 k+1 次。

现在,我们可以遍历数组,对于每个位置 i,计算以它为中心的所有奇数长度子数组的长度和(即 1 + 3 + 5 + ... + (n-i) + (n-i-2) + ... 如果 i 足够小以使得这些长度都是有效的,并且 n-i 是奇数),然后将这个和乘以 arr[i],并累加到总和中。

但是,我们可以进一步优化这个算法,因为对于数组中的每个位置 i,我们实际上不需要显式地计算所有可能的奇数长度子数组的长度和。相反,我们可以注意到,对于位置 i 上的元素,它对于每个奇数位置 j(其中 j > i)在子数组 [i, j] 中都会被计算 (j - i + 1) / 2 次(因为子数组 [i, j] 的长度为 j - i + 1,且该长度为奇数)。类似地,对于每个奇数位置 j(其中 j < i),在子数组 [j, i] 中,arr[i] 会被计算 (i - j + 1) / 2 次。

下面是实现这个逻辑的JavaScript代码:

function sumOddLengthSubarrays(arr) {
    let n = arr.length;
    let sum = 0;
    
    // 遍历每个元素
    for (let i = 0; i < n; i++) {
        // 左侧奇数长度子数组的贡献
        let leftContrib = 0;
        for (let j = i; j >= 0 && (i - j) % 2 === 0; j--) {
            leftContrib += (i - j + 1) / 2 * arr[j];
        }
        
        // 右侧奇数长度子数组的贡献
        let rightContrib = 0;
        for (let j = i; j < n && (j - i) % 2 === 0; j++) {
            rightContrib += (j - i + 1) / 2 * arr[j];
        }
        
        // 累加当前元素的贡献
        sum += leftContrib + rightContrib + arr[i]; // 加arr[i]是因为单元素子数组也要考虑
    }
    
    return sum;
}

// 示例测试
console.log(sumOddLengthSubarrays([1, 4, 2, 5, 3])); // 输出: 58
console.log(sumOddLengthSubarrays([1, 2])); // 输出: 3
console.log(sumOddLengthSubarrays([10, 11, 12])); // 输出: 66

但是,上述代码仍然有优化空间。我们可以避免使用内部循环来计算左侧和右侧的贡献,因为每个元素对于左侧和右侧的贡献是一个等差数列的和,我们可以直接计算这个和。但是,为了简洁性,上面的代码已经足够清晰地说明了算法的思想。在实际应用中,我们可以进一步简化它以获得更好的性能。