3578. 统计极差最大为 K 的分割方式数

考点

  • 前缀和
  • 单调队列

思路

比较难的一道综合题,为什么评级只是中等?

根据题意是划分DP裸题,可以得到下面的超时代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
static const int maxn = 5e4 + 5, mod = 1e9 + 7, inf = 0x3f3f3f3f,
neg = 0xcfcfcfcf;
int countPartitions(vector<int>& nums, int k) {
long long f[maxn];
int n = nums.size();
memset(f, 0, sizeof(f));
f[0] = 1;
for (int i = 1; i <= n; ++i) {
int mx = neg, mi = inf;
for (int j = i; j >= 1; --j) {
mx = max(mx, nums[j - 1]), mi = min(mi, nums[j - 1]);
if (mx - mi > k) break;
f[i] = (f[i]+f[j - 1])%mod;
}
}
return f[n];
}
};

难点在于优化:

首先可以发现,对于任何一个区间,在包含一个新的元素时,新区间它的k有且只有三种可能:

  1. 保持不变,即没有改变maxmin
  2. 改变了maxk变大
  3. 改变了mink变大

综上,k只可能是不变或者是变大

那么对于当前的i,只需要暴力地一直向左扩展枚举连续区间,直到不满足条件即可退出

这也是上面的暴力DP的解决方案


假设现在已知f[i - 1],它的最大向左扩展连续区间为[L, i - 1]

那么f[i - 1] = f[L - 1] + ... + f[i - 2]

考虑转移到f[i]时的情况,对于i而言,它的最大向左扩展连续区间为[L, i],或者向右缩小,不可能反向向左增大

道理很简单,对于i - 1而言,[L, i - 1]已经是极限,如果再加上一个i变成[L, i],则说明i对该区间的maxmin并无贡献,没有理由还会向左增大

明白这一层后,就可以用前缀和+单调队列进行优化了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
static const int maxn = 5e4 + 5, mod = 1e9 + 7, inf = 0x3f3f3f3f,
neg = 0xcfcfcfcf;
int countPartitions(vector<int>& nums, int k) {
deque<int> mx, mi;
int l = 1, f[maxn], n = nums.size();
long long s = 0;
memset(f, 0, sizeof(f));
f[0] = 1;
for (int i = 1; i <= n; ++i) {
// 先更新极值
int x = nums[i - 1];
while (!mi.empty() && nums[mi.back() - 1] >= x) mi.pop_back();
mi.push_back(i);
while (!mx.empty() && nums[mx.back() - 1] <= x) mx.pop_back();
mx.push_back(i);
// 收缩边界
s = (s + f[i - 1]) % mod;
while (!mx.empty() && !mi.empty() &&
nums[mx.front() - 1] - nums[mi.front() - 1] > k) {
s = (s - f[l - 1] + mod) % mod;
l++;
while (!mx.empty() && mx.front() < l) mx.pop_front();
while (!mi.empty() && mi.front() < l) mi.pop_front();
}
f[i] = s;
}
return f[n];
}
};

注意,上面当中的

1
2
while (!mx.empty() && mx.front() < l) mx.pop_front();
while (!mi.empty() && mi.front() < l) mi.pop_front();

完全可以改成下面

1
2
if (!mx.empty() && mx.front() < l) mx.pop_front();
if (!mi.empty() && mi.front() < l) mi.pop_front();

因为本题的单调队列保存的下标一定是单调递增的,而其对应的值也一定是单调递减/递增的,

向右缩减区间要删除队列中的无效点时,它要么不存在队列,要么一定在队头,不可能在队中