dp给我的感觉就相当于dfs。只不过dp是从一个集合推到另一个集合。在思考dp的时候可以思考一下如果dfs的话,它是如何推出答案的。也就是说,我们不用关心具体的dfs过程,但要知道它是如何从无到有,算法推进过程中得到中间结果的顺序是什么。把这些中间结果放到一个集合中,就是dp。
背包模型和最长上升子序列模型的区别,在于最优的区别。最长上升子序列模型因为它们本身的性质最优,而背包模型最优不是因为它们本身的性质,而在于外界给予它们的约束条件。就比如,最长上升子序列模型的最优子序列,本身就满足最长、最短、和最大、花费最小……而背包模型的最优子序列,你的序列最优,是因为你的总体积<=v,序列再长、再短、和在大、体积在大都没用。
有些题目,让求给定序列的最长的子序列,这个子序列是***形状的。(序列因为它们本身的性质,所以最优)
五一到了,ACM队组织大家去登山观光,队员们发现山上一个有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。
同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。
队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?
自己的做法
题目让求最长的子序列,该子序列满足单调递增或者先递增后递减或者递减。
我们要得到最长的子序列,就需要枚举出A(1, n)中的所有的子序列(不用关心如何枚举)。我们把枚举过程中得到的一类中间子序列放到集合中。
为了表示更精确的划分集合,我们在集合表示时,需要用某一种方式,来对集合里的内容做更详细的描述。我们用0表示单调递增,1表示单调递减。
(大部分集合只涵盖了算法推进过程中的中间序列)
集合{i, 0}:以a[i]结尾,并且该集合中的所有序列从头元素单调递增至a[i]或者只包含a[i]
集合划分:看倒数第二个数。根据集合的定义,集合中的所有序列,倒数第二个数可能不存在;如果存在,因为单调递增,所以a[k] < a[i]。
集合{i, 1}:以a[i]结尾,并且该集合中的所有序列在某一段连续单调递减至a[i]或者只包含a[i]。
集合划分:看倒数第二个数。倒数第二个数可能不存在;如果存在,根据集合的定义,因为所有序列在某一段连续单调递减至a[i],所以倒数第二个数a[k]肯定小于a[i]。
网上的做法
集合定义稍不一样。
我们要求出题目所需要的序列。题目所需的序列肯定有一个顶点。我们枚举出以这个顶点结尾的所有递增子序列,再枚举出这个顶点开头的所有递减子序列,然后拼接,就得到答案。
因此设计两个集合:(所有集合里的内容涵盖了算法推进的中间序列和目标序列)
dp_Up[i]:以a[i]结尾的最长递增子序列
dp_Down[i]:以a[i]开头的最长递减子序列
属性都是:max Len
因此dp_Up[i] + dp_Down[i] - 1就是我们想要的结果。
总结:
自己的思路比较偏暴力一点。如果让我去暴力做这个题,我会直接枚举出所有子序列。
网上的思路的方法巧妙一点。如果按网上的思路暴力枚举,肯定是先枚举每个顶点,然后枚举以这个顶点结尾的所有递增子序列,以得到最长递增子序列(目的),再枚举这个顶点开头的所有递减子序列,以得到最长递减子序列。
有的题目需要转化:
N 位同学站成一排,音乐老师要请其中的 (N−K) 位同学出列,使得剩下的 K 位同学排成合唱队形。
合唱队形是指这样的一种队形:设 K 位同学从左到右依次编号为 1,2…,K ,他们的身高分别为 T1,T2,…,TK , 则他们的身高满足 T1<…Ti+1>…>TK(1≤i≤K) 。
你的任务是,已知所有 N 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
题目可以等价转化为,求最长的合唱队形。而合唱队形就是上一个题目让求的子序列。
Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。
北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。
每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。
编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。
自己的做法
题目等价于让求最长的键值对序列。如果我们把南岸和北岸的城市画出来,把每个键值对当成一个斜线,题目等价于让求这些斜线序列的最长不相交子序列。
我们要枚举出最长的斜线序列,必须要按某一顺序枚举出所有的斜线序列。我们可以按南岸坐标(或者北岸)从小到大枚举斜线序列。将枚举过程中的合法中间斜线序列放到集合中:
dp[i]:以a[i]结尾的斜线序列集合。
属性:MAX LEN
集合划分:
按倒数第二个数进行划分。
网上的做法
将键值对按南岸序列从小到大排好后,等价于再去求北岸序列的最长上升子序列。
有些题目,会给你两个序列,然后根据这两个序列求一个最优序列。(不一定是子序列)
给定两个字符串 A 和 B ,现在要将 A 经过若干操作变为 B ,可进行的操作有:
删除–将字符串 A 中的某个字符删除。
插入–在字符串 A 的某个位置插入某个字符。
替换–将字符串 A 中的某个字符替换为另一个字符。
现在请你求出,将 A 变为 B 至少需要进行多少次操作。
题目等价于让我们求一个包含删除,插入,替换的操作序列,这个操作序列,可以将A变为B,并且序列长度最短。
我们要将A变为B,肯定要从无到有,列举操作。因此,在算法推进的过程中,肯定有一个没列举完的操作序列(a[0]替换,在a[0]后插入……这样的操作),这个操作序列,可以将A的一部分变为B的一部分。
{i,j}:将A[1……i]变为B[1……j]的所有操作序列集合。
属性:MIN len
集合划分:对于集合中的所有操作序列看最后一个的操作。
第一类,a[i] != b[j]
- 如果最后一个操作是删除a[i],然后将A[1……i]变为B[1……j],那么这类操作序列,如果“遮掉” "删除a[i]"这个操作,投影出来的操作序列肯定可以把A[1……i - 1]变为B[1……j]。这类操作序列的最小长度,就是“遮掉”"删除a[i]"操作后,操作序列的最小长度,再加上1。
- 如果最后一个操作是替换a[i],替换的值肯定是b[j]。那么遮掉这个操作后的操作序列,应该能够把A[1……i - 1]变为B[1……j - 1]。
- 如果最后一个操作时在a[i]后插入,那么插入的值肯定是b[j]。遮掉这个操作的操作序列,可以把A[1……i]变为B[1……j - 1]。
第二类, a[i] == b[j]
- 如果a[i] == b[j],那么实际上不用对a[i]进行任何操作。这堆操作序列不用遮掉,就能够将A[1……i - 1]变为B[1……j - 1]。因此直接包含在{i - 1, j - 1}中。
如何获得状态:算法推进。要得到最长用重复去求它。
公共上升子序列,你必须通过某种搜索过程,得到全部的公共上升子序列;将搜索过程中得到的这些子序列分类划分成不同的
集合;保存这些集合的属性,使我们不用重复去求。
在算法推进过程中,我们按某一顺序同时遍历数组A和数组B,会获得一堆公共上升子序列。我们要从这些公共子序列中得到最长公共子序列。
将这些公共子序列划分:
dp[i][j]:序列包含在A[1……i]和B[1……j]中,且以B[j]结尾的子序列长度。
1.对于不包含A[i]的子序列,也就是同时位于{i - 1, j}集合,其最长公共子序列的长度为dp[i - 1][j]
2.对于包含A[i]的子序列,因为A[i]是A的最后一个元素,B[j]是B的最后一个元素,序列同时含有A[i]和B[j],所以肯定有A[i] == B[j]。
对这些子序列进行进一步的划分:遮掉B[j],然后进行映射,可以得到一些子序列。这些子序列肯定全部位于{i - 1, k}集合(这些集合可以
为空集)中(k是倒数第二个元素的位置)。因此,这些公共子序列的最长长度为:dp[i - 1][k] + 1。
得:dp[i][j] = max(dp[i - 1][j], dp[i - 1][k] + 1 (0 <= k < j) )