动态规划与排列组合

来源:互联网 发布:网络渗透 编辑:程序博客网 时间:2024/05/02 19:33

转:动态规划与排列组合
2008年11月22日 星期六 下午 09:54
最近在学习动态规划,感觉很难彻底理解,也许这真的不是一时半会能会的。
有人说:DP非一日之功。我想也是,先转个帖子做启发吧。

<动态规划与排列组合>
原作者不详

   1. 像所有的新手一样,对一种算法思想的理解需要经历从肤浅(流于表面形式)到逐渐触摸到本质的过程。为什么说"逐渐"触摸到本质,是因为很多时候你并不确定一个解释是不是最本质的,有时候会有好几个等价的解释,各自在不同的场景下具有启发。
   2. 比如对动态规划(DP)的理解,一开始我理解为"递推",但实际上这是最肤浅的理解,对于如何在特定的问题中找到递推关系毫无帮助和启发。换言之,这只是一个描述性的总结,而不是一个建设性的总结,不含方法论。
   3. 做(看)了一些题目之后我开始总结关于"How"的方法,怎样寻找到递推关系(递推关系蕴含着子问题)。得到一个简单的观察,如果一个问题里面含有n这个变量,考虑把n变成n-1的情况。
   4. 当然,这个方法是非常特殊(狭窄)的。实际上更具一般性的方法是不仅可以从n-1后面切一刀,还可以在任意地方切(譬如切成两个n/2规模的),以任意标准切(比如像快排那样的)。所有考虑各种切法中哪种能够最有利于建立子问题是有帮助的。
   5. 这个我姑且把它叫做通过直接切分问题来寻找子问题。
   6. 可是。这还是特殊了。因为显然这个方法不能解决所有的DP问题,连"多数"DP问题都未必能解决。譬如大家熟悉的"最大(小)和连续子序列"问题,就难以通过这种方法求解(可行,但思维难度较大。);再譬如上次Lee给的敲石头问题,以及DD出的不听话的机器人问题。因此,在知道了这几个问题的解法之后我继续思考这些解法里面是不是蕴含着更具一般性的解题方法。
   7. 看起来我找到了一个,就是分类讨论法,具体做法是:首先,写出可行解的一般形式,譬如a1, a2, ..., an。然后对其中的某个不确定的ai进行讨论。譬如旅行商问题里面对"下一个城市"进行讨论。当不确定时,讨论。讨论的每个分支都带来了进一步的确定性,从而将问题转化成一个子问题。
   8. 然后Lee提到,这个方法是自顶向下的,有时候不适于思考,譬如对敲石头问题。并提到一种自底向上,着重于构造"状态"的外推法。
   9. 于是,到目前为止,DP的一般性启发式思考方法就已经有了三种:
10. 1. 通过直接对问题切分来探索子问题。
11. 2. 通过对可行解的分类讨论来探索子问题。
12. 3. 通过建立"状态"来自底向上地推导最终解。
13. 可是,我心里还是不踏实,因为"离散"的知识是极其不利于记忆的,需要更多的练习才能在运用的时候"不由自主"的联想起来,而且也容易遗忘。每次做DP题的时候,我都得把好几种指导性的思路一一费劲从脑袋里翻出来,然后尝试。实在很不爽。虽然DP题也许并没有万用的解题手法,但我还是希望能够尽量提取出不同手法之间的本质联系,如果能够将一组看上去离散的知识点统一在一个更具一般性,更本质的知识点下面,我们的知识树就多出了一个根节点,于是下次提取的时候只要提取出那个根节点,下面的几个子节点就会一一乖乖闪现,极大的降低记忆的复杂性。
14. 那么,上面三种手法的本质联系到底是什么呢?有一次在路上走的时候我想出了一种解释。它也许不是最本质的,也许大牛们早就想到过,但是我觉得至少有两个好处:
15. 1. 它涵盖以上三种手法,通过增加一个根节点,减少知识的记忆复杂性和易提取性。
16. 2. 归纳抽象的过程本身就是一种锻炼,锻炼的是归纳抽象的能力。
17. 这个解释就是:大多DP问题的可行解的形式是一个排列组合(典型的——旅行商问题、最短路径问题)。大家都知道,穷举一个规模为N的排列组合复杂性是 a^n的,也就是"组合复杂性"。而求解DP问题的核心步骤:发现“子问题”,这个“子问题”实际上就是对应最终解的那个排列组合的某个"子排列组合" (某种子集);而这里的“子排列组合”的数目则往往是多项式的(silwile,XuYou,g9指出并非总是如此),这就是为什么一个组合复杂性的穷举问题可以DP优化为多项式复杂度的问题。将重复出现的子排列组合对应的子问题的解缓存起来,就是DP 的缓存优化了。
18. 此外,这一解释也提供了如何探索子问题的一个通用方案:寻找形式相同的子排列组合。还是拿最大和连续子序列说事,其解的形式是:A[i], A[i+1], .., A[j-1], A[j],其中i,j不确定。那么如何得到形式相同的子组合呢?首先讨论A[j],为什么要讨论,因为只要A[j]不确定,A[j-1]就不确定,就拿不出子组合来。对于一个确定的A[j0],解的可能性为A[i], A[i+1], .., A[j0-1], A[j0]。其最优解依赖于子组合A[i], A[i+1], .., A[j0-1],到这里子问题就不请子现了:A[i], A[i+1], .., A[j0-1], A[j0]和A[i], A[i+1], .., A[j0-1]的形式相同,意味着它们是同一个问题的不同阶表示。
19. 当然,由于这个指导思想一般性大了点,所以实际问题中往往没有前面提到的三种手法寻找方案来得快——众所周知的是,越特定的手法解题面虽然越窄,但如果题目对口了解决起来也越快。但它至少有两个好处(前面说过了)。所以考虑一般性和特殊性的手法都是有帮助的。
20. 类似的,敲石头问题也可以通过这种手法来探索子问题。至少目前我做过的DP题似乎都可以借助这种手法来探索,当然,刚才说过了,未必是最快的,所以也许可以考虑用来做后备方案,当其它方案没有头绪的时候试试。
21. 我的解题经验还很有限,所以不清楚这个手法的覆盖范围有多广。实际上一个更广的领域是"组合优化"。更上面提到的很像。但针对的问题就不仅止于DP了。
22. 参考
23. 1. 《Introduction To Algorithms》的DP章节。
24. 2. 《Algorithms》的DP章节。
25. 3. 《Algorithm Design Manual》的DP章节。
26. 4. DD的《背包问题九讲》
27. 5. wikipedia
28. 6. 网络搜索出来的一堆题目和讲解。(如《动态规划经典题集》)
29. 7. TopLanguage上的帖子:矩阵也疯狂、DPDP、DPDP(二)

回帖:

#Eastsun 发表于2008-06-05 20:21:14 IP: 202.113.19.*
    我对DP的理解就是:用空间换时间
    将计算过程中可能重复使用的结果保存起来避免重复计算就是DP

#kusk 发表于2008-06-05 21:22:11 IP: 123.112.70.*
    同意Eastsun的观点。“问题可分解为相互重叠子问题”也是一般对DP的定义吧。

    基本上关于离散数据处理方面的难题都可以归结为组合问题。因为基本上能见得到的有意义的题目,通常只有涉及到组合才会带来爆炸式的复杂度,否则一般都只能是多项式级的。

    DP适用于组合问题,也适用于非组合问题。但对于非组合问题,因为本身就是多项式的问题,所以我们通常以直观的方式去理解它,直接就把它作为问题的解法,不把它叫DP了……比如求斐波那契数列,如果你写作:
    int f(int n)
    {
    if (n == 0) return 0;
    else if (n == 1) return 1;
    else return f(n - 1) + f(n - 2);
    }

    那么这就是递归。但因为计算f(n-1)和f(n-2)时包含了重复计算,所以满足了DP的条件,使用DP求解,把重复的部分缓存:
    int f_DP(int n)
    {
    int cache[MAX_SIZE] = { -1, -1, ... };
    if (cache[n] != -1) return cache[n];
    else
    {
    cache[n] = f(n);
    return cache[n];
    }
    }

    此即“自顶向下”的DP。因为计算顺序仍然是从结果向下推。

    另外,由于是尾递归,因此容易改写成"自底向上"的DP,即从边界值开始一层一层往目标推:

    int f_DP_bu(n)
    {
    int a = 0, b = 1;
    while (n-- > 0)
    {
    b = a + b;
    a = b - a;
    }
    return a;
    }

    这实际上已经是问题的迭代解法了。你完全可以把它视为DP。但只是可能没有人把它作为DP的例子,并冠以DP解法之名而已……

#刘未鹏 发表于2008-06-05 22:37:22 IP: 222.94.44.*
    @Eastsun:
    你说得没错。用空间换时间是DP的一个重要性质,事实上,它还是许多算法的一个性质。所以说,“用空间换时间”并没有“充分”地描述DP的特点,更没有对 “如何探索一个DP问题的解”提供建设性的帮助。对于后者,如何寻找递推关系或“子问题”才是重点,一旦找到了子问题之后通过缓存子问题的解来优化复杂度就相对比较trivial了。

    用“空间换时间”来总结DP的本质就好比用“它们都是一种算法思想”来总结各大算法分支的本质一样,因过于泛化和描述性而并不能对解决实际问题带来什么帮助。

    @kusk:


        基本上关于离散数据处理方面的难题都可以归结为组合问题。因为基本上能见得到的有意义的题目,通常只有涉及到组合才会带来爆炸式的复杂度,否则一般都只能是多项式级的。


    没错。

#Eastsun 发表于2008-06-05 23:14:22 IP: 202.113.19.*
    其实我觉得把动态规划当成是一种思想也未尝不可。
    大多数“空间换时间”的算法本质上可以归结为DP。
    不过通常的DP有个很明显的特征:就是可以写出状态转移方程。kusk所举的斐波那契数列的递归公式在DP里就是状态转移方程。然后根据状态转移方程就可以算出该算法的时间/空间复杂度,并写出具体的程序代码。
    刘汝佳的《算法艺术与信息学竞赛》一书里面列举并讲解了很多非常不错的关于DP的题目。

#pongba 发表于2008-06-06 00:11:29 IP: 222.94.44.*
    @EastSun:
    我倒是认为转移方程只是表象,是在寻找出了子问题,并构建出了递推关系之后,自底向上地编写程序的时候的事情,而不属于一开始的事情。
    一开始的事情是“如何”写出(找到)转移方程,而我想总结的则是关于如何找出那个关键的转移方程的一些启发式探索方法。
    DP的思想理解起来容易,一开始的时候我看了几本书的DP章节,觉得,哦,这很容易,递推嘛。但真正自己做题的时候,发现寻找递推关系是最困难的一步,non-trivial的DP题里面递推关系并不是显而易见的。
    TopLang里面也有人推荐《算法艺术与信息学竞赛》,但提到里面的题目偏难,不适合初学者。作为题集应该还是不错的。据说几本众所周知的经典算法书里面最适合程序员学习的是《Algorithm Design》,以下引一段g9的介绍:

        我也喜欢Algorithm Design这本书。个人觉得非常适合初学者和程序员。书一开始就给出稳定婚姻问题。从解决问题的角度入手,层层递进地讲解怎么设计,引出算法设计的一般方法。然后用几道常见题目引入常用算法的设计思路。奠定总纲后,后面的书就从不同的角度深化算法设计的方法。例题也有趣。个人相当喜欢从设计到分析的讲解思路。这种方法给读者(至少是程序员)诱人的动机。毕竟很多人读算法书是为了写出更强大的程序。尤其像我这种老人家,NADD症状严重,尤其需要令人信服的动机,不然就老走神。而且作者强调设计,反而提供了钻研算法分析的充分理由,很好地解释了要设计出正确高效的算法,形式化的分析是必要手段。为什要扩展?为什么要到处一堆引理?为什么要追究复杂度?为什么要泛化?为什么要特化?这些看来学究的行为自然地有了现实基础。加上这本书写得晓畅通透,就算你只看书不动手,也可以获得"好舒服,原来我也可以设计出这些算法啊。一点都不难嘛"这种类似K粉后的波浪状快感和幻觉。:-D


#Eastsun 发表于2008-06-06 01:25:03 IP: 202.113.19.*
    [quote="pongba"]我倒是认为转移方程只是表象,是在寻找出了子问题,并构建出了递推关系之后,自底向上地编写程序的时候的事情,而不属于一开始的事情。
    一开始的事情是“如何”写出(找到)转移方程,而我想总结的则是关于如何找出那个关键的转移方程的一些启发式探索方法。
    DP的思想理解起来容易,一开始的时候我看了几本书的DP章节,觉得,哦,这很容易,递推嘛。但真正自己做题的时候,发现寻找递推关系是最困难的一步,non-trivial的DP题里面递推关系并不是显而易见的。[/quote]

    非常认同你的看法
    像“最大和连续子序列”这种问题都是很显而易见的DP问题
    有些问题表明上看来与DP毫无干系,需要通过一些巧妙的变换或构造才能得到状态以及状态转移方程。
    《算法艺术与信息学竞赛》里面就有一些这样的问题。
    不过这本书里面好像没有讲Tree型DP,我觉得这也是一类常见且相对复杂的DP

原创粉丝点击