您的位置:首页 > 其它

动态规划之最长递增子序列

2016-02-03 09:06 281 查看
动态规划(Dynamic Programming)是一种设计的技巧,是解决多阶段决策过程最优化问题的通用方法。

基本思想:将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解(这部分与分治法相似)。与分治法不同的是,适合于用动态规划求解的问题,经分解得到的子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。通常可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划的基本思路。

采用动态规划求解的问题需要具有两个特性:

最优子结构(Optimal Substructure):问题的一个最优解中所包含的子问题的解也是最优的。

重叠子问题(Overlapping Subproblems):用递归算法对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。

问题具有最优子结构性质,我们才能写出最优解的递归方程;具有重叠子问题特性,我们才能通过避免重复计算来减少运行时间。

综上所述,动态规划的关键是 —— 记忆,空间换时间,不重复求解,从较小问题解逐步决策,构造较大问题的解。

最长递增子序列,Longest Increasing Subsequence 下面我们简记为 LIS。

假设存在一个数组 int [] A ={ 2, 1, 5, 3, 6, 4, 8, 9, 7},我们可以看出来它的LIS长度为5 即{2,5,6,8,9}或者{2,3,4,8,9}或者{1,3,4,8,9}。

动态规划

我们定义一个数组B,它的空间和A的一样大,B数组内任意元素B[i],记录的是最长递增子序列长度为i的序列的末尾元素的值,也就是这个最长递增子序列的最大元素的大小值。

然后循环遍历数组A。此外,我们用一个变量Len来记录现在最长算到多少了。

首先,把A[0]有序地放到B里,令B[0] = 2,就是说当只有一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1

然后,把A[1]有序地放到B里,令B[0] = 1,就是说长度为1的LIS的最小末尾是1,A[1]=2已经没用了,这时Len=1

接着,A[2] = 5,A[2]>B[0],所以令B[0+1]=B[1]=A[2]=5,就是说长度为2的LIS的最小末尾是5,这时候B = {1, 5},Len=2

再来,A[3] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B= {1, 3},Len = 2

继续,A[4] = 6,它在3后面,因为B[1] = 3, 而6在3后面,于是很容易可以推知B[2] = 6, 这时B= {1, 3, 6}, Len = 3 。

第6个, A[5] = 4,它在3和6之间,于是我们就可以把6替换掉,得到B[2] = 4。B ={ 1, 3, 4}, Len=3

第7个, A[6] = 8,它4大,于是B[3] = 8,此时B={1,3,4,8},Len=4

第8个, A[7] = 9,它比B[3]大,于是B[4] = 9,此时B={1,3,4,8,9},Len=5。

最后一个, A[8] = 7,它在B[2] = 4和B[3] = 8之间,所以我们知道,最新的B[3] =7,B={ 1, 3, 4, 7, 9},Len = 5。

于是我们知道了LIS的长度为5。

注意:这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个A[8] = 7 更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到B[4], 9更新到B[5],得出LIS的长度为6。

然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN),于是算法的时间复杂度就降低到了O(NlogN)!

代码如下:

public class DP {

/**

* @param args

*/

public static void main(String[] args) {

// TODO Auto-generated method stub

int[] A = { 2, 1, 5, 3, 6, 4, 8, 9, 7 };

DP dp=new DP();

System.out.println(dp.lisDp(A,A.length));

}

private int lisDp(int[] arr, int n) {

int len = 1;

int [] B=new int
;

B[0]=arr[0];

for (int i = 1; i < n; i++) {

if (arr[i]>B[len-1]) {

B[len]=arr[i];

len++;

}

else {

int pos=binarySearch(B, len, arr[i]);

B[pos]=arr[i];

}

}

return len;

}

//二分查找算法,返回数组元素需要插入的位置。

int binarySearch(int [] arr, int len, int val)

{

int left = 0, right = len - 1;

int mid;

while (left <= right)

{

mid = (right+left)/2;

if (arr[mid] > val)

right = mid - 1;

else if (arr[mid] < val)

left = mid + 1;

else //找到了该元素,则直接返回

return mid;

}

return left;//数组arr中不存在该元素,则返回该元素应该插入的位置

}

}

总结:

动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余(重复计算),这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。

从上面的例子中,我们可以总结动态规划解决最优化问题的一般步骤:

分析最优解的性质,并刻划其结构特征。

递归地定义最优值。

以自底向上的方式或自顶向下的记忆化方法计算出最优值。

根据计算最优值时得到的信息,构造一个最优解。

步骤(1)—(3)是动态规划算法的基本步骤。在只需要求出最优值的情形,步骤(4)可以省略,若需要求出问题的一个最优解,则必须执行步骤(4)。此时,在步骤(3)中计算最优值时,通常需记录更多的信息,以便在步骤(4)中,根据所记录的信息,快速地构造出一个最优解。

这个讲的不错: http://songlee24.github.io/

http://www.xuebuyuan.com/990532.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: