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


    简介

    无后效性是动态规划的一个基本特征之一,只有具备了无后效性的问题才可以使用动态规划求解。直观上讲,无后效性是指“现在不会影响未来”,或者说现在的决策不会影响未来如何决策。一个不具有无后效性的例子是矩阵寻路算法。设想一个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...(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 题解

  • 相关阅读:
    spring学习记录_Spring中的新注解
    spring学习记录_spring的 注解
    spring学习记录_spring的 ioc核心容器
    关于myeclipse项目运行报错:Access denied for user 'root'@'localhost' (using password: YES)
    vue项目中实现多语言切换
    OC中限制UITextView的最大字数的实现
    简单瀑布流的实现
    仿购物车的实现
    仿QQ好友列表界面的实现
    类似QQ侧滑菜单功能实现
  • 原文地址:https://www.cnblogs.com/ljt12138/p/6684391.html
Copyright © 2020-2023  润新知