动态规划

三种常见实现方法

对于一个斐波那契数列,我们想要求第n项的值,就需要一项一项的递归来求

来看代码

  1. f[o] = 0;
  2. f[1] = 1;
  3. for (int i = 2; i <= n; ++i)
  4. f[i] = f[i-1] + f[i-2];
  5. cout << f[n] << endl;

这种的属于用其他的结果来算自己的结,如果我们换一种写法,就成了用自己的值更新别人的值

  1. f[o] = 0;
  2. f[1] = 1;
  3. for (int i = 0;i <= n; ++i)
  4. {
  5. f[i + 1] += f[i];
  6. f[i + 2] += f[i];
  7. }

这两种方式都是存在的,但是存在某些题会有卡掉不同写法的毒瘤操作,所以两种都要掌握

最后一种写法就是

记忆化搜索

  1. int dfs (int n) {
  2. if (n == 0)
  3. return 0;
  4. if (n == 1)
  5. return 1;
  6. return dfs(n - 1) + dfs(n - 2);
  7. }

时间复杂度就是 O(F[n])的,因为我们只能是用1,0来慢慢加上去的

有意思的是斐波那契的第n项和2^n差不多是一个级别的,死慢,为什么??

你考虑f[n-1]被算了两次,这就造成了时间上的极大浪费,所以我们考虑能不能对已经求出来的数值进行记录, 这就是记忆化搜索

g[i]代表这个数有没有被算出来

  1. int dfs (int n) {
  2. if (n == 0)
  3. return 0;
  4. if (n == 1)
  5. return 1;
  6. if (g[n])
  7. return f[n];
  8. f[n] = dfs(n - 2) + dfs(n - 1);
  9. g[n] = true;
  10. return f[n];
  11. }

如果已经被算出来过,就直接返回就行,所以就不用二次计算

终于进入动态规划啦

状态是说你要算什么,转移方程是说你要怎么算,对于无后效性的理解,就是动态规划的所有状态之间组成了一个DAG

拿斐波那契举例

1.状态就是f[n]

2.转移方程就是f[n] = f[n-1] + f[n-2]

当然这是一个无后效性的东西

当我们碰到一些转移顺序比较变态的题呢?考虑到状态之间都是DAG,我们就可以对状态跑一边拓扑排序,然后就可以for一遍就行了

一些特殊类型的DP

主要就是以下几种

背包问题

最easy的问题

luogu 1616 疯狂的采药

  1. 给定n种物品和一个背包。
  2. 物品i的体积是vi,其价值为wi,背包的容量为m
  3. 应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

怎么设计状态呢?考虑要一个物品一个物品的放,所以有一个变化量就是我现在放好了前i个物品了,第二个维度就是我现在放进去的物品体积之和是多少

所以这个题的状态就应该是f[i][j] 表示已经试过了放前i个物品(不保证都被放进去),消耗的体积和是j

转移方程就是考虑下一个物品怎么放,显然只有放或者不放,那么状态转移方程就是

  1. 从别人更新自己
  2. f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i] ] + w[i]);

代码如下

  1. int n, m, w[233], v[233];
  2. int f[233][12345678];
  3. int main()
  4. {
  5. cin >> n >> m;
  6. for (int i = 1; i <= n; ++i)
  7. cin >> w[i] >> v[i];
  8. for (int i = 1; i <= n; i++)
  9. for (int j = V; j >= w[i]; j--)
  10. f[j] = max(f[j], f[j - w[i]] + v[i]);
  11. }

物品可以放无限次的背包(无限背包问题)

枚举一下一个物品放几个就行了

  1. for (int i = 1; i <= n; ++i)
  2. for (int j = 0; j <= n; ++j)
  3. for (int k = 0; k * v[i] <= j; k++)
  4. f[i][j] = max(f[i][j], f[i][j - k * v[i]] + k * w[i]);

但是你用的是三重循环, 那肯定会t的飞起

我们考虑一个问题,同一个物品取多个,其实也就是重复转移了多少次对吧,所以我们改一下代码

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

行了,完事

有限背包

对于有限次的个数,我们可以把它拆分成几个集合,利用二进制的方式来拆分,比如拆30

我就可以拆成 1, 2, 4, 8, 15。值得注意的是我们最后那个数是不一定符合2的整数次幂的,实际上就是最后的那个差而已

上代码

  1. int n, m, w[233], v[233], cnt = 0;
  2. int f[233][12345678];
  3. int main()
  4. {
  5. cin >> n >> m;
  6. for (int i = 1; i <= n; ++i)
  7. {
  8. cin >> w[i] >> v[i] >> z;
  9. while (x <= z)
  10. {
  11. v[++cnt] = v[i] * x;
  12. w[cnt] = w[i] * x;
  13. z -= x;
  14. x *= 2;
  15. }
  16. if (z > 0)
  17. {
  18. v[++cnt] = v[i] * z;
  19. w[cnt] = w[i] * z;
  20. }
  21. }
  22. for (int i = 1; i <= n; i++)
  23. for (int j = V; j >= w[i]; j--)
  24. f[j] = max(f[j], f[j - w[i]] + v[i]);
  25. }

背包就这么讲完了

基础DP

例题一 P1216 [IOI1994][USACO1.5]数字三角形 Number Triangles

题面我就不贴了,可以想到的肯定是dp了,不然他也不会在这啊hhh

考虑到对于一个点,是他从左上角还是右上角过来,所以我们就能得到转移方程

  1. f[i][j] = max(f[i - 1][j], f[i - 1][j - 1]);

行了写完了

改造题目(EX NumberTriangles)

我们要使得找出来的权值和%m之后的值是最大的。

起初我的想法是用一个结构体来存实际权值和取模后的权值,但是是不行的,因为不再满足最优子结构原则了。(就是说实际权值和取模权值之间的大小没有必然联系,所以我们无法用状态转移方程来求最大最小)

我们考虑加一个维度,开一个bool数组f

f [i] [j] [k]

代表走到第i行第j列是路径权值和 % m = k 可不可能,那我们怎么转移呢

还是考虑一个点只有可能从它的左上方和右上方求值过来,那么我们就能得出状态转移方程了

  1. f[i][j][k] = f[i - 1][j - 1][(k - a[i][j]) % m] || f[i - 1][j][(k - a[i][j]) % m]

边界情况的话,就是

  1. f[1][1][a[1][1] % m] = true(按照状态的定义来理解就行,很好理解)

然后我们就遍历一遍最终结果就可以了

  1. for (int i = 1; i <= n; ++i)
  2. {
  3. for (int j = 1; j <= m; ++j)
  4. {
  5. if (f[n][i][j])
  6. ans = max(ans, j);
  7. }
  8. }

我寻思着代不代码也无所谓了呗,但是反正zhx写了那我就无耻白嫖一下hhh

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. const int Mod = 100;
  4. const int maxn = 25 + 5;
  5. int maps[maxn][maxn];
  6. bool dp[maxn][maxn][100];
  7. int main()
  8. {
  9. int n;
  10. while (~scanf("%d", &n))
  11. {
  12. memset(maps, 0, sizeof(maps));
  13. memset(dp, 0, sizeof(dp));
  14. for (int i = 1; i <= n; i++)
  15. for (int j = 1; j <= i; j++)
  16. scanf("%d", &maps[i][j]);
  17. dp[0][0][0] = dp[0][1][0] = true; //这个初始化很重要
  18. for (int i = 1; i <= n; i++)
  19. for (int j = 1; j <= i; j++)
  20. {
  21. for (int k = 0; k <= 99; k++)
  22. {
  23. int now;
  24. if (dp[i - 1][j][k]) //当上一个这个数值存在时就可以加上当前状态的值
  25. {
  26. now = (maps[i][j] + k) % Mod;
  27. dp[i][j][now] = true;
  28. }
  29. if (dp[i - 1][j - 1][k])
  30. {
  31. now = (maps[i][j] + k) % Mod;
  32. dp[i][j][now] = true;
  33. }
  34. }
  35. }
  36. int ans = 0;
  37. for (int i = 1; i <= n; i++)
  38. for (int k = 0; k <= 99; k++)
  39. {
  40. if (dp[n][i][k])
  41. {
  42. ans = max(ans, k);
  43. }
  44. }
  45. printf("%d\n", ans);
  46. }
  47. }

一些话

钟神的dp极其牛逼,按照他本人的说法,对于一个dp题目,当前维度解决不了的时候,那就再加一个维度,总能跑出来的

最长上升子序列

dp[i]表示以i为结尾的最长上升子序列

转移的话就是枚举i前面的aj小于Ai的数

  1. for (int i = 1; i <= n; ++i)
  2. for (int j = 1; j <= i; ++j)
  3. f[i] = max(f[i], f[j] + i);

不是很难啊,时间复杂度是N^2

但是我们把数据加强到n<=1e5了,那么怎么办呢

我们肯定不能再跑一次O(n^2)了啊,这里就考虑用线段树,就是运用一次昨天学习的线段树求区间最值的方法,把时间复杂度直接降到logn,这样一来,总的时间复杂度就是nlogn

v=max(a1,a2,a3,.....an),每次计算f[i]

我们建一颗长度为n的线段树,然后直接进行划分,就是说两端区间的最长上升子序列就肯定是他的两个儿子的和,这样算起来就快不少了

区间DP

区间dp的特征就是给你一堆东西,你每次只能合并两个相邻的东西,一般状态就是

f [l] [r]

代表第l堆石子到第r堆石子的最小代价是多少

合并石子luogu1880

有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动相邻的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费最小(或最大)。

F[l][r]

表示把第l堆石子到第r堆石子合并为一对石子的代价

我们会发现做任何DP题的时候的都是走老三步(找状态,找边界条件,找状态转移方程)

这个题的边界条件就是L=R的时候,所以F[l][r]=0(l=r)

那么我们再来看状态转移方程,我们最后一次合并一定是把两堆石头合并成一堆石头,是把左边某一堆和右边某一堆,由于题目的限制,我们可以发现不管怎样合并,石头的顺序是不变的。

所以我们一定是可以找到一条分界线,使得左边是A1Ap的结果,右边是Ap+1Ar,那么我们只需要枚举一下p的位置就可以了。所以状态转移方程就是(所有的)

  1. min(f[l][p] + F[p+1][r] + sum[l][r]);

看代码

  1. int z[manx],f[maxn][maxn];
  2. int main()
  3. {
  4. cin>>n;
  5. for(int a=1;a<=n;a++)
  6. cin>>z[a];//表示石子数
  7. memset(f,0x3f,sizeof(f));//初始化为无穷大
  8. for(int a=1;a<=n;a++)
  9. f[a][a]=0;
  10. //枚举左端点,右端点,断点
  11. for(int l=1;i<=n;l++)//枚举左端点
  12. {
  13. for(int r=l+1;r<=n;i++)//枚举右端点
  14. {
  15. for(int p=l;p<r;p++)
  16. {
  17. f[l][r]=min(f[l][r],f[l][p]+f[p+1][r]+sum[l][p]);
  18. //左边合并的代价+右边合并的代价+这段区间的石子之和
  19. }
  20. }
  21. }
  22. cout<<f[1][n]<<endl;
  23. return 0
  24. }

先把所有的数组赋值,然后改一下边界条件

我们枚举一个左端点,枚举一个右端点,再枚举一个断点

很不幸,这种枚举方式是错的

贴图片

我明明认真听了好久啊喂

因为在任何时候,他都不满足DP的阶段性

你看啊,你在算f[1][n]时要用到f[2][n],但它还没有被算过(什么鬼东西)

所以我们就得枚举区间长度,然后修改一下代码就可以了

一个不知道干啥的题(惆怅

给你n个矩阵,自定义顺序,使得运算次数最少

和上一个题比较像,大概意思就是把一堆矩阵合并成一个的最小代价

转移方程就是

  1. s[j][i] = max(s[j][i], s[j][k] + s[k+1][i] + e[j] * e[k+1] * e[i+1]);

状压DP

在平面上有n个点,要求从1号点出发把每一个点都走一遍,求最短距离,这个和最小生成树没啥关系,就是个非常简单的二维平面图,你可以随便走(大雾

首先,我们没必要走两次同一个点,然后01表示一个点走没走过,我们就可以很容易的用一个n位二进制数来存当前状态下的最短距离,做到了状压

转移的话,枚举一个j

行了黈力就行

种草

john要在一片牧场上种草,每块草坪之间没有相邻的边

状态dp[i][j]表示前i行的草都种完了,s表示第i行的草种成了什么样

k国王

f[i][j][k]表示前i行的国王都种完了,s表示第i行的

数位dp

读入两个正整数L,R,问从L到R有多少个数,答案就是L-R+1,这肯定是没有什么挑战性啊是吧,为了挑战自己,我们用一下数位DP(大雾)

有一个叫做前缀和转换的东西

D5(太长了md没写完)的更多相关文章

  1. Word 双栏排版最后多一页空白页删不掉、左栏文字没写完就到右栏了

    1. 问题 问题:Word双栏排版,最后多一页空白页,删不掉.如图: 原因分析:删不掉是因为末尾文字处其实有个下一页分节符,只不过可能看不到. 如何清晰的看到? 视图 > 大纲,就可以看到了.如 ...

  2. XObject.java 对象还没写完,希望电脑不会丢失。坏笑,早点见。

    /*面向对象强调的是对象, 面向过程强调的是功能行为,打开行为,关闭行为,执行行为,把多个行为封装成对象执行更强大的功能就是面向对象,是把多个函数, 多 个行为封装在一起,单一的函数执行对象的功能太困 ...

  3. 【Luogu】【关卡2-7】深度优先搜索(2017年10月)【AK】【题解没写完】

    任务说明:搜索可以穷举各种情况.很多题目都可以用搜索完成.就算不能,搜索也是骗分神器. P1219 八皇后 直接dfs.对角线怎么判断:同一条对角线的横纵坐标的和或者差相同. #include < ...

  4. QBXT Day2主要是数据结构(没写完先占坑)

    简单数据结构 本节课可能用到的一些复杂度: O(log n). 1/1+1/1/.....1/N+O(n log n) 在我们初学OI的时候,总会遇到这么一道题. 给出N次操作,每次加入一个数,或者询 ...

  5. python Web抓取(一)[没写完]

    需要的模块: python web抓取通过: webbrowser:是python自带的,打开浏览器获取指定页面 requests:从因特网上下载文件和网页 Beautiful Soup:解析HTML ...

  6. 前端性能测试工具 : dynaTrace Ajax (还没写完)

    今天开始写这个工具, #什么是dynaTrace Ajax? 随着 jQuery.Dojo.YUI 等框架的兴起让构建 Web2.0 应用更加容易,但随之带来的定位等应用问题也越来越难,尤其是与性能相 ...

  7. D3(没写完

    说在博客前 这篇博客有许多使用到 STL 的地方,由于本人实在是记不全,所以我也参考了北大的一些教材,就别说我黈力了 QwQ 数据结构 今天讲的是数据结构啦(也是我这个蒟蒻最喜欢的 一些天天见面的好盆 ...

  8. mock的使用及取消,node模仿本地请求:为了解决前后端分离,用户后台没写完接口的情况下

    借鉴:https://www.jianshu.com/p/dd23a6547114 1.说到这里还有一种是配置node模拟本地请求 (1)node模拟本地请求: 补充一下 [1]首先在根目录下建一个d ...

  9. Codeforces Round 212 Div 2 报告(以前没写完,现在也没心情补了,先就这样吧)

    A. Two Semiknights Meet 题目大意:有一个8x8的棋盘,上面放有两个骑士,骑士以“田字”的方式走.每个方格都被定义为good或者bad,问骑士能否在good的格子中相遇? 由于骑 ...

随机推荐

  1. openCV3 Python编译指南

    这里主要对openCV官网的<Installation in Linux>文档进行了翻译和解释 原文见:https://docs.opencv.org/3.4.1/doc/tutorial ...

  2. Arch Linux 安装rust

    Arch Linux 安装rust 0. 参考 Rust Toolchain 反向代理使用帮助 1. 安装 安装rustup和toolchain yaourt -S rustup rustup ins ...

  3. ceph问题汇总

    1. [ceph_deploy][ERROR ]RuntimeError: Failed to execute command: yum -y install epel-release 解决方案 进入 ...

  4. maven中配置jboss仓库

    有两种方式,一种是在项目的pom.xml中<repositories>中添加,这是配置是针对具体的某一个项目,更多时候,我们想把jboss仓库作为所有项目的仓库,这就需要在maven的se ...

  5. 前端面试题-clearfix(清除浮动)

    一.浮动的概念 浮动的框可以向左或向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止.由于浮动框不在文档的普通流中,所以文档的普通流中的块框表现得就像浮动框不存在一样. 二.浮动的影响 1. ...

  6. 本地安装sass出错问题解析

    2016年3月23日个人博客文章--迁移到segmentfault 安装sass ruby安装因为sass依赖于ruby环境,所以装sass之前先确认装了ruby.先导官网下载个ruby 在安装的时候 ...

  7. 对React性能优化的研究-----------------引用

    JSX的背后 这个过程一般在前端会称为“转译”,但其实“汇编”将是一个更精确的术语. React开发人员敦促你在编写组件时使用一种称为JSX的语法,混合了HTML和JavaScript.但浏览器对JS ...

  8. C# 文件排序

    一.C#文件排序 1.按名称顺序排列 /// <summary> /// C#按文件名排序(顺序) /// </summary> /// <param name=&quo ...

  9. EasyPrtSc sec[1.2] 发布!

    //HOMETAG #include<bits/stdc++.h> namespace EasilyPrtSc{ //this namespace is for you to be mor ...

  10. (Java多线程系列六)join()的用法和线程的优先级

    join()的用法和线程的优先级 1.join()的用法 join()作用就是让其他线程处于等待状态 先看一个需求:创建一个线程,子线程执行完毕后,主线程才能执行 public class JoinT ...