Get to the points first. The article comes from LawsonAbs!
动态规划(dp)问题是编程从入门到进阶的一个分水岭,可以这么说:几乎任何一份有质量的题中都有dp的影子。【当然,我这里的dp指的是广义上的递推】这也就是数学中的逻辑思维的体现,将得到的代码手动敲出代码实现,就是物理中的实证思维的体现。所以说编程能够体现一个IT人员的基本素养,而dp问题则是能力的主要试金石。
这里笔者总结了常见的dp入门题,希望能够帮助到相关人员。如下是我总结出处理dp问题时,总结到的一些经验。
LCS
;LCIS
等等dp
中,不同的操作体现在对维度的操作上。对各个维度取不同值时选择其最优解。dp
问题无非就是找出相应的维度。这道题讲的是如何求出两个序列的最大相似值。
针对两个序列可以增添一个‘-’字符用于匹配。本题的难点也就是在于这个‘-’字符的处理。但是如果定义好了维度就会发现很简单。
有两个序列,(求最大相似值),是不是让我们想到了LCS,类似的定义,就有f[i][j]表示A1-Ai 与序列B1-Bj 所能达到的最大相似值。于是。
问题转化,问需要几枚导弹才能把所有的目标都拦截?
其实就是给定一个序列,让你求出最长上升子序列的长度。
有几个问题需要注意一下:
【递减情况:(5 4 3)】假设有序列:(5 4 3 1 2 3),其LIS为(1 2 3)。很显然,不再分析。
【有增有减(5 3 4)】假设序列(5 3 4 1 2 3),其LIS为(1 2 3)。因为(3 4)并不是LIS,所以一定可以在消灭LIS之前把(3 4)消掉。其余同理分析。
其实
这题有两种解决方法,分别是LCS和 区间DP。这里采用LCS来做。
方法1:LCS
主要是问题的转换。原文题等价于求出LCS。至于为什么这么做,我尝试将我的思路说给大家听听。
这题其实就是一个完全背包的变题。主要思路如下:
给出一个集合,集合中是若干个字符串,再给出一个字符串s。判断由集合中的元素组成的字符串s的前缀最大长度是多少?
如果将集合中的每个字符串看成是物品,每个字符串可以放无限次。其长度就是该字符串的价值。但是不同于朴素的完全背包,放下物品(字符串)的时候,不仅需要判断之前的前缀是否可达,还要判断接下来若干长度的字符串长度是否匹配。比如测试用例
A AB BA
.
AABAA
在放集合中的第一个元素的时候,最大前缀可达2,在匹配第三个的时候因为A!=B 失败了。
本题需要注意的点是:循环的层次。我曾像完全背包那样使用双层for循环【第一层遍历物品(即这里的集合),第二层遍历价值(即这里的前缀)】,结果却是错误的。正确的是,第一层先遍历价值,第二层再遍历集合。
类完全背包。
主要思路,依次放入前i个物品,然后判断到达价值j时的方案数,依次累加即可。
类似LIS 问题
只不过这里的“上升子序列”的定义方式是:若一个字符串s1的前缀是另外一个字符串s2,则由s2,s1构成的序列则是上升的。
实现的时候需要注意:应该初始化f[]为0,然后在输出的时候输出max(f[i])+1,因为题目要求输出最长单词链中单词的个数。
类似0/1背包问题
有n个问题,每个问题都是0/1背包问题。让求出这n个问题中共同的最大的价值(即本题的高度)。
简单的0/1背包问题
只需要判断集合中每个元素可到达的方式种类,最后再减去集合中元素的个数即可。
本题的问题主要有:
主要方法
方法1:dp
其实本题是一个类完全背包的问题。
设f[i][j]表示 数i可由j个数的平方和表示的个数,其中j取值范围为1-4;转移的方程为:如果f[j-squ[i]][0-3]>0,则 f[j][(0-3)+1] += f[j-squ[i]][0-3]; 这么做是不会造成 25 = 3^2 + 4^2 = 4^2 +3^2 这样的重复解。因为放数字的是有顺序的,【在本题中,也就是按照先放3,再放4的顺序来的】
但是有如下几个技巧:
bfs可用于求朴素版的最短路。之所以说是朴素版,就是因为其搜索的图中的顶点间的边没有边权(或者说权重值都是相同的);或者是单纯的搜索次数就是“优越性”的比较。但如果在一个不是以走的路径次数为优先的题目中,就很难确定最优解。例如络谷的P1649题。
这题打上dp的标签是想说明dp的过程就是不断更新迭代的过程吗?
主要思想
本题坑点:
很简单的dp算法
主要思想:
if(arr[i][j]!=arr[i-1][j] && arr[i][j]!= arr[i][j-1]
&& arr[i][j] == arr[i-1][j-1]){
f[i][j] = min( min(f[i-1][j],f[i][j-1]),
f[i-1][j-1])
+1;
}
主要思路
有两种方法解决这道题目。
方法一:
使用row[i][j]记录 第i行前j列数的和; col[i][j]记录第i列前j行的和。在遍历二维坐标点的时候,找出以该坐标点为方形左上角元素时,得到的最大和。得到方形元素的最大和的方法是:
方法二
使用二维数组的前缀和。这个很好写,建议使用这种方式。
主要思路:
方法1:
我刚开始拿到这道题,没有想太多。直接写成了贪心。40分!!!因为局部最优得不到全局最优。然后明白应该用之前的每头牛去更新当前的牛所需要的最小价值。
rec[i]表示第i头牛是船中的第几头牛; dp[i]表示第i头牛过河需要的最短时间
方法2:
仔细思考这题,就会发现这题其实是一道简单的背包问题(我更倾向于认为这是个0/1背包)。将一次运送1,2,……i头牛的代价分别记为d[i],则需要找出一个运送方案(其实就是加数集合)使得d[n]最小即可。
转移方程就是: d[i] = min(d[i],d[j+]d[i-j]+d[0]);
问题转换!!
本题和P1044 栈很像!【将售票员拿到50元看成是入栈操作,售票员找零50元看成是出栈操作,于是问题就转换成n个50元有多少个出入栈的次数问题,也就是1-n这n个数,在出入栈之后,有多少个不同的排列次序问题。】
将实际的问题抽象成二维坐标点,然后根据状态的转移,得到递推方程,从而得到解。下面分别分析。
问题1:
if(i>j)
{
dp[i][j]=dp[i-1][j]+dp[i][j-1];//如果来的50的多于100的,那么这次来50和100的都可以
}
else if(i==j)
{
dp[i][j]=dp[i][j-1];//如果100和50的一样多,那么这次只能来50的
}
实现的时候,只需要让i>=j恒成立即可。
疑问是:
01.为何在将长度排完序之后,从宽度就可以得到最优解了?为何是最优解?
主要思想
坑点:
主要思路
dfs+剪枝
搜索每个节点,如果和到s,则返回,否则继续深搜,直到和超过s,或者到达边界。
剪枝操作是:如果该点的子孙节点的所有和都不能到达s,那么就不用再遍历其子孙节点。
主要思路:
深搜即可
给出地窖中的地雷信息,每个地窖的连接信息,求出可以从任一个地窖出发可以排查出的最大地雷数。
水题一道。
边记录,边找出最小的值,然后用当前的值减去已经找到的最小值。用res记录差值的最大,最后输出res即可。
这题要变换思路。
将其转换成背包来做。需要注意的问题就是,令dp[i]为到达价值i所需的最小邮票数,而不是dp[i]=1表示价值i可达。【这种令法在dp题中很常见!!】 如果是像后者那么假设,则会得到一个错误的3重for循环。
for(int i = 1;i<=k;i++){//放第i张
for(int j = maxV;j>=0;j--){//价值j
for(int l = 1;l<n;l++){
...
}
}
}
做dp题不能死板,要根据题意灵活使用模板。
简单的dp题。
直接给出了显性的坐标了,就等于说明这是一道水题了。
主要思路
坑点:
并不是所有的点都可以进行移动的,比如说,最开始只有坐标(1,1)可以往右移动,但是(2,1),(3,1)就没有资格往右移动。【所以说即使由(2,1)出发构成最大值也是不能要的。所以用个标记数组标记,只能从已经访问过的坐标开始往右遍历。】
简单的递推。
需要注意的点有:
学会从题目中得到解题信息。比如:
dp题的关键是如何设置状态转移方程。
本题,很多人都能令 dp[i][j]是从前i本书中选出j本书所能够达到的最小不整齐度。但是还有一个关键点是:dp[i][j]表示的是第i本书要被放到这j本书中【也就是作为这j本书的最后一本】。 可能会有人问为什么?但是我想说,为什么要问为什么?
如果dp[i][j] 就是从前i本书中选出j本书。那么当我们更新从前i本书中取出j本书时得到的最小不整齐度时,我们的更新值是需要用当前加入的这本书同上一次放(j-1本)书的宽度值作为新的损耗值加进来,但是这时问题就来了,我们怎么知道上一次放(j-1)本书结尾的那个宽度值?
即使你用数组rec[i][j]表示从前i本书中选出j本书时,第j本书的编号。你只是用这个数组来存储,但是你没有定义它是怎么放的,所以仍然无法确定这个rec[i][j]值该取什么。就拿一个特殊的例子,
rec[1][1] = 1?
rec[2][1] = 1? 2?
rec[3][1] = 1? 2? 3?
……
所以,必须定义清楚,因为这涉及到dp更新的问题,是个关键问题,在编码之前就必须考虑清楚。
题目中有什么重要元素,就应该考虑将这个重要元素作为一个维度存储起来。
水题一道。【可供dp新手练习时间复杂度为O(NlogN)的最长不下降子序列长度】
主要思路
还是问题的转换。
不是所有的DP标签题都要用dp解。例如本题。
主要思路
方法一:
用bfs遍历每个点,用set集合存储每个点所能到达的值的集合。【肯定是需要保存的,用于提供给后续的点】
优化的方法:
直接用一个三维数组arr[x][y][z]存储点(x,y)能否到达值z。数组大小也就1e3。所以比用set更好。
bfs可以解决dfs深搜带来的重复问题。【很多题dfs解都会十分复杂,但是bfs则是较优的方案】
方法二:使用dp
dp[x][y][z] 表示在位置(x,y)是否可以得到值z。然后一个简单的三重for循环即可。
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
for(int l=0;l<k;l++) //因为mod k 后得到的数一定小于k,所以从0到k枚举
if(!dp[i][j][l*num[i][j]%k]) //没有计算过
dp[i][j][l*num[i][j]%k]=dp[i-1][j][l]||dp[i][j-1][l]; //l*num[i][j]%k表示当前格子数乘从左边或上边传下来的数l再mod k,dp[i-1][j][l]和dp[i][j-1][l]表示在上方或左方能不能得到l
方法三:深搜
如果想用深搜,恐怕朴素的深搜不大好使【不好剪枝(我太菜),只有20分】。分太少,主要是因为打开深搜的方式不对。
同上面的定义,判断点(x,y)在取值z时是否访问过,如果访问过,则不再深搜,否则深搜,这样就可以避免重复的搜索了。这要求我们和dp解题一样,定义一个数组为:vis[x][y][z],深搜时注意重复即可。
可以看到无论是深搜,还是广搜,还是dp,其实质上的数组维度都是一样的,都是三维【在set版本的bfs中,其set相当于第三维】。这些是巧合吗?
简单的dp题,仔细分析一下,有点儿像背包,但实质上不是背包。
可以发现,问题就是在M天内到达城市N,求最小的疲劳度。根据求什么设什么的原则,我们可以令dp[i][j] 为前i天到达城市j,花费的最小疲劳度 。【求什么设什么是一个高度总结,新手可能会问为什么,但是仔细想想,就应该是这样。】
主要思路
dp[i][j] = dp[i-1][j];
dp[i][j] = min(dp[i][j], dp[i-1][j-1]+ d[j]*c[i] );
本题要转化问题。感觉洛谷的题都是这个样子,
背包问题的组合题。【多重背包】该物体有使用次数限制,【完全背包】该物品可以无限次使用。
针对题意可以有如下朴素思路:
看到数N的取值范围是<=9。那么就可以想到是否可以有很多重循环来解决这个问题。几乎都是这样的,事实证明这题的朴素做法就是要用一个四维数组来搞。
主要思路:
f[i][j][k][l] 表示两个人到坐标(i,j),(k,l)所能达到的最大值。然后不停的进行更新即可。需要注意的是,如果(i,j) = (k,l),那么就不能加两倍的数。
方法2: