Algo: Dynamic programming

Copyright © 1900-2016, NORYES, All Rights Reserved.

http://www.cnblogs.com/noryes/

欢迎转载,请保留此版权声明。

-----------------------------------------------------------------------------------------


动态规划的本质不在于是递推或是递归,也不需要纠结是不是内存换时间。

理解动态规划并不需要数学公式介入,只是完全解释清楚需要点篇幅…首先需要明白哪些问题不是动态规划可以解决的,才能明白为神马需要动态规划。不过好处时顺便也就搞明白了递推贪心搜索和动规之间有什么关系,以及帮助那些总是把动规当成搜索解的同学建立动规的思路。当然熟悉了之后可以直接根据问题的描述得到思路,如果有需要的话再补充吧。

动态规划是对于 某一类问题 的解决方法!!重点在于如何鉴定“某一类问题”是动态规划可解的而不是纠结解决方法是递归还是递推!

怎么鉴定dp可解的一类问题需要从计算机是怎么工作的说起…计算机的本质是一个状态机,内存里存储的所有数据构成了当前的状态,CPU只能利用当前的状态计算出下一个状态(不要纠结硬盘之类的外部存储,就算考虑他们也只是扩大了状态的存储容量而已,并不能改变下一个状态只能从当前状态计算出来这一条铁律)


当你企图使用计算机解决一个问题是,其实就是在思考如何将这个问题表达成状态(用哪些变量存储哪些数据)以及如何在状态中转移(怎样根据一些变量计算出另一些变量)。所以所谓的空间复杂度就是为了支持你的计算所必需存储的状态最多有多少,所谓时间复杂度就是从初始状态到达最终状态中间需要多少步!

太抽象了还是举个例子吧:

比如说我想计算第100个非波那契数,每一个非波那契数就是这个问题的一个状态,每求一个新数字只需要之前的两个状态。所以同一个时刻,最多只需要保存两个状态,空间复杂度就是常数;每计算一个新状态所需要的时间也是常数且状态是线性递增的,所以时间复杂度也是线性的。

上面这种状态计算很直接,只需要依照固定的模式从旧状态计算出新状态就行(a[i]=a[i-1]+a[i-2]),不需要考虑是不是需要更多的状态,也不需要选择哪些旧状态来计算新状态。对于这样的解法,我们叫递推。

非波那契那个例子过于简单,以至于让人忽视了阶段的概念,所谓阶段是指随着问题的解决,在同一个时刻可能会得到的不同状态的集合。非波那契数列中,每一步会计算得到一个新数字,所以每个阶段只有一个状态。想象另外一个问题情景,假如把你放在一个围棋棋盘上的某一点,你每一步只能走一格,因为你可以东南西北随便走,所以你当你同样走四步可能会处于很多个不同的位置。从头开始走了几步就是第几个阶段,走了n步可能处于的位置称为一个状态,走了这n步所有可能到达的位置的集合就是这个阶段下所有可能的状态。

现在问题来了,有了阶段之后,计算新状态可能会遇到各种奇葩的情况,针对不同的情况,就需要不同的算法,下面就分情况来说明一下:


假如问题有n个阶段,每个阶段都有多个状态,不同阶段的状态数不必相同,一个阶段的一个状态可以得到下个阶段的所有状态中的几个。那我们要计算出最终阶段的状态数自然要经历之前每个阶段的某些状态。

好消息是,有时候我们并不需要真的计算所有状态,比如这样一个弱智的棋盘问题:从棋盘的左上角到达右下角最短需要几步。答案很显然,用这样一个弱智的问题是为了帮助我们理解阶段和状态。某个阶段确实可以有多个状态,正如这个问题中走n步可以走到很多位置一样。但是同样n步中,有哪些位置可以让我们在第n+1步中走的最远呢?没错,正是第n步中走的最远的位置。换成一句熟悉话叫做“下一步最优是从当前最优得到的”。所以为了计算最终的最优值,只需要存储每一步的最优值即可,解决符合这种性质的问题的算法就叫贪心。如果只看最优状态之间的计算过程是不是和非波那契数列的计算很像?所以计算的方法是递推。

既然问题都是可以划分成阶段和状态的。这样一来我们一下子解决了一大类问题:一个阶段的最优可以由前一个阶段的最优得到。

如果一个阶段的最优无法用前一个阶段的最优得到呢?

什么你说只需要之前两个阶段就可以得到当前最优?那跟只用之前一个阶段并没有本质区别。最麻烦的情况在于你需要之前所有的情况才行。

再来一个迷宫的例子。在计算从起点到终点的最短路线时,你不能只保存当前阶段的状态,因为题目要求你最短,所以你必须知道之前走过的所有位置。因为即便你当前再的位置不变,之前的路线不同会影响你的之后走的路线。这时你需要保存的是之前每个阶段所经历的那个状态,根据这些信息才能计算出下一个状态!

每个阶段的状态或许不多,但是每个状态都可以转移到下一阶段的多个状态,所以解的复杂度就是指数的,因此时间复杂度也是指数的。哦哦,刚刚提到的之前的路线会影响到下一步的选择,这个令人不开心的情况就叫做有后效性。

刚刚的情况实在太普遍,解决方法实在太暴力,有没有哪些情况可以避免如此的暴力呢?

契机就在于后效性。

有一类问题,看似需要之前所有的状态,其实不用。不妨也是拿最长上升子序列的例子来说明为什么他不必需要暴力搜索,进而引出动态规划的思路。

假装我们年幼无知想用搜索去寻找最长上升子序列。怎么搜索呢?需要从头到尾依次枚举是否选择当前的数字,每选定一个数字就要去看看是不是满足“上升”的性质,这里第i个阶段就是去思考是否要选择第i个数,第i个阶段有两个状态,分别是选和不选。哈哈,依稀出现了刚刚迷宫找路的影子!咦慢着,每次当我决定要选择当前数字的时候,只需要和之前选定的一个数字比较就行了!这是和之前迷宫问题的本质不同!这就可以纵容我们不需要记录之前所有的状态啊!既然我们的选择已经不受之前状态的组合的影响了,那时间复杂度自然也不是指数的了啊!虽然我们不在乎某序列之前都是什么元素,但我们还是需要这个序列的长度的。所以我们只需要记录以某个元素结尾的LIS长度就好!因此第i个阶段的最优解只是由前i-1个阶段的最优解得到的,然后就得到了DP方程
LIS(i)=max{LIS(j)+1} j<i and a[j] < a[i]

所以一个问题是该用递推、贪心、搜索还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定的!

每个阶段只有一个状态->递推;
每个阶段的最优状态都是由上一个阶段的最优状态得到的->贪心;
每个阶段的最优状态是由之前所有阶段的状态的组合得到的->搜索;
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而不管之前这个状态是如何得到的->动态规划。

每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到
这个性质叫做最优子结构;

而不管之前这个状态是如何得到的
这个性质叫做无后效性。

另:其实动态规划中的最优状态的说法容易产生误导,以为只需要计算最优状态就好,LIS问题确实如此,转移时只用到了每个阶段“选”的状态。但实际上有的问题往往需要对每个阶段的所有状态都算出一个最优值,然后根据这些最优值再来找最优状态。比如背包问题就需要对前i个包(阶段)容量为j时(状态)计算出最大价值。然后在最后一个阶段中的所有状态种找到最优值

 
作者:Hawstein
出处:http://hawstein.com/posts/dp-novice-to-advanced.html
声明:本文采用以下协议进行授权: 自由转载-非商用-非衍生-保持署名|Creative Commons BY-NC-ND 3.0 ,转载请注明作者及出处。

前言

本文翻译自TopCoder上的一篇文章: Dynamic Programming: From novice to advanced ,并非严格逐字逐句翻译,其中加入了自己的一些理解。水平有限,还望指摘。

前言_

我们遇到的问题中,有很大一部分可以用动态规划(简称DP)来解。 解决这类问题可以很大地提升你的能力与技巧,我会试着帮助你理解如何使用DP来解题。 这篇文章是基于实例展开来讲的,因为干巴巴的理论实在不好理解。

注意:如果你对于其中某一节已经了解并且不想阅读它,没关系,直接跳过它即可。

简介(入门)

什么是动态规划,我们要如何描述它?

动态规划算法通常基于一个递推公式及一个或多个初始状态。 当前子问题的解将由上一次子问题的解推出。使用动态规划来解题只需要多项式时间复杂度, 因此它比回溯法、暴力法等要快许多。

现在让我们通过一个例子来了解一下DP的基本原理。

首先,我们要找到某个状态的最优解,然后在它的帮助下,找到下一个状态的最优解。

“状态”代表什么及如何找到它?

“状态”用来描述该问题的子问题的解。原文中有两段作者阐述得不太清楚,跳过直接上例子。

如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元? (表面上这道题可以用贪心算法,但贪心算法无法保证可以求出解,比如1元换成2元的时候)

首先我们思考一个问题,如何用最少的硬币凑够i元(i<11)?为什么要这么问呢? 两个原因:1.当我们遇到一个大问题时,总是习惯把问题的规模变小,这样便于分析讨论。 2.这个规模变小后的问题和原来的问题是同质的,除了规模变小,其它的都是一样的, 本质上它还是同一个问题(规模变小后的问题其实是原问题的子问题)。

好了,让我们从最小的i开始吧。当i=0,即我们需要多少个硬币来凑够0元。 由于1,3,5都大于0,即没有比0小的币值,因此凑够0元我们最少需要0个硬币。 (这个分析很傻是不是?别着急,这个思路有利于我们理清动态规划究竟在做些什么。) 这时候我们发现用一个标记来表示这句“凑够0元我们最少需要0个硬币。”会比较方便, 如果一直用纯文字来表述,不出一会儿你就会觉得很绕了。那么, 我们用d(i)=j来表示凑够i元最少需要j个硬币。于是我们已经得到了d(0)=0, 表示凑够0元最小需要0个硬币。当i=1时,只有面值为1元的硬币可用, 因此我们拿起一个面值为1的硬币,接下来只需要凑够0元即可,而这个是已经知道答案的, 即d(0)=0。所以,d(1)=d(1-1)+1=d(0)+1=0+1=1。当i=2时, 仍然只有面值为1的硬币可用,于是我拿起一个面值为1的硬币, 接下来我只需要再凑够2-1=1元即可(记得要用最小的硬币数量),而这个答案也已经知道了。 所以d(2)=d(2-1)+1=d(1)+1=1+1=2。一直到这里,你都可能会觉得,好无聊, 感觉像做小学生的题目似的。因为我们一直都只能操作面值为1的硬币!耐心点, 让我们看看i=3时的情况。当i=3时,我们能用的硬币就有两种了:1元的和3元的( 5元的仍然没用,因为你需要凑的数目是3元!5元太多了亲)。 既然能用的硬币有两种,我就有两种方案。如果我拿了一个1元的硬币,我的目标就变为了: 凑够3-1=2元需要的最少硬币数量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。 这个方案说的是,我拿3个1元的硬币;第二种方案是我拿起一个3元的硬币, 我的目标就变成:凑够3-3=0元需要的最少硬币数量。即d(3)=d(3-3)+1=d(0)+1=0+1=1. 这个方案说的是,我拿1个3元的硬币。好了,这两种方案哪种更优呢? 记得我们可是要用最少的硬币数量来凑够3元的。所以, 选择d(3)=1,怎么来的呢?具体是这样得到的:d(3)=min{d(3-1)+1, d(3-3)+1}。

OK,码了这么多字讲具体的东西,让我们来点抽象的。从以上的文字中, 我们要抽出动态规划里非常重要的两个概念:状态和状态转移方程。

上文中d(i)表示凑够i元需要的最少硬币数量,我们将它定义为该问题的”状态”, 这个状态是怎么找出来的呢?我在另一篇文章 动态规划之背包问题(一)中写过: 根据子问题定义状态。你找到子问题,状态也就浮出水面了。 最终我们要求解的问题,可以用这个状态来表示:d(11),即凑够11元最少需要多少个硬币。 那状态转移方程是什么呢?既然我们用d(i)表示状态,那么状态转移方程自然包含d(i), 上文中包含状态d(i)的方程是:d(3)=min{d(3-1)+1, d(3-3)+1}。没错, 它就是状态转移方程,描述状态之间是如何转移的。当然,我们要对它抽象一下,

d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示第j个硬币的面值;

有了状态和状态转移方程,这个问题基本上也就解决了。当然了,Talk is cheap,show me the code!

伪代码如下:

下图是当i从0到11时的解:

从上图可以得出,要凑够11元至少需要3枚硬币。

此外,通过追踪我们是如何从前一个状态值得到当前状态值的, 可以找到每一次我们用的是什么面值的硬币。比如,从上面的图我们可以看出, 最终结果d(11)=d(10)+1(面值为1),而d(10)=d(5)+1(面值为5),最后d(5)=d(0)+1 (面值为5)。所以我们凑够11元最少需要的3枚硬币是:1元、5元、5元。

注意:原文中这里本来还有一段的,但我反反复复读了几遍, 大概的意思我已经在上文从i=0到i=3的分析中有所体现了。作者本来想讲的通俗一些, 结果没写好,反而更不好懂,所以这段不翻译了。

初级

上面讨论了一个非常简单的例子。现在让我们来看看对于更复杂的问题, 如何找到状态之间的转移方式(即找到状态转移方程)。 为此我们要引入一个新词叫递推关系来将状态联系起来(说的还是状态转移方程)

OK,上例子,看看它是如何工作的。

一个序列有N个数:A[1],A[2],…,A[N],求出最长非降子序列的长度。 (讲DP基本都会讲到的一个问题LIS:longest increasing subsequence)

正如上面我们讲的,面对这样一个问题,我们首先要定义一个“状态”来代表它的子问题, 并且找到它的解。注意,大部分情况下,某个状态只与它前面出现的状态有关, 而独立于后面的状态。

让我们沿用“入门”一节里那道简单题的思路来一步步找到“状态”和“状态转移方程”。 假如我们考虑求A[1],A[2],…,A[i]的最长非降子序列的长度,其中i<N, 那么上面的问题变成了原问题的一个子问题(问题规模变小了,你可以让i=1,2,3等来分析) 然后我们定义d(i),表示前i个数中以A[i]结尾的最长非降子序列的长度。OK, 对照“入门”中的简单题,你应该可以估计到这个d(i)就是我们要找的状态。 如果我们把d(1)到d(N)都计算出来,那么最终我们要找的答案就是这里面最大的那个。 状态找到了,下一步找出状态转移方程。

为了方便理解我们是如何找到状态转移方程的,我先把下面的例子提到前面来讲。 如果我们要求的这N个数的序列是:

5,3,4,8,6,7

根据上面找到的状态,我们可以得到:(下文的最长非降子序列都用LIS表示)

  • 前1个数的LIS长度d(1)=1(序列:5)
  • 前2个数的LIS长度d(2)=1(序列:3;3前面没有比3小的)
  • 前3个数的LIS长度d(3)=2(序列:3,4;4前面有个比它小的3,所以d(3)=d(2)+1)
  • 前4个数的LIS长度d(4)=3(序列:3,4,8;8前面比它小的有3个数,所以 d(4)=max{d(1),d(2),d(3)}+1=3)

OK,分析到这,我觉得状态转移方程已经很明显了,如果我们已经求出了d(1)到d(i-1), 那么d(i)可以用下面的状态转移方程得到:

d(i) = max{1, d(j)+1},其中j<i,A[j]<=A[i]

用大白话解释就是,想要求d(i),就把i前面的各个子序列中, 最后一个数不大于A[i]的序列长度加1,然后取出最大的长度即为d(i)。 当然了,有可能i前面的各个子序列中最后一个数都大于A[i],那么d(i)=1, 即它自身成为一个长度为1的子序列。

分析完了,上图:(第二列表示前i个数中LIS的长度, 第三列表示,LIS中到达当前这个数的上一个数的下标,根据这个可以求出LIS序列)

Talk is cheap, show me the code:

#include <iostream>
using namespace std;

int lis(int A[], int n){
    int *d = new int[n];
    int len = 1;
    for(int i=0; i<n; ++i){
        d[i] = 1;
        for(int j=0; j<i; ++j)
            if(A[j]<=A[i] && d[j]+1>d[i])
                d[i] = d[j] + 1;
        if(d[i]>len) len = d[i];
    }
    delete[] d;
    return len;
}
int main(){
    int A[] = {
        5, 3, 4, 8, 6, 7
    };
    cout<<lis(A, 6)<<endl;
    return 0;
}

该算法的时间复杂度是O(n^2 ),并不是最优的解法。 还有一种很巧妙的算法可以将时间复杂度降到O(nlogn),网上已经有各种文章介绍它, 这里就不再赘述。传送门: LIS的O(nlogn)解法。 此题还可以用“排序+LCS”来解,感兴趣的话可自行Google。

练习题

无向图G有N个结点(1<N<=1000)及一些边,每一条边上带有正的权重值。 找到结点1到结点N的最短路径,或者输出不存在这样的路径。

提示:在每一步中,对于那些没有计算过的结点, 及那些已经计算出从结点1到它的最短路径的结点,如果它们间有边, 则计算从结点1到未计算结点的最短路径。

尝试解决以下来自topcoder竞赛的问题:

中级

接下来,让我们来看看如何解决二维的DP问题。

平面上有N*M个格子,每个格子中放着一定数量的苹果。你从左上角的格子开始, 每一步只能向下走或是向右走,每次走到一个格子上就把格子里的苹果收集起来, 这样下去,你最多能收集到多少个苹果。

解这个问题与解其它的DP问题几乎没有什么两样。第一步找到问题的“状态”, 第二步找到“状态转移方程”,然后基本上问题就解决了。

首先,我们要找到这个问题中的“状态”是什么?我们必须注意到的一点是, 到达一个格子的方式最多只有两种:从左边来的(除了第一列)和从上边来的(除了第一行)。 因此为了求出到达当前格子后最多能收集到多少个苹果, 我们就要先去考察那些能到达当前这个格子的格子,到达它们最多能收集到多少个苹果。 (是不是有点绕,但这句话的本质其实是DP的关键:欲求问题的解,先要去求子问题的解)

经过上面的分析,很容易可以得出问题的状态和状态转移方程。 状态S[i][j]表示我们走到(i, j)这个格子时,最多能收集到多少个苹果。那么, 状态转移方程如下:

S[i][j]=A[i][j] + max(S[i-1][j], if i>0 ; S[i][j-1], if j>0)

其中i代表行,j代表列,下标均从0开始;A[i][j]代表格子(i, j)处的苹果数量。

S[i][j]有两种计算方式:1.对于每一行,从左向右计算,然后从上到下逐行处理;2. 对于每一列,从上到下计算,然后从左向右逐列处理。 这样做的目的是为了在计算S[i][j]时,S[i-1][j]和S[i][j-1]都已经计算出来了。

伪代码如下:

以下两道题来自topcoder,练习用的。

中高级

这一节要讨论的是带有额外条件的DP问题。

以下的这个问题是个很好的例子。

无向图G有N个结点,它的边上带有正的权重值。

你从结点1开始走,并且一开始的时候你身上带有M元钱。如果你经过结点i, 那么你就要花掉S[i]元(可以把这想象为收过路费)。如果你没有足够的钱, 就不能从那个结点经过。在这样的限制条件下,找到从结点1到结点N的最短路径。 或者输出该路径不存在。如果存在多条最短路径,那么输出花钱数量最少的那条。 限制:1<N<=100 ; 0<=M<=100 ; 对于每个i,0<=S[i]<=100;正如我们所看到的, 如果没有额外的限制条件(在结点处要收费,费用不足还不给过),那么, 这个问题就和经典的迪杰斯特拉问题一样了(找到两结点间的最短路径)。 在经典的迪杰斯特拉问题中, 我们使用一个一维数组来保存从开始结点到每个结点的最短路径的长度, 即M[i]表示从开始结点到结点i的最短路径的长度。然而在这个问题中, 我们还要保存我们身上剩余多少钱这个信息。因此,很自然的, 我们将一维数组扩展为二维数组。M[i][j]表示从开始结点到结点i的最短路径长度, 且剩余j元。通过这种方式,我们将这个问题规约到原始的路径寻找问题。 在每一步中,对于已经找到的最短路径,我们找到它所能到达的下一个未标记状态(i,j), 将它标记为已访问(之后不再访问这个结点),并且在能到达这个结点的各个最短路径中, 找到加上当前边权重值后最小值对应的路径,即为该结点的最短路径。 (写起来真是绕,建议画个图就会明了很多)。不断重复上面的步骤, 直到所有的结点都访问到为止(这里的访问并不是要求我们要经过它, 比如有个结点收费很高,你没有足够的钱去经过它,但你已经访问过它) 最后Min[N-1][j]中的最小值即是问题的答案(如果有多个最小值, 即有多条最短路径,那么选择j最大的那条路径,即,使你剩余钱数最多的最短路径)。

伪代码:

下面有几道topcoder上的题以供练习:

高级

以下问题需要仔细的揣摩才能将其规约为可用DP解的问题。

问题:StarAdventure - SRM 208 Div 1:

给定一个M行N列的矩阵(M*N个格子),每个格子中放着一定数量的苹果。 你从左上角的格子开始,只能向下或向右走,目的地是右下角的格子。 你每走过一个格子,就把格子上的苹果都收集起来。然后你从右下角走回左上角的格子, 每次只能向左或是向上走,同样的,走过一个格子就把里面的苹果都收集起来。 最后,你再一次从左上角走到右下角,每过一个格子同样要收集起里面的苹果 (如果格子里的苹果数为0,就不用收集)。求你最多能收集到多少苹果。

注意:当你经过一个格子时,你要一次性把格子里的苹果都拿走。

限制条件:1 < N, M <= 50;每个格子里的苹果数量是0到1000(包含0和1000)。

如果我们只需要从左上角的格子走到右下角的格子一次,并且收集最大数量的苹果, 那么问题就退化为“中级”一节里的那个问题。将这里的问题规约为“中级”里的简单题, 这样一来会比较好解。让我们来分析一下这个问题,要如何规约或是修改才能用上DP。 首先,对于第二次从右下角走到左上角得出的这条路径, 我们可以将它视为从左上角走到右下角得出的路径,没有任何的差别。 (即从B走到A的最优路径和从A走到B的最优路径是一样的)通过这种方式, 我们得到了三条从顶走到底的路径。对于这一点的理解可以稍微减小问题的难度。 于是,我们可以将这3条路径记为左,中,右路径。对于两条相交路径(如下图):

在不影响结果的情况下,我们可以将它们视为两条不相交的路径:

这样一来,我们将得到左,中,右3条路径。此外,如果我们要得到最优解, 路径之间不能相交(除了左上角和右下角必然会相交的格子)。因此对于每一行y( 除了第一行和最后一行),三条路径对应的x坐标要满足:x1[y] < x2[y] < x3[y]。 经过这一步的分析,问题的DP解法就进一步地清晰了。让我们考虑行y, 对于每一个x1[y-1],x2[y-1]和x3[y-1],我们已经找到了能收集到最多苹果数量的路径。 根据它们,我们能求出行y的最优解。现在我们要做的就是找到从一行移动到下一行的方式。 令Max[i][j][k]表示到第y-1行为止收集到苹果的最大数量, 其中3条路径分别止于第i,j,k列。对于下一行y,对每个Max[i][j][k] 都加上格子(y,i),(y,j)和(y,k)内的苹果数量。因此,每一步我们都向下移动。 我们做了这一步移动之后,还要考虑到,一条路径是有可能向右移动的。 (对于每一个格子,我们有可能是从它上面向下移动到它, 也可能是从它左边向右移动到它)。为了保证3条路径互不相交, 我们首先要考虑左边的路径向右移动的情况,然后是中间,最后是右边的路径。 为了更好的理解,让我们来考虑左边的路径向右移动的情况,对于每一个可能的j,k对(j<k), 对每个i(i<j),考虑从位置(i-1,j,k)移动到位置(i,j,k)。处理完左边的路径, 再处理中间的路径,最后处理右边的路径。方法都差不多。

用于练习的topcoder题目:

其它

当阅读一个题目并且开始尝试解决它时,首先看一下它的限制。 如果要求在多项式时间内解决,那么该问题就很可能要用DP来解。遇到这种情况, 最重要的就是找到问题的“状态”和“状态转移方程”。(状态不是随便定义的, 一般定义完状态,你要找到当前状态是如何从前面的状态得到的, 即找到状态转移方程)如果看起来是个DP问题,但你却无法定义出状态, 那么试着将问题规约到一个已知的DP问题(正如“高级”一节中的例子一样)。

后记

看完这教程离DP专家还差得远,好好coding才是王道。


/*
https://blog.csdn.net/u013445530/article/details/45645307
https://blog.csdn.net/baidu_28312631/article/details/47418773

什么是动态规划?
动态规划(Dynamic Programming,所以我们简称动态规划为DP)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957年出版了他的名著《Dynamic Programming》,这是该领域的第一本著作。

动态规划算法通常基于一个递推公式及一个或多个初始状态。当前子问题的解将由上一次子问题的解推出。使用动态规划来解题只需要多项式时间复杂度,因此它比回溯法、暴力法等要快许多。

说了这么多术语,想必大家都很头疼,现在让我们通过一个例子来了解一下DP的基本原理。

首先,我们要找到某个状态的最优解,然后在它的帮助下,找到下一个状态的最优解。这句话暂时理解不了没关系,请看下面的例子:

=========================================================
Q1:
如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?

我们凭直观感觉告诉自己,先选面值最大,因此最多选2枚5元的硬币,现在是10元了,还差一元,接下来我们挑选第二大的3元硬币,发现不行(10+3=13超了),因此我们继续选第三大的硬币也就是1元硬币,选一个就可以(10+1=11),所以总共用了3枚硬币凑够了11元。这就是贪心法,每次选最大的。但是我们将面值改为2元,3元和5元的硬币,再用贪心法就不行了。为什么呢?按照贪心思路,我们同样先取2枚最大5元硬币,现在10元了,还差一元,接下来选第二大的,发现不行,再选第三大的,还是不行,这时用贪心方法永远凑不出11元,但是你仔细看看,其实我们可以凑出11元的,2枚3元硬币和1枚五元硬币就行了,这是人经过思考判断出来了的,但是怎么让计算机算出来呢?这就要用动态规划的思想:

首先我们思考一个问题,如何用最少的硬币凑够i元(i<11)?为什么要这么问呢?两个原因:1.当我们遇到一个大问题时,总是习惯把问题的规模变小,这样便于分析讨论。 2.这个规模变小后的问题和原来的问题是同质的,除了规模变小,其它的都是一样的,本质上它还是同一个问题(规模变小后的问题其实是原问题的子问题)。

好了,让我们从最小的i开始吧。当i=0,即我们需要多少个硬币来凑够0元。由于1,3,5都大于0,即没有比0小的币值,因此凑够0元我们最少需要0个硬币。 (这个分析很傻是不是?别着急,这个思路有利于我们理清动态规划究竟在做些什么。) 这时候我们发现用一个标记来表示这句“凑够0元我们最少需要0个硬币。”会比较方便,如果一直用纯文字来表述,不出一会儿你就会觉得很绕了。那么,我们用d(i)=j来表示凑够i元最少需要j个硬币。于是我们已经得到了d(0)=0,表示凑够0元最小需要0个硬币。当i=1时,只有面值为1元的硬币可用,因此我们拿起一个面值为1的硬币,接下来只需要凑够0元即可,而这个是已经知道答案的,即d(0)=0。所以,d(1)=d(1-1)+1=d(0)+1=0+1=1。当i=2时,仍然只有面值为1的硬币可用,于是我拿起一个面值为1的硬币,接下来我只需要再凑够2-1=1元即可(记得要用最小的硬币数量),而这个答案也已经知道了。所以d(2)=d(2-1)+1=d(1)+1=1+1=2。一直到这里,你都可能会觉得,好无聊,感觉像做小学生的题目似的。因为我们一直都只能操作面值为1的硬币!耐心点,让我们看看i=3时的情况。当i=3时,我们能用的硬币就有两种了:1元的和3元的( 5元的仍然没用,因为你需要凑的数目是3元!5元太多了亲)。既然能用的硬币有两种,我就有两种方案。如果我拿了一个1元的硬币,我的目标就变为了:凑够3-1=2元需要的最少硬币数量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。这个方案说的是,我拿3个1元的硬币;第二种方案是我拿起一个3元的硬币,我的目标就变成:凑够3-3=0元需要的最少硬币数量。即d(3)=d(3-3)+1=d(0)+1=0+1=1. 这个方案说的是,我拿1个3元的硬币。好了,这两种方案哪种更优呢?记得我们可是要用最少的硬币数量来凑够3元的。所以,选择d(3)=1,怎么来的呢?具体是这样得到的:d(3)=min{d(3-1)+1, d(3-3)+1}。

OK,码了这么多字讲具体的东西,让我们来点抽象的。从以上的文字中,我们要抽出动态规划里非常重要的两个概念:状态和状态转移方程。

上文中d(i)表示凑够i元需要的最少硬币数量,我们将它定义为该问题的"状态",这个状态是怎么找出来的呢?根据子问题定义状态。你找到子问题,状态也就浮出水面了。最终我们要求解的问题,可以用这个状态来表示:d(11),即凑够11元最少需要多少个硬币。那状态转移方程是什么呢?既然我们用d(i)表示状态,那么状态转移方程自然包含d(i),上文中包含状态d(i)的方程是:d(3)=min{d(3-1)+1, d(3-3)+1}。没错,它就是状态转移方程,描述状态之间是如何转移的。当然,我们要对它抽象一下,

d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示第j个硬币的面值;

有了状态和状态转移方程,这个问题基本上也就解决了。

*/

/*
#include <iostream>

int main()
{
int a[3] = {1,3,5}, sum = 11, cent = 0, dp[12];
dp[0] = 0;

for(int i = 1; i <= sum; i++)
{
dp[i] = i;
}

for(int i = 1; i <= sum; i++)
{
for(int j = 0; j < 3; j++)
{
if(i >= a[j] && dp[i] > dp[i - a[j]] + 1)
{
dp[i] = dp[i- a[j]] + 1;
}
}
}

std::cout << dp[sum] << std::endl;

return 0;
}


*/


/*

下图是当i从0到11时的解:

要凑够11元至少需要3枚硬币。

此外,通过追踪我们是如何从前一个状态值得到当前状态值的,可以找到每一次我们用的是什么面值的硬币。比如,从上面的图我们可以看出,最终结果d(11)=d(10)+1(面值为1),而d(10)=d(5)+1(面值为5),最后d(5)=d(0)+1 (面值为5)。所以我们凑够11元最少需要的3枚硬币是:1元、5元、5元。

通过硬币问题我们初识DP的原理,其实可以说贪心问题是DP问题的特例,现在我们通过几道题目加深对DP问题的理解

=========================================================

=========================================================
Q2:
数塔问题是动态规划经典的题目,下面来初步讲解下

将一个由N行数字组成的三角形,如图所以,设计一个算法,计算出三角形的由顶至底的一条路径,使该路径经过的数字总和最大。

学弟学妹们你们之前学过DFS和BFS,第一眼看过去这题应该用DFS解决,没错,DFS也可以,但是我们观察下n行总共有(1 + 2 + 3 + 4+...+n) = (1+n)*n/2个节点,在递归求解的过程中很多节点被重复访问了,这就导致时间大大增加,必然超时

比如用递归的话,18这个节点被访问了两次

但是如果用DP的话这个节点可以只访问一次

好了,现在我们用DP解决这道问题

将上图转化一下:


假设上图用map[][]数组保存。

令f[i][j]表示从顶点(1, 1)到顶点(i, j)的最大值。

则可以得到状态转移方程:

f[i][j] = max(f[i+1][j], f[i+1][j+1]) + map[i][j]

此题既适合自顶而下的方法做,也适合自底而上的方法,

当用自顶而下的方法做时,最后要在在最后一列数中找出最大值,

而用自底而上的方法做时,f[1][1]即为最大值。

所以我们将图2根据状态转移方程可以得到图3:


最大值是30.

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int a[2000][2000];

int main()
{
int t,n,i,j;
while(~scanf("%d", &n))
{
for(i=0; i<n; i++)
for(j=0; j<=i; j++)
scanf("%d",&a[i][j]);

for(i=n-1; i>0; i--)
for(j=0; j<i; j++)
a[i-1][j]+=max(a[i][j],a[i][j+1]);

printf("%d ",a[0][0]);
}

return 0;
}

=========================================================

=========================================================
Q3:

上面讨论了两个非常简单的例子。现在让我们来看看对于更复杂的问题,如何找到状态之间的转移方式(即找到状态转移方程)。为此我们要引入一个新词叫递推关系来将状态联系起来(说的还是状态转移方程)

OK,上例子,看看它是如何工作的。

一个序列有N个数:A[1],A[2],…,A[N],求出最长非降子序列的长度。 (讲DP基本都会讲到的一个问题LIS:longest increasing subsequence)

正如上面我们讲的,面对这样一个问题,我们首先要定义一个“状态”来代表它的子问题,并且找到它的解。注意,大部分情况下,某个状态只与它前面出现的状态有关,而独立于后面的状态。

让我们沿用“入门”一节里那道简单题的思路来一步步找到“状态”和“状态转移方程”。假如我们考虑求A[1],A[2],…,A[i]的最长非降子序列的长度,其中i<N,那么上面的问题变成了原问题的一个子问题(问题规模变小了,你可以让i=1,2,3等来分析) 然后我们定义d(i),表示前i个数中以A[i]结尾的最长非降子序列的长度。OK,对照“入门”中的简单题,你应该可以估计到这个d(i)就是我们要找的状态。如果我们把d(1)到d(N)都计算出来,那么最终我们要找的答案就是这里面最大的那个。状态找到了,下一步找出状态转移方程。

为了方便理解我们是如何找到状态转移方程的,我先把下面的例子提到前面来讲。如果我们要求的这N个数的序列是:

5,3,4,8,6,7

根据上面找到的状态,我们可以得到:(下文的最长非降子序列都用LIS表示)

· 前1个数的LIS长度d(1)=1(序列:5)

· 前2个数的LIS长度d(2)=1(序列:3;3前面没有比3小的)

· 前3个数的LIS长度d(3)=2(序列:3,4;4前面有个比它小的3,所以d(3)=d(2)+1)

· 前4个数的LIS长度d(4)=3(序列:3,4,8;8前面比它小的有3个数,所以 d(4)=max{d(1),d(2),d(3)}+1=3)

OK,分析到这,我觉得状态转移方程已经很明显了,如果我们已经求出了d(1)到d(i-1),那么d(i)可以用下面的状态转移方程得到:

d(i) = max{1, d(j)+1},其中j<i,A[j]<=A[i]

用大白话解释就是,想要求d(i),就把i前面的各个子序列中,最后一个数不大于A[i]的序列长度加1,然后取出最大的长度即为d(i)。当然了,有可能i前面的各个子序列中最后一个数都大于A[i],那么d(i)=1,即它自身成为一个长度为1的子序列。

分析完了,上图:(第二列表示前i个数中LIS的长度,第三列表示,LIS中到达当前这个数的上一个数的下标,根据这个可以求出LIS序列)

*/

/*
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>


int main()
{
int dp[2000], a[2000], n;
while(std::cin >> n)
{
memset(dp, 0, sizeof(dp));
int res = 0;

for(int i = 0; i < n; i++)
{
std::cin >> a[i];
}

for(int i = 0; i < n; i++)
{
dp[i] = 1;
for(int j = 0; j < i; j++)
{
if(a[j] < a[i])
{
dp[i] = std::max(dp[i], dp[j] + 1);
}
}

res = std::max(res, dp[i]);
}

std::cout << res << std::endl;
}

return 0;
}
*/

/*
该算法的时间复杂度是O(n2 ),并不是最优的解法。还有一种很巧妙的算法可以将时间复杂度降到O(nlogn),网上已经有各种文章介绍它,这里就不再赘述。此题还可以用“排序+LCS”来解,感兴趣的话可自行Google,Baidu。

=========================================================

=========================================================
Q4:

最后讲一下最长上升公共子序列问题:

问题描述
什么是最长公共子序列呢?好比一个数列S,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则S 称为已知序列的最长公共子序列。

举个例子,如:有两条随机序列,如 1 3 4 5 5 ,and 2 4 5 5 7 6,则它们的最长公共子序列便是:4 5 5。

LCS问题的解决思路
·

穷举法
·

解最长公共子序列问题时最容易想到的算法是穷举搜索法,即对X的每一个子序列,检查它是否也是Y的子序列,从而确定它是否为X和Y的公共子序列,并且在检查过程中选出最长的公共子序列。X和Y的所有子序列都检查过后即可求出X和Y的最长公共子序列。X的一个子序列相应于下标序列{1, 2, …, m}的一个子序列,因此,X共有2m个不同子序列(Y亦如此,如为2^n),从而穷举搜索法需要指数时间(2^m * 2^n)。

· 动态规划算法

事实上,最长公共子序列问题也有最优子结构性质。

记:

Xi=﹤x1,⋯,xi﹥即X序列的前i个字符 (1≤i≤m)(前缀)

Yj=﹤y1,⋯,yj﹥即Y序列的前j个字符 (1≤j≤n)(前缀)

假定Z=﹤z1,⋯,zk﹥∈LCS(X , Y)。

·

若xm=yn(最后一个字符相同),则不难用反证法证明:该字符必是X与Y的任一最长公共子序列Z(设长度为k)的最后一个字符,即有zk = xm = yn 且显然有Zk-1∈LCS(Xm-1 , Yn-1)即Z的前缀Zk-1是Xm-1与Yn-1的最长公共子序列。此时,问题化归成求Xm-1与Yn-1的LCS(LCS(X , Y)的长度等于LCS(Xm-1 , Yn-1)的长度加1)。

·

·

若xm≠yn,则亦不难用反证法证明:要么Z∈LCS(Xm-1, Y),要么Z∈LCS(X , Yn-1)。由于zk≠xm与zk≠yn其中至少有一个必成立,若zk≠xm则有Z∈LCS(Xm-1 , Y),类似的,若zk≠yn 则有Z∈LCS(X , Yn-1)。此时,问题化归成求Xm-1与Y的LCS及X与Yn-1的LCS。LCS(X , Y)的长度为:max{LCS(Xm-1 , Y)的长度, LCS(X , Yn-1)的长度}。

·

由于上述当xm≠yn的情况中,求LCS(Xm-1 , Y)的长度与LCS(X , Yn-1)的长度,这两个问题不是相互独立的:两者都需要求LCS(Xm-1,Yn-1)的长度。另外两个序列的LCS中包含了两个序列的前缀的LCS,故问题具有最优子结构性质考虑用动态规划法。

也就是说,解决这个LCS问题,你要求三个方面的东西:1、LCS(Xm-1,Yn-1)+1;2、LCS(Xm-1,Y),LCS(X,Yn-1);3、max{LCS(Xm-1,Y),LCS(X,Yn-1)}。

行文至此,其实对这个LCS的动态规划解法已叙述殆尽,不过,为了成书的某种必要性,下面,我试着再多加详细阐述这个问题。


第三节、动态规划算法解LCS问题
3.1、最长公共子序列的结构

最长公共子序列的结构有如下表示:

设序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的一个最长公共子序列Z=<z1, z2, …, zk>,则:

1. 若xm=yn,则zk=xm=yn且Zk-1是Xm-1和Yn-1的最长公共子序列;

2. 若xm≠yn且zk≠xm ,则Z是Xm-1和Y的最长公共子序列;

3. 若xm≠yn且zk≠yn ,则Z是X和Yn-1的最长公共子序列。

其中Xm-1=<x1, x2, …, xm-1>,Yn-1=<y1, y2, …, yn-1>,Zk-1=<z1, z2, …, zk-1>。

3、2.子问题的递归结构
由最长公共子序列问题的最优子结构性质可知,要找出X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最长公共子序列,可按以下方式递归地进行:当xm=yn时,找出Xm-1和Yn-1的最长公共子序列,然后在其尾部加上xm(=yn)即可得X和Y的一个最长公共子序列。当xm≠yn时,必须解两个子问题,即找出Xm-1和Y的一个最长公共子序列及X和Yn-1的一个最长公共子序列。这两个公共子序列中较长者即为X和Y的一个最长公共子序列。

由此递归结构容易看到最长公共子序列问题具有子问题重叠性质。例如,在计算X和Y的最长公共子序列时,可能要计算出X和Yn-1及Xm-1和Y的最长公共子序列。而这两个子问题都包含一个公共子问题,即计算Xm-1和Yn-1的最长公共子序列。

与矩阵连乘积最优计算次序问题类似,我们来建立子问题的最优值的递归关系。用c[i,j]记录序列Xi和Yj的最长公共子序列的长度。其中Xi=<x1, x2, …, xi>,Yj=<y1, y2, …, yj>。当i=0或j=0时,空序列是Xi和Yj的最长公共子序列,故c[i,j]=0。其他情况下,由定理可建立递归关系如下:

*/

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

int main()
{
string str1,str2;
int dp[200][200];
while(cin>>str1>>str2)
{
memset(dp,0,sizeof(dp));

int la = str1.length();
int lb = str2.length();

for(int i = 1; i <= la; i++)
for(int j = 1; j <= lb; j++)
{
if(str1[i - 1] == str2[j - 1])
{
dp[i][j] = dp[i-1][j-1]+1;
}
else dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
cout<<dp[la][lb]<<endl;
}
return 0;
}



/*


HDU2191

HDU1159

HDU1432

HDU2084

DP问题是ACM里面最难的,因为太考思维能力了,只有将状态转移方程推出来才能解决问题,DP问题也是面试的时候最容易考到的,希望大家好好学DP,至少在面试的时候不吃亏。

DP问题还有比较难的,分为数字DP,插头DP,状态压缩DP,概率DP,组合DP,树状DP等等都是非常难理解的,但是也很有趣,有兴趣的可以找资料学习

*/

/*
https://www.cnblogs.com/clnchanpin/p/6933665.html

1,最大公共子串问题


这个是动态规划的基础题目。

动态规划就是递推和反复子结构。

确定了递推关系后。找到一个能极大地降低反复运算的子结构至关重要。选的好了,时间效率会非常好。

这个问题,最好还是设第一个串为a,长度为n,第二个串为b。长度m。那么最长的子序列长度为f(n,m)

当a[n]=a[m]时

f(n,m)=1+f(n-1,m-1)

否则f(n,m)=max(f(n-1),f(m-1))

同一时候建立一个存储计算过的f(x,y)的矩阵,假设计算过了就直接使用

程序例如以下所看到的

[cpp] view plaincopy
#include <iostream>
#define MAXLen 1000
char seq1[MAXLen];
char seq2[MAXLen];
int maxLen[MAXLen][MAXLen];
int MaxCommonMain()
{
while(scanf("%s%s",seq1+1,seq2+1)>0)
{

int length1=strlen(seq1+1);
int length2=strlen(seq2+1);


for(int i=0;i<=length1;i++)
maxLen[i][0]=0;
for(int j=0;j<=length2;j++)
maxLen[0][j]=0;




for(int i=1;i<=length1;i++)
{
for(int j=1;j<=length2;j++)
{
if(seq1[i]==seq2[j])
maxLen[i][j]=maxLen[i-1][j-1]+1;
else
maxLen[i][j]=maxLen[i-1][j]>maxLen[i][j-1]?maxLen[i-1][j]:maxLen[i][j-1];
}
}

printf("%d/n",maxLen[length1][length2]);

// printf("%d",maxLen[length2-1][length1-1]);

}
return 0;
}

2,陪审团问题

问题描写叙述:

问题描写叙述
在遥远的国家佛罗布尼亚。嫌犯是否有罪,须由陪审团决定。陪审团是由法官从公众中
挑选的。先随机挑选n 个人作为陪审团的候选人。然后再从这n 个人中选m 人组成陪审团。


选m 人的办法是:
控方和辩方会依据对候选人的喜欢程度,给全部候选人打分。分值从0 到20。

为了公
平起见。法官选出陪审团的原则是:选出的m 个人,必须满足辩方总分和控方总分的差的
绝对值最小。假设有多种选择方案的辩方总分和控方总分的之差的绝对值同样。那么选辩控
两方总分之和最大的方案就可以。终于选出的方案称为陪审团方案。


输入数据
输入包括多组数据。每组数据的第一行是两个整数n 和m,n 是候选人数目。m 是陪审
团人数。

注意,1<=n<=200, 1<=m<=20 并且 m<=n。

接下来的n 行。每行表示一个候选人
的信息。它包括2 个整数。先后是控方和辩方对该候选人的打分。

候选人按出现的先后从1
開始编号。

两组有效数据之间以空行分隔。最后一组数据n=m=0
输出要求
对每组数据,先输出一行。表示答案所属的组号, 如 'Jury #1', 'Jury #2', 等。

接下来的
一行要象样例那样输出陪审团的控方总分和辩方总分。

再下来一行要以升序输出陪审团里每
个成员的编号,两个成员编号之间用空格分隔。每组输出数据须以一个空行结束。
输入例子
4 2
1 2
2 3
4 1
6 2
0 0
输出例子
Jury #1
Best jury has value 6 for prosecution and value 4 for defence:
2 3

用动态规划来解

动态规划能够看成是反复子问题+递归

设p为控方向量,d为辩方向量

评分从-400到400
f(j,k)为选取第j个人,评分差为k的辩控总分
对于随意的i,i大于0小于n,则有
f(j,k)=f(j-1+1,x+p[i]-d[i])
则就有
f(j,k)=f(j-1,x)+p[i]+d[i];

假设新的f(j,k)大于旧的,则替换旧的。否则不替换

初始值为k=0。在程序中也许是某个值,以免避免序号k出现负值。
那么初始的f(0,0)=0。
在第一轮的时候仅仅有辩控差为0的合理
对全部的候选人做
f(1,p[i]-d[i])=max(f(1,p[i]-d[i]),f(0,0)+p[i]+d[i]);
这样就能够完毕选一个人的操作。


做完之后离k=0近期的非负的就是我们要的最大的辩控和。

下标就是辩控差。


同理,假设选两个的话,第二轮
k=-400~400
假设f(1,k)合理(此时,仅仅有在第一轮中算出来的合理)
f(2。k+p[i]-d[i])=max(f(2,k+p[i]-d[i]),f(1,k)+p[i]+d[i]);

假设要保存搜索得到的路径
设路径path保存路径向量
path(j,k)保存推荐第j个陪审员时候,辩控差为k的候选人。


相应上面的f函数。初始值都设为0。


假设仅仅找一个候选人
则本身path中没有不论什么的值。即没有推荐不论什么的人。也就是说,推荐第一个人的时候。谁都能够(若推荐第二个人。则曾经推荐过的不能再次推荐)
在推荐第一个候选人的时候。仅仅管保存path[0][p[i]-d[i]]=i ;

当选取多个候选人的时候,在更新f(j,k)的同一时候更新path。由于更新f(j,k)的同一时候。意味着选了新的候选人。所以须要更新路径


当以上都做完之后,
定义一个数组result来取路径
对i:1~m
result[i]=path[m-i+1][k];
k=k-p[result[i]]+d[result[i]];
这样就都攻克了。


[cpp] view plaincopy
#include<stdlib.h>
#include<iostream>
#include<memory.h>
int p[300];
int d[300];
int path[30][1000];
int f[30][1000];
int result[30];
int compareInt(const void *el, const void *e2)
{
return *((int *)el)-*((int *)e2);
}
int jurymain()
{
int n,m;
scanf("%d%d",&n,&m);
int no=0;
int i=0;
while(n+m)//n=0,m=0结束条件
{
no++;
for(i=1;i<=n;i++)
scanf("%d%d",&p[i],&d[i]);
memset(f,-1,sizeof(f));
memset(path,0,sizeof(path));
int minD=20*m;
f[0][minD]=0;
for(int j=0;j<m;j++)
{
for(int k=0;k<=minD*2;k++)
{
if(f[j][k]>=0)
{
i=1;
int temp1;
int temp2;
for(;i<=n;i++)
{
if(f[j][k]+p[i]+d[i]>f[j+1][k+p[i]-d[i]])
{
temp1=j;
temp2=k;
while(temp1>0&&path[temp1][temp2]!=i)
{

temp2=temp2-p[path[temp1][temp2]]+d[path[temp1][temp2]];
temp1--;

}
if(temp1==0)
{
f[j+1][k+p[i]-d[i]]=f[j][k]+p[i]+d[i];
path[j+1][k+p[i]-d[i]]=i;
}
}


}
}
}
}
i=minD;
int j=0;
int k=0;
while(f[m][i+j]<0&&f[m][i-j]<0)
j++;
if(f[m][i+j]>f[m][i-j])
k=i+j;
else
k=i-j;

printf("Jury #%d/n",no);
printf("Best jury has value %d for prosecution and value %d for defence:/n",(k-minD+f[m][k])/2,(minD-k+f[m][k])/2);


for(i=1;i<=m;i++)
{
result[i]=path[m-i+1][k];
k-=p[result[i]]-d[result[i]];
// std::cout<<"result "<<i<<" "<<result[i]<<std::endl;
}
qsort(result+1,m,sizeof(int),compareInt);
for(i=1;i<=m;i++)
printf("%d ",result[i]);
printf("/n/n");
scanf("%d%d",&n,&m);

}
//system("pause");
return 0;
}

3,小花店问题

F束花从左到右放在V个花瓶里面(1<=F<=V<=100)。花要依照花的标志数从小到大排列。不同的花在不同的花瓶可以产生不同的美学价值。v[i,j]来表示第i朵花在第j个花瓶产生的美学价值。

求把n朵花放在m个花瓶可以产生的最大的美学价值。

这个题和最大公共子串的思考角度相似。因为花必需要小于等于瓶子。并且花的编号由小到大。不能乱序。

比如就不能把出现【2,4】 【1,5】这样的现象。也就是说插在花瓶中的花依照花瓶的顺序。序号升序排列。

最好还是用f(i,j)来表示把前i朵花插入前个瓶子中。当i=j时,f(i,j)=v[1,1]+v[2,2]+...+v[i,i];

当i=0时。f(0,j)=0; 当i!=j, 且i!=0时f(i,j)=max(f(i,j-1),f(i-1,j-1)+v[i,j]) ,i<=j-1.

[cpp] view plaincopy
#include <stdlib.h>
#include <stdio.h>
void getMaxValue(int **f,int **v,int m,int n)
{




for(int j=1;j<=m;j++)
{
for(int i=1;i<=j-1;i++)
{
if(f[i-1][j-1]+v[i][j]>f[i][j-1])
f[i][j]=f[i-1][j-1]+v[i][j];
else
f[i][j]=f[i][j-1];
}
}


}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
int **v=new int[n+1][m+1];
int **f=new int[n+1][m+1];
v[0][0]=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("$d",v[i][j]);

for(int j=0;j<=m;j++)
v[0][j]=0;

int tempSum=0;
for(int i=0;i<=n;i++)
{
sum+=v[i][i];
f[i][i]=sum;
}
getMaxValue(f,v,m,n);

delete(v);
return 0;

}

4,最佳旅行线问题

简单描写叙述:某航空公司给的奖品。一张免费的机票。可以从最西边的一个城市出发,必须到达最东边的一个城市,然后返回起点城市。每一个城市职能经过一次。起点被訪问2次。问最多可以訪问多少个城市。

这个题能够使用动态规划的方法。这个题目也使我想起了《算法导论》中装配线的问题,事实上基本是一致的。

在非常多的书或者是解法中。都用了这个方案:从起点開始,两条路一起走f[i,j]表示两条不相交的路线分别到达i和j时,所需乘坐航线的最大值

f(1,1)=0;

f(i,j)= max{f(k,j)}+1,i>j ;k是i的前驱节点。

max{f(i,k)}+1,i<j

f(i,i)无意义

该算法是没有错的。

非常多地方也用这个算法做了一些程序版本号。可是这个算法有一个限制。就是前驱节点。在t大某位老师的书中,他枚举全部的点对。然后对f矩阵进行更新(传统的动态规划方法)。可是他这个有一个前提,那就是全部节点的序号符合拓扑序列。可是题目中并没有说城市符合拓扑序列。假如说序号比較任意,t大的这本书中的就错了。可是他并没有在代码中说明。算是对读者的一个误导。

[cpp] view plaincopy
#ifndef TICKETAWARD_H
#define TICKETAWARD_H
class TicketAward
{
int *b;//度数
int **g;//g(i,j)表示第i个点的第j个孩子
int **f;//函数f
int n;//总共同拥有n个节点
public:
TicketAward();
int max(int x,int y);
};
#endif

[cpp] view plaincopy
#include "stdafx.h"
#include <iostream>
#include "TicketAward.h"
TicketAward::TicketAward()
{
std::cin>>n;
g=new int *[n+1];
b=new int[n+1];
f=new int*[n+1];
for(int i=1;i<=n;i++)
{
g[i]=new int[n+1];
f[i]=new int[n+1];
b[i]=0;
for(int j=1;j<=n;j++)
{

}
}
int temp=0;
for(int i=1;i<=n;i++)
{
std::cin>>temp;//输入i的第一个孩子。假设不为0,即就是存在孩子节点,则继续
while(temp!=0)
{
b[i]++;
g[i][b[i]]=temp;
std::cin>>temp;
}
}
f[1][1]=0;
for(int i=1;i<=n;i++)
{

for(int j=1;j<=b[i];j++)
{
if(i==j==1)
continue;
if(i<j)
for(int k=1;k<=b[i];k++)
{
f[i][j]=max(f[i][j],(f[g[i][k]][j]+1));
}
if(i>j)
for(int k=1;k<=b[j];k++)
{
f[i][j]=max(f[i][j],(f[g[j][k]][j]+1));
}
}
}
std::cout<<(f[n][n]-2);
}
int TicketAward::max(int x, int y)
{
return x>y?
x:y;

}

5, 最长词链问题

给定一个只包括小写字母的英文单词表。当中每一个单词至少包括一个字母,最多包括75个字母。且依照字典序排列。

全部单词包括的单词个数之和不超过200w

假设在一张由一个单词或者多个单词组成的表中。每一个单词都被其后的一个单词所包括。则成此表为一个链。

如今要求从单词表中找到一个包括单词书最多的最长词链。

该问题能够由动态规划解。

这让我想起了一个题:在一个连续的长字符串中。找出匹配次数最多的一个词组,不能有重叠。同样的方法。

令F(k)表示第k个单词的时候的最长的链。

则F(k)=maxF(i)+1。i从1,到k-1,而且第i个单词是第k个的前缀。这儿问题就解了。

可是,对于这道题还有其它的解法。

由于字典序并不是乱序。每一个单词的前一个单词所包括的前缀非常可能就是这个单词的前缀,而且前一个单词也可能就是该单词的前缀。由于前缀的特殊性。所以仅仅用检查前一个单词的前缀(包括该单词本身)就能找出该前缀链

代码为非动态规划算法

[cpp] view plaincopy
#include "stdafx.h"
#ifndef MOSTLENGTHLETTER_H
#define MOSTLENGTHLETTER_H
#include <iostream>
#include <string>
struct Pnode
{
int len;
Pnode *next[26];
};
struct MostLengthLetters
{
public:
MostLengthLetters()
{
for(int i=0;i<=25;i++)
{
root.next[i]=NULL;
}
ans=0;
std::cout<<"please input the num of words"<<std::endl;
std::cin>>wordsNum;

this->getWords();

}
void getWords();
void update(std::string temp);
int wordsNum;
Pnode root;
int ans;
};
void MostLengthLetters::getWords()
{
std::string temp;
for(int i=0;i<this->wordsNum;i++)
{
std::cin>>temp;
this->update(temp);
}
}
void MostLengthLetters::update(std::string temp)
{
int len=temp.length();
Pnode p=root;
int max=0;
int i=1;
int tempInt=0;
while(i<len)
{
tempInt=temp[i]-'a';
if(p.next[i]==NULL)
{
Pnode *node=new Pnode;
node->len=0;
for(int j=0;j<=25;j++)
{
node->next[j]=NULL;
}
}
if(max<p.len)
max=p.len;
i++;
p=*p.next[tempInt];
}
p.len=max+1;
this->ans=p.len;
return;
}
#endif
6, 整数划分问题

给定一个自然数。分成k部分,A1,A2..的数的和,要求A1<=A2...求有多少种?

原理:整数n拆分成最多不超过m个数的和的拆分数,和n 拆分成最大不超过m的拆分数相等。
依据这个原理,原问题就转化成了求最大拆分为k的拆分个数与最大拆分为k-1的拆分个数的差
f(n,k)=f(n,k-1)+f(n-k,k)。
[cpp] view plaincopy
#include "stdafx.h"
#ifndef SPLITTOKNUM_H
#define SPLITTOKNUM_H
#include<iostream>
#include <memory.h>
#define MaxN 100
class SplitToKNum
{

public:
SplitToKNum()
{
std::cin>>n;
std::cin>>k;
memset(f,0,sizeof(f));
for(int i=1;i<=k;i++)
{
f[MaxN][i]=1;
}
for(int j=1;j<=k;j++)
for(int i=MaxN+1;i<=MaxN+n;i++)
f[i][j]=f[i][j-1]+f[i-j][j];
std::cout<<f[n+MaxN][k]-f[n+MaxN][k-1]<<std::endl;

}
int n;
int k;
int f[2*MaxN+1][MaxN];
};
#endif

同一时候附上ferrer图的几个原理:
a,整数n拆分成k个数的和的拆分数,和数n拆分最大数位k的拆分数相等
b。整数n拆分成最多不超过m个数的和的拆分数。和n拆分成最大不超过m的拆分数相等。
c。整数n拆分成互不同样的若干奇数的和的的拆分数,和n拆分成自共轭的Ferrers图像的拆分数相等.
7,青蛙的烦恼

池塘里面有n片荷花,正好形成一个凸多边形。依照顺时针方向将这n片和也顺次编号为1,2,3,4,5...。n。一仅仅青蛙想要跳过这些荷叶,每一个仅仅能跳过一次。希望跳过的距离最短。

输入荷叶数n。每片荷叶的坐标xy,输出最短距离。

分析:凸多边形。首先不能出现交叉路。否则。由两边之和大于第三边能够知道,这样实际上是路程变长了。

其次,直接依照凸多边形也不对,能够參考平行四边形,当中一个角为30度,请自行绘图查明。

方法:每次每一个顶点仅仅能到与自己相邻的两个中的一个(到其它顶点边会出现交叉),剩下的n-1个顶点也遵从这一规则。则能够使用动态规划的方法。

从s到L

则假设从s出发,则f(s,L,0)=min{dis(s。s+1)+f(s+1,L-1,0),f(s+1,L-1,1)+dis(s+L-1,s)}

假设从s+L-1出发则f(s,L,1)=min{dis(s,s+L-1)+f(s+L-1,L-1,0),f(s+L-1,L-1,0)+dis(s+L-1,s)}

[cpp] view plaincopy
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#define MAXN 1000
double x[MAXN];// x radix
double y[MAXN];
double dis[MAXN][MAXN];
int n;// the num of
double f[MAXN][2][2];
double min(double a,double b)
{
return a<b?
a:b;

}
int main()
{
std::cout<<"please input the num of the points"<<std::endl;
std::cin>>n;
for(int i=0;i<n;i++)
{
std::cin>>x[i]>>y[i];
}
for(int i=0;i<n-1;i++)
{
for(int j=i+1;j<n;j++)
{
dis[i][j]=(x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]);
dis[j][i]=dis[i][j];
}
}
int sign=0;
double ans=1000000;
for(int j=1;j<=n;j++)
{
sign=1-sign;
for(int i=n-1;i>=0;i--)
{
if(j==1)
{
f[i][sign][0]=0;
f[i][sign][1]=0;
}
else
{
f[i][sign][1]=min(f[(i+1)%n][1-sign][1]+dis[i][(i+1)%n],f[(i+1)%n][1-sign][0]+dis[i][(i+j-1)%n]);
f[i][sign][0]=min(f[i][1-sign][1]+dis[(i+j-1)%n][i],f[i][1-sign][0]+dis[(i+j-1)%n][(i+j-2)%n]);
}
if(j==n)
ans=min(ans,f[i][sign][1]);
}
}
//std::cout<<f[1][sign][1]<<"/t"<<f[1][sign][0]<<std::endl;
std::cout<<ans<<std::endl;

return 0;
}

8排列问题

在整数1,2,。。。,n中,有些排列满足以下的性质:该排列中除了最后一个数字以外,每一个整数后面都跟有一个同它相差为1的整数。比如:N=4,排列1432满足上述性质。求,假设 随意指定p个位置的值,求最多有几个排列满足如上性质

这样的排列满足一下两条性质

a,不论什么一个排列的后k位是若干连续整数的集合

b,假设一个排列的后k位(1<=k<=n)是软干连续整数组成的集合,则这个排列满足题目所述的性质。

动态规划的方法就可以。

设s为起始的数的大小。r为连续的多少数的个数

g(s,r)=g(s+1,j-1),若第1个位置为s。即倒数第j个位置为s

g(s,j-1),若第1个位置为s+r-1,即倒数的第j个位置为s+r-1

不确定则为上述两个之和

代码例如以下


[cpp] view plaincopy
#include <stdlib.h>
#include <iostream>
#include <memory.h>
class RankProgram
{
public:
RankProgram()
{
std::cout<<"please input the testing data"<<std::endl;
std::cin>>n>>p;
s=new int[n+2];
g=new int* [n+2];
for(int i=0;i<=n+1;i++)
{
g[i]=new int[n+2];
memset(g[i],0,sizeof(int)*(n+2));
g[i][1]=1;
}
memset(s,0,sizeof(int)*(n+2));
int tempData;
int tempP;
for(int i=1;i<=p;i++)
{
std::cin>>tempP>>tempData;
s[n-tempP+1]=tempData;
}
for(int i=n;i>=1;i--)
{
for(int j=2;j<=n-i+1;j++)
{
if(s[j]==i)
g[i][j]=g[i+1][j-1];
else if(s[j]==(i+j-1))
g[i][j]=g[i][j-1];
else
g[i][j]=g[i+1][j-1]+g[i][j-1];
}
}
std::cout<<g[1][n]<<std::endl;
}
~RankProgram()
{
delete [] s;
for(int i=0;i<=n+1;i++)
delete [] g[i];
delete [] g;
}
private:
int *s;
int **g;
int p;
int n;
};
int main()
{
RankProgram rp;
}

9,画室问题

实际上该题能够描写叙述为。给定一个尺寸为N的矩阵,行和列宽度为2^N,将该矩阵分为4分,左上角不为零,其它三部分都包括零。

递归定义这三部分,直到最小单位,即矩阵仅仅有一个数,为0。

求假设让该矩阵移动一个尺寸(x,y),两个矩阵反复部分的0的个数。

思考:实际上就是推断移动前和移动后相应的位置是否都为0。假设都为0,则总0个数+1。

假设移动到了左上角的位置则不加。其它三部分由于是一样的,该部分递归定义的上层+1

在以下的程序中,k1,k2 表示的位置,在当前区域的哪一块

a,b表示三个区域,当中左上的直接被忽略了。

kk1。kk2,表示移动到的了哪一个区域

if(!((a+p[i-1]+k1)%2==0&&(b+q[i-1]+k2)%2==1))表示移动到的区域的哪一块。假设是左上直接忽略

[c-sharp] view plaincopy
#ifndef MATRIXMOVE_H
#define MATRIXMOVE_H
#include <iostream>
#include <memory.h>
#define MAXN 101
class MatrixMove
{
public:
MatrixMove()
{
std::cin>>n;

memset(f,0,sizeof(f));
f[n+1][0][0]=1;
f[n+1][0][1]=f[n+1][1][0]=f[n+1][1][1]=0;
std::cin>>x>>y;
for(int i=1;i<=n;i++)
{
p[n-i+1]=x%2;
q[n-i+1]=y%2;
x=x/2;
y=y/2;
}
for(int i=n+1;i>=2;i--)
{
for(int k1=0;k1<=1;k1++)
for(int k2=0;k2<=1;k2++)
for(int a=0;a<=1;a++)
for(int b=0;b<=1;b++)
{
if(!(a==0&&b==1))
{
int kk1=(a+p[i-1]+k1)/2;
int kk2=(b+q[i-1]+k2)/2;
if(!((a+p[i-1]+k1)%2==0&&(b+q[i-1]+k2)%2==1))
{
f[i-1][kk1][kk2]+=f[i][k1][k2];
}
}
}
}
std::cout<<f[1][0][0]<<std::endl;
}
private:
int n;
int x;
int y;
int f[MAXN][2][2];
int p[MAXN];
int q[MAXN];
};
#endif

10, 中世纪剑士

n个人决斗,两两之间有强弱关系。强弱关系不传递,比如a>b,b>c,c>a。n个剑士围成一个圈,一次抽签。抽中的人和他右边的人决斗,输了的人出圈。

如今问是否存在一种决斗方式让第k个人生出。计算可能胜出的的人数和方案。

这个题目让我想起了围成一个圈的猴子的题目,那个题目是约瑟夫问题。

和这个不一样。

这个题目:一个人要胜出。则要胜了全部右边的人,同一时候也要胜出左边的人。由于是围成一个圈,所以该人胜出的话,终于肯定是自己跟自己相遇。

那么,这样的情况下,把圈展开成一个链,将该链延长一倍。假设i和i+n能够相遇,则说明i能够胜出。

i人向右决斗。i+n向左决斗

假设两个人能够相遇。用meet[i,j]来表示

meet[i,j]= true if meet[i,k] and meet[k,j] and (e[i,k] or e[j,k])=true

false

[cpp] view plaincopy
#ifndef ACIENTSOLDIER_H
#define ACIENTSOLDIER_H
#include <iostream>
#include <memory.h>
class AcientSoldier
{
public:
AcientSoldier()
{
std::cin>>n;
a=new int*[n+1];
meet=new bool*[2*n+1];
for(int i=1;i<=n;i++)
{
a[i]=new int[n+1];
meet[i]=new bool[2*n+1];
meet[i+n]=new bool[2*n+1];
memset(meet[i],0,sizeof(bool)*(2*n+1));
memset(meet[i+n],0,sizeof(bool)*(2*n+1));
meet[i][i+1]=true;
meet[i+1][i]=true;
meet[i+n][(i+n)%2+1]=true;
meet[(i+n)%2+1][i+n]=true;
for(int j=1;j<=n;j++)
std::cin>>a[i][j];
}
for(int k=1;k<=2*n;k++)
for(int i=1;i<=2*n;i++)
for(int j=1;j<=2*n;j++)
{
if((i<k)&&(j>k)&&meet[i][k]&&meet[k][j]&&a[(i-1)%n+1][(k-1)%n+1]&&a[(j-1)%n+1][(k-1)%n+1])
{
meet[i][j]=true;
meet[j][i]=true;
}
}
ans=0;
for(int i=1;i<=n;i++)
{
if(meet[i][i+n])
ans++;
}
std::cout<<ans<<std::endl;;


}
private:
int n;
int **a;
bool **meet;
int ans;
};
#endif

11, 科学家实验陨石

总共获得了n1块A星的和n2块B星的,每次实验须要各一块,实验完了后不能回收。

总共实验min(n1,n2);

求每次试验的绝对值和的最小值

先证明一下两个递增序列,a1<b1,a2<b2, |a2-a1|+|b2-b1|<=|b2-a1|+|b1-a2|

------a1----------------b1-------------

- -

-

- O -

------a2----------------b2--------------

由上图依据两条边之和大于第三边能够直接证明上面的式子

所以就能够放心的使用动态规划了

对两列数据由小到大排序,少的用i表示。多的用j表示

F[i,j]=min(F[i,j-1],F[i-1][j-1]+dis(i,j)) j>i

F[i-1][j-1]+dis(i,j) j=i

编程临时省略,由于这个题目比較简单

12,理想收入问题

仅仅有一元的本金,知道每天的股价为v[i], 求M天之后的理想收入,可以获得的最大的收入

如果第i天的理想收入为F[i]

则有F[i]=max(F[j]/v[k])*v[i]; j<=k<i

令M[i]=max(F[j]/v[k])=max(M[i-1],MF[i-1]/v[i]);M[i]表示在第i天能够得到的最多股票数,MF[i-1]表示前i-1天能够取得的最大收入

MF[i]=Max(MF(i-1),F(i-1))

13. 一般的RMQ问题 (Range Minimum/Maximum Query)

对于给定的N与N个整数A[1...N],M与M对下标(x,y)(x<=y),对于每对下标(x,y)求出A[x...y]中最小数的下标
解法:
预处理,对于区间中每个点A,设计以A为左端点的floor(logN)+1个区间
[A,A+2^0-1], [A,A+2^1-1],...,[A, A+2^floor(logN)-1]
在求解的过程中。依照区间长度依次求解。[A,A+2^(i+1)-1]。能够通过两个等长的子区间[A,A+2^i-1]和[A+2^i,A+2^(i+1)-1];
取用方法:
A。当区间的重叠对结果无影响的时候,比如求最大最小值(本题所述,则依据区间[A,A+2^k-1]与[B-2^k+1,B]来计算
B,当区间的重叠对结果有影响的时候,比如统计数字的多少等等,将[A,B]划分为[A,A+2^k-1],
[A+2^K,A+2^k+2^(floor(log(B-(A+2^k)+1)))-1],每次使用对数直接获得划分点。所以至多分成了floor(log(B-A+1))+1个区间。
所以这样处理的时间复杂度为logN
本题中,所述的全部对数。均是以2为底。
本题中。开一个数组,行下标表示区间開始端A。列坐标表示区间结束端B

[cpp] view plaincopy
#include "stdafx.h"
#include <iostream>
#include <math.h>
using namespace std;
class RMQProblem
{
int N;
int M;
int *data;
int **b;
public:
RMQProblem()
{
cin>>N>>M;
data=new int[N+1];
b=new int*[N+1];
int tempRankNum=int(floor(log2(double(N))))+1;
for(int i=1;i<=N;i++)
{
cin>>data[i];
b[i]=new int[tempRankNum];
b[i][0]=data[i];
}
for(int j=1;j<=tempRankNum;j++)
{
int k=1<<j;
int kk=1<<(j-1);
for(int i=1;i<=N-k+1;i++)
{
b[i][j]=min(b[i][j-1],b[i+kk][j-1]);
}
}

int tempX,tempY,tempZ;
for(int i=1;i<=M;i++)
{
cin>>tempX>>tempY;
tempZ=int(floor(log2(tempY-tempX)));
cout<<min(b[tempX][tempZ],b[tempY-int(1<<tempZ)][tempZ]);

}
}
double log2(double x)
{

return log(double(x))/log(2.0);
}
int min(int x,int y)
{
return x<y?x:y;
}
};
14, 关于分石子问题的动态规划解法
有n个石头。k个框子。把n个石头按顺序放入k个框子,比方1~3放入1,4~6放入2,要求最大重量的框子的重量最小的放法。

设石子的重量分别为Q1,Q2,...

g(i,j)=Qi+,...,+Qj;

f(i,j)表示把i个石子放到j个框的最大重量框的重量。

则f(i,j)=minj-1<=p<i (f(i,j),max(f(p,j-1),g(p+1,i)));

g(i,i)=Qi ,f(1,1)=g(1,1),f(i,1)=g(1,i);

1<=i<=n;

1<=j<=k;

假设提前把Si =Q1+,..,+Qi 计算出来,g(j,k)=Sk -Sj-1 则时间复杂度为O(N2 )

*/

原文地址:https://www.cnblogs.com/noryes/p/5716677.html