维护无后效性的技巧——立即计算代价
简介
无后效性是动态规划的一个基本特征之一,只有具备了无后效性的问题才可以使用动态规划求解。直观上讲,无后效性是指“现在不会影响未来”,或者说现在的决策不会影响未来如何决策。一个不具有无后效性的例子是矩阵寻路算法。设想一个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 0
1 0 1 1
1 1 1 1
1 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...(T个F)。
所以时间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]}}
其中 i ≤ j ≤ n
边界是 dp[n+1]=0
目标是 dp[1]
深刻理解这个方程,会受益匪浅。
总结
从以上三例可以找出一个共同点——都是有乘法的区间dp!事实上,乘法看成加法是立即计算代价一种很自然的方式。正是通过直接将决策造成的所有后效性直接计算出来, 使得不需要考虑是否需要为以前的决策买单。 立即计算代价无疑是一种省心的方法。
参考资料
《算法艺术与信息学竞赛》
《算法导论》
部分网上内容,http://www.tyvj.cn 题解