动态规划笔记

  • 一、基础dp
    • 1.性质:
    • 2.处理:
  • 二、基础dp题型
    • 1.爬楼梯
    • 2.数塔问题
    • 3. 硬币问题
    • 4. 最大子段和
    • 5. LIS(最长上升子序列)
    • 6. LCS(最长公共子序列)
    • 7. DAG(有向无环图)
    • 8. 背包问题
  • 三、区间dp
  • 四、树形dp
  • 五、状压dp
  • 六、数位dp
  • 七、闫式dp分析法(重点)
    • 1.集合的角度看问题
    • 2.集合的状态表示
    • 3.集合的状态计算
    • 4.分析流程图

看完了紫书第九章还是有一点迷茫,dp果然是一个比较抽象和困难的知识点,这里结合学长(CSDN账号:蹲坑玩手机)上课的课件和看完紫书后自己的一些思考来记录一下dp学习的心得。

一、基础dp

1.性质:

  • 拥有子问题,子问题最优解(即拥有最优子结构),对于一个原问题解最优,其子问题必定也是最优,同时原问题的最优解依赖于其子问题的最优解
  • 子问题重复性,一个子问题可能会影响多个不同的下一阶段的原问题
  • 无后效性,即此时的之前状态无法直接影响未来的决策,换句话说就是之前的每个状态如何得来并不影响未来对此时(当前)状态的利用或者查找,因为我们最后对此时(当前)状态的利用只考虑结果不考虑过程。

2.处理:

  • 保存的信息不够:加数组维度
  • 时间复杂度:优化手段
  • 优化手段
    • 时间:线段树
    • 空间:滚动数组、多开一个数组记录上一阶段的所有状态值
  • 填表法和刷表法
    • 填表法:利用已知信息修改当前值
    • 刷表法:利用当前已知信息来修改其它相关值

二、基础dp题型

1.爬楼梯

  • 定义:dp[n] 代表达到第n阶需要的步数
  • 方程dp[n] = dp[n - 1] + dp[n - 2]+....(这里方程的取决于题目给出条件)
  • 初始化:dp[1] = dp[0] = 1;(走到第0级和第一级方案数为1)
  • 答案:dp[n]

2.数塔问题

  • 定义:dp[i][j]表示:到第i行第j列的能获得的最大的收益
  • 方程:dp[i][j] = dp[i + 1][j] + dp[i + 1][j + 1]
  • 初始化:dp[n][j] = a[n][i]
  • 答案:dp[1][1]

3. 硬币问题

  • 定义:dp[n]表示凑齐n面值所用的最少的张数
  • 方程:dp[i] = min(dp[i], dp[i - coin[i]] + 1)
  • 初始化:dp[0] = 0
  • 答案:dp[n]

4. 最大子段和

  • 定义:dp[i] 表示第i项必选往前的最大的字段的和
  • 方程:dp[i] = max(dp[i - 1] + nums[i], nums[i]) (考虑到dp[i-1]是否为负数的情况)
  • 初始化:dp[0] = -inf
  • 答案:dp[1 ~ n]的最大值

5. LIS(最长上升子序列)

  • 定义:dp[i]第i项必选往前找的所能找的最长上升子序列的长度
  • 转移:dp[i] = max{dp[j] + 1} a[j] < a[i]
  • 答案:dp[1 ~ n]的最大值
  • 线段树优化

6. LCS(最长公共子序列)

  • 定义:dp[i][j]表示a字符串选前i长度和b字符串选前j长度的最长公共长度
  • 转移:dp[i][j] = dp[i - 1][j - 1] + 1 : a[i] == b[j]
    • dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
  • 答案:dp[n][m]

7. DAG(有向无环图)

  • 有时候dp的最大最小值问题可以转化为求最短路最长路或者方案数的图论问题,然后就结合图论的知识点和dp的知识点结合解决。

8. 背包问题

  • 01背包(每个物品只有一个)

    • 最基础的背包问题 01背包问题
    • 状态转移方程:dp[i][j]=max(dp[i-1][j-v[i]]+w[i],dp[i][j])
    • 可以优化掉第一维,不过要每一次循环从后往前
  • 完全背包(每个物品无限)

    • 01背包的衍生,代码的长相和01背包就一点不一样
    • 状态转移方程:dp[i][j]=max(dp[i][j-v[i]]+w[i],dp[i][j])
    • 同样可以优化掉第一维,每一次循环依然从前往后
  • 多重背包(每个物品规定个数)

    • 转化为01背包问题就可以了
    • 主要代码部分
 for (int j = 1; j <= m; ++j) {dp[i][j] = dp[i - 1][j];for (int k = 1; k <= s; ++k) {if (j >= k * v) {dp[i][j] = max(dp[i][j], dp[i - 1][j - v * k] + k * w);}}}

三、区间dp

  • 将某个区间的状态进行保存
  • dp[l][r] 保存[l, r]里面的状态,
  • 例题:石子合并:dp[l][r]表示合并[l,r]区间的最小答案
#include <cstdio>
#include <cstring>
#include <algorithm>using namespace std;int n, w[305], sum[305], dp[305][305];int main() {scanf("%d\n", &n);memset(dp, 0x3f, sizeof dp);for (int i = 1; i <= n; ++i) {scanf("%d", w + i);sum[i] = sum[i - 1] + w[i];dp[i][i] = 0;}for (int len = 2; len <= n; ++len) { // 枚举区间长度for (int i = 1, j = i + len - 1; j <= n; ++j, ++i) { // 枚举左右区间 [i, j]for (int k = i; k < j; ++k) {dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);}}}printf("%d", dp[1][n]);return 0;
}

四、树形dp

  • 普通树形dp

    • dp的核心思想是状态的转移,而在树中,我们可以通过树的子节点来转移出父节点代表的的状态
    • 没有上司的舞会 : 树上最大独立集,dp[i][0/1]表示以第i号结点为根的子树且i号结点选1(不选0)的最大快乐值代码:
    #include <iostream>
    #include <algorithm>
    #include <vector>
    using namespace std;
    const int N = 6e3 + 10;
    vector<int> mp[N];
    int du[N], hp[N], dp[N][2];void dfs(int u, int fa) {dp[u][1] = hp[u];for (int v : mp[u]) {if (v == fa) continue;dfs(v, u);dp[u][0] += max(dp[v][1], dp[v][0]);dp[u][1] += dp[v][0];}
    }int main() {int n;cin >> n;for (int i = 1; i <= n; ++i) cin >> hp[i];int u, v;while (cin >> u >> v) {mp[u].push_back(v);mp[v].push_back(u);du[u] += 1;}int root = 1;for (int i = 1; i <= n; ++i) if (du[i] == 0) root = i;dfs(root, -1);cout << max(dp[root][1], dp[root][0]);return 0;
    }
    
    • 树的重心:重心是指树中的一个结点,如果将这个结点删除后剩余的各个连通块中结点数的最大值最小,则称为树的重心

      • 树的重心的一些重要性质:

        • 一棵树最少有一个重心,最多有两个重心,若有两个重心,则他们相邻(即连有直接边)
        • 树上所有点到某个点的距离和里,到重心的距离和最小;若有两个重心,则其距离和相同
        • 若以重心为根,则所有子树的大小都不超过整棵树的一半
      • 在一棵树上添加或删除一个叶子节点,其重心最多平移一条边的距离
      • 两棵树通过连一条边组合成新树,则新树重心在原来两棵树的重心的连线上
    • 树的直径:树上两点间距离的最大值
    • 树的中心:一个结点,对于每一个点的距离的最大值最小
  • 树形背包

    • 本质就是分组背包
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <iostream>
#include <cctype>
#define ll long long
#define pb emplace_backusing namespace std;
const int M = 105, N = 1e3 + 5, inf = 1e9;
vector<int> mp[M];
int root, n, m, dp[M][M], w[M], v[M], last[M];
// 刷表法
int dfs(int u) { // 0(nnm) int sum = v[u];dp[u][v[u]] = w[u]; // 必须选当前的根的物品for (int& son : mp[u]) {int siz = dfs(son);//m = 100000//sum = 100for (int i = v[u]; i <= min(sum, m); ++i) last[i] = dp[u][i];for (int i = v[u]; i <= min(sum, m); ++i) { // 遍历之前更新u的决策for (int j = 0; j <= siz; ++j) {if (i + j > m) continue;dp[u][i + j] = max(dp[u][i + j], last[i] + dp[son][j]); // 刷表法}}// for (int i = 100000; i >= v[u]; --i)// for (int i = 100; i >= v[u]; --i)// for (int i = m; i >= v[u]; --i) { // 01背包//     for (int j = 0; j <= siz; ++j) { // 分组每一组的每个物品(决策)//         if (i - j < v[u]) continue;//         dp[u][i] = max(dp[u][i], dp[u][i - j] + dp[son][j]); // 填表法//     }// }sum += siz;}return sum;
}int main() {scanf("%d %d\n", &n, &m);for (int i = 1; i <= n; ++i) {int p;scanf("%d %d %d\n", &v[i], &w[i], &p);if (p != -1) mp[p].pb(i);else root = i;}dfs(root);printf("%d\n", dp[root][m]);return 0;
}

五、状压dp

  • 对二维数组中每一行的状态进行压缩成二进制处理,用二进制01来表示每个位置的两种状态

  • 二进制的处理

    • 检查是否有相邻的1:if ((i & (i >> 1)) != 0) 1 2 3 4 1100
    • 检查a是够是b的子集 if ((a & b) == a)证明a是b的子集,if ((a | b) == b) 证明a是b的子集
  • 例题:小国王 LOJ 10170

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#include <vector>using namespace std;
using ll = long long;
const int N = 1 << 10;
ll dp[15][N + 5][110];int cal(int x) { // 计算二进制中 1 的个数int ans = 0;// while (x) {// if ((x & 1) == 1) ans += 1;// x >>= 1;// }while (x) {ans += 1;x &= (x - 1);}return ans;
}int main() {int n, m;cin >> n >> m;int len = (1 << n);  // 3 111   1 << 3 [0  ~  1000)dp[0][0][0] = 1;// O(n * 2^n * 2^n * k)// O(10 * 100 * 100 * 100) = O(<1e8)for (int i = 1; i <= n; ++i) { // 第i行for (int s1 = 0; s1 < len; ++s1) {// 当前行的状态// 判断s1的状态不能有相邻的两个国王放在一起if (s1 & (s1 >> 1)) continue; for (int s2 = 0; s2 < len; ++s2) { // 上一行的状态// 判断s2的状态不能有相邻的两个国王放在一起if (s2 & (s2 >> 1)) continue;// 101   100/*s1 = 101s2 = 100*/// 一下判断的是s1和s2冲突的情况if (s1 & s2) continue;/*s1 = 100s2 = 010*/if ((s1 >> 1) & s2) continue;/*s1 = 001s2 = 010*/if ((s1 << 1) & s2) continue;for (int k = 0; k <= m; ++k) { // 上一行的个数//if (dp[i - 1][s2][k] == 0) continue;dp[i][s1][k + cal(s1)] += dp[i - 1][s2][k];}}}}ll ans = 0;for (int i = 0; i < len; ++i) ans += dp[n][i][m];cout << ans << endl;return 0;
}

六、数位dp

  • 数位dp:有时候,我们会被要求找出满足某些条件的数字,但是我们要满足这些条件更加倾向于把这个数当作字符串来处理,通过对每一位数字来进行状态转移。
  • 例题:数位dp入门题 不要62
  • 递归的模板
int num[100], dp[100];int dfs(int indx, int limit, /*参数根据题意来添加*/) {if (indx == 0) {return 1;// 根据题意来返回}int &ref = dp[indx];if (!limit && ref != -1) return ref;int res = 0;int up = (limit ? num[indx] : 9);for (int i = 0; i <= up; ++i) {// 更新resdfs(indx - 1, limit && i == up);}if (!limit) ref = res;return res;
}int solve(int x) {if (!x) return 1; // 根据题意来决定返回值int len = 0;while (x) {num[++len] = x % 10;x /= 10;}return dfs(len, 1);
}
int main() {memset(dp, - 1, sizeof dp);// 此行省略读入cout << solve(r) - solve(l - 1) endl;
}
  • 递推的模板
void prework() { // 对每一位未超过最高数时的信息进行预处理
}int solve(int x) {int num[10], len = 0, res = 0; // 不同题意res初始化不同,即一般是0算不算的区别if (!x) return 1;// 返回什么根据题意来while (x) { // 抠每一位出来num[++len] = x % 10;x /= 10;}int last = 0; // 记录前几位需要信息,初始化根据题意来for (int i = len; i >= 1; --i) {for (int j = 0; j < num[i]; ++j) { // 注意如果题目对前导0有要求可在第一位开始从1开始再到25行处处理第一位如果是0的情况if (/*j 和 last放在一起不符合题意*/) continue;res += // 对每一位填未超过此位的数字时的答案进行累加,一般这里利用到prework预处理出来的东西}// 来到这一步说明此位填num[i]if (/*此位填num[i]不符合题意*/) break;last += // 更新lastif (last /*符合题意*/  && i == 1) {// 更新res}}for (int i = len - 1; i >= 1; --i) { // 假设不能有前导0,则枚举从次高位开始到低位for (int j = 1; j < 10; ++j) { // 枚举每一位都放1~9res += // 更新res}}return res;
}int main() {prework();// 此行省略读入cout << solve(r) - solve(l - 1) endl;
}

七、闫式dp分析法(重点)

  • 闫式dp分析法来源于Acwing网站的创始人闫学灿,该方法是对dp问题的思考过程。虽然使用该方法并不能降低dp问题本身的难度,但是我们可以通过这个方法去分析所有的dp问题。

1.集合的角度看问题

首先我们需要思考这么一个问题----我们为什么要用dp来解决一些问题?或者说,为什么dp能解决一些暴力无法解决的问题?

我们要想到这样一个事实,暴力枚举之所以会超时,是因为我们是在个体的角度看问题。通过暴力来枚举每一个个体的状态所以才导致了时间复杂度的不足。

但是如果换一个角度想,在实际问题中,每一个个体其实都可以看作一个集合的问题。如果我们把问题划分为集合,那么我们眼中的将是一个个集合而不是个体,从而降低了数量级。

举一个不太严谨的例子:一个老师如果要知道40人的班级的考试最高分,如果一个个的查,那么就要查遍40人。但是如果老师事先分好10人一组,让小组长来事先统计出本组最高分,那么老师只要比较4个组的最高分就可以知道全班的最高分,这样就轻松多了。

然后你可能会有疑问:小组长不一样要查遍班里每一个人吗?只是老师的工作量少了啊但是全班总的工作量没有变啊!别急,假设班级有一个人的成绩搞错了,那么在修改过后重新统计最高分,我该怎么办呢?

如果我们没有分组,老师重新暴力搜索就又要把全班全遍历一遍。但是分组后老师只需要看看是哪一个组的同学成绩被修改了,最后只要那个小组的组长修改本组状态,最后老师再修改全班的状态就行,其余3个组的状态不需要进行任何改动。

这里,老师统计成绩的角度从来就不是一个一个的人,而是被分成的4个小组、4个集合。这就是从集合角度看问题。

2.集合的状态表示

  • 集合的描述一般是有套路的:所有满足条件1,条件2的元素的集合。
    这个条件1和条件2一般对应到我们的状态表示的每一维。
  • 集合的表示:我们通常用一个多维数组dp[i][j][k]...来表示集合,每一维都表示一个状态。例如在上面成绩统计的例子中,我们可以用dp[i]来表示第i组的最高分,集合里的每一个元素都有一个共同点----是第i组的成员,如果状态约束变多就加数组的维度,如用dp[i][0]来表示第i组中男生的最高分 dp[i][1]来表示女生的最高分。这样我们就做到了用集合的角度看待问题。
  • 集合的意义:我们要确定集合里存下的是什么东西,这里根据题意确定。它可以是最大值、最小值、数量等。在上面的例子中dp[i]存下的是成绩的最大值。
  • 注意:集合表示的方法不唯一,并不是所有表示方法都能解决问题!比如举例中的表示方法只是方便大家更好的理解集合角度看问题的思想,实际上计算起来不能很好的体现动态规划的优势。

只有确定好了集合的表示方法和表示意义,我们才能继续求解dp问题。

3.集合的状态计算

确定了集合的状态后我们开始集合的状态计算。这里用楼梯问题来作为例题讲解:(一共有n级台阶,一个人一次可以爬一级台阶或两级台阶,请问爬到第n级有几种方法?)

  • 第一步、化零为整,确定状态表示和意义dp[i]代表爬到第i级的方案数。

  • 第二步、化整为零:首先了解到我们答案是最大的集合dp[n],我们试着把最大的的问题拆分成子问题(小集合)

    • 由题意得 要爬到第n级,我们先要爬到n-1级或者第n-2级,问题就转化成了求dp[n-1]dp[n-2],然后求dp[n-1]又转化成了求dp[n-2]dp[n-3]…这样,我们把要求的集合越分越小,直到分到最小的集合dp[0]dp[1]。这样我们就完成了集合的划分,把 大集合的任务分成了若干个小子集合的任务
    • 我们发现,最小的集合dp[0]dp[1]显然全部等于1。到0层的方案数显然全部为1种就不解释了,而到第一层只能从0到1,所以也显然为1。如果状态表示的方法没错的话,我们会发现最小的子集合的值是显然的
  • 第三步、状态计算

    • 状态转移方程:在完成划分后,我们要通过小集合的答案来求出大集合的答案了。而小集合的状态求出大集合的状态是要按照规律来的,这个规律就是状态转移方程。有时方程要根据题意来求出,有时候方程要自己进行推导。在本题中根据题意可得,方程为dp[i]=dp[i-1]+dp[i-2]。要是无法理解就继续看下去。
    • 通过dp[0]dp[1]全部等于1,我们求dp[2]:如果要走到第2级台阶,我们只能从第1级或者第0级台阶走到。那么走到第2级的方案数就是走到第0级的方案数和走到第1级的方案数之和,即dp[2]=dp[0]+dp[1]=1+1=2。同理dp[3]=dp[1]+dp[2]=1+2=3…最后我们通过方程不断转移,就能得到dp[n]的值了。

4.分析流程图

闫式dp分析法都可以使用上述图来进行分析。

作者:Avalon Demerzel,喜欢我的博客就点个赞吧,更多紫书知识点请见作者专栏《紫书学习笔记》

【紫书第九章】动态规划(DP)常见模型汇总与DP问题分析方法相关推荐

  1. 信息学奥赛一本通(C++版) 第二部分 基础算法 第九章 动态规划

    总目录详见:https://blog.csdn.net/mrcrack/article/details/86501716 信息学奥赛一本通(C++版) 第二部分 基础算法 第九章 动态规划 第一节 动 ...

  2. OpenGL蓝宝书第九章学习笔记:片段着色器和帧缓存

    前言 本篇在讲什么 OpenGL蓝宝书第九章学习笔记之片段着色器和帧缓存 本篇适合什么 适合初学OpenGL的小白 本篇需要什么 对C++语法有简单认知 对OpenGL有简单认知 最好是有OpenGL ...

  3. day46第九章动态规划(二刷)

    今日任务 139.单词拆分 关于多重背包,你该了解这些! 背包问题总结篇! 关于多重背包,力扣上没有相关的题目,所以今天大家的重点就是回顾一波自己做的背包题目吧. 139.单词拆分 题目链接: htt ...

  4. 【水文模型】05 参数不确定性分析方法

    摘录自<流域水文模型参数不确定性量化理论方法与应用>第5章. 概述 #mermaid-svg-EhUXzi8dmphIEQsb .label{font-family:'trebuchet ...

  5. 六种常见的「用户行为」分析方法

    日常的用户行为分析中,常用的六大分析方法有: 行为事件分析 页面点击分析 用户行为路径分析 用户健康度分析 漏斗模型分析 用户画像分析 用户分析能够更好地了解用户的行为习惯,发现产品在推广.拉新.用户 ...

  6. dx12 龙书第九章学习笔记 -- 纹理贴图

    1.纹理与资源的回顾 我们其实很早就接触过纹理了,之前的深度缓冲区与后台缓冲区,它们都是通过ID3D12Resource接口表示,并以D3D12_RESOURCE_DESC::Dimension成员中 ...

  7. python - 啃书 第九章 文件访问

    概述 计算机文件是存储在外部存储器上的数据集合.通常计算机处理的大量数据都是以文件的形式组织存放的,操作系统也是以文件为单位对数据进行管理的. 每个文件都有一个文件名,文件名由基本名和扩展名组成,不同 ...

  8. 第九章 动态规划-1278:【例9.22】复制书稿(book)

    1278:[例9.22]复制书稿(book) 时间限制: 1000 ms 内存限制: 65536 KB [题目描述] 现在要把m本有顺序的书分给k个人复制(抄写),每一个人的抄写速度都一样,一本书不允 ...

  9. 玩转算法之面试第九章-动态规划

    动态规划: 9-12 斐波那契数列 对重复计算,进行优化,进行记忆化搜索 假设基本的问题已经被解决,依次内推. 动态规划:将原问题拆解成若干个子问题,同时保存子问题的答案,使得每个子问题只求解一次,最 ...

  10. 第九章 动态规划-1261:【例9.5】城市交通路网

    1261:[例9.5]城市交通路网 时间限制: 1000 ms 内存限制: 65536 KB 提交数: 3909 通过数: 2854 [题目描述] 下图表示城市之间的交通路网,线段上的数字表示费用,单 ...

最新文章

  1. Android 判断是否网络连接, 判断是否为WIFI,移动网络以及跳转网络设置界面
  2. FASTQ! BAM! VCF
  3. 《CSS 禅意花园》读书笔记1
  4. python导入模块找不到什么原因_找不到Python导入模块错误
  5. 凭实力搞砸公司重大项目,老板看到直呼内行
  6. Java Web学习总结(23)——Distributed Configuration Management Platform(分布式配置管理平台)
  7. python 系统当前时间向前推2天_当前日期往前推N天,当前日期往后推N天
  8. 每天一个linux命令(55)--at命令
  9. React 路由 中 BrowserHistory 刷新报404
  10. 文件内容快速搜索工具(Everything、Listary、DocFetcher)下载
  11. iOS-性能优化的那些事
  12. Why do we insist? 打卡
  13. 【笔记本Windows的两个ctrl键失效解决办法大全解】
  14. 串口发送+RAM+VGA传图
  15. 温度补偿 matlab,基于传感器温度补偿方法的双指数函数模型的温度补偿算法设计...
  16. 有趣的巴什博弈(Bash Game)
  17. Android AccountManager帐号管理(一)
  18. java全栈系列之JavaSE-面向对象(类与对象的创建)032
  19. 【转载】特来电电动汽车群智能充电系统,充电网、车联网、互联网“三网融合”新能源互联网平台
  20. PowerManager屏幕休眠断网与距离感应器P-Sensor

热门文章

  1. excel数据导入到 mysql 中
  2. 学习OpenCV——SVM 手写数字检测
  3. 第24周SDAI缓解能否预测远期RA骨破坏受抑制
  4. CSS中的position 和z-index
  5. 还原 idea undo commit
  6. 关于 myeclipse 里面没有 add hibernate capabilities 问题解决方法
  7. SAP Brazil J1BTAX 为税收例外创建税收组(翻译)
  8. URLRewrite 在 iis6+iis7中的配置
  9. 稳定不掉线,翀旭用飞鱼星解决高密Wi-Fi接入
  10. 跟我一起数据挖掘(22)——spark入门