Java&C++题解与拓展——leetcode907.子数组的最小值之和【单调栈+乘法原理】

89 阅读2分钟
每日一题做题记录,参考官方和三叶的题解

题目要求

image.png

思路:单调栈+乘法原理

  • 根据示例可以发现直接统计每个arr[i]arr[i]对答案的贡献会比较快,也就是找arr[i]arr[i]在哪些区间里为最小值,这就很熟悉了,用单调栈找保证arr[i]arr[i]最小的左右边界l,rl,r,然后用乘法原理计算其在[l,r][l,r]内的子数组数量;
  • 左右边界找法:
    • l[i]l[i]ii左边第一个不大于arr[i]arr[i]的下标;
    • r[i]r[i]ii右边第一个不小于arr[i]arr[i]的下标;
    • 为避免重复统计,采用不大于不小于的判别条件。

Java

class Solution {
    int MOD = (int)1e9 + 7;
    public int sumSubarrayMins(int[] arr) {
        int n = arr.length, res = 0;
        // 边界
        int[] l = new int[n], r = new int[n];
        Arrays.fill(l, -1);
        Arrays.fill(r, n);
        Deque<Integer> sta = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            while (!sta.isEmpty() && arr[sta.peekLast()] >= arr[i])
                r[sta.pollLast()] = i;
            sta.addLast(i);
        }
        sta.clear();
        for (int i = n - 1; i >= 0; i--) {
            while (!sta.isEmpty() && arr[sta.peekLast()] > arr[i])
                l[sta.pollLast()] = i;
            sta.addLast(i);
        }
        // 乘法原理
        for (int i = 0; i < n; i++) {
            int left = i - l[i], right = r[i] - i;
            res += left * 1L * right % MOD * arr[i] % MOD;
            res %= MOD;
        }
        return res;
    }
}
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

C++

  • stack不能直接clear()所以用两个栈;
  • 忘了memset()的初始化限制,错了半天……
class Solution {
public:
    long long MOD = (int)1e9 + 7;
    int sumSubarrayMins(vector<int>& arr) {
        int n = arr.size();
        long long res = 0;
        // 边界
        int l[n], r[n];
        memset(l, -1, sizeof(l));
        stack<int> lsta,rsta; // 不能clear
        for (int i = 0; i < n; i++) {
            r[i] = n; // 初始化
            while (!rsta.empty() && arr[rsta.top()] >= arr[i]) {
                r[rsta.top()] = i;
                rsta.pop();
            }                
            rsta.emplace(i);
        }
        for (int i = n - 1; i >= 0; i--) {
            while (!lsta.empty() && arr[lsta.top()] > arr[i]) {
                l[lsta.top()] = i;
                lsta.pop();
            }                
            lsta.emplace(i);
        }
        // 乘法原理
        for (int i = 0; i < n; i++) {
            int left = i - l[i], right = r[i] - i;
            res += left * 1L * right % MOD * arr[i] % MOD;
            res %= MOD;
        }
        return res;
    }
};
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

Rust

  • 存usize的vec不能初始化为负值,所以左边界整体加11计算,最后答案里减掉。
const MOD:i64 = 1000000007;
impl Solution {
    pub fn sum_subarray_mins(arr: Vec<i32>) -> i32 {
        let n = arr.len();
        let mut res:i64 = 0;
        // 边界
        let (mut l, mut r) = (vec![0; n], vec![n; n]);
        let mut sta = vec![];
        for i in 0..n {
            while !sta.is_empty() && arr[sta[sta.len() - 1]] >= arr[i] {
                r[sta.pop().unwrap()] = i;
            }                
            sta.push(i);
        }
        sta.clear();
        for i in (0..n).rev() {
            while !sta.is_empty() && arr[sta[sta.len() - 1]] > arr[i] {
                l[sta.pop().unwrap()] = i + 1;
            }                
            sta.push(i);
        }
        // 乘法原理
        for i in 0..n {
            res += (i - l[i] + 1) as i64 * (r[i] - i) as i64 % MOD * arr[i] as i64 % MOD;
            res %= MOD;
        }
        res as i32
    }
}
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

总结

  • 子区间个数的题,想到用乘法原理计算之后,倒推回去想如何获得两个乘数(合法边界到当前的距离),找最值就用单调栈了,思路清晰!
  • 用java的代码改C++和rust,出了各种各样关于初始化的小bug,remind me to pay more attention to details

欢迎指正与讨论!