算法入门--动态规划
动态规划专题
1. 递归问题
从最小值走上几组寻找、推断规律.
1. 走台阶问题
- 题目:每次只能往前走 1 级或 2 级,问输入楼梯级数时,一共有多少中走法
- 思路:假设 f(n-2),f(n-1) 都已经求出,那么不难得知楼梯级数为 n 时,等价于 n-2 级楼梯时往前走两步或 n-1 级时往前走 1 步。
- 状态转移方程:
dp[n] = dp[n-1] + dp[n-2]
- 代码:太简单了,就是斐波那契数列
2. 全错问题
- 题目:输入要发送的邮件总数目 n(1<1n<=20),试求给这 n 个人发邮件,全部发错的可能排列方式的数量。
- 思路:n 个全错:① n-1个全错,然后将第 n 个不断和前 n-1 个交换,得到的还是全错排序 ② n-1 个中只有一个对的(即 n-2 个全错),将第 n 个和这唯一一个对的交换
- 状态转移方程:
dp[n] = dp[n-1]*(n-1)+dp[n-2]*(n-1)
- 代码:还是很简单。
2. 最长递增序列
题目:从数列中按照先后顺序取出数字,组成新数列且递增,求符合该要求的最长子列长度。
解法:dp[i] 表示考虑第 i 个字符时的最长子序列。外层 for 循环数列每一个元素,内层 for 将 list[i] 和 list[j] 比较,判断 list[i] 可以插在哪个子列后面,则更新子列。如果可以插在某个子列后面(假设子列取到前 j 个元素),那么 dp[i] = dp[j]+1
状态转移方程:
dp[i] = max{1, dp[j]+1 | aj<ai && j<i}
边界条件:dp[i] = 1
代码
// 炮弹防御问题:输入炮弹个数、每个炮弹高度,每次拦截炮弹的高度不能超过前一次,求最多拦截多少个炮弹 // 测试数据: 300 207 155 300 299 170 158 65 // 输出:6 void topic03(){int n=0, dp[100], list[100], ans=0;scanf("%d", &n);for (int i=0; i<n; i++) scanf("%d", &list[i]);fill(dp, dp+n, 1);for (int i=0; i<n; i++){for (int j=0; j<i; j++)if (list[i] <= list[j]) dp[i]=max(dp[i], dp[j]+1);if (ans < dp[i]) ans = dp[i];}printf("%d\n", ans); }
3. 最长公共子串
- 题目:有两个字符串 S1 和 S2,求一个最长公共子串,即求字符串 S3,它同时为 S1 和 S2 的子串,且要求它的长度最长,并确定这个长度
- 解法:A[i]、B[j] 分别为 S1、S2 的前 i、j 个长度的子串,那么 dp[i] 的值等于A[i-1] 与B[j-1] 的最长子串数 +1 如果 list[i]==list[j],否则等于 max{ A[i]和B[j-1]的最长子串,A[i-1]和B[j]最长子串 }
- 状态转移方程:
dp[i][j] = dp[i-1][j-1]+1 | list[i]==list[j]
或dp[i][j] = max{ dp[i][j-1], dp[i-1][j] }
边界条件:dp[i][0]=0, dp[0][j]=0
- 代码:
for (int i=0; i<=len(s1); i++) dp[i][0] = 0; for (int i=0; i<=len(s2); i++) dp[0][i] = 0; for (int i=1; i<=len(s1); i++)for (int j=1; j<=len(s2); j++)if (s1[i] == s2[j])dp[i][j] = dp[i-1][j-1]+1;elsedp[i][j] = max(dp[i][j-1], dp[i-1][j]);
4. 最长回文子串
- 题目:给出一个字符串 S,求 S 的最长回文子串
- 分析:令 dp[i][j] 表示 S[i] 到 S[j],
- 如果 S[i] == S[j] 那么只要 S[i+1] 到 S[j-1] 是回文串,那么 S[i] 到 S[j] 就是回文串,若 S[i+1] 到 S[j-1] 不是回文串,那 S[i] 到 S[j] 也不是回文串。
- 如果 S[i] != S[j],那么 S[i] 到 S[j] 一定不是回文串。
- 状态转移方程:
d p [ i ] [ j ] = { d p [ i + 1 ] [ j − 1 ] , S [ i ] = S [ j ] 0 , S [ i ] ̸ = S [ j ] dp[i][j] = \begin{cases} dp[i+1][j-1] & \text{, } S[i]=S[j] \\ 0 & \text{, } S[i]\not =S[j] \end{cases} dp[i][j]={dp[i+1][j−1]0, S[i]=S[j], S[i]̸=S[j]
边界: d p [ i ] [ i ] = 1 , d p [ i ] [ i + 1 ] = ( S [ i ] = = S [ i + 1 ] ) ? ( 1 ) : ( 0 ) ; dp[i][i] = 1,dp[i][i+1]=(S[i]==S[i+1])? (1): (0); dp[i][i]=1,dp[i][i+1]=(S[i]==S[i+1])?(1):(0); - 代码
int MAX = 2001;
char in[MAX];
int dp[MAX][MAX];
void MaxSubStr(){scanf("%s", in);fill(dp, dp+MAX*MAX, 0);int ans=1, len=strlen(in);// 初始化 dp[i][i] 与 dp[i][i+1]for (int i=0; i<len; i++){dp[i][i] = 1;if (i<len-1 && in[i]==in[i+1]){dp[i][i+1] = 1cccccccccccc; ans=2;}}// 状态转移方程for (int L=3; L<len; L++) // 枚举子串长度,从 3 开始for (int i=0; i+L<=len; i++){ // 枚举子串起始位置int j = i+L-1; if (in[i]=in[j] && dp[i+1][j-1]==1){ // 如果收尾相等且[i+1,j-1]回文,当前串为回文串dp[i][j] = 1; ans = L;}}printf("%d\n", ans); // 输出最大值
}
5. 状态转移方程
1. 最低疲劳度问题
2. 分柑橘问题
6. 背包问题:0-1 背包
- 题目:每个物品有其价值和重量且每种物品数量为1,背包允许最大负重 T,求怎样放物品能使得背包价值最大
- 思路:我们求背包在负重为 j 时背包的最大价值,j 的范围为 [0, T],对于背包处于重量 j 时,我们从 1 开始循环考虑到第 n 种物品,对于物品 i,其有两种状态:在背包中、不在背包中。不在背包中时,背包的最大价值等于考虑 i-1 且背包重 j 时背包的最大价值。在背包中时,背包的最大价值等于考虑 i-1 且背包中为 j-items[i].cost 时的价值加上当前物品 i 的价值。所以当考虑 i 时,背包的最大价值为上述两个价值中的最大值。
- 状态转移方程:
dp[j] = max{ dp[j-items[i].cost]+items[i].value, dp[j] }
边界条件:dp[i]=0
(代表背包重量为 0 时,考虑任何个物品价值都为 0) - 代码:
struct Item{int c;int v; }items[1001]; int dps[1001]; void topic06(){int n=0, m=0;scanf("%d %d", &n, &m);for (int i=1; i<=m; i++) scanf("%d %d", &items[i].c, &items[i].v);for (int i=0; i<=n; i++) dps[i] = 0; // 当背包重量为 0 时,最大价值为 0for (int i=1; i<=m; i++)for (int j=n; j>=items[i].c; j--)dps[j] = max(dps[j-items[i].c]+items[i].v, dps[j]);printf("%d\n", dps[n]); }
0-1 背包:每个物体的个数为 1
分析:dp[i][j] 表示当考虑第 i 个物品且背包负重为 j 时背包的最大价值。有如下状态转移方程:
dp[i][j] = max{ dp[i-1][j-cost[i]]+value[i], dp[i-1][j] }
解释:当我们考虑当前的背包状态时,背包最大价值为 dp[i][j],首先要确定一个事实,当前背包的重量 j 是已经确定了的,但是 j 的组成方式有两个不同的情况:
- 当前物品 i 在背包中,此时 j = other01 + cost[i]
- 当前物品不在背包中,则 j = other01 + (cost[x]+cost[y]),其中 cost[x]+cost[y]<=cost[i]。
所谓当前物品不在背包中(也就是 ②),即把 ① 中的物品 i 移除背包,而用 i 之前的几种物品组合代替 i 放到背包中x
说两点:
1.组合后的重量不一定完全等于 i 的重量。
2.可以看到,物品 x、y 和 i 不会同时出现在背包中,因为 other 的重量在两种情况中是相同的,放入了 x、y 就无法放入 i所以,当前物品 i 在背包中时:
- 背包的最大价值 = 当前物品不在背包且没有其他物品替代当前物品时背包的最大价值 + 当前物品的价值。
反之,当前物品 i 不在背包时:
- 背包的最大价值 = 当前物品不在背包且当前物品空出的重量被其他物品代替时背包的最大价值
*注:我们无需考虑第一种情况中的 dp[i-1][j-cost[i]] 和第二种情况中的 dp[i-1][j],这些会在程序递推的过程中会算出来(这也就是动态转移方程的精髓!)
优化:我们的动态转移方程为:
dp[i][j] = max{ dp[i-1][j-cost[i]]+value[i], dp[i-1][j] }
很显然,我们要用两个 for 循环来分别迭代 i 和 j,但我们发现 max 函数中的两个状态的物品数量都是 i-1,事实上我们可以把 dp[i][j] 优化为 dp[j],即去掉考虑第几个物品这个属性,可以这么优化的原因在于我们对只考虑 x 个物品(x < m)并时背包的最大价值是多少并不感兴趣,因此我们在求考虑第 i 个物品且重量为 j 时的最大价值时,我们直接用新的数据覆盖掉上一次(i-1, j)时的数据即可。我们在递推中利用了上一次(i-1, j)数据,但我们最终不需要,因此我们直接覆盖他就行。
(我比较喜欢这么写是因为我认为使用 dp[i] 比使用 dp[i][j] 看起来要优雅~)*注:此种方式要注意:内层 for 循环,求考虑 i 个物品背包不同重量对应的最大价值时,必须由大到小更新。因为 j-items[i].c < j,如果 j 由小到大,则意味着 dp[j-items[i].c] 在 dp[j] 已经被更新过了(小的先被更新)!
7. 背包问题:完全背包
- 题目:每个物品数量不限,背包必须满
- 思路:完全背包的解法和 0-1 背包的解法基本相同,唯一的差别在于怎么更新 dp[j]。完全背包和 0-1 背包相比,不同点在于 0-1 背包中物体只有一个,其状态也只有:放入、不放入两种,但是完全背包中物体数量不限,相当于物体状态有放入 1 个、2 个、3 个……不确定种。完全背包的关键在于如何在 0-1 背包的基础上实现描述一种物品在背包中放入不同的数量。我们在 0-1 背包中为了防止更新 dp[j] 时,dp[j-items[i].c] 已经被更新过的情况,所以让 j 从大到小更新。而完全背包刚好相反,我们就是要在更新 j 之前将 dp[j-items[i].c] 更新了(这里的更新,就是在背包中加入一个物品 i,这样我们遍历 j 从 items[i].c - t,这样我们就能实现对同一物品的不同数量的分析)
- 状态转移方程:
dp[j] = max{ dp[j-items[i].cost]+items[i].value, dp[j] }
- 代码:
void topic06(){int n=0, m=0;scanf("%d %d", &n, &m);for (int i=1; i<=m; i++) scanf("%d %d", &items[i].c, &items[i].v);for (int i=0; i<=n; i++) dps[i] = 0; // 当背包重量为 0 时,最大价值为 0for (int i=1; i<=m; i++)for (int j=items[i].c; j<=n; j--)dps[j] = max(dps[j-items[i].c]+items[i].v, dps[j]);printf("%d\n", dps[n]); }
8. 背包问题:多重背包
- 题目:每个物品数量为1,背包可以不满
- 思路:将物品拆分为 x 份,每份的个数分别为: 2 0 , 2 1 , 2 2 , … … 2 x − 1 , k − 2 x + 1 2^0, 2^1, 2^2, …… 2^{x-1}, k-2^x+1 20,21,22,……2x−1,k−2x+1。可以由数学规律可知,这 x 份物体能表示出[1, k]内的任意一个值。这样处理之后就是一个 0-1 背包问题了。所以解题流程如下:
- 将每种物品分为 x 份保存在 lists 里面。(要格外注意,lists 的容量应该大过输入的种类)
- 将 lists 看做新的输入,对他进行 0-1 背包处理
- 状态转移方程:
dp[j] = max{ dp[j-items[i].cost]+items[i].value, dp[j] }
- 代码:
Item lists[2001]; void topic07(){int t=0, n=0, c=0, v=0, s=0, p=1;for (int i=1; i<=n; i++){scanf("%d %d %d", &c, &v, &s);for (int x=1; s>x; s-=x,x*=2,p++)lists[p].c = c*x, lists[p].v = v*x;}// 0-1 背包处理for (int i=1; i<=n; i++)for (int j=p-1; j>=lists[i].c; j--)dps[j] = max(dps[j], dp[j-list[i].c]+lists[i].v);printf("%d\n", dps[t]); }
9.基于DAG的动态规划
对于二元关系,将题目抽象为求有向无环图的最长、短路径
- 题目:输入 n 个矩形(每个矩形包括长宽信息),大矩形可以嵌套小矩形,求最大的嵌套个数。
- 分析:把每个矩形看做一个节点,矩形 a 可以嵌套在矩形 b 中,则认为 a 指向 b(存在一条单向路径),则可以将输入抽象为一个有向无环图,而求最大嵌套个数,即求最长路径长度。
- 状态转移方程:
dp[i] = max{dp[i], dp[j]+1}
- 代码:
// 求以 i 节点为起始节点时的最长路径
// 如果当前点已经求过最长路径,则退出。
// 否则:枚举其所有的邻接点,以当前节点为起始点的最长路径长度为原状态(
// 上一个最大值)与以当前邻接点为起始点的最长路径长度加 1
int items[1001][2];
int G[1001][1001];
int dp[1001];
int GetMaxLongPath(int i){if (dp[i] != 0) return dp[i]; // 记忆化搜索。减少损耗。for (int j=1; j<=n; j++)if (G[i][j] == 1) // 如果联通dp[i] = max(dp[i], GetMaxLongPath(j)+1);return dp[i];
}
int main(){// ① 输入与初始化int n=0;scanf("%d", &n);for (int i=1; i<=n; i++)scanf("%d %d", &items[i][0], &items[i][1]);fill(G, G+1001*1001, 0);fill(dp. dp+1001, 0);// ② 建图for (int i=1; i<=n; i++)for (int j=1; j<=n; j++)if (items[i][0]>items[j][0] && items[i][1]>items[j][1]) G[i][j] = 1;// ③ 递归求解for (int i=1; i<=n; i++)GetMaxLongPath(i);printf("%d\n", );return 0;
}
10. 背包问题总结
注意,背包问题最终都可以化归到 0-1 背包问题上来,所以我们可以看到状态转移方程都是一样的。注意,上述问题都是求最大背包数量,如果要求最小背包数量应该也会修改代码(简单的修改为 min 即可)。
算法入门--动态规划相关推荐
- Java入门算法(动态规划篇2:01背包精讲)
本专栏已参加蓄力计划,感谢读者支持❤ 往期文章 一. Java入门算法(贪心篇)丨蓄力计划 二. Java入门算法(暴力篇)丨蓄力计划 三. Java入门算法(排序篇)丨蓄力计划 四. Java入门算 ...
- 牛客:【2021秋季算法入门班第七章习题:动态规划1】部分题解:方块与收纳盒、舔狗舔到最后一无所有、可爱の星空、[NOIP1999]拦截导弹
题单链接:牛客竞赛_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ (nowcoder.com) P1001 方块与收纳盒 传送门:1001-方块与收纳盒_2021秋季 ...
- 递归算法 流程图_什么是算法?如何学习算法?算法入门的学习路径
什么是算法? 有一个很著名的公式 "程序=数据结构+算法". 曾经跟朋友吃饭的时候我问他什么是算法,他说算法嘛,就是一套方法,需要的时候拿过来,套用就可以,我吐槽他,他说的是小学数 ...
- NOI入门级:算法之动态规划
糖糖讲动态规划算法,找零钱完全背包问题,LeetCode 322 糖糖讲动态规划算法,找零钱完全背包问题,LeetCode 322_哔哩哔哩_bilibili 程序员面试再也不怕动态规划了,看动画,学 ...
- 《算法图解》读书笔记—像小说一样有趣的算法入门书
前言 学习算法课程的时候,老师推荐了两本算法和数据结构入门书,一本是<算法图解>.一本是<大话数据结构>,<算法图解>这本书最近读完了,读完的最大感受就是对算法不再 ...
- 语法入门*算法入门题单
作者:王清楚 链接:https://ac.nowcoder.com/discuss/817596?type=101&order=0&pos=1&page=4&chann ...
- 【新手上路】语法入门算法入门题单
作者:王清楚 链接:[新手上路]语法入门&算法入门题单_ACM竞赛_ACM/CSP/ICPC/CCPC/比赛经验/题解/资讯_牛客竞赛OJ_牛客网 来源:牛客网 介绍:本题单分为语法入门和算法 ...
- 算法复习——动态规划篇之最长公共子序列问题
算法复习--动态规划篇之最长公共子序列问题 以下内容主要参考中国大学MOOC<算法设计与分析>,墙裂推荐希望入门算法的童鞋学习! 1. 问题背景 子序列:将给定序列中零个或多个元素(如字符 ...
- LeetCode《算法入门》刷题笔记(31 题全)
LeetCode<算法入门>刷题笔记(31 题全) 二分查找 1. 二分查找 _解法1:二分搜索(迭代) 解法2:二分搜索(递归) 2. 第一个错误的版本 _解法1:二分 3. 搜索插入位 ...
最新文章
- Linux网络 - 数据包的发送过程
- 【OpenCV十六新手教程】OpenCV角检测Harris角点检测
- LAMP部署搭建————重要文件备份
- 斯坦福CS231n项目实战(四):浅层神经网络
- Spring MVC——POST请求application/x-www-form-urlencoded方式参数嵌套POJO解决方案
- php导出csv文件乱码问题解决方法
- [转载] application/json 四种常见的 POST 提交数据方式
- Javascript this 的一些学习总结
- 新手学电脑入门教程_3Dmax难学嘛?3Dmax到底好不好学
- 日本公司为东京大学开设区块链课程捐款80万美元
- vue中手机号码+座机号码、邮箱正则校验规则封装
- python 批量修改文件夹和子文件夹的名称
- 计算机管理 没有初始化,win7系统电脑新增的硬盘没有初始化的解决方法
- 移动APP切图术语解读:什么是@1x @2x和@3x
- 微信公众号第三方平台授权流程
- 监控摄像机安装的正确位置是哪里
- 基于Java的员工管理系统
- 原型图 线框图_16个原型设计和线框图设计工具
- 亚信科技前端实习面试题
- 程序员与女朋友相处之道