阅读视图

发现新文章,点击刷新页面。

DP & 双指针

解法:DP & 双指针

维护 $f(i)$ 表示前 $i$ 个元素的分割方案。转移时,枚举上一个子段的末尾在哪,有转移方程

$$
f(i) = \sum f(j)
$$

其中 $j$ 满足 $\max(a_{j + 1}, a_{j + 2}, \cdots, a_i) - \min(a_{j + 1}, a_{j + 2}, \cdots, a_i) \le k$

直接计算 DP 方程的复杂度为 $\mathcal{O}(n^2)$,还需要进一步观察合法的 $j$ 有什么特征。

注意到,如果某个子数组的极值之差小于等于 $k$,那么它的子数组的极值之差也小于等于 $k$。这是典型的双指针特征,因此合法的 $j$ 就是一段连续值,从某个值一直取到 $(i - 1)$。用双指针算出最小的合法 $j$,再用前缀和计算区间和即可。复杂度 $\mathcal{O}(n\log n)$,这里的 $\log n$ 主要是我们需要用数据结构(比如 multiset)动态维护滑动窗口里的最小值和最大值。

参考代码(c++)

class Solution {
public:
    int countPartitions(vector<int>& nums, int K) {
        int n = nums.size();

        const int MOD = 1e9 + 7;
        // f[i]:前 i 个元素的分割方案数
        // g[i]:f 的前缀和
        long long f[n + 1], g[n + 1];
        f[0] = g[0] = 1;

        // 用 multiset 记录滑动窗口里的数,方便求出最小值和最大值
        multiset<int> ms;
        // 枚举双指针的右端点 i,计算合法子段左端点的最小值 j
        for (int i = 1, j = 1; i <= n; i++) {
            ms.insert(nums[i - 1]);
            while (j < i && *prev(ms.end()) - *ms.begin() > K) {
                ms.erase(ms.find(nums[j - 1]));
                j++;
            }

            // j 是最小的左端点
            // 那么上一个子段最小的右端点就是 j - 1
            // 前缀和就得减去 j - 2 的值
            f[i] = (g[i - 1] - (j - 2 >= 0 ? g[j - 2] : 0) + MOD) % MOD;
            g[i] = (g[i - 1] + f[i]) % MOD;
        }
        return f[n];
    }
};
❌