动态规划题目
109. 数字三角形
比如,给出下列数字三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
从顶到底部的最小路径和为11 ( 2 + 3 + 5 + 1 = 11)。
最简单的动态规划思想: 从底层逐层向上计算最小路径并保存
代码:
class Solution {
public:
/**
* @param triangle: a list of lists of integers
* @return: An integer, minimum path sum
*/
int minimumTotal(vector<vector<int>> &triangle) {
// write your code here
int r=triangle.size();
int c=triangle[r-1].size();
if(r==0&&c==0)
return 0;
int D[r][c];
// int Min[r][c];
for(int i=0;i<c;i++)
D[r-1][i]=triangle[r-1][i];
for(int i=r-2;i>=0;i--)
for(int j=0;j<=i;j++)
D[i][j]=min(D[i+1][j],D[i+1][j+1])+triangle[i][j];
return D[0][0];
}
};
110. 最小路径和
给定一个只含非负整数的m*n网格,找到一条从左上角到右下角的可以使数字和最小的路径。
注意事项
你在同一时间只能向下或者向右移动一步
class Solution {
public:
/*
* @param grid: a list of lists of integers
* @return: An integer, minimizes the sum of all numbers along its path
*/
int minPathSum(vector<vector<int>> &grid) {
// write your code here
int m=grid.size();
int n=grid[0].size();
for(int i=1;i<n;i++)
grid[0][i]=grid[0][i-1]+grid[0][i];//列初始状态
for(int i=1;i<m;i++)
grid[i][0]=grid[i-1][0]+grid[i][0];//行初始状态
for(int i=1;i<m;i++)
for(int j=1;j<n;j++)
{
grid[i][j]+=min(grid[i-1][j],grid[i][j-1]);
}//根据状态转移方程求解
return grid[m-1][n-1];
}
};
111. 爬楼梯
假设你正在爬楼梯,需要n步你才能到达顶部。但每次你只能爬一步或者两步,你能有多少种不同的方法爬到楼顶部?样例
比如n=3,1+1+1=1+2=2+1=3,共有3种不同的方法
返回 3
即step[i]=step[i-1]+step[i-2],很有意思。
class Solution {
public:
/**
* @param n: An integer
* @return: An integer
*/
int climbStairs(int n) {
// write your code here
int a[n];
a[0]=0;
a[1]=1;
a[2]=2;
for(int i=3;i<=n;i++)
a[i]=a[i-1]+a[i-2];
return a[n];
}
};
给出三个字符串:s1、s2、s3,判断s3是否由s1和s2交叉构成。
样例
比如 s1 = "aabcc" s2 = "dbbca"
- 当 s3 = "aadbbcbcac",返回 true.
- 当 s3 = "aadbbbaccc", 返回 false.
要求时间复杂度为O(n^2)或者更好
初始化是:假设s1为空,那么s2每一位跟s3匹配放入dp[0][j];假设s2为空,那么s1每一位跟s3匹配放入dp[i][0]。
代码:
class Solution {
public:
/*
* @param s1: A string
* @param s2: A string
* @param s3: A string
* @return: Determine whether s3 is formed by interleaving of s1 and s2
*/
bool isInterleave(string &s1, string &s2, string &s3) {
// write your code here
if(s3.size()!=s1.size()+s2.size())
return false;
vector<vector<bool> > dp(s1.size()+1,vector<bool>(s2.size()+1,false));
dp[0][0] = true;
//先让s1、s2对S3初始匹配一下,看s3的前S1.size()和s2.size()与S3的匹配程度。
for(int i=1;i<=s1.size();i++)
dp[i][0] = dp[i-1][0]&&(s3[i-1]==s1[i-1]);
for(int i=1;i<=s2.size();i++)
dp[0][i] = dp[0][i-1]&&(s3[i-1]==s2[i-1]);
for(int i=1;i<=s1.size();i++)
{
for(int j=1;j<=s2.size();j++)
{
int t=i+j;
if(s1[i-1]==s3[t-1]) //从s3的第二个字符开始(前一个已经匹配过)与 s1、s2进行匹配
dp[i][j] = dp[i][j]||dp[i-1][j]; //dp[i-1][j]表示S2中前j个字符的匹配情况
if(s2[j-1]==s3[t-1])
dp[i][j] = dp[i][j]||dp[i][j-1]; //dp[i][j-1]表示S1中前i个字符的匹配情况
//dp[i][j]||中的dp[i][j]是为了预防当s1和s2中都有与s3匹配的位置时如
//"aba"
//"a"
//"aaba"
} } return dp[s1.size()][s2.size()]; } }; 77. 最长公共子序列
给出两个字符串,找到最长公共子序列(LCS),返回LCS的长度。最长公共子序列的定义:
-
最长公共子序列问题是在一组序列(通常2个)中找到最长公共子序列(注意:不同于子串,LCS不需要是连续的子串)。该问题是典型的计算机科学问题,是文件差异比较程序的基础,在生物信息学中也有所应用。
- https://en.wikipedia.org/wiki/Longest_common_subsequence_problem
给出"ABCD" 和 "EDCA",这个LCS是 "A" (或 D或C),返回1
给出 "ABCD" 和 "EACB",这个LCS是"AC"返回 2
1.基本概念
首先需要科普一下,最长公共子序列(longest common sequence)和最长公共子串(longest common substring)不是一回事儿。什么是子序列呢?即一个给定的序列的子序列,就是将给定序列中零个或多个元素去掉之后得到的结果。什么是子串呢?给定串中任意个连续的字符组成的子序列称为该串的子串。给一个图再解释一下:
如上图,给定的字符序列: {a,b,c,d,e,f,g,h},它的子序列示例: {a,c,e,f} 即元素b,d,g,h被去掉后,保持原有的元素序列所得到的结果就是子序列。同理,{a,h},{c,d,e}等都是它的子序列。
它的字串示例:{c,d,e,f} 即连续元素c,d,e,f组成的串是给定序列的字串。同理,{a,b,c,d},{g,h}等都是它的字串。
这个问题说明白后,最长公共子序列(以下都简称LCS)就很好理解了。
给定序列s1={1,3,4,5,6,7,7,8},s2={3,5,7,4,8,6,7,8,2},s1和s2的相同子序列,且该子序列的长度最长,即是LCS。
s1和s2的其中一个最长公共子序列是 {3,4,6,7,8}
2.动态规划
求解LCS问题,不能使用暴力搜索方法。一个长度为n的序列拥有 2的n次方个子序列,它的时间复杂度是指数阶,太恐怖了。解决LCS问题,需要借助动态规划的思想。
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。
3.特征分析
解决LCS问题,需要把原问题分解成若干个子问题,所以需要刻画LCS的特征。
设A=“a0,a1,…,am”,B=“b0,b1,…,bn”,且Z=“z0,z1,…,zk”为它们的最长公共子序列。不难证明有以下性质:
如果am=bn,则zk=am=bn,且“z0,z1,…,z(k-1)”是“a0,a1,…,a(m-1)”和“b0,b1,…,b(n-1)”的一个最长公共子序列;
如果am!=bn,则若zk!=am,蕴涵“z0,z1,…,zk”是“a0,a1,…,a(m-1)”和“b0,b1,…,bn”的一个最长公共子序列;
如果am!=bn,则若zk!=bn,蕴涵“z0,z1,…,zk”是“a0,a1,…,am”和“b0,b1,…,b(n-1)”的一个最长公共子序列。
有些同学,一看性质就容易晕菜,所以我给出一个图来让这些同学理解一下:
以我在第1小节举的例子(S1={1,3,4,5,6,7,7,8}和S2={3,5,7,4,8,6,7,8,2}),并结合上图来说:
假如S1的最后一个元素 与 S2的最后一个元素相等,那么S1和S2的LCS就等于 {S1减去最后一个元素} 与 {S2减去最后一个元素} 的 LCS 再加上 S1和S2相等的最后一个元素。
假如S1的最后一个元素 与 S2的最后一个元素不等(本例子就是属于这种情况),那么S1和S2的LCS就等于 : {S1减去最后一个元素} 与 S2 的LCS, {S2减去最后一个元素} 与 S1 的LCS 中的最大的那个序列。
4.递归公式
第3节说了LCS的特征,我们可以发现,假设我需要求 a1 ... am 和 b1 .. b(n-1)的LCS 和 a1 ... a(m-1) 和 b1 .. bn的LCS,一定会递归地并且重复地把如a1... a(m-1) 与 b1 ... b(n-1) 的 LCS 计算几次。所以我们需要一个数据结构来记录中间结果,避免重复计算。
假设我们用c[i,j]表示Xi 和 Yj 的LCS的长度(直接保存最长公共子序列的中间结果不现实,需要先借助LCS的长度)。其中X = {x1 ... xm},Y ={y1...yn},Xi = {x1 ... xi},Yj={y1... yj}。可得递归公式如下:
5.计算LCS的长度
这里我不打算贴出相应的代码,只想把这个过程说明白。还是以s1={1,3,4,5,6,7,7,8},s2={3,5,7,4,8,6,7,8,2}为例。我们借用《算法导论》中的推导图:
图中的空白格子需要填上相应的数字(这个数字就是c[i,j]的定义,记录的LCS的长度值)。填的规则依据递归公式,简单来说:如果横竖(i,j)对应的两个元素相等,该格子的值 = c[i-1,j-1] + 1。如果不等,取c[i-1,j] 和 c[i,j-1]的最大值。首先初始化该表:
然后,一行一行地从上往下填:
S1的元素3 与 S2的元素3 相等,所以 c[2,1] = c[1,0] + 1。继续填充:
S1的元素3 与 S2的元素5 不等,c[2,2] =max(c[1,2],c[2,1]),图中c[1,2] 和 c[2,1] 背景色为浅黄色。
继续填充:
中间几行填写规则不变,直接跳到最后一行:
至此,该表填完。根据性质,c[8,9] = S1 和 S2 的 LCS的长度,即为5。
6.构造LCS
本文S1和S2的最LCS并不是只有1个,本文并不是着重讲输出两个序列的所有LCS,只是介绍如何通过上表,输出其中一个LCS。
我们根据递归公式构建了上表,我们将从最后一个元素c[8][9]倒推出S1和S2的LCS。
c[8][9] = 5,且S1[8] != S2[9],所以倒推回去,c[8][9]的值来源于c[8][8]的值(因为c[8][8] > c[7][9])。
c[8][8] = 5, 且S1[8] = S2[8], 所以倒推回去,c[8][8]的值来源于 c[7][7]。
以此类推,如果遇到S1[i] != S2[j] ,且c[i-1][j] = c[i][j-1] 这种存在分支的情况,这里请都选择一个方向(之后遇到这样的情况,也选择相同的方向)。
第一种结果为:
这就是倒推回去的路径,棕色方格为相等元素,即LCS = {3,4,6,7,8},这是其中一个结果。
如果如果遇到S1[i] != S2[j] ,且c[i-1][j] = c[i][j-1] 这种存在分支的情况,选择另一个方向,会得到另一个结果。
即LCS ={3,5,7,7,8}。
7.关于时间复杂度
构建c[i][j]表需要Θ(mn),输出1个LCS的序列需要Θ(m+n)。
总结:做题关键,写出状态转移方程,最好列出表格,找出初始化条件,然后就可以很容易的写出代码
代码 :
class Solution {
public:
/*
* @param A: A string
* @param B: A string
* @return: The length of longest common subsequence of A and B
*/
int longestCommonSubsequence(string &A, string &B) {
// write your code here
vector<vector<int>>dp(A.size()+1,vector<int>(B.size()+1,0));//注意dp的维度行数位A.size()+1,列数位B.size()+1,因为还有dp取零的情况。
for(int i=1;i<=A.size();i++)
dp[i][0]=0;
for(int i=1;i<=B.size();i++)
dp[0][i]=0;
dp[0][0]=0;
for(int i=1;i<=A.size();i++)
for(int j=1;j<=B.size();j++)
{
if(A[i-1]==B[j-1])
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
return dp[A.size()][B.size()];
}
};
76. 最长上升子序列
最长上升子序列的定义:
最长上升子序列问题是在一个无序的给定序列中找到一个尽可能长的由低到高排列的子序列,这种子序列不一定是连续的或者唯一的。
https://en.wikipedia.org/wiki/Longest_increasing_subsequence
给出 [5,4,1,2,3]
,LIS 是 [1,2,3]
,返回 3
给出 [4,2,4,5,3,7]
,LIS 是 [2,4,5,7]
,返回 4
本文作者frankchenfu,blogs网址http://www.cnblogs.com/frankchenfu/
什么是最长上升子序列? 就是给你一个序列,请你在其中求出一段不断严格上升的部分,它不一定要连续。
就像这样:2,3,4,7和2,3,4,6就是序列2 5 3 4 1 7 6的两种选取方案。最长的长度是4.
那么,怎么求出它的最大上升子序列长度为4呢?这里介绍两种方法,都是以动态规划为基础的。
首先,我们先介绍较慢O(n2)的方法。我们记num为到这个数为止,最长上升子序列的长度。
这种方法就是每一次寻找“可以接下去的”,换句话说,设原序列为a,则
当aj<ai(j<i)且numj+1>numi时,numi=numj+1。
即
对于每一个数,他都是在“可以接下去”的中,从前面的最优值+1转移而来。
因此,这个算法是可以求出正确答案的。复杂度很明显,外层i枚举每个数,内层j枚举目前i的最优值,即O(n2)。
代码:
class Solution {
public:
/*
* @param nums: An integer array
* @return: The length of LIS (longest increasing subsequence)
*/
int longestIncreasingSubsequence(vector<int> &nums) {
// write your code here
int f[nums.size()];
int max = 0;
for (int i = 0; i < nums.size(); i++)
{
f[i] = 1;
for (int j = 0; j < i; j++)
{
if (nums[j] < nums[i])
{
f[i] = f[i] > f[j] + 1 ? f[i] : f[j] + 1;
}
}
if (f[i] > max)
{
max = f[i];
}
}
return max;
}
};
那么,有没有更快的方法呢?当然有。这回要用到二分。
思想:
最长上升子序列(LIS)的典型变形,熟悉的n^2的动归会超时。LIS问题可以优化为nlogn的算法。
定义d[k]:长度为k的上升子序列的最末元素,若有多个长度为k的上升子序列,则记录最小的那个最末元素。
注意d中元素是单调递增的,下面要用到这个性质。
首先len = 1,d[1] = a[1],然后对a[i]:若a[i]>d[len],那么len++,d[len] = a[i];
否则,我们要从d[1]到d[len-1]中找到一个j,满足d[j-1]<a[i]<d[j],则根据D的定义,我们需要更新长度为j的上升子序列的最末元素(使之为最小的)即 d[j] = a[i];
最终答案就是len
利用d的单调性,在查找j的时候可以二分查找,从而时间复杂度为nlogn。
最长上升子序列nlogn算法
最长递增子序列,Longest Increasing Subsequence 下面我们简记为 LIS。
排序+LCS算法 以及 DP算法就忽略了,这两个太容易理解了。
假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。
下面一步一步试着找出它。
我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了
首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1
然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1
接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2
再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2
继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。
第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3
第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了
第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。
最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。
于是我们知道了LIS的长度为5。
!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。
然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!
利用二分法代码1:
class Solution {
public:
/*
* @param nums: An integer array
* @return: The length of LIS (longest increasing subsequence)
*/
int longestIncreasingSubsequence(vector<int> &nums) {
// write your code here
vector<int> minLast(nums.size() + 1);
minLast[0] = INT_MIN;
for (int i = 1; i <= nums.size(); i++)
{
minLast[i] =INT_MAX;
}
for (int i = 0; i < nums.size(); i++)
{
// find the first number in minLast >= nums[i]
//二分法的逆用
int index = binarySearch(minLast, nums[i]);
minLast[index] = nums[i];
}
for (int i = nums.size(); i >= 1; i--)
{
if (minLast[i] !=INT_MAX)
{
return i;
}
}
return 0;
}
// find the first number > num
int binarySearch(vector<int> minLast, int num)
{
int start = 0, end = minLast.size() - 1;
while (start + 1 < end)
{
int mid = (end - start) / 2 + start;
if (minLast[mid] < num)
{
start = mid;
} else
{
end = mid;
}
}
return end;
}
};
利用二分法代码二:求最长递增子序列,此题中没有要求求出具体序列,因此: 只需要将比前一个数大的压入数组,如果一个数比其数组中最大的数小,则在数组中二分查找找到它该在的位置。
二分法思想与前一个相同,增加了空间复杂度,但更简洁
//如果只要求最长递增子序列的长度
class Solution {
public:
/*
* @param nums: An integer array
* @return: The length of LIS (longest increasing subsequence)
*/
int longestIncreasingSubsequence(vector<int> &nums) {
// write your code here
vector<int> v;
for (int i = 0; i<nums.size(); i++) {
if (v.size() == 0 || v.back()<nums[i])
v.push_back(nums[i]);
else {
int low = 0, high = v.size() - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (v[mid]<nums[i]) low = mid + 1;
else high = mid - 1;
}
v[low] = nums[i];
}
}
return v.size();
}
};
类似的,我们可以通过二分查找中改变“上确界”和“下确界”,以及符号(“<”和“<=”或“>”、“>=”等),求出最长不下降、不上升、严格下降子序列等问题。
下面是求这个最长递增子序列的序列,其中dp[i]为以i位置结尾的最长递增子序列的个数。
#include <iostream>
#include <vector>
#include<string>
#include<algorithm>
using namespace std;
//求DP
vector<int> getLIS(vector<int> &num){
vector<int> ivec; //help
int length = num.size();
vector<int> dp(length);
dp[0] = 1;
ivec.push_back(num[0]);
for (int i = 1; i < length; ++i) {
if (ivec.back() < num[i]) {
ivec.push_back(num[i]);
dp[i] = dp[i - 1] + 1;
}
else {
int low = 0, high = ivec.size() - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (ivec[mid] < num[i])
low = mid + 1;
else
high = mid - 1;
}
ivec[low] = num[i];
dp[i] = low + 1;
}
}
return dp;
}
//求最长递归子序列
vector<int> subArray(vector<int> &nums,vector<int> &dp) {
int len = 0, index = 0;
for (int i = 0; i < dp.size(); i++) {
if (dp[i] > len) { //找到最长递增子序列的最后一个值
len = dp[i];
index = i;
}
}
vector<int> result(len);
result[--len] = nums[index];
for (int i = index; i >= 0; i--) {
if (nums[i] < nums[index] && dp[i] == dp[index] - 1) {
result[--len] = nums[i];
index = i;
}
}
return result;
}
int main() {
vector<int> n = { 3,5,6,2,5,4,19,5,6,7,12 };
vector<int>dp = getLIS(n);
vector<int>result = subArray(n, dp);
for (auto c : result)
cout << c << endl;
}