线段树优化dp学习笔记

线段树优化dp学习笔记

Fri Nov 22 2024
7 分钟

到底是什么算法让我觉得两道题就足以让我写一篇学习笔记呢?

虽然两年半以前写过一道dp,正解的优化是单调队列,但是我拿线段树过了(卡着空间过的),所以那个dp并不能叫线段树优化dp。

CF115E Linear Kingdom Races#

这个算是最 “原汁原味” 线段树优化dp。
dpi,jdp_{i,j} 表示第 jj 条路到第 ii 条路全修的最大收益。(至于第 11j1j - 1 条路,那不是我们该考虑的,这就是dp的精髓)那么有转移:

dpi,j={dpi1,j+(lkj,rk=ipk)cij<i(max1yx<idpx,y)+(lk=rk=ipk)cij=idp_{i,j} = \begin{cases} dp_{i - 1,j} + (\sum\limits_{l_{k} \ge j,r_{k} = i}p_{k}) - c_{i} & j < i \\ (\max\limits_{1 \le y \le x < i}dp_{x,y}) + (\sum\limits_{l_{k} = r_{k} = i}p_{k}) - c_{i} & j = i \\ \end{cases}

其中:lkj,rk=ipk\sum\limits_{l_{k} \ge j,r_{k} = i}p_{k}lk=rk=ipk\sum\limits_{l_{k} = r_{k} = i}p_{k} 分别表示右端点在 ii 且被区间 [i,j][i,j] 覆盖的的比赛收益之和 和 左端点和右端点都在 ii 的比赛收益之和。 那为何在 j<ij < i 时只用考虑 lkj,rk=il_{k} \ge j,r_{k} = i 的比赛呢?因为 rk<ir_{k} < i 的比赛都在 dpi1,?dp_{i - 1,?} 考虑过了,而 rk>ir_{k} > i 的不是现在该考虑的。
但是,这题不可能这么简单,单是存状态和枚举状态就可以让我们爆炸了 先来优化空间:我们发现:max1yx<idpx,y\max\limits_{1 \le y \le x < i}dp_{x,y} 就是对于 i1i - 1 的答案,于是第二个方程就可以表示成 ans+(lk=rk=ipk)cians + (\sum\limits_{l_{k} = r_{k} = i}p_{k}) - c_{i} 。而我们发现第一个方程又只会从 i1i - 1 来转移,那么我们可以考虑什么?滚动数组啊。那么空间就优化完了。
现在来优化时间。我们设每一个 dpi,jdp_{i,j} 都有一个待定值数组 tmptmp ,那么 dpi,jdp_{i,j} 一定由 tmptmp 里的某一个值转移而来。对于暴力来说,就是每次新枚举一个新的状态,都给他一个新的 tmptmp 数组,但是我们要优化,我们就不可能每次用 O(n)\mathcal{O}(n) 给他一个新的 tmptmp 数组,我们应该继承上一个状态的 tmptmp 数组,并操作它使它变成当前状态的 tmptmp 数组。我们发现,每次转移,tmptmp 数组的 [1,i][1,i] 项都会加上 cic_{i} ,也会加上 lkj,rk=ipk\sum\limits_{l_{k} \ge j,r_{k} = i}p_{k} 。这不就是区间操作吗?我们就可以用线段树来维护这个值。线段树里装的就是待定值数组 tmptmp 。而对于 i=ji = j 的情况,因为本来只有 [1,i1][1,i - 1] 的下标里有值,而我们要用到 tmpitmp_{i} 的值,我们就可以给他赋值一个 ansans ,正如同方程里的一样,而题目里让我们求最大的收益,那么线段树查询的就是区间 [1,i][1,i] 里的最大值。这样,我们就做完了。
有些细节比如如何记录 rk=ir_{k} = i 的比赛会在代码里说明。

CPP
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include<bits/extc++.h>
#define int long long
#define ls (rt << 1)
#define rs (rt << 1 | 1)
using namespace std;
const int maxn = 2e5 + 5;
int n,m;
int a[maxn];
struct edge
{
    int l,p;
    edge *nxt;
}*head[maxn];
struct Nahida//不要关心这个名字
{
    int l,r;
    int val,lazy;
}tree[maxn << 2];
void push_up(int rt){tree[rt].val = max(tree[ls].val,tree[rs].val);}
void push_down(int rt)
{
    if (!tree[rt].lazy)
        return;
    tree[ls].val += tree[rt].lazy;
    tree[rs].val += tree[rt].lazy;
    tree[ls].lazy += tree[rt].lazy;
    tree[rs].lazy += tree[rt].lazy;
    tree[rt].lazy = 0;
}
void build(int l,int r,int rt)
{
    tree[rt].l = l;
    tree[rt].r = r;
    if (l == r)
    {
        tree[rt].val = tree[rt].lazy = 0;
        return;
    }
    int mid = (l + r) >> 1;
    build(l,mid,ls);
    build(mid + 1,r,rs);
}
void upd(int ql,int qr,int x,int rt = 1)
{
    int l = tree[rt].l;
    int r = tree[rt].r;
    if (ql <= l && r <= qr)
    {
        tree[rt].val += x;
        tree[rt].lazy += x;
        return;
    }
    push_down(rt);
    int mid = (l + r) >> 1;
    if (ql <= mid)
        upd(ql,qr,x,ls);
    if (qr > mid)
        upd(ql,qr,x,rs);
    push_up(rt);
}
void adde(int l,int r,int p)
{
    auto tmp = new edge;
    tmp->l = l;
    tmp->p = p;
    tmp->nxt = head[r];
    head[r] = tmp;
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    int l,r,p;
    for (int i = 1; i <= m; i++)
    {
        cin >> l >> r >> p;
        adde(l,r,p);//用链式前向星(邻接表)记录右端点为i的比赛。
    }
    build(1,n,1);
    int ans = 0;
    for (int i = 1; i <= n; i++)//滚动数组滚掉一维,就可以存下了。
    {//这里没有重新建树就是继承了上一个的tmp数组
        upd(i,i,ans);//单点修改,增添一个ans
        upd(1,i,-a[i]);//上面说的:区间更改
        for (auto j = head[i]; j; j = j->nxt)
            upd(1,j->l,j->p);//也是区间修改
        ans = max(ans,tree[1].val);//记录答案
    }
    cout << ans;
    return 0;
}

好题推荐:#

P2605 基站选址