在大多数题解中,我们经常看到这样的表述:“唔~普及组水题,这题显然是一个线性动规,那么肯定是第一时间想到设f[i]:1~i时刻的最大空闲时间。。。”请问为什么你就第一时间想到i就表示时刻,表示任务不行么?我们经常会出现这种情况:面对题解中各种“显然”的状态定义和转移方程一头雾水,不是看不懂,而是一种无解的纳闷:这货到底怎么想到的?
我们都知道解一道动态规划题首先要明确状态,然后推出状态转移方程,而我们拆电器的本质就是回答:究竟怎么明确状态?到底如何根据题目已知推出转移方程?
为什么动态规划的正解总是那么“显然”?其实没有哪一步思维过程真的是“显然”或“不难看出”,检验一道题是否真正理解,就是把一道题中你的思维过程像拆电视一样把零件全部摆出,然后一个个拿起来追问自己:这一步有卡壳吗?那一步真的是显然吗?你从这一步跳到下一步的理由到底是什么?
因此我的大概心得是,不要仅仅局限于正解,在开始思考时,将所有的状态的可能性列出来(f[i]的i除了表示时刻,可以表示任务吗?),在各自状态的基础上,推出最自然的转移方程,然后分别检验可行性(完美满足题目条件了吗?)。当你列出了所有可能性并一一尝试排除后,你就会发现真相就那一个或寥寥几个。
这样我们就能回答标题的问题:如果你擦去了上述的过程分析,你就会得到正解(结论),而没有分析过程(证明),所以才看起来那么“显然”。如果你疑惑这种“显然”,你就要“知其所以然”,就是自己尝试进行上述的过程分析。
下面我将以一道DP“水”题尼克的任务为例,强调“知其所以然”。我会尝试回答:
一.正解是怎么做的?
二.其他方法是怎么做的?
三.为什么正解可以其他方法不行?
四.其他方法真的不行吗?他们的适用范围在哪里呢?
解释申明:什么叫定义一个动态规划的状态?
就是声明一个数组,并赋予该数组及其每一个维度各自的意义,使它拥有推导和记录最优解的功能。如: 定义数组f[i],f存储的的值表示尼克积累的偷懒时间,i表示从第1个时刻到第i个时刻,因此f[i]这个状态就被定义了,表示在尼克从开始到第i时刻时已经积累的偷懒时间,就是一个状态的定义。
经朋友点醒,觉得有必要补充一点点申明:
这篇文章只是我的一点思考上的心得,很遗憾不是题解。。如果你是跟我一样的动规新手,建议搜罗其他本题题解,或先换其他更直白的题目(我觉得这道题简单但很不直白-_-),因为我并不打算手把手从无后效性是什么意思,最优子结构对应本题的什么东西开始讲起。非常抱歉。。
这篇文章主要展示了我思考这道题时的头脑风暴,我认为不经过这种头脑风暴不足以得出“显然”的正解,因此本文重点是我的思考过程
一.我们直接从该题正解的状态定义开始
正解定义f[i]表示在第i时刻时已经积累的偷懒时间(先别问怎么想出来的,后面我们会讨论其他不同的定义)。我们可以推出方程:
①当前有任务t,a[t]表示任务持续时间,则f[i]=max(f[i+a[t]]);②当前无任务,则f[i]=f[i-1]+1。
然后题解的方法是:因为f[i]跟后面的f[i+a[t]]有关,所以必须倒过来for循环,为了倒过来for循环,就要尝试修改第二个方程。因为f[i]=f[i-1]+1,等价于f[i]=f[i+1]-1。因为这个减一只是表示一种继承关系(每过一个时刻偷懒时间加一),如果我们决定逆向循环的话,这个方程完全可以写成f[i]=f[i+1]+1,与式子的变形无关,但本质是一样的(继承关系)。
综上,正确方程为:
①当前有任务t,a[t]表示任务持续时间,则f[i]=max(f[i+a[t]]);②当前无任务,则f[i]=f[i+1]+1。
这样这道题就能A了。但是我有问题:
为什么因为f[i]跟后面的f[i+a[t]]有关,就必须倒过来for循环?
听题解的口气,“f[i]跟后面的f[i+a[t]]有关”这个逻辑顺序是不可动摇的(我的意思是,f[i]是由f[i+a[t]]这个已知状态推出来的新状态,也就是说当前的i表示的f[i]是由大于i的f[i+a[t]]推出来的),那么:
二.“f[i]跟f[i+a[t]]有关” 与 “f[i+a[t]]跟f[i]有关”是一回事吗?
如果是,我们就很自然想到刷表法,
f[i]=max(f[i],f[i-1]+1),同时f[i+a[t]]=max(f[i+a[t]],f[i])
或者转换为普遍的填表法,其实是等效的
f[i]=max(f[i],f[i-1]+1),同时f[i]=max(f[i-a[t]],f[i])
于是交上去WA了。我们的方程不符合题目条件,就是说,忽略了该题的某些特征。什么特征?
注意到该方程与上述的区别了吗?上面的有条件语句(如果有任务则①,无任务则②),而修改后的情况没有。因为当尼克来到某个有任务结束的第i时刻,他可能是刚刚硬着头皮做任务熬过来的,也可能是在此之前早早结束了任务玩手机混过来的,但我们无法知道他到底是怎么过来的,所以我们只能把这两种情况考虑到最优解f[i]上来了。
以填表法为例,这样的结果是,因为f[i]=f[i-a[t]]是没有偷懒时间加成的,而 f[i]=f[i-1]+1有(这不每次转移都加了1嘛),因此f[i]的最优结果必定是由f[i-1]转移过来,f[i-1]又由f[i-2]转移过来,最后f[i-...]又由f[0]转移过来,那多爽,不用工作了。可惜注意到题目有先决条件,尼克在当前时刻有工作的时候必须工作,这种转移方式完全没有顾及这一点,因此这样for循环不可行。
于是我们回到问题,做出这样的马后炮解释:
“f[i]跟f[i+a[t]]有关”,意思f[i]的空闲时间必须只能和f[i+a[t]]一样,是强制性的转移。
而“跟f[i]有关”,意思是,f[i+a[t]]如果由f[i]推导过来,可能有更优的情况。
三.能不能定义f[i]表示做了第i个任务后积累的工作时间呢?
如果这样定义,因为变量i与任务有关,我们只能直接记录任务持续的总时间。但这不是问题,我们先求出最少的工作时间,被上班总时间相减不就得到最长偷懒时间了吗?
于是我们像模像样的推出转移方程:
for i 1->n,for j 0->i-1 ,若i在做完任务j后,则f[i]=min(f[i],f[j]+a[i])
这就好笑了,如果有的选,谁会去工作呢?那么每个f[i]最优情况当然是从f[0]转移过来,即从啥事也不做到只干了这一件事(因为f[i]的定义,必须要做了第i个任务)。因此还是老问题,无法满足题目的限制条件:尼克在当前时刻有工作的时候必须要工作。
我们把问题推向下一个阶段。
如果尼克是个工作狂,我把题目求解改成:写一个程序计算尼克应该如何选取任务,才能获得最大的工作时间。那会怎样?
一.能不能定义f[i]表示在第i时刻时已经积累的工作时间呢?
可以。我们看推出的方程:
f[i]=f[i-1]; f[i]=max(f[i],f[i-a[t]+1]+a[t])。a[t]表示在该时刻结束的任务的时长。文字解释就是尼克可以选择回到过去(即i-a[t]+1,指该任务开始时的那个时刻)补做该任务,这样完全可以。
同时,像下面一样,倒着for循环也没问题:
f[i]=f[i+1]; f[i]=max(f[i],f[[i+a[t]-1]+a[t]);
为什么这样子正着倒着都没问题,而原题正解必须倒着for循环呢?请留意题目条件所求,然后比较一下方程。思考:尼克在当前时刻有工作的时候必须要工作吗?这次可以了,因为这次是多多益善,就算老板不逼迫,尼克也会在在当前时刻有工作的时候去工作,自动遵循这个限制了。
二.能不能定义f[i]表示做完第i个任务后积累的工作时间呢?
这种方法可以解锁了,原因同上。
注意不能定义成f[i]表示前i个任务后积累的工作时间,因为我们需要通过判断第i个任务和第j个任务是否冲突来限制j,因此必须要记录当前情况所做的最后一个任务是什么(是i)
方程就是,f[i]=max(f[j]+a[i]),最终结果取所有计算过的f[i]的最大值(不一定是f[n]哦)。
我这里提出的“工作狂尼克的任务”问题其实就是“挤奶的时间”这道题。
这两道题还有一种图论的方法,不过这就超纲了,再说。
这个博客耗时有点长,到后面有点憋出来的感觉,润色细节是个问题。不知是不是正常现象,需要再写多几篇博客,一番比较才显优劣。