| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 数据结构与算法 -> 7.1 基础DP -> 正文阅读 |
|
[数据结构与算法]7.1 基础DP |
7.1 基础DPDP在紫书那里也有一个单章介绍这个专项,不过当时并没有正式的介绍DP思想而是从例题直接入手的,黑书这里从基础开始一步一步讲解。 首先DP和贪心,分治法一样,DP并不指一个特定的算法,而是一种解决问题的算法思想,简单解释来说是将一个复杂的问题分割成相对简单的子问题,再一个个解决,最后得到复杂问题的最优解。 与分治法不同的是,分治法的子问题之间互相独立,每个子问题能够独立解决。DP的子问题,之间互相联系,最终解即是子问题的其中之一。 7.1.1 硬币问题有n种硬币,面值分别为v1,v2······,每种都有无限多。给定非负整数S,可以选用多少个硬币,使得面值之和恰好为S?输出硬币数目的最小值和最大值。 分析:几乎可以说是动态规划最为经典的问题之一了,紫书上已经做了非常充分的从求解到打印解的过程,这里只引用代码:
用的写法是类似记忆化搜索而非完全递归的写法,虽然黑书上记忆化搜索和递推是第二节的内容但其实放到第一节也无可厚非。 不过黑书在这里是将硬币面值放在了循环的外层,硬币和放在了内层,代码的编写上避免了if的判断(代码详见hdu2069)。 黑书后面拓展了一类问题: 所有硬币组合有n种硬币,面值分别为v1,v2·····vn,数量无限。输入非负整数s,选用硬币,使其和为s,输出所有可能的硬币组合总数。 如果问题只是这样,那和前面的硬币问题几乎没有任何区别。事实上会给上一个限制:可能的硬币组合使用的硬币个数不能超过x。 分析:添加了一个条件后明显感觉到dp的状态设置的太简单了,如果用dp[i]只能存储硬币组合总数,不能确定满足一定条件的硬币个数。 那如果给dp的状态添加一个限制来满足条件,就又出现了另一个问题:因为我们没有记录每种组合使用的硬币总数,我们无法确定dp后的新状态是否满足条件。 其实思考到这步,解决的方案也基本出来了:既然没有缺少硬币组合的硬币总数这个属性,我们给它添加一个即可。用dp[i][j]表示硬币和为i,使用硬币个数为j的方案总数,进行一个二维dp,最终将dp[s][0]~dp[s][100]想加即可。样题如下: hdu 2069 “Coin Change”分析:面值是给定的,就按照上面的思路打代码就可以了,dp的代码还是不太难的,不过对于多维度dp最需要注意的还是将哪个属性放在循环的哪一层。 可以发现紫书和黑书在设置属性的循环层数这里还是有区别的:
虽然没有学习记忆化搜索,但这种存储方案非纯递归的做法已经十分接近于记忆化搜索了。 7.1.2 0/1背包硬币问题几乎是dp问题最经典的问题之一,0/1背包问题就是最经典的问题没有之一。 dp问题中国的背包问题分为物品无限的背包问题和0/1背包问题,每种物品有无限多个就是物品无限的背包问题,每种物品只有1个就是0/1背包问题。当然还有种最简单的,一般背包问题,就是里面的物品甚至可以分割,这种用贪心法来处理就可以了。 0/1背包的解决方紫书学习那里已经介绍过了,就不重复介绍了。其实就是用d(i,j)表示将前i个物品装到容量为j的背包中的最大重量,d(i,j)=max(d(i-1,j),d(i-1,j-V[i])+W[i])(代表是否放入第i个物品),答案为d[n][C]。 hdu 2602 “Bone Collector”就是0/1背包问题。 分析:根据上面的状态转移方程有代码如下:
紫书上的写法基本一致,就是多一种一遍输入一边处理的写法,但由于我们这里不是输入一个v输入一个w,所以也不能用那种写法。 紫书和黑书这里同时介绍了一种小技巧——滚动数组,即用一个一维的数组来实现二维的dp,以节省空间。因为观察可以发现,每一行都是通过正上方元素的值和左上方元素的值取较大值得到的。那么我们将上一行当作原本的数组遗留在那里,用新的一行的元素来覆盖原来的一行就好了。
因为是一个逆序的更新,dp[j-V]中仍保存着上一行没有更改过的值,效果和二维dp是一样的。 这么做,虽然能够节省空间,但同样由于只保留了最后的状态,所以会损失很多信息,导致无法输出方案。 完全背包就是在内层的循环中进行相反顺序的更新:
hdu 1024 “Max Sum Plus Plus”将给定的长度为n的序列,选出m个不相交的子序列,使得这m个子序列的元素和最大。 分析:dp状态的定义还是很好想的,dp[i][j]表示前面j个数分成了i段的最大值,转移方程其实就是看第j个数字放在的位置。 第一种可能是第j个数字和最后的区间并成一组,于是考虑之前的j-1个数字分成i段的最大情况,在最后的区间并入即可,于是dp[i][j]=dp[i][j-1]+num[j]。 第二种可能是第j个数字单独的分成一段,前j-1个数中的前若干个数分成不包括第j-1个数在内的i-1段。 于是有:dp[i][j]=max(dp[[i][j-1],dp[i-1][k])+num[j],其中k=i-1(因为i-1段最少占用i-1个数),i,…,j-2(第j-1个数不能选取)。 当然这么做无论从空间还是时间上都是不合格的,不过我们注意到第二种可能,前j-1个数中的前若干个数分成不包括第j-1个数在内的i-1段,可以直接将它定义为一个新的状态pre[j]表示不包括num[j]的前i个数的最大和。于是有: dp[i][j]=max(dp[i][j-1],pre[j-1])+num[j]。 再用滚动数组优化掉第一维,dp[j]=max(dp[j-1],pre[j-1])+num[j]。 pre[j-1]=max{dp[k]},k=i-1,i,…,j-2,需要注意到的是,由于k最大等到j-2,pre的更新始终要慢dp更新一步,但为了更新顺利,也需要一个变量在更新pre[j-1]的同时,记录pre[j]的值,pre数组最大只求到pre[n-1],最终返回的是变量的值。
其实设置一个pre状态作为辅助是比较容易想到的,我个人觉得想清楚pre数组是怎么更新的,更新的时候为什么要慢一步比较困难。 hdu 4576 “Robot”多组输入n,m,l,r。表示在一个环上有n个格子,接下来输入m个w表示连续的一段命令,每个w表示机器人沿顺时针或者逆时针方向前进w格,已知机器人是从1号点出发的,输出最后机器人停在环上[l,r]区间的概率。 分析:一道非常水的概率题,我们设dp[i][j]表示第i轮落在x格的概率,i=0表示初始状态:dp[0][1]=1,dp[0][j]=0。如果我们想要在某一轮落到x,这一轮移动了w格,那么上一轮可能的位置只有(w-x+n)%n或者(w+x)%n,也就是说: dp[i][x]=dp[i-1][(w-x+n)%n]+dp[i-1][(w+x)%n] 由于第i轮只与第i-1轮有关,可以用滚动数组。但这里存在一个问题,就是说,滚动数组经常需要考虑从哪边进行修改,但这道题目中从左修改和从右修改似乎都无法得到我们想要的结果。 我原本的打算是像上道题一样,用个pre保存上一轮的状态,但这么做很耗时。我在别人的博客中看到了一种精彩绝伦的处理方法:
第一轮将状态存储在dp[0][j]中,第二轮将状态存储在dp[1][j],0和1交替使用每过一轮切换一次。这样做避免了前后的状态纠缠,因为一个存储在1的位置,另一个存储在0的位置,也节省了大量的空间。而且还不需要对定义pre数组,对pre数组进行赋值,节省了大量的时间。 是不知道从哪里开始更新的滚动数组的极好的做法。 至于那个get函数,如果不这么写会超时,事实上我也不知道为什么······ hdu 5119 “Happy Matt Friends”有N个人,每个人有一个权值,挑选一些人人并将他们的权值异或,求最后得到的值大于M的取法有多少种。 分析:这个问题首先要知道一点就是,当a先后与b进行两次异或运算,最终的结果一定为a,即a xor b xor b = a(因为b xor b =0)。 我们用dp[i][j]表示前i个数选取一定数进行异或,最终值为j的方案总数,对于dp[i][j],除了前i-1个数选取一定数异或得到j的方案数,还有可能是前i-1个数选取一定数异或得到k,k xor a[i]=j,即: dp[i][j]=dp[i-1][j]+dp[i-1][k], k xor a[i]=j,之前说a xor b xor b = a,可以推测出k=j xor a[i],于是:dp[i][j]=dp[i-1][j]+dp[i-1][j xor a[i]]。 由于i的状态只与i-1有关,可以用滚动数组,同时j xor a[i]与j的关系不确定,用和上题一样的方法进行处理。
7.1.3 最长公共子序列给定两个子序列A和B。求长度最大的公共子序列,例如1,5,2,6,8,7和2,3,5,6,9,8,4的最长公共子序列为5,6,8。 这个问题紫书那里也讲过了: 状态d(i,j)设为以Ai和Bj分别作为最后一个元素的序列的LCS长度(最长公共子序列),如果A[i]=B[j],那么肯定有d(i,j)=d(i-1,j-1)+1(相当于在状态(i-1,j-1)的最长公共子序列后面添加了一个A[i]=B[j]),否则,d(i,j)=max(d(i-1,j),d(i,j-1)(相当于添加后的元素被无效化了,不能增加最长公共子序列的长度)。 紫书那里讲错了,我在黑书上又验证了一遍,我写的动态规划的方法是没有任何问题的。 hdu 1159 “Common Subsequence”求两个序列的最长公共子序列。 分析:代码如下:
可以进行滚动数组的优化。 7.1.4 最长递增子序列给定n个整数,按从左到右的顺序选出尽可能多的整数,组成一个严格上升子序列。 这里只提供O(nlogn)复杂度的简化代码,具体的讲解过程,还是看之前紫书的博客。
基础习题hdu 2018 “母牛的故事”有一头母牛,它每年年初生一头小母牛。每头小母牛从第四个年头开始,每年年初也生一头小母牛。请编程实现在第n年的时候,共有多少头母牛? 分析:简单的dp基础题,用dp[i]表示第i年的母牛头数,第i年的母牛头数等于第i-1年原本的母牛头数+第i年能够生育的母牛头数,一只牛出生后的三年后可以生育,于是: dp[i]=dp[i-1]+dp[i-3]。 代码如下:
hdu 2041 “超级楼梯”有一楼梯共M级,刚开始时你在第一级,若每次只能跨上一级或二级,要走上第M级,共有多少种走法? 分析:斐波那契数列就是说:
hdu 2044 “一只小蜜蜂”有一只经过训练的蜜蜂只能爬向右侧相邻的蜂房,不能反向爬行。请编程计算蜜蜂从蜂房a爬到蜂房b的可能路线数。 分析:对于位置i,我们可以从位置i-1前往,也可以从位置i-2前往,这么想的话和斐波那契数列是一样的:
hdu 2050 “折线分割平面”我们看到过很多直线分割平面的题目,今天的这个题目稍微有些变化,我们要求的是n条折线分割平面的最大数目。比如,一条折线可以将平面分成两部分,两条折线最多可以将平面分成7部分。 分析:对于新增的一条折线,最好的情况是与之前存在的每条直线相交两次,多一个交点产生两个区域,所以多出的部分为2*2*(i-1)+1。
(题面不严谨) hdu 2182 “Frog”有一只青蛙,在一条长度为n的路上,总共可以跳k次,每次跳的t个单元满足(a<=t<=b),不能后退,而路上每个位置都放置有昆虫,求青蛙最多可以吃到的昆虫数。 分析:状态就比较直接,用dp[i][j]表示跳了i次后到达j位置的最多能够吃到的昆虫数,num[i]表示第i个位置能吃到的虫子,那么有: dp[i][j]=max{dp[i-1][j-r]},j-b<=r<=j-a。
背包问题hdu 2159 “FATE”最近xhd正在玩一款叫做FATE的游戏,为了得到极品装备,xhd在不停的杀怪做任务。久而久之xhd开始对杀怪产生的厌恶感,但又不得不通过杀怪来升完这最后一级。现在的问题是,xhd升掉最后一级还需n的经验值,xhd还留有m的忍耐度,每杀一个怪xhd会得到相应的经验,并减掉相应的忍耐度。当忍耐度降到0或者0以下时,xhd就不会玩这游戏。xhd还说了他最多只杀s只怪。请问他能升掉这最后一级吗? 分析:dp[i]表示消耗i点耐久度下,最多能获得的经验值,v[i]表示第i只怪消耗的耐久度,w[i]表示第i只怪获得的经验值,那么有: dp[j]=dp[j-v[i]]+w[i],1<=i<=k。 通过dp得到状态对应的值之后,寻找最小的能够至少获得n点经验值的耐久度i,m-i即是剩余的耐久度,找不到说明无法升级。 代码如下:
hdu 2844 “Coins”给了n件物品,每件物品有对应的价值Ai和数量Ci,求1-m价值之间有多少价值能够通过这些物品的价值凑出来。 分析:数量最大到1000,n最大为100,100*1000=100000的规模直接做很有可能会超时,所以有一种优化的方法称为多重背包。 假设价值为1的物品有20个,完全背包来看我们需要进行20次状态转移,但如果我们将20通过类似于快速幂的方法进行分割: 20=1+2+4+8+5。 那么1-20中的每一个状态我们都能由这五个状态组成,并且转移次数从20次减少了5次。优化结束后相当于,每种新的面值转化成只有一张,就变成一个基础的0/1背包问题。
hdu 2955 “Robberies”分析:模板是0/1背包这点是没有问题的,但是状态的转移不是加法而是乘法,因为不被抓的概率是相乘而不是相加(否则就变成偷的越多越不容易被抓到)。 找答案还是一样的逆序查找:
poj 1015 “Jury Compromise”LIShdu 1003 “Max Sum”求一个序列子序列元素和的最大值。 分析:关于最大连续和,一般常见的解法有:暴力法,前缀和,分治法,累积遍历法,动态规划。对于这道题的数据范围用紫书上介绍过的分治法就可以解决,这里主要介绍后面两种做法。 动态规划: 用dp[i]表示以第i个数结尾的子序列元素和的最大值,那么dp[i]=max{dp[i-1]+num[i],num[i]}。 最后求dp[i],1<=i<=n的最大值即可,属于O(n)的做法。 前缀和: 直接前缀和是O(n2)的做法,但如果我们在计算前缀和的同时维护当前计算出的所有前缀和中的最小值,用当前计算出的整个序列的和减去这个最小值记作max2[i],最终结果子序列一定能表示成max2[i]的结果(至于为什么,用反证法想一想就能明白了)。 最后求max2[i],1<=i<=n的最大值,也是O(n)的做法。 累计遍历法: hdu 1087 “Super Jumping”分析:LIS问题求的是最长的递增子序列,这个问题求的是递增子序列中元素和最大的那个,一样用dp[i]表示以第i个数结尾的元素和最大的递增子序列元素之和,那么有: dp[i]=max{dp[j]}+num[i],0<j<i,num[j]<num[i] 最后求,dp[i]的最大值。
LCShdu 1503 “Advanced Fruits”输入两个字符串,输入一个最短的字符串使得两个字符串都为这个字符串序列的子序列。 分析:事实上这个字符串就是两个字符串相加除去一个最大公共子序列的部分,问题就在于我们如何打印这个结果。 我的第一个想法是保存每一种状态时的路径,但是路径也需要时间。第二种方法,是用flag标记出dp过程中每一步的选择,然后根据标记往回寻找方案。
需要注意的一点就是回退时,当某个序列回退完全了,此时不用再看flag的值直接往前输出另一个序列即可,所以需要对flag[i][0]和flag[0][j]进行一次初始化。 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 | -2025/1/9 15:55:57- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |