简单dp 学习笔记

1. 背包

1.1 背包模型的概述

\(n\) 种物品,每种物品有若干个。拿一件物品付出 \(w_i\) 代价获得 \(v_i\) 价值,问最多花费 \(V\) 的代价能获得的最大价值。

1.2 0/1背包

考虑每种物品只有一个的情况。

我们设 \(f_{i,j}\) 表示前 \(i\) 个物品,花费了 \(j\) 的最大价值。

于是得出 dp 方程:\(f_{i,j}=\max(f_{i-1,j},f_{i-1.j-w_i}+v_i)\)

我们发现,\(f_{i,j}\) 都是从 \(f_{i-1,j}\) 转移而来,所以考虑压掉 \(i\)。需要发现,在转移时要倒序转移,使得每个物体只被选一次。

    for(int i=1; i<=n; i++)
        for(int j=V; j>=w[i]; j--)
            f[j]=max(f[j],f[j-w[i]]+v[i]);

1.3 完全背包问题

即每种物品有无数个。

考虑同样的 dp 转移,只是将倒序枚举改为正序,这样就变成选无限个了。

    for(int i=1; i<=n; i++)
        for(int j=w[i]; j>=V; j++)
            f[j]=max(f[j],f[j-w[i]]+v[i]);

1.4 多重背包问题

给出每种物品有 \(s_i\) 个,怎么做?

一个最简单的想法是直接把 \(s_i\) 个看成不同的 \(s_i\) 种,但是这样的复杂度是 \(O(\sum s\times V)\) 的,不可接受。

考虑倍增,将物品拆成 \(2^0,2^1,2^2,\dots,2^{\log s}\) 个,复杂度变成了 \(O(\sum{\log s}\times V)\)

    int cnt=0;
    for(int i=1,v,w,s;i<=n;i++){
        rd(w,v,s);
        for(int j=1; j<=s; j<<=1) w[++cnt]=j*w,v[cnt]=j*v,s-=j;
        if(s) w[++cnt]=s*w,v[cnt]=s*v;
    }
    for(int i=1; i<=cnt; i++)
        for(int j=V; j>=w[i]; j--)
            f[j]=max(f[j],f[j-w[i]]+v[i]);

1.5 “恰好为 V”

问能否从中选取若干件总价值恰好为 \(V\)

\(f_{i,j}\) 表示前 \(i\) 件物品能否取到恰好 \(j\)

转移同理。

1.6 有依赖的背包

P1064

考虑依赖关系,对子集进行讨论,枚举子树哪些被选。

1.7 背包的方案计数

考虑如果不需要最优解,直接将取 max 改为求和即可。

否则记 \(g_i\) 表示 \(f_i\) 最大时的方案数,看从哪里转移过来输出即可。

2. 区间 dp

区间 dp 经典方程式:\(f_{i,j}=\max(f_{i,k}+f_{k+1,j}+cost(i,j))\)

P1880

断环为链,直接做。

3. 树形 dp

3.1 simple

P1352

\(f_{i,0/1}\) 表示选/不选 \(i\) 的最优情况。

于是可以知道 \(f_{i,0}=\max(f_{j,0},f_{j,1})+a_i,f_{i,1}=f_{j,0}+a_i\),其中 \(j\)\(i\) 的儿子。

dfs 转移。

3.2 树上背包

P2014

建图,发现是一个森林。

我们设 \(f_{u,i,j}\) 表示以 \(u\) 号点为根的子树中,已经遍历了 \(u\) 号点的前 \(i\) 棵子树,选了 \(j\) 门课程的最大学分。

转移的过程结合了树形 DP 和 背包 的特点,我们枚举 \(u\) 点的每个子结点 \(v\),同时枚举以 \(v\) 为根的子树选了几门课程,将子树的结果合并到 \(u\) 上。

记点 \(x\) 的儿子个数为 \(s_x\),以 \(x\) 为根的子树大小为 \(\textit{siz_x}\),可以写出下面的状态转移方程:

\( f_{u,i,j}=\max_{v,k \leq j,k \leq \textit{siz_v}} f_{u,i-1,j-k}+f_{v,s_v,k} \)

注意上面状态转移方程中的几个限制条件,这些限制条件确保了一些无意义的状态不会被访问到。

\(f\) 的第二维可以很轻松地用滚动数组的方式省略掉,注意这时需要倒序枚举 \(j\) 的值。

可以证明,该做法的时间复杂度为 \(O(nm)\)

3.3 换根 dp

换根 dp 通过预处理换根的贡献,使得仅用两次 dfs 可以处理一些问题。

通常在出现在无根树或者需要对每个点计算答案时考虑换根。

P3478"

不妨令 \(u\) 为当前结点,\(v\) 为当前结点的子结点。首先需要用 \(s_i\) 来表示以 \(i\) 为根的子树中的结点个数。\(s_u=1+\sum s_v\)
考虑计 \(f_u\) 为以 \(u\) 为根时,所有结点的深度之和。

考虑让根从 u 换到 v,这样 v 的子树中的点的 dep--, v 字数外的点的 dep++,可以知道 \(f_v=f_u+n-2\times s_v\)

直接转移即可。

4.DAG 上 dp

考虑拓扑排序,从入度为 0 的点开始记忆化搜索,这样可以方便的计算答案。

5.状压 dp

5.1 朴素的状压 dp

如果遇到 \(n\) 较小的题目,考虑用二进制表示每个位置的状态。

考虑 P2704,记录当前有炮兵的位置,在计数时对其他的地方加上贡献即可。

5.2 一些常用的优化技巧:

两边 dp(与 meet in middle 同理)、阈值分治。

5.3 子集 dp

有时候需要枚举一个数的子集来转移给这个数。

但是如何快速的枚举子集的子集?

考虑这样一个东西:

    int full=1<<n;
    for(int s=0; s<full; s++)
        for(int s1=s; s1; s1=(s1-1)&s)
            ......

考虑这个 s1 的意义:依次消去 s 的每一位,故为枚举子集。

这个东西用二项式定理证出来是 \(O(3^n)\) 的。

posted @ 2024-03-01 14:17  lgh_2009  阅读(4)  评论(0编辑  收藏  举报