题解 P1295 【[TJOI2011]书架】
※题目分析
给出一个长度为
n 的序列h ,请将h 分成若干段,满足每段数字之和都不超过m ,最小化每段的最大值之和。
众所周知,不会做的题目就
这个式子有很多种处理方法,最简单的直接
本文要提的是题解区的那个
这种做法大概可以理解为用单调性对堆优化吧……
※性质分析
引理一:
\max\limits_{k=j+1}^{i}h_k 中,随着k 的增大,其值单调不增。
- 证明显然,这边不再赘述。
引理二:
f 单调不降。
- 考虑从题目本身出发,由于多分一段必然要让代价加上本段最大值,此时会使得
f_i 增大;若当前的数h_i 和前面一段分在一起(满足条件的话),则f_i 与前面相同。因此f 是单调不降的。
参考 CSP-S 2020 T4,我们可以把转移分成两段。
1、对于一个合法的
那么此时对于
2、对于一个合法的
具体的证明大概是这样的:第一种情况维护的是函数的极小值,其中突出的部分会被包含在两个相邻的极小值当中,但这种转移对队列中的
因为这种情况即从
但维护单调队列的时候就可能产生一些意外使得一些值没被更新到。所以可以维护单调栈防止漏掉情况。具体是这样的,对于第二种情况,有可能直接
- 综上,实际上我们需要维护单调下降的一个单调队列,用来维护上式中的
\max 值;然后维护f 用单调栈,为了保证复杂度可以从队列中点开始向两端维护两个单调下降的栈用于保存f 。更新时,如果队列端点超过了原先的中点,可以考虑重构两个单调栈。
我们发现,当队列元素越多,重构单调栈的次数也就越多,但总的重构次数也越少(仅在对队列弹出过期元素可能重构)。可以考虑把随机数据拆成几段单调下降的序列,最长的最多是最长下降子序列,假设长度为
p 。则最多重构n/p 次,每次最多重构p 个数,则复杂度最多O(n/p·p)=O(n) 。取到最大时,当最长子序列尽量长,也即h 严格单调下降。当然并不是严格单调下降就可以取到最大值,还要考虑h 和m 之间的关系。
(纯属口胡)
- 实际上,在随机数据下均摊重构是
O(n/2) 的(可以理解为分治,总的节点数是线性的)。
那么考虑
可能有人有疑问这里的第一种情况去哪了?其实在一开始维护第一种情况就直接扔到单调栈里就可以了,反正最后是维护最小的。(根据上文提到的单调性,显然是正确的)
(如果后面想到更好的对单调栈的解释我会回来补充的,不过 )
综上我们得到了一个
※代码
我知道你们只看这个
/*
BY xiejinhao
2020-11-20 9:19 from XWSF
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int h[N], q[N], stk[2][N];
int top[2], l = 1, mid, r;
long long f[N], tmpf[N];
// stk[0/1] 左/右 栈,指针对应 top[0/1]
// q 队列 h 原数组 tmpf 对应队列中的 f 值
void push(int x, int i) {
if(!top[i]) stk[i][++top[i]] = x;
else if(tmpf[stk[i][top[i]]] > tmpf[x])
stk[i][++top[i]] = x;
}
void rebuild() {
mid = (l + r) >> 1, top[0] = top[1] = 0;
for(int i = mid; i >= l; i--) push(i, 0);
for(int i = mid + 1; i <= r; i++) push(i, 1);
}
int main() {
int n, m, st = 1, sum = 0;
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf("%d", h + i), sum += h[i];
while(sum > m) sum -= h[st++];
while(l <= r and h[q[r]] <= h[i]) {
if(top[0] and stk[0][top[0]] == r) --top[0];
if(top[1] and stk[1][top[1]] == r) --top[1];
if(--r <= mid) rebuild();
} // 维护单调队列
if(l > r) tmpf[r + 1] = f[st - 1] + h[i];
else tmpf[r + 1] = f[q[r]] + h[i];
q[++r] = i, push(r, 1);
// 队头的情况要特判
if(stk[0][top[0]] == l) --top[0];
if(stk[1][top[1]] == l) --top[1];
while(l <= r and q[l] < st) {
if(++l > mid) rebuild();
// 同样特判队头
if(top[0] and stk[0][top[0]] == l) --top[0];
if(top[1] and stk[1][top[1]] == l) --top[1];
} // 弹出过期元素
f[i] = f[st - 1] + h[q[l]]; // 和开头并为一段
// --- 与开头之后的某个数开始并为一段 ---
if(top[0]) f[i] = min(f[i], tmpf[stk[0][top[0]]]);
if(top[1]) f[i] = min(f[i], tmpf[stk[1][top[1]]]);
}
printf("%lld\n", f[n]);
return 0;
}
※写在后面
1、本文同步发布在我的博客园:点我
2、翻了下提交记录,各位以后抄题解稍微改一下行嘛
3、能点个赞吗(光速逃