知识点: 线段树优化DP
原题面
双倍经验 P1848 [USACO12OPEN]Bookshelf G。
这边有 dalao 的神仙题解。
?我一开始为什么要写二维 DP
题意简述
给出一个长度为 \(n\) 的序列 \(h\)。
将 \(h\) 分成若干段,满足每段数字之和都不超过 \(m\)。
最小化每段的最大值之和。
\(1\le n\le 10^5, 1\le h_i\le 10^9\)
分析题意
有个非常显然的 DP :
设 \(f_i\) 表示,已经分好 \(i\) 个数字的最小代价。
转移时枚举这一段的开头 \(k\),将 \([k,i]\) 作为新的一段,则有:
暴力 DP 复杂度 \(O(n^2)\),期望得分 \(30\text{pts}\)。
实际上能水 \(50\text{pts}\)(大雾
考虑优化。
显然,对于一个给定的 \(i\),当 \(k\) 单增时,\(\max\limits_{j=k}^{i}h_j\) 单调不增,\(f_{k-1}\) 单调不降。
当枚举到 \(i\) 时,\(f_{k-1}\) 不会再改变,考虑 \(h_i\) 对 \(\max\limits_{j=k}^{i}h_j\) 的影响。
如图,设 \(i\) 左侧第一个满足 \(h > h_i\) 的位置为 \(pre_i\),显然 \(\max\limits_{j=k}^{i}h_j(k> pre_i)\) 都会变为 \(h_i\)。
\(\max\limits_{j=k}^{i}h_j\) 可用支持区间赋值的数据结构进行维护,转移 \(f_i\) 时,需要进行区间查询。
考虑线段树。
线段树维护位置 \(k\) 的 \(f_{k-1}\) 和 \(f_k+\max\limits_{j=k}^{i} h_j\)。
当枚举到一个新的 \(h_i\) 时:
- 单点修改,更新位置 \(k=i\) 时的 \(f_{k-1}\)。
- 根据 \(h_i\) 更新区间 \([pre_{i}+1,i]\) 的 \(f_k+\max\limits_{j=k+1}^{i} h_j\)。
- 二分得到 第一个不满足 \(\sum\limits_{k}^{i}h_i\le m\) 的位置 \(l\),则 \(k\in [l+1,i]\)。
- 查询 \([l+1,i]\) 中最小的 \(f_{k-1} + \max\limits_{j=k}^{i}h_j\)。
复杂度 \(O(n\log n)\),期望得分 \(100\text{pts}\)。
代码实现
//知识点: 线段树优化DP
/*
By:Luckyblock
*/
#include
#include
#include
#include
#define ll long long
#define ls (now<<1)
#define rs (now<<1|1)
const int kMaxn = 1e5 + 10;
const ll kInf = 1e12 + 2077;
//=============================================================
struct SegmentTree {
int L, R;
ll f, ans, tag;
} t[kMaxn << 2];
ll n, m, h[kMaxn], w[kMaxn], sum[kMaxn], pre[kMaxn], f[kMaxn];
ll top, sta[kMaxn];
//=============================================================
inline ll read() {
ll f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Pushup(int now) {
t[now].f = std :: min(t[ls].f, t[rs].f);
t[now].ans = std :: min(t[ls].ans, t[rs].ans);
}
void Pushdown(int now) {
t[ls].ans = t[ls].f + t[now].tag;
t[rs].ans = t[rs].f + t[now].tag;
t[ls].tag = t[rs].tag = t[now].tag;
t[now].tag = kInf;
}
void Build(int now, int L, int R) {
t[now].L = L, t[now].R = R;
if (L == R) {
t[now].f = t[now].ans = t[now].tag = kInf;
return ;
}
int mid = (L + R) >> 1;
Build(ls, L, mid), Build(rs, mid + 1, R);
Pushup(now);
}
ll Query(int now, int L, int R) {
if (L <= t[now].L && t[now].R <= R) return t[now].ans;
if (t[now].tag != kInf) Pushdown(now);
int mid = (t[now].L + t[now].R) >> 1;
ll ret = kInf;
if (L <= mid) ret = std :: min(ret, Query(ls, L, R));
if (R > mid) ret = std :: min(ret, Query(rs, L, R));
return ret;
}
void Update(int now, int L, int R, ll val) {
if (L <= t[now].L && t[now].R <= R) {
t[now].ans = t[now].f + val;
t[now].tag = val;
return ;
}
if (t[now].tag != kInf) Pushdown(now);
int mid = (t[now].L + t[now].R) >> 1;
if (L <= mid) Update(ls, L, R, val);
if (R > mid) Update(rs, L, R, val);
Pushup(now);
}
void Modify(int now, int pos) {
if (t[now].L == t[now].R) {
t[now].ans = kInf;
t[now].f = f[pos - 1];
return ;
}
if (t[now].tag != kInf) Pushdown(now);
int mid = (t[now].L + t[now].R) >> 1;
if (pos <= mid) Modify(ls, pos);
else Modify(rs, pos);
Pushup(now);
}
void Prepare() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
h[i] = read();
sum[i] = sum[i - 1] + h[i];
}
sta[++ top] = 1;
for (int i = 2; i <= n; ++ i) {
while (top && h[sta[top]] < h[i]) top --;
if (top) pre[i] = sta[top];
sta[++ top] = i;
}
Build(1, 1, n);
}
//=============================================================
int main() {
Prepare();
for (int i = 1; i <= n; ++ i) {
Modify(1, i);
if (pre[i] < i) Update(1, pre[i] + 1, i, h[i]);
int l = std ::lower_bound(sum, sum + i + 1, sum[i] - m) - sum;
if (l < i) f[i] = Query(1, l + 1, i);
}
printf("%lld", f[n]);
return 0;
}