【学习笔记】线段树优化DP

dp,即动态规划中有一类很重要的优化,叫做线段树优化。本文将介绍几种常见的类型及其套路和一些例题。

前置知识:线性 dp、线段树。

被 NOIP2023 暴打之后爬来补 dp 了。

定义

为了方便理解,先声明一下本博客中的如下定义(可能有误,所以仅限于本博客,可能不能作为参考):

  • 当前:包括本轮待计算的 dp 值和遍历到的信息。
  • 决策(jc):即更新 dp 值,可以理解为转移。
  • 决策点:即某个 dp 值的转移处。(如 \(dp_i\) 可以从 \(dp_j+x\) 转移,且 \(dp_j+x\) 为它最终取到的(即最优的)值,那么称 \(j\) 为决策点)
  • 决策值:即某个 dp 值的转移值。

以最长上升子序列为例。

假设本轮要计算 \(dp_i\)。那么 \(dp_i\)当前的 dp 值,\(a_i\)当前的信息。

\(dp_i\) 可以取 \(dp_j+1\)(此时 \(a_j < a_i\)),那么称 \(j\)决策点,称 \(dp_j + 1\)决策值

分类

我把线段树优化 dp 决策分为「普通修查」「维护数组」和「维护决策值」,这样分类是因为用法不一样:

  • 「普通修查」就是需要快速查到区间最值之类的信息,一般用于填表法;

  • 「维护数组」相当于把 dp 数组装进线段树,一般用于推表法;

  • 「维护决策值」是动态维护所有决策点的答案。

「普通修查」和「维护数组」基本上可以相互转化,但是「维护数组」更费脑。

普通修查 / 维护数组

CF1304F Animal Observation

Easy version

\(f_{i,j}\) 表示第 \(i\) 天拍摄,区间左端点为 \(j\)

容易得到一个枚举上一行和这一行的左端点 \(j,l\)\(O(nm^2)\) dp。

但是注意到当 \([j,j+k-1]\)\([l,l+k-1]\) 无交时,这一次拍照的贡献与 \(j\) 的位置无关,可以维护前后缀 \(\max\) 快速查询。有交的区间只有 \(O(k)\) 个,暴力枚举这些 \(l\) 即可。得到一个 \(O(nmk)\) 做法。

Hard version

考虑优化上面区间有交时的转移。

记第 \(i\) 行的前缀和为 \(sum_i\)

我们这次不一一枚举 \(j\),只枚举 \(l\)。对于 \(l <= j\) 的,这一行的贡献为 \(f_{i-1,j} + sum_{i,j-1} - sum_{i,l-1}\),要最大化这个东西,只需最大化 \(f_{i-1,j} + sum_{i,j-1}\),扔到线段树上区间 \(\max\) 即可。\(l>j\) 同理。

显然这只是个用来单点修、区间最值的线段树,是最简单的「普通修查」。

维护数组就是反过来,枚举 \(j\),然后把一段 \(f_{i,l}\) 做取 \(\max\) 操作,好处是能实时维护 dp 数组,坏处就是思维方式比较麻烦,而且这个题需要区间修。

“等等,这不是能单调队列么?”

“唔,你说的对,但是我们的专题是线段树优化 dp。”

线段树优化虽然带只 \(\log\),但是不用动脑子,很方便呐~

代码(普通修查):

for (int i = 1; i <= n; i++)
{
    for (int j = 1; j + k - 1 <= m; j++)
    {
        if (i > 1)
        {
            if (k < j)
                dp[i][j] = max(dp[i][j], mx[0][i - 1][j - k]);
            if (j + k + k - 1 <= m)
                dp[i][j] = max(dp[i][j], mx[1][i - 1][j + k]);
            dp[i][j] = max(dp[i][j], t1[i - 1].query(1, 1, tlen, j - k + 1, j) + s[i][j - 1]);
            dp[i][j] = max(dp[i][j], t2[i - 1].query(1, 1, tlen, j, j + k - 1) - s[i][j + k - 1]);
        }
        dp[i][j] += sum(i, j) + sum(i + 1, j);
        mx[0][i][j] = max(mx[0][i][j - 1], dp[i][j]);
        t1[i].update(1, 1, tlen, j, dp[i][j] - s[i + 1][j + k - 1]);
        t2[i].update(1, 1, tlen, j, dp[i][j] + s[i + 1][j - 1]);
    }
    for (int j = m - k + 1; j >= 1; j--)
        mx[1][i][j] = max(mx[1][i][j + 1], dp[i][j]);
}

CF597C Subsequences

给定一个 \(1\sim n\) 的排列 \(a\),求 \(a\) 中长度为 \(m+1\) 的上升子序列个数(IS)。
\(1\le n\le 10^5\)\(0\le m\le 10\)\(1\le a_i\le n\),答案不超过 \(8\times 10^{18}\)

首先设计 dp,\(dp_{i,k}\) 表示以第 \(i\) 位为结尾,构成长度为 \(k\) 的 IS 的个数。

转移有

\[dp_{i,k}=\sum_{j=0}^{i-1} dp_{j,k-1} (a_j < a_i) \]

如果没有 \(a_j < a_i\) 的限制,显然可以前缀和优化。那有了这个限制之后,是不是可以写成:

\[dp_{i,k}=\sum_{l=1}^{a_i - 1}\sum_{j=0}^{i-1} dp_{j,k-1} (a_j = l) \]

如果令 \(s_{i,k,x}\) 表示 \(\sum_{j=0}^{i-1} dp_{j,k} (a_j = x)\),那么有:

\[dp_{i,k}=\sum_{l=1}^{a_i - 1}s_{i,k-1,l} \]

这时候就看的很清楚了,可以开个值域线段树,把 \(s_{i,k-1,l}\) 丢到线段树上的第 \(l\) 位。枚举 \(k\),那么 \(dp_{i,k}\) 就是查线段树上 \(l\in [1,a_i-1]\)\(\sum s_{i,k-1,l}\)

单点修,区间和。当然,树状数组也是完全可以的。

时间复杂度 \(\mathcal{O}(nk\log n)\)

P3431 [POI2005] AUT-The Bus

先离散化,然后枚举横坐标,再枚举纵坐标。从上一个横坐标转移,转移的合法位置是所有纵坐标不大于当前纵坐标的位置。但是这样复杂度不够好!

注意到我们可以只保留关键点的值(就是有人的地方),所以可以按人的横坐标排序。

单点修,前缀 \(\max\)\(O(k\log k)\)

CF474E Pillars

跟维护上升子序列类似,权值线段树上单点修,前/后缀 \(\max\)

CF960F Pathwalks

首先是二维偏序,所以顺次枚举输入的边,去掉一维,还剩值域的一维。

然后这个东西和我们的 LIS 很像,于是考虑设 \(dp_{i}\) 表示以第 \(i\) 条边为结尾的方案数。其次,我们需要满足两个条件:

  • 决策点的 \(v\) 等于当前的 \(u\)
  • 决策点的 \(w\) 小于当前的 \(w\)

没有第一个的话可以考虑开权值线段树维护。有了第一个的话,可以考虑开 \(n\) 棵线段树,那就要动态开点了。

时间复杂度 \(\mathcal{O}(m\log max\{w_i\})\)。空间复杂度也有 \(\log\)

维护决策值

首先,我是从 lsj2009博客里学习的这类套路,在此万分感谢!

这种思想就是,想像一个数组,第 \(j\) 的位置的值是对当前考虑的位置 \(i\) 产生的贡献。说人话就是形如 \(f_i = \max\{g_j\}\) 这样的东西的 \(g_j\) 数组。

如果 dp 转移方程有一个动态贡献,就可能可以用线段树维护决策值的变化。

这类题在初次遇到常常难以下手,不过只要会了「维护决策值」的思考方式,这种题就全都是套路不至于愣着不会做了。

CF833B The Bakery

假设我们已经有了上一轮(\(i-1\))的决策值数组,即 \(g_j = f_j + w(j+1,i-1)\),其中 \(w(j+1,i-1)\) 表示 \(j+1\)\(i-1\) 的颜色数。那么当 \(i-1\) 变为 \(i\) 之后。。哎,举个例子!

这是 \(i=6\) 时的 \(g\) 数组:

我们能很方便的得到 \(f[6]\)——就是 \(max_{0\le j<6} g_j\)

而这是 \(i=7\) 时的 \(g\) 数组:

我们要如何从 \(i-1\) 时的 \(g\) 得到现在的 \(g\) 呢?注意到标红的数组增加了一,也就是说它右边的颜色数多了 1。而 \(g_0\)\(g_1\) 却没有变化,因为第 2 个位置已经是蓝色了,再来一个蓝色不会改变颜色数。

所以我们的得到了结论:设 \(i\) 位置上颜色上一个出现的位置是 \(lst\),则我们只需让 \([lst,i)\) 上的 \(g\) 增加 1 即可。

另外还得把 \(f_{i-1}\) 加到 \(g_{i-1}\) 上。

线段树,支持区间加,区间 \(\max\)

另外这个题限制区间个数,再加一维、开 \(m\) 棵线段树就好。

for (int i = 1; i <= n; i++)
{
    for (int j = 1; j <= m; j++)
    {
        h[j - 1].update(1, 0, n, i - 1, i - 1, dp[i - 1][j - 1]);
        h[j - 1].update(1, 0, n, pre[a[i]], i - 1, 1);
        dp[i][j] = h[j - 1].query(1, 0, n, 0, i - 1);
    }
    pre[a[i]] = i;
}

P9871 [NOIP2023] 天天爱打卡

先考虑 \(O(n\log n)\) 怎么做。

还是一样的方法。首先,我们得写成 \(f_i = \max g_j\) 的形式。容易想到记 \(f_i\) 表示强制第 \(i\) 位选,但是这样没法知道连续多少天跑步了!

如果我们强制 \(j\) 天不跑,\(j+1\sim i\) 天都跑,就知道是否满足连续跑的条件了。因此,令 \(f_i\)\(i\) 天及以后不跑即可。也可以说是第一次定义的 \(f_{i-1}\) 的前缀 \(\max\)

好,现在考虑 \(i-1\)\(i\)\(g\) 需要发生什么变化。发现题目给出,跑步一天消耗 \(d\) 的能量,完成挑战可以获得能量。 那么首先,既然我强制所有的 \(j\)\([j+1,i-1]\) 天都要跑,因此需要让 \([1,i-1]\) 上的 \(g\) 减去 \(d\)。然后,我们枚举以 \(i\) 为右端点的挑战区间(挑战可以看成区间),则 \([1,l-1]\) 上的 \(g\) 要加上 \(v\)

最后,注意题目说不能连续跑 \(>k\) 天,所以只需查 \([i-k,i-1]\) 上的 \(g\) 的最大值即可!

另外别忘记把 \(max_{0\le i<i} f_i\) 加到 \(g_i\) 上。

那么,现在想要优化这个 \(O(n\log n)\) 就只需保留 \(l-1\)\(r\) 的位置即可——只有它们是有用的。

线段树,支持区间加,区间 \(\max\)

时间复杂度 \(O(m\log m)\)

点击查看代码
void solve()
{
    scanf("%d%d%d%lld", &n, &m, &k, &d);
    tt[++pos] = 0;
    for (int i = 1; i <= m; i++)
    {
        scanf("%d%d%d", &a[i].r, &a[i].l, &a[i].w);
        a[i].l = a[i].r - a[i].l + 1;
        tt[++pos] = a[i].r;
        tt[++pos] = a[i].l - 1;
    }
    sort(tt + 1, tt + pos + 1);
    pos = unique(tt + 1, tt + pos + 1) - tt - 1;
    sort(a + 1, a + m + 1);
    for (int i = 1; i <= m; i++)
        query[lsh(a[i].r)].push_back(i);
    for (int i = 1; i <= pos; i++)
    {
        t.update(1, 1, pos, i, i, ans);
        t.update(1, 1, pos, 1, i - 1, -d * (tt[i] - tt[i - 1]));
        for (auto j : query[i])
            t.update(1, 1, pos, 1, lsh(a[j].l - 1), a[j].w);
        f[i] = t.query(1, 1, pos, lsh(tt[i] - k), i - 1);
        ans = max(ans, f[i]);
    }
    printf("%lld\n", ans);
}

P1848 [USACO12OPEN] Bookshelf G

到这里的话,如果你已经理解了这种套路,类似的题可以直接根据套路秒掉。现在你可以试试把这道 USACO 的题秒掉。另外还有道一模一样(而且只有一个 \(h_i\))的 P1295 [TJOI2011] 书架


to do:

posted @ 2024-11-18 14:38  Aquizahv  阅读(592)  评论(0)    收藏  举报