引入:斐波那契数列
斐波那契数列(Fibonacci sequence)是数学家斐波那契以研究兔子繁殖为例研究的数列,故称“兔子数列”,又称为黄金分割数列。数列如下:1,1,2,3,5,8,13.......。
兔子问题:假设一对初生兔子要一个月才到成熟期,而一对成熟兔子每月会生一对兔子,那么,由一对初生兔子开始,12 个月后会有多少对兔子呢?
可以发现兔子第一月有一对,第二月才成熟,第三月生兔子,此时兔子有两对,第四月第一对兔子又生一对兔子,三对兔子,第五月,两对成熟兔子生两对新兔子,有五对兔子.....每一个月的兔子总和为上一个月的兔子数量+上上个月的兔子数量,因为上一个月新生的兔子还不具备繁殖能力。如若没有需要成熟这一条件,则每一个月的兔子总和为生一个月兔子数量的两倍(成倍繁殖)。
兔子数列递推公式为F(n)=F(n-1)+F(n-2)(每一项为前两项的结果之和),在动态规划算法中体现了一种间隔性,是一维dp的一种体现。
解决斐波那契数列问题可以采用递归或者动态规划的思想,下面会介绍两者在时间复杂度上的区别。
动态规划问题的解题思路
动态规划问题(dp)是将原问题分解成相对简单的子问题,所有子问题只解决一次并存储子问题的解,本质上是一种记忆化搜索的思想。
思路如下:
先定义状态dp[i]的含义,然后写出状态转移方程(前两步很重要,也是动态规划问题容易遇到的瓶颈),进行状态的初始化,最后返回结果。
1、爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?(1<=n<=45)
错解:
以下递归方法会超时(测试卡在n=44的情况),是因为作了多余的调用,进行了很多重复计算,(如最底层的clibstairs(1)就调用了很多遍),递归次数过多,时间复杂度过大,接近O(),形成一种树状结构。
int climbStairs(int n) {
if(n==1||n==0)return 1;
else return climbStairs(n-1)+climbStairs(n-2);
}
题解:
每一个台阶视为一个状态,每个状态可由前面的状态推出。即若此时在第n层台阶,那么前一个状态为第n-1或n-2层台阶。
a[i]表示到达第i层的方法数,得状态转移方程a[i]=a[i-1]+a[i-2]。
动态规划用了将状态存储下来的思想,减少了不必要的函数调用,时间复杂度O(n)。
另外,此题可以不用数组a[]存储,改为使用变量a,b,c,这样可以降低空间复杂度。
int climbStairs(int n) {
int a[100];
a[0]=1;a[1]=1;
for(int i=2;i<=n;++i){
a[i]=a[i-1]+a[i-2];
}
return a[n];
}
2、使用最小花费爬楼梯
题目:746. 使用最小花费爬楼梯 - 力扣(LeetCode)
给你一个整数数组 cost
,其中 cost[i]
是从楼梯第 i
个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
示例 :
输入:cost = [10,15,20] 输出:15 解释:你将从下标为 1 的台阶开始。 - 支付 15 ,向上爬两个台阶,到达楼梯顶部。 总花费为 15 。
题解:
此题为上题的变式。
dp[i]表示到达第i层需要的最低花费。
状态转移方程:dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
int minCostClimbingStairs(vector<int>& cost) {
int dp[1005];
dp[0]=0;dp[1]=0;
int n=cost.size();
for(int i=2;i<=n;++i){
dp[i]=min(dp[i-1] + cost[i-1],dp[i-2]+cost[i-2]);
}
return dp[n];
}
3、删除并获得点数
题目:740. 删除并获得点数 - 力扣(LeetCode)
给你一个整数数组 nums
,你可以对它进行一些操作。每次操作中,选择任意一个 nums[i]
,删除它并获得 nums[i]
的点数。之后,你必须删除 所有 等于 nums[i] - 1
和 nums[i] + 1
的元素。开始你拥有 0
个点数。返回你能通过这些操作获得的最大点数。
示例 :
输入:nums = [3,4,2] 输出:6 解释: 删除 4 获得 4 个点数,因此 3 也被删除。 之后,删除 2 获得 2 个点数。总共获得 6 个点数。
题解:
用数组a[i]映射value为i的个数,若取数字nums[i],则为了尽量获得最大点数会将同样的数字取完,因为相邻大小的数字无法都取到,则需要找出最优取法。(和打家劫舍问题很像)
dp[i]表示扫描到数字i时能得到的最大点数,状态转移方程dp[i]=max(dp[i-1],dp[i-2]+i*a[i])。
int deleteAndEarn(vector<int>& nums) {
int maxx=0;//存储最大值
int dp[10005],a[10005];//a[i]数组记录有a[i]个i
for(int i=0;i<=10000;++i)a[i]=0;
for(int i=0;i<nums.size();++i){
a[nums[i]]++;
maxx=max(maxx,nums[i]);
}
dp[0]=0;dp[1]=a[1];
for(int i=2;i<=maxx;++i)dp[i]=max(i*a[i]+dp[i-2],dp[i-1]);
return dp[maxx];
}
总结
斐波那契问题在动态规划上的体现为当前状态由前两个状态决定,时间复杂度O(n),空间复杂度若使用数组为O(n),使用滚动数组为O(1)(用变量的方式)。
备注
题目均来源于leetcode动态规划(基础版)专题。
更多例题: