维护无后效性的技巧——立即计算代价

来源:互联网 发布:东快傻 知乎 编辑:程序博客网 时间:2024/06/03 01:14

简介

无后效性是动态规划的一个基本特征之一,只有具备了无后效性的问题才可以使用动态规划求解。直观上讲,无后效性是指“现在不会影响未来”,或者说现在的决策不会影响未来如何决策。一个不具有无后效性的例子是矩阵寻路算法。设想一个0-1矩阵,寻找一条从1,1到n,n的最短路,不能使用下面的记忆化搜索算法:

FIND-PATH(x,y)  if (out of matrix) return ∞  if (x,y == n,n) return 0  return dp[x][y] = min(FIND-PATH(x+1,y), FIND-PATH(x-1,y), FIND-PATH(x,y+1), FIND-PATH(x,y-1) )/*例如1 0 0 01 0 1 11 1 1 11 0 0 1这个算法会陷入无限递归*/

为什么这个算法无法正确结束呢?不难发现,这个算法并没有在决策(x,y)后要求以后不再递归计算(x,y),这就导致递归计算(x,y)的前提包括求解(x,y),算法会一直执着于递归计算(x,y)而无法正确结束。
不难想到增加一个辅助数组now[][] = {0}。没要计算一个(x,y)的时候就now[x][y] = 1防止落入死循环。

FIND-PATH(x,y)  if (out of matrix or now[x][y]) return ∞  if (x,y == n,n) return 0  now[x][y] = 1  // 锁死(x,y)  ans = min(FIND-PATH(x+1,y), FIND-PATH(x-1,y), FIND-PATH(x,y+1), FIND-PATH(x,y-1) )  now[x][y] = 0  // 释放(x,y)  return ans

显然这个方法是正确的。那么能否直接加入dp[][]实现记忆化搜索呢?不行!考虑记忆化搜索(dp)的原理——无论何时计算dfs(x,y),得出的结果都是同一个值,因此不必从新计算。然而由于辅助数组now的加入,导致由于now的不同,不同时候询问dfs(x,y)的结果可能不同。我们便称这个问题违背无后效性原则,更准确的,这个问题的子问题图存在环。
很多dp问题的无后效性都是显然的。然而一些时候,无后效性需要通过一些方法来维护。下面用几个例子简单分析如何维护无后效性。

最优二分检索树

  • 给定N个单调增数据a1..aN的权值f1..fN,构造一棵二分检索树,ai的深度记作di,使得代价sum{di*fi}最小。

很显然的区间dp题目,在区间i..j中枚举一个k,递归计算i..k-1的代价和k+1..j的代价,再加上k的代价即可。然而深度的计算遇到了麻烦。每一次试图将序列分成两部分时,会使左右子树每一个数据深度+1,这会影响到之后的决策。也就是说,对于同一个区间i..j来说,由于所处的深度不同,结果也不同。违背无后效性。
一个显然的思路是更改状态,用dp[i][j][d]表示i..j深度为d时的代价。但是显然这个方法复杂度为Θ(n^4),难以接受。
状态不能更改,不如考虑决策。未进行操作的序列每一个元素深度为0;每对i..j进行一次决策,会导致其间的所有元素深度加一——这就是违背无后效性的一点。为了消除后效,我们必须一次性结算这次决策造成之后决策改变的总量。更直观地,我们将di*fi看作fi连续加法,即 fi+fi+fi...di个fi,每进行一个决策,当前区间内每一个元素ai代价都会加上fi,也就是当前区间所有元素的代价和。dp方程如下:

dp[i,j] = min {dp[i,k-1] + dp[k+1,j]} + sum{a[i]..a[j]}i ≤ k ≤ j

用记忆化搜索实现即可,注意处理边界。复杂度Θ(n^3)。据说可以优化到Θ(n^2)不过我不会

删数 ——tyvj

  • 对于一个数列a1..an,每次从左面和右面删除一个数,第i次删去aj的代价是i*aj,求将数列全部删除的最小代价。

和上一个题目有异曲同工之妙。这里不再分析为何需要维护无后效性,直接给出维护方法。
不妨称题目中的i为一个元素aj的操作时间。每一次决策(删除一个数),会导致剩下的所有数的操作时间加一;如果把代价i(j)*aj看作连续加法aj+aj+aj...i(j)个aj,每一次i加一会导致每一个未删去元素aj的代价增加aj。dp[i,j]表示删去ai..aj的最小代价,则有方程:

dp[i,j] = min (dp[i+1,j], dp[i,j-1]) + sum{a[i]..a[j]}

任务安排——tyvj

N个任务排成一个序列在一台机器上等待完成(顺序不得改变),这N个任务被分成若干批,每批包含相邻的若干任务。从时刻0开始,这些任务被分批加工,第i个任务单独完成所需的时间是Ti。在每批任务开始前,机器需要启动时间S,而完成这批任务所需的时间是各个任务需要时间的总和(同一批任务将在同一时刻完成)。每个任务的费用是它的完成时刻乘以一个费用系数Fi。请确定一个分组方案,使得总费用最小。
例如:S=1;T={1,3,4,2,1};F={3,2,3,3,4}。如果分组方案是{1,2}、{3}、{4,5},则完成时间分别为{5,5,10,14,14},费用C={15,10,30,42,56},总费用就是153。

输入格式

  • 第一行是N(1<=N<=5000)。
  • 第二行是S(0<=S<=50)。
  • 下面N行每行有一对数,分别为Ti和Fi,均为不大于100的正整数,表示第i个任务单独完成所需的时间是Ti及其费用系数Fi。

输出格式

  • 一个数,最小的总费用。

测试样例

  • 输入
5 1 1 3 3 2 4 3 2 3 1 4
  • 输出
153

很显然,由于顺序不能改变,所以可以使用dp来求解。状态是关键! .
虽然时间就是金钱,但是这里我们会把F值看作金钱,而时间看成金钱的单位。

费用S = T * F,显然S = F+F+F...(TF)。所以时间T每增加k,即T' = T+k,           那么S' - S = kF。我们便说:金钱又被收了k次。

我们仍然考虑每当一个决策可能在未来产生消费时,就立刻预先支付这个价值从而维护无后效性。
分析问题: 每个任务的费用是它的完成时刻乘以一个费用系数Fi。不妨看成费用系数Fi进行了连续加法,每当当前决策之前的决策每使时间过去了1,就要在当前决策的费用中加上一个Fi;反过来,当前的决策每使时间增加k分钟,或者如开头所说收了k次钱,就会使它以及其后的每一个任务的费用加上Fi。由此得出dp方程:

dp[i]表示第i个到第n个任务所需的费用dp[i] = min{dp[j+1] + (sum{T[i..j]}+S) * sum{F[i..n]}}其中 ij ≤ n边界是 dp[n+1]=0目标是 dp[1]

深刻理解这个方程,会受益匪浅。

总结

从以上三例可以找出一个共同点——都是有乘法的区间dp!事实上,乘法看成加法是立即计算代价一种很自然的方式。正是通过直接将决策造成的所有后效性直接计算出来, 使得不需要考虑是否需要为以前的决策买单。 立即计算代价无疑是一种省心的方法。

参考资料

《算法艺术与信息学竞赛》
《算法导论》
部分网上内容,http://www.tyvj.cn 题解

0 0
原创粉丝点击