初学动态规划签到及记录——尝试理解

月初开始的50道DP题完成度差不多了(完全盗用了henry_y的博客,感谢),仪式感需求便想装模作样写个告示,目的主要是记录,应该不算总结(因为在我的捋顺过程中我学到的并不多),毕竟刚刚才入门,还没有深入学习便想着一昧输出自己不成型的想法是大忌(思而不学则殆)。

 

目录

一.从背包问题到动态规划,谈谈我的理解

二.动态规划的本质

三.   部分题目举例

 

 

 

什么是背包问题

一个容量为V的背包,想装n件物品,每件物品价值为Wi,体积为Vi,求背包能装物品的最大价值

 

一般怎么解背包问题

搜索,枚举所有的方案的情况,判断可行性(装得下)并求出价值,从中找出最大价值的方案。

对于解集{1,2,3,。。。,n}(表示背包装了第1件,第2件,第3件。。。第n件物品),有2^n个子解集(因为对于集合里的第一个元素,要么保留,要么剔除,有2种方案;对于集合里的第二个元素,要么保留,要么剔除,与前面的方案相乘有2^2种方案,以此类推),该子解集个数即为方案总数。故总时间复杂度0(2^n) 

 

DP怎么解决背包问题

方程:F[i][j]=f[i-1][j+v[i]]+w[i]。F[i][j]是一个状态,表示考虑了前i个物品后,目前还有j体积时的最大价值。

该方程就是一个考虑第i件物品到底选与不选的过程,即比较第i件物品选与不选的方案哪个最优。方法是考虑所有的以前求解过的状态(即考虑了前i-1个物品后的状态),并在他们的基础上,都考虑了第i件物品选与不选的情况。这样就保证了一点:不会有没考虑的情况。只要满足这一点,这个算法就是能得出正确答案的。

  

DP的时间复杂度是多少,以及为什么更高效的DP(动态规划)更高效

对于每个物品都考虑了一次选与不选,考虑了n次,对第i件物品的考虑都要枚举所有的f[i-1][j],这里0<=j<=V。因此时间复杂度O(nV)

为什么DP更高效?我们先用一个简单的例子类比一下。

 

考虑基于比较的排序为什么时间下限都是O(nlog2n)?(出自mindhacks.cn)

对于给定的n个数,有n!种,我们要从中找出所有数恰好从小到大的一种。每次比较a,b,要么前者小于后者,要么反之,但两种情况分别都占总排列方式的一半(即n!/2种)。

 

如果每次都能完美的2分,那么找到那个唯一点最终需要的步骤就是log(n!) = O(nlogn)。如此就不难理解什么基于比较的排序算法的复杂度最好不过如此了

笔者留坑:

1如何推导log(n!) = O(nlogn)?

2冒泡,快排,选择,插入等主要区别应该在如何选定a,b,那么各自代码上又是如何突出“如何选定a,b”的思想呢?

 

回到DP,可以理解为通过每次考虑第i件物品到底选与不选,将总的可能方案折半,这就是最优秀的方法了。 

 

什么算背包问题,或者说背包问题的本质(抽象)是什么

在“什么是背包问题”的基础上,我们尝试普遍化问题

原:一个容量为V的背包,想装n件物品,每件物品价值为Wi,体积为Vi,求背包能装物品的最大价值

分析:

1.容量为V的背包是限制条件

2.n件物品是决策数量,决策可以获得收益,同时付出代价,当代价积累到一定程度(达到限制条件)将无法决策。(收益不一定是好的,代价不一定是坏的,如“出租车拼车”这道题)

3.每件物品选与不选,就是在不违背限制条件下尝试做一个决策,使收益最大化。

该题还有一些隐含信息

4.求背包能装物品的最大价值,就是求最大收益

5.对于一件物品,只要装了就行,先装还是后装没有区别,这满足无后效性

6.定义一种状态,对于每个状态,只要可由其他状态转移而来,必然(或必须)满足最优子结构,能写出状态转移方程

 

背包问题本质是动态规划,当我们抽象出背包问题的本质,实际上就是抽象出普遍的动态规划问题。

 

因此,当题目有以下特征,可用动态规划:

1.求最大(最小)总收益

2.分阶段性(这个要找出来)

3.有决策可选,从而获得收益

4.决策有限制条件

5.该阶段收益可能由某个前面的阶段通过合适的决策推来(能转移或分治)

6.决策不影响其他事件,即满足最优子结构和无后效性

 

 

如何利用上述特征搭建动态规划(首先默认满足上述第6点)

  1. 明确状态(如背包问题的f[i][j],f对应上述第1点,i对应2,j对应4)
  2. 代入决策描述(对应3,5),推导转移方程

 

 

提醒

DP是很灵活的算法,不要被做过的模板(比如阶段i非得由i-1转移,状态非得记录下阶段i和限制条件j等)束缚,搞清楚你的定义和转移方式的本质。

 

 

对我有价值的题选例:

5.分队问题

决策只有两个:新来的人,加入队伍或自成一队。当状态确认时,把决策抽象代入就可,完全不要考虑到底怎么分,DP的优势正是忽略这些。

 

10.创意吃鱼法

先做“最大正方形”才有思路。

但不管哪道题转移方式都很难想,更别提定义状态了。

对我而言是难题。

 

15.出租车拼车

想到决策是什么(这辆车能上几个人),其他的也很好求了(无非就是车(状态)和人(体积))。不容易看出这类似一道背包题。

 

 14.UVA11400 Lighting System Design

很有国外题的风格,不简单。

决策似乎显而易见(用或不用原装电池),但实现起来倍感困难(到底用谁的?)

正因如此转移方程才比较微妙:每次到下一步更新f[i],相当于给以前的f[j](j<i)一次可能2获得更优利益的决策的机会:你要不试试用我们(i)的电池?

最终版的转移方程更抽象:直接枚举k,表示i-k到i-1都尝试用我的电池。能推出这一步,就是真正理解了f[i]的本质意义(i及i以前的灯泡,最多用i及i以前的电池)

 

 24.[USACO08JAN]跑步Running

这道题可以用一维DP做,状态无需记录疲劳值。想到这点就是理解了疲劳值的本质(限制条件,让你跑i秒后必须休息i秒)

有些状态定义很直接很容易想,如果能看出题目所给条件的本质(划分了什么阶段?限制了什么内容?)就牛逼了。

类似的含蓄的题还有“打鼹鼠”

 

 26.[USACO07FEB]牛的词汇The Cow Lexicon

本来不算复杂因为路子只有一条:(f[i]表示前i个字母要删的最少词汇),但初看就是有点无从下手。

 

 34.教主的花园

抽象出决策,阶段等量是解DP的关键,怎样抽象最合理这个需要斟酌了(换句话说怎样定义状态最合理)。有时候直觉不是不可靠,但可能不太优美。 

 

 35.烹调方案    36.经营与开发 

改编背包问题相当成功的题,都是考察对后效性的理解。解决方案都是推公式,很有数学难度。

 

 39.机器分配     40.花店橱窗布置

这是两道几乎完全一样的题, 但因题面不同导致我们很难想到把他们放在一起比较。与“出租车拼车”一样是高度抽象的背包问题,有助于理解动规。

也都是输出方案(print函数)的例题

 

 41.看球泡妹子

我觉得这个阶段的划分不算显然。。做题的敏感度不够吧

 

 48.传纸条

加深对状态的理解:记录下所有信息点,保证每个状态都是独一无二的

 

全文完

希望以后我能补充总结

 

 

 

 

 

 下面仅做存储个人信息使用

judge note

铺地毯,倒序枚举。不一定要用直接方法,想想要求什么。

多项式输出,WA,漏了一个点没考虑。把问题和程序细分的意识,

机器翻译,队列

排座椅,结构体,调用排序。没完全看清弄懂题目漏了

时间复杂度,scanf("%d ",&l); getline(cin,o)。留空格

均分纸牌,有思路要动手试一下,可能有出现一些情况


动态规划纪元
特点
无后效性
最优子结构

找状态,和状态转移方程


金明的预算方案
一直卡在主件和附件那里,没有意识到附件数目非常少,无非就是把有和无两个状态变成,无,有,有1,2,12附件。
思维点:注意到主附件不可拆,一定是捆绑的。怎么捆绑?
(附件数目少可能是暗示)
或者问,这道题跟背包的区别在哪
在主附件,主附件的区别又在哪?
背包有n件物品,首先不能将主附件的搭配当成不同的物品
或许只能通过在状态转移方程里修改,然后才发现答案——这才是重点

环形石子合并
总是WA不知原因,于是暂时放弃,一段时间不编程
某天随意打开,突然就发现了与题解一个明显的差别,len——i——k的循环,i要开到2*n
可以说是小细节吧,也可以说理解不到位
最主要的是,当初一直死磕,都没悟到或找到本质。要避免

多米诺骨牌
变向的背包问题
因为不可能随便换的,所以一开始一定要全部推向极端(上面都比下面大)
开始解决问题了,体积=点数,价值=1
等等,因为一开始其实是乱序的,但是你默认上面比下面大了,怎么解决?
因为这跟翻转有关,翻转跟价值有关
其实不要想太多就按照操作一步步来,必须要翻转的先默认价值为1,如果后来要翻回去拿价值是0,相当于-1

尼克的任务
不可能以任务为状态,只能以时间。但是任务怎么搞?
原来要倒着来。而且尽管任务有开始时间和结束时间,真正只需要看开始时间。
抽象出真正有用的


相似基因

const int tab[5][5]=
{
{5,-1,-2,-1,-3},
{-1,5,-3,-2,-4},
{-2,-3,5,-2,-2},
{-1,-2,-2,5,-1},
{-3,-4,-2,-1,0}
};

int la,lb;string sa,sb
对于输入
7 AGTGATG
5 GTTAG
cin>>la>>sa>>lb>>sb;
发现可以自动忽略数和字符之间的空格
对于输出最大值,答案可能是负值,陷阱
对于初始化,不仅仅是1,0, 0,1, 0,0 ,考虑到ij二重循环,还要初始化i,0 等等,本质没用心去初始化
卡顿点
怎么定义状态?怎么转移?
首先,有两个字符串,必须有ij二维,自然就是f[i,j]为最大值,那要不要将长的先放在前面呢?先想出充分理由,如果没有就先别动
所以关键在于转移
看上去很难其实深入想一下很简单,画个图,就三种转移方式

 

传纸条
首先,两条路线,可以直接定义4维,没有问题
然后,1两张纸条对称性。2意思是两张纸同时传递的,所以每一次都是两张纸状态同步改变——重点
纸条不能重合,所以最后一刻状态就不是直接定位到终点(反正终点好感度为0),这个妙
肯定要优化,突破口在同步改变
注意:在原来的基础上修改状态后数组范围记得检查

 

分队问题
关于无后效性:注意当f【i】确定后,并不是说前i个已经捆绑在一起了,他仅仅表示如果数据只有前i个的话的最大值被存下来方便调用
对于这道题,只有和归到前面的队及自己带领成一队的选择。dp最重要的是不用关心具体怎么分
注意:虽然养成了检查数组的习惯,但这次取值范围数少了个0


低价购买
当初校内赛第一题乱搞的运气题
当初注意到了要将序列不同的删去,现在反而没注意到,这个是一个问题
首先错在,最后找到最大答案后要都一一统计最大答案的方案数
然后,答案(ans)可以在过程中记录,这样可以不用单独for循环
问题就只剩下如何去重了,一条式子就行


最长不下降子序列(LIS)
n方做法过
nlogn,改变状态:f[i]表示不下降序列长度为i,该序列的最后一个数是多少。
因为贪心:若两个序列长度一样,那么最后一个数较小的必然较优
这是完全转变思路,思路是初始化长度len=1,每次维护1到len的f值最小,以便每次按顺序有新的数(for i-1到n)出现时,都尽可能拓长len(当a[i]>f[len]即可)
维护的过程是二分的过程。
很妙,属于别人家的算法

最长公共子序列(LCS)(1到n的排列)
n方做法过
正解:离散化,变为LIS问题。
别人家的想法。

回文字串
原来是转换为LCS问题,考虑到回文字串本质上是找正串和反串的共同子串

 

最大正方形
一错:ans初始化为0,但是至少为1,本来判断ans=max(f[i][j])却因限制压根就没进入到判断
二错:代码非语法的其他打错了错误


创意吃鱼法
一直在想状态转移是什么
思维还是在如何抽象成正方形
其实,不要以为是曾经见过的某道题,就从这题直接思路就行。
肯定是斜边上的转移,约束条件是什么,怎样解决正方形其他地方不能有鱼,那就想,想不到就算,但最起码不要只往正方形角度靠拢
第一次WA,数组的意义从左到右从右到左当时自己都弄错了
第二次WA,这次不同了,ans不是最起码为1了,抄了上一题模板然而没改
第三次WA,转移方程整个都错了,还是套用上一题的,背离了本质。而且,第二次WA的错误说要改结果忘了
关键是,f[i][j]对于f[i-1][j-1]的,不是简单地继承不继承,这是个考虑不全的坑(用暴力的尤其注意)


prince and princess
LCS的一个变通,对于长度不同的两个序列,仍然可以离散化,然后求两者长度较短的序列的LIS(因为若有的数只有长的有,显然不可能是公共序列)

木棍加工
对于有两个关键字的,只需要对其中一个关键字排序,就变成了普通的单变量序列问题LIS

跨河
memset(a,0x3f,sizeof(a))
sum=0x3f3f3f3f表示无穷大,且该sum加上某个数不会溢出

我的方法,定义f[i][j]表示已经运了i头牛,并且最后一次运的船上有j头牛。这是我蒙出来的定义,但事实上这个j的意义看上去很迷但刚刚合适
因为开始考虑新的一头牛,要么就上原来的船,要么就为他另开一条船,而对于新开的状态是建立在搞定了前i-1头牛的基础上,那么我只需要用min值记录前i-1头牛的最小费用就行

我们考虑下题解的方法,我定义的二维的这个j真的需要吗
我是一头一头牛这样加,然后每头牛都单独考虑。那能不能爽快一点,直接用j表示床上的牛的数量
如果这样定义,好像就是一维了,但我的思路的话必须要二维
背包问题

 

出租车拼车
前面定义了变量k,而后面又用它来循环,重复定义不报错的

要注意状态是从上一辆车转移到下一辆车,而不是人数。因为人数是不分层次的,很明显是宰割品(背包的重量),认识到这一点,会发现这就是背包
f[i][j],首先i是第i辆车,j是剩余人数,意思是过了第i辆车后还剩下j人,此时的积累的总费用。
为什么j要这样定义?因为我需要求总等待时间,也就是需要当前人数(思考题目每一个条件如何覆盖或转换)
首先,还没上车的人要么不上车,此时积累上一辆车的费用加上这段等待时间
要么上车,那么f[i][j]则由f[i-1][j+z[i]]转移而来

然后就是初始化问题,不要认为f[0][1]是还没出发所以费用为0,因为初状态只有一个就是f[0][n],其余都是不合法的,状态不能从不合法的地方转移

如果该车能上那么有车位就尽量上,但当只剩一个人时有两个车位也是要上的,尾端的特殊情况要想到
这里有滚动数组,类似01背包的压缩成一维

这题让我对DP理解加深了一层


摆花
初始化要设为第一种花的所有可能的方案数为1,然后累加,想到了
f[i][j]表示到了第i盘花时还有j的空间(用上一题的思路,i表示过了这个花的讨论就下一个了),然后第三个循环k,表示该种花放多少盆


最佳课题选择
乘方pow(a,b)
cmath自带的max函数只能用于int
#define min(a,b) a<b?a:b
前面都是背包问题


能量项链
卡在很别扭
要新建一个头和尾,这样就好看些,然后列举一下样例才能解决。
for (int i=1;i<=n*2-len;i++)//start wrong,i这里不能只到n
(i,k)表示合并后的子串左端为a[i],右端为b[k],(k+1,j)同理。 因此当合并时,等于左区间左端乘左区间右端乘右区间右端
for (int i=1;i<=n;i++) ans=max(ans,f[i][i+n-1]);/// 这里i<=n漏了等号
总结:模拟化降低抽象思维难度
分治问题

 

POGO的牛Pogo-Cow
n立方的做法
怎么定义状态?因为我必须要知道该次跳跃的距离和上次跳跃的距离(比较),我可以记f[i][j]为最后一次跳跃为i到j一次跳跃的最值。
这次如果i表示已经来到了第i个平台会怎样?不行,因为虽然都是到达这个平台,但是路径不一样导致上次跳跃的距离不一样
又不可能把跳跃的距离存起来,那就存跳跃的两个端点了(表示状态)
转移方程很好推

然后我尝试单调队列优化
for i,for j,for k,我发现对于每个固定的i,只是更改终点j,那k的结果是可以一直重复用的,就想把f[k][i]做成单调队列
对queue操作搞了很久,最后调出来后发现我无法把队列pop掉,因为——
写到这里突然发现是可以的,因为i增加,j最远到n,那么i到n有变近的趋势,如果i到n(最远)都有对应的f[k][i]不符合,以后他也不会符合,就可以pop;但i到n-1不符合就不能pop了

但是这样仍然要两个for一个while,然后我突然意识到自己的路走得太远了,赶紧回头看了看题解
然后意识到了什么是真正的单调队列:两个for,另一个k++或--,在第二个for结束后,k++也到头了
<--k i j-->
这是正解,固定i不动(i放到第一层for循环),j与i距离逐渐变大,但是当j不是很远时,k也不能够离i太远,而是随着j变大逐渐解锁更远的地方,两者相互制约
一开始k从i出发,尽量往后走直到受到j的约束停了下来;当j一往后走,k也会从原先停下来的位置尽量往后走。
k--> i j-->
这是我的方法,由于当i和j拉近时(for i到n,j每次最远也到n),k也不能离i太久,也要被动向右走,
问题出在,尽管若j=n时不符合k可以pop了,但是对于k+1,仍然不能武断他是否符合(也就是在j等于哪里时才不符合)
确实是把k做成单调队列了,但pop的代价是一次j的for循环。


照明系统设计
必须电压从小到大排序
从大到小的话就是贪心,但从小到大,每次遇到一个新电源,这个必须买,但之前买过电源的可以选择不买找更优的;
即使这个新电源以后不会去买,因为下一个新电源的灯泡价格更低,那么下一个新电源出现时一定会统统搜一遍,这时就考虑了不买之前电源的情况


奶牛零食
常规题
定义状态,i记录多少天。但是今天和昨天真的无后效性吗?有的,你不知道昨天怎么拿零食,所以不知道今天怎么下手(从哪里转移)。
又考虑到状态只有两种,从左边拿和从右边拿,我们需要引入一个j来复原昨天的状态。那就是j表示从左边拿的次数了,这样右边也就能推出来


跳房子
没想到金币的效果是单调的,可以用二分。
动归也想出来了。
没想到还能用单调队列优化。
算大题吧,也卡了比较久,好多细节和启示
define inf (1<<30),这样可以直接用-inf

什么时候要longlong注意,单个数据不超累加起来就超

freopen("D:\\in.txt","r",stdin);
搞队列有关的操作(while)时,h<t的条件必须时时刻刻写上

最后一个问题,判断时小于等于漏了等于号,导致有些结果差一点然后WA。注意

最难一个问题,考虑点1,点2,点3之间距离为3,3.机器人最大跳跃距离为5正负1,
那么当j等于2时now停在了1,但当j变成3时now就可以加一,哪怕他现在反而超过了最远距离,但是也要now++啊,不然下面都走不了了,而这个不符合条件会在下面有专门语句解除
这个现在我的解释可能有点含糊,但这是我调了最久的地方,详见代码

 

乌龟棋
一开始方程给了一个状态i记录到第几格,然后一共四维,剩下一个指令的信息可以推出来。
但完全可以变形一下,因为知道任意四个就能推出第五个,那么直接推出步数就可以了,这样不会TLE。当时我没有这种意识
因为固化了一个正确的想法,很难变通到一个更好的怎么破?
很惨痛的一次教训
WA,因为N《=350,其他《=40,然后maxn就只开了50,然后爆数组了,但完全不报错,只是W,调了一个小时
递推问题

 

跑步
写转移方程时用最直观的写法就行了,比如对于疲劳值不为0时要休息,则只能休息到疲劳值为0,f[i+j][0]=max(f[i][j],f[i+j][0])。
刷表法
还有一种一维写法,因为某个点到某个点必然是跑了i时间又休息了i时间。挺妙
递推问题


挤奶的时间
和尼克的任务一起写了个博客
坑:一longlong,二a[0]结束时间要设-100,
一开始交了尼克的任务的思路,结果最后检查题目没有坑啊于是提交上去忘了开数组结果有的WA了,以为思路有问题,其实没问题。
递推问题


书本整理
一开始找定义时发现了f[i][j]剔除书时不能剔除刚刚加入的第i本书
后来发现有可能一连串若干本书都剔除掉了,而不是一本到一本的转移
都机智的发现了这些问题,当然就是反应有点慢,那就慢慢练吧
递推


积木城堡
是否有的01背包问题(装箱问题),f[j]=max(f[j],f[j-a[len][i]],可以把有和没有理解成价值。我开始时漏了这个max

音量调节
符号打错,样例刚好过
递推

机器分配
关于输出最佳分案,两种方法
1f[i][j][i]=k表示前i个公司前j台机器分掉了后,第i个公司分了多少机器,每次转移时,将第1到i-1个公司分别有多少机器从以前状态拷贝过来
2f[i][j]=k直接就好了,每次调用f[i-1][j-k]


花店橱窗布置
注意有负数的都要先检查是否要初始化-inf
循序渐进,确保这一步没有疑问再下一步。这个必须练习!!!
然后找了好久找不到从机器分配改过来的代码的错误
后来意识到,f[i][j]表示前i朵花前j个花瓶,与上一题共同点是一串连续的花瓶属于一朵花,但不同的是最后一朵花插了后后面的花瓶没用了
所以答案应该是f[n][i]而不是f[n][m]
当然还有一种写法,f[i][j]定义不变,每次选择取和不取,这种更优只用两个for循环。为什么?
这种写法我一开始初始化想的太简单,后来仔细分析才正确了初始化。
但是看别人为什么人家初始化跟我不一样都能过?
人家for(int i=1;i<=m;i++)f[i][0]=-inf;
因为我们初始化不从0开始就是为了防止从f[i][0]=0转移过来的情况,人家的初始化就是从本质入手,看来以后要慎重写代码。
题解怎么print?
这些题的print艺术感不错,可以总结总结。


教主的花园
对于树高树低, 因为只有三种树,可以一一枚举所有合法的(121,231,131等等)
但对于这些条件,完全有可能有更优的定义,如定义[0]表示树低,[1]表示树高


烹调方案
问题在于对于f[i][j](二维背包),设前i个物品里有物品i和j,对于i,j先枚举哪个是有讲究的(蛋糕在融化且融化效率不同)
题解是对于每一个i,j都比较然后排序,因为阻碍的本质是无法确定f[i][j]里面物品的顺序,这里顺序影响答案。
要么就开多一维存状态,但这里最优顺序是可以式子化简比较出来的,所以直接排序即可
有意思的是,排序的本质是两个东西基于比较的排序,而不只是单纯的比大小

经营与开发
与上题一样有后效性,秦九韶算法拆开,倒着枚举就好


道路游戏
大概转移方程大概能推出来,当时用的是f[i][j]表示i时刻过了j道路。
问题1:解决前缀和问题,需要找原点,用点数学烧电脑其实还好。
昨天晚上写的大概,然后不想调了,今天早上一看发现之前很多细节都是错的,看来真的要一个字一个字的抠意思看
测试样例时突然发现因为每次重新买机器人都要指定地点,原来第二维的j没用
事实证明最后大概也能过了,就是代码有点不优美,导致调试和检查时需要花更多时间,这点怎么该???
DP[i][j]=DP[i-1][j]+DP[i][j-1]-DP[i-1][j-1]+map[i][j]二维前缀和

满分要二维单调队列优化,觉得太麻烦了不写了。

47 41 27 50

字串距离
scanf("%s%s",a+1,b+1);
int m=strlen(a+1),n=strlen(b+1);
前面+1后面也要+1


牛的词汇
f[i]表示第i位之后的最少删减数,然后推转移,暴力枚举就行了。怎么想到的?


牛交通
对于一条边a-b(a<b)
我们计算出来所有的入度为0的点到a的方案,在计算n点到b的方案
两者相乘,岂不是所有入度为0 的点到n且经过这条边的方案
原来题目是求边,f[i]只是记录点的
然后dfs意义搞错了,dfs(n)是终点为n的dfs,只是因为回溯才反过来了,对于a<b,入度为0的点到a的方案是通过dfs(n)来求的


看球泡妹子
数组开太大了,结果memset效率很低
一开始的思路有点乱来,想着可以什么读入合并到一起,还没有推导好公式就顺着感觉码代码,当然错了

猜你喜欢

转载自www.cnblogs.com/reshuffle/p/11511696.html
今日推荐