学习目标:
目标:学习动态规划相关知识
学习内容:
本文内容:学习动态规划的思想并使用动态规划解决斐波那契数列、青蛙跳台阶、连续子数组的最大和、拆分语句等相关问题。
文章目录
一、Dynamic Programming 动态规划
1.1 动态规划的定义
动态规划是分治思想的延伸,通俗一点解释大事化小,小事化无的艺术。
在将大问题转化为小问题的分治过程中,保存对这些小问题已将处理好的结果,并供后面处理更大规模问题时直接使用这些结果
1.2 动态规划的特点
- 把原来的问题分解成了几个相似的小问题;
- 所有的子问题只需要解决一次;
- 储存子问题的解;
1.3 使用动态规划思想解决问题
动态规划的本状质是对问题状态的定义和状态转移方程的定义(状态及状态之间的递推关系)
动态规划问题一般从以下四个角度考虑:
- 状态定义;
- 状态间的转移方程定义;
- 状态的初始化;
- 返回结果;
定义的状态一定要形成递推关系
二、Fibonacci 斐波那契数列
1.1 题目描述
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1 F(N) = F(N - 1) + F(N - 2), 其中 N > 1. 斐波那契数列由 0 和
1 开始,之后的斐波那契数就是由之前的两数相加而得出。
示例 1:
输入:n = 2
输出:1
示例 2:
输入:n = 5
输出:5
1.2实现代码
- 方法一:递归
public int Fibonacci(int n) {
// 初始值
if(n <= 0)
return 0;
if(n == 1 || n == 2)
return 1;
// F(n)=F(n-1)+F(n-2)
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
递归的方法时间复杂度为O(2^n),随着n的增大呈现指数增长,效率低下 当输入比较大时,可能导致栈溢出,在递归过程中有大量的重复计算
- 方法二: 动态规划
状态:F(n)
状态递推:F(n)=F(n-1)+F(n-2)
初始值:F(1)=F(2)=1
返回结果:F(N)
public int Fibonacci(int n) {
int[] ary=new int[40];
//初始值
ary[0]=0;
ary[1]=1;
ary[2]=1;
for(int i=3;i<=n;i++){
// F(n)=F(n-1)+F(n-2)
ary[i]=ary[i-1]+ary[i-2];
}
return ary[n];
}
上述解法的空间复杂度为O(n) 其实F(n)只与它相邻的前两项有关,
所以没有必要保存所有子问题的解 只需要保存两个子问题的解就可以
下面方法的空间复杂度将为O(1)
- 方法三:
public int Fibonacci(int n) {
//初始值
if(n==0){
return 0;
}
if(n<=2){
return 1;
}
int a=1;
int b=1;
int ret=0;
for(int i=0;i<n-2;i++){
// F(n)=F(n-1)+F(n-2)
ret=a+b;
a=b;
b=ret;
}
return ret;
}
三、青蛙跳台阶问题
3.1 题目题目描述
一只青蛙一次可以跳上1级台阶,也可以一次跳上2级,求该青蛙跳上一个n级的台阶总共有多少种跳法。
3.2 递推关系
状态:
子状态:跳上1级,2级,3级…n级台阶的跳法数
f(n):第n个台阶的跳法数
初始值:
f(0)=1;
f(1)=1;
f(2)=2;
递推:
n节台阶有 f(n)种跳法;
在第n级跳法可以分为两种:
- 在第n-1级跳1级到第n级;
- 在第n-2级跳两级到第n级;
所以f(n)=f(n-1)+f(n-2);
3.3 实现代码
public int Fibonacci(int n) {
// 初始值
if(n<=1){
return 1;
}
// F(n)=F(n-1)+F(n-2)
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
其他方法可参照斐波那契数列的做法,与之类似
四、变态青蛙跳台阶问题
4.1题目描述
一只青蛙一次可以跳上1级台阶,也可以一次跳上2级,也可以跳上n级,求该青蛙跳上一个n级的台阶总共有多少种跳法。
4.2 递推关系
状态:
子状态:跳上1级,2级,3级,…,n级台阶的跳法数
f(n):还剩n个台阶的跳法数
状态递推:
n级台阶,第一步有n种跳法:跳1级、跳2级、到跳n级
跳1级,剩下n-1级,则剩下跳法是f(n-1)
跳2级,剩下n-2级,则剩下跳法是f(n-2)
f(n) = f(n-1)+f(n-2)+…+f(n-n)
f(n) = f(n-1)+f(n-2)+…+f(0)
f(n-1) = f(n-2)+…+f(0)
f(n) = 2f(n-1)
初始值:
f(1) = 1
f(2) = 2f(1) = 2
f(3) = 2f(2) = 4
f(4) = 2f(3) = 8
所以它是一个等比数列
f(n) = 2^(n-1)
返回结果:f(N)
4.3 实现代码
public class Solution {
public int JumpFloorII(int target) {
int ret = 1;
for(int i = 2; i <= target; ++i){
ret *= 2;
}
return ret;
}
}
五、最大连续子数组的和
5.1 题目描述
输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
5.2 题目分析
原数组 array[9,5,2,7,4];
状态:
子状态:原数组的子数组长为1,2,3…n的最大和
F(i):以第i个元素为结尾的最大子数组和
递推:
F(i)=max(F(i)+array[i],F(i));
初始值:F(0) = array[0]
返回结果:
maxsum:所有F(i)中的最大值
5.3 实现代码
public int FindGreatestSumOfSubArray(int[] array) {
//初始化curSum,maxSum
int curSum=array[0];
int maxSum=array[0];
for(int i=1;i<array.length;i++){
curSum=Math.max(curSum+array[i],array[i]);//记录子数组的和
maxSum=Math.max(curSum,maxSum);//记录当前最大值
}
return maxSum;
}
六、拆分词句
6.1 题目描述
给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。
例如:
给定s=“nowcode”;
dict=[“now”, “code”].
返回true,
因为"nowcode"可以被分割成"now ,code".
题目分析:
方法:动态规划
状态:
子状态:前1,2,3,…,n个字符能否根据词典中的词被成功分词
F(i): 前i个字符能否根据词典中的词被成功分词
状态递推:
F(i): j <i && F(j) && substr[j+1,i]能在词典中找到
在j小于i中,只要能找到一个F(j)为true,并且从j+1到i之间的字符能在词典
中找到,则F(i)为true
初始值:
对于初始值无法确定的,可以引入一个不代表实际意义的空状态,作为状态的起始
空状态的值需要保证状态递推可以正确且顺利的进行,到底取什么值可以通过简单
的例子进行验证
F(0) = true
返回结果:F(n)
实现代码
public boolean wordBreak(String s, Set<String> dict) {
boolean[] res=new boolean[s.length()+1];//储存当前位置是否能被分割
for(int i=1;i<=s.length();i++){
if(dict.contains(s.substring(0,i))){
//判断当前位置之前的字符串是否存在dict中,
//如果存在,则在res[]中记录true,表示当前位置可以分割
res[i]=true;
continue;
}
//如果程序能够执行到这里,表示当前位置之前的字符串不存在dict中,
//则判断是否可以继续分割成更短的字符串
for(int j=i-1;j>0;j--){
if(res[j]&&dict.contains(s.substring(j,i))){
//该循环条件表示j位置之前的字符串存在dict中,并且j~i长度的字符串也存在dict中
//则在位置就赋值为true,表示可以在此分割
res[i]=true;
break;
}
}
}
//返回res的最后一个值,
//如果是true表示字符串s可以分割成所有子串都在dict中
return res[s.length()];
}