动态规划经典例题二
对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)
定义的状态一定要形成递推关系
一句话概括:三特点四要素两本质
适用场景: 最大值/最小值,可不可行,是不是,方案个数
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39
斐波那契数列定义:F(n)=F(n-1)+F(n-2)(n>=2,n∈N*),其中F(1)=1,F(2)=1
方法一:递归
class Solution {
public:
int Fibonacci(int n) {
if(n<=0)
return 0;
if(n==1||n==2)
return 1;
return Fibonacci(n-2)+Fibonacci(n-1);
}
};
方法二:数组
class Solution {
public:
int Fibonacci(int n) {
vector<int> arr(n+1);
arr[0]=0;
arr[1]=1;
for(int i=2;i<=n;i++)
arr[i]=arr[i-1]+arr[i-2];
return arr[n];
}
};
上述解法的空间复杂度为O(n),其实F(n)只与它相邻的前两项有关,所以没有必要保存所有子问题的解,只需要保存两个子问题的解就可以。
下面方法的空间复杂度将为O(1)
方法三:交换
class Solution {
public:
int Fibonacci(int n) {
int a=0;
int b=1;
int c;
if(n<=0)
return 0;
if(n==1||n==2)
return 1;
for(int i=2;i<=n;++i)
{
c=a+b;
a=b;
b=c;
}
return c;
}
};
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
状态递推:
状态:
F(n):跳上n级台阶的方法
F(n)=F(n-1)+F(n-2)+F(n-3)+······F(1)+F(0)
F(n-1)=F(n-2)+F(n-3)······+F(1)+F(0)
右上可得:
F(n)=2*F(n-1)
初始值:
F(1) = 1
F(2) = 2F(1) = 2
F(3) = 2F(2) = 4
F(4) = 2*F(3) = 8
所以它是一个等比数列
F(n) = 2^(n-1)
方法一:递归
class Solution {
public:
int jumpFloorII(int number) {
if(number<2)
{
return number;
}
else
return 2*jumpFloorII(number-1);
}
};
方法二:排列
每个台阶看成一个位置,除过最后一个位置,其它位置都有两种可能性,所以总的排列数为2^(n-1)*1 = 2^(n-1)
class Solution {
public:
int jumpFloorII(int number) {
int ret=1;
for(int i=2;i<=number;i++)
{
ret=2*ret;
}
return ret;
}
};
扩展:降低时间复杂度
class Solution {
public:
int jumpFloorII(int number) {
if(number==0)
return 0;
return 1<<(number-1);
}
};
总结:
此题看似复杂,通过抽象和归纳,可以找出问题的内在规律
定义问题的状态,以及状态间的递推关系,找到问题的答案
扩展1:
上述问题为变态青蛙跳台阶,太疯狂,这只青蛙像是吃了大力丸身上充满了无穷的力量。现在让它变成一个正常的青蛙,限制它一次只能跳1阶或者2阶,现在该如何解答
扩展2:
牛客网上另一个题目:矩形覆盖
我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
状态F(i):用i个21的小矩形无重叠地覆盖一个2i的大矩形的方法
递归:
不推荐,太占空间
class Solution {
public:
int rectCover(int number) {
if(number <=2)
return number;
return rectCover(number-2)+rectCover(number-1);
}
};
迭代
class Solution {
public:
int rectCover(int number) {
if ( number < 1 )
return 0;
int a = 1, b = 2;
while ( --number ) {
b = b + a;
a = b - a;
}
return a;
}
};
上述两个题目都可以用斐波那契数列求解
HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)
F(i):以第i项结尾的连续子序列的最大和
F(i):max(F(i-1)+a[i],a[i])
方法一
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array) {
if(array.empty())
return 0;
vector<int> maxSum(array.size(),0);
//F[0]=array[0]
maxSum[0]=array[0];
for(int i=1;i<array.size();i++)
//F[i]=max(F[i-1]+array[i],array[i])
maxSum[i]=max(maxSum[i-1]+array[i],array[i]);
//max(F[i])
int ret=maxSum[0];
for(int i=1;i<array.size();i++)
ret=max(ret,maxSum[i]);
return ret;
}
};
方法二
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array) {
int maxnum=array[0];
int count=array[0];
for(int i=1;i<array.size();i++)
{
if(count>=0)
count+=array[i];
else
count=array[i];
maxnum=max(count,maxnum);
}
return maxnum;
}
};
给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。
例如:
给定s=“leetcode”;
dict=[“leet”, “code”].
返回true,因为"leetcode"可以被分割成"leet code".
状态:
F(i):前i个字符能否被分割
F(1) “l”:dict.find(s(1,1)) false
F(2) “le”:F(1) && dict.find(s(2,2)) false
F(3) “lee”:F(2) && dict.find(s(3,3)) ,F(1) && dict.find(s(2,3)) false
F(4) “lee”:F(3) && dict.find(s(4,4)) ,F(2) && dict.find(s(3,4)),F(1) && dict.find(s(2,4)) ,F(0) && dict.find(s(1,4)) true
···········
F(i):F(j)&&dict.find(s(j+1,i)),(j
即s=“leetcode”;dict=[“leet”, “code”].
F(0)—>"": true
F(1)—>“l” :"" “l” F(0)&&dict.find(“l”) false
F(2)—>“le”:“l” “e”, “” “le” F(0)&&dict.find(“le”) false
F(3)—>“lee”:“le” “e” F(2)&&dict.find(“e”), “l” “ee”, “” “lee” false
F(4)—>“leet”:“lee” “t”,“le” “et”,“l” “eet”,“leet” F(0)&&dict.find(“leet”) true
状态:
子状态:前1,2,3,…,n个字符能否根据词典中的词被成功分词
F(i): 前i个字符能否根据词典中的词被成功分词
状态递推:
F(i): true{j 在j小于i中,只要能找到一个F(j)为true,并且从j+1到i之间的字符能在词典
中找到,则F(i)为true
初始值:
对于初始值无法确定的,可以引入一个不代表实际意义的空状态,作为状态的起始
空状态的值需要保证状态递推可以正确且顺利的进行,到底取什么值可以通过简单
的例子进行验证
F(0) = true
返回结果:F(n)
class Solution {
public:
bool wordBreak(string s, unordered_set<string> &dict) {
if(s.empty())
return false;
if(dict.empty())
return false;
vector<bool> F(s.size()+1,false);
//F(0)=true;
F[0]=true;
for(int i=1;i<=s.size();++i)
{
//F(i)=F(j)&&dict.find(s(j+1,i)) j
for(int j=0;j<i;++j)
{
//1~j j+1~i
if(F[j]&&dict.find(s.substr(j,i-j))!=dict.end())
{
F[i]=true;
break;
}
}
}
return F[s.size()];
}
};
给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字,
例如,给出的三角形如下:
[
[2],↵
[3,4],↵
[6,5,7],↵
[4,1,8,3]↵
]
最小的从顶部到底部的路径和是2 + 3 + 5 + 1 = 11。
注意:
如果你能只用O(N)的额外的空间来完成这项工作的话,就可以得到附加分,其中N是三角形中的行总数。
方法一:动态规划
(i,j)—>(i+1,j),(i+1,j+1)
(i,j)—>(i-1,j),(i-1,j-1)
状态:
F(i,j):从(0,0)到(i,j)的最短路径
F(i,j)=min(F(i-1,j),F(i-1,j-1))+a[i][j]
F(i,0)=F(i-1,0)+a[i][0]
F(i,i)=F(i-1,i-1)+a[i][i]
初始状态:
F(0,0)=a[0][0]
返回结果:
min(F(row-1,j))
class Solution {
public:
int minimumTotal(vector<vector<int> > &triangle) {
if(triangle.empty())
return 0;
//F(0,0)=a[0][0]
vector<vector<int>> minsum(triangle);
int row=triangle.size();
for(int i=1;i<row;i++)
{
minsum[i][0]=minsum[i-1][0]+triangle[i][0];
}
for(int i=1;i<row;i++)
{
minsum[i][i]=minsum[i-1][i-1]+triangle[i][i];
}
for(int i=2;i<row;i++)
{
for(int j=1;j<i;j++)
{
//F(i,j)=min(F(i-1,j),F(i-1,j-1))+a[i][j]
minsum[i][j]=min(minsum[i-1][j],minsum[i-1][j-1])+triangle[i][j];
}
}
int minret=minsum[row-1][0];
for(int i=1;i<row;i++)
{
minret=min(minsum[row-1][i],minret);
}
return minret;
}
};
方法二:动态规划(反向思维)
状态:
子状态:从(n,n),(n,n-1),…(1,0),(1,1),(0,0)到最后一行的最短路径和
F(i,j): 从(i,j)到最后一行的最短路径和
状态递推:
F(i,j) = min( F(i+1, j), F(i+1, j+1)) + triangle[i][j]
初始状态:
F(n-1,0) = triangle[n-1][0], F(n-1,1) = triangle[n-1][1],…, F(n-1,n-1) = triangle[n1][n-1]
返回结果:
F(0, 0)
这种逆向思维不需要考虑边界,也不需要最后寻找最小值,直接返回F(0,0)即可
class Solution {
public:
int minimumTotal(vector<vector<int> > &triangle) {
if (triangle.empty())
return 0;
// F[n-1][n-1],...F[n-1][0]初始化
vector<vector<int>> min_sum(triangle);
int line = triangle.size();
// 从倒数第二行开始
for (int i = line - 2; i >= 0; i--)
{
for (int j = 0; j <= i; j++)
{
// F(i,j) = min( F(i+1, j), F(i+1, j+1)) + triangle[i][j]
min_sum[i][j] = min(min_sum[i + 1][j], min_sum[i + 1][j + 1]) +triangle[i][j];
}
}
return min_sum[0][0];
}
};
注:易错点
总结:
遇到关于矩阵,网格,字符串间的比较,匹配的问题,单序列(一维)动规解决不了的情况下,就需要考虑双序列(二维)动规
一个机器人在m×n大小的地图的左上角(起点,下图中的标记“start"的位置)。
机器人每次向下或向右移动。机器人要到达地图的右下角。(终点,下图中的标记“Finish"的位置)。
可以有多少种不同的路径从起点走到终点
上图是3×7大小的地图,有多少不同的路径?
备注:m和n小于等于100
状态:
F(i,j):从左上角到达(i,j)的路径总数
F(i,j): 从(0,0)到达F(i,j)的路径数
状态递推:
F(i,j) = F(i-1,j) + F(i,j-1)
初始化:
特殊情况:第0行和第0列
F(0,i) = 1
F(i,0) = 1
返回结果:
F(m-1,n-1)
class Solution {
public:
int uniquePaths(int m, int n) {
if(m<1||n<1)
return 0;
vector<vector<int>> ret(m,vector<int>(n,1));
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
ret[i][j]=ret[i-1][j]+ret[i][j-1];
}
}
return ret[m-1][n-1];
}
};