力扣每日一刷Day 19
Practice Day nineteen:Leetcode T 70
今天,我们来讲解动态规划中的最优子结构。
定义
如果一个问题的最优解包含其子问题的最优解,那么这个问题就具有最优子结构。换句话说,一个问题的最优解可以通过其子问题的最优解来构建。
特征
子问题的独立性:每个子问题的解是独立的,互不干扰。
子问题的重叠性:在求解过程中,相同的子问题会被多次计算,动态规划通过存储这些子问题的解来避免重复计算。
应用
最优子结构是动态规划算法能够高效解决问题的基础。通过将问题分解为子问题,并存储子问题的解(通常使用一个表格),可以避免重复计算,从而大大提高算法的效率。
了解最优子结构后,我们就开始解答今天的题目吧
题目条件:阶梯数目、步长为1和2
题目目的:找到所给阶梯的所有方案组合数
事实上今天的是easy题,我们只拿他来理解思想,所以这是恰好的
今天我们依旧使用Leetcode的官方解答
我们知道:每做下一次决定时,我们都有两种选择,两种选择为1和2(2可以拆为1和1,这实际上也算步长选取为1的一种情况,所以可以合并,故只有两种选择)。
那么我们的函数关系式可以被表示为:f(x)=f(x−1)+f(x−2)
其中x即为当前台阶编号,f(x) 表示爬到第 x 级台阶的方案数,事实上,每进行一次步进时,都可以把当前台阶序号看作最后的台阶序号,所以这个公式对最后一级台阶也是适用的。
是的,也许你会疑惑:为什么不是f(x)=f(x−1)+1+f(x−2)+1?以符合我们上面所说的,只有两种选择,每做出一种选择,方案数目就加上1?
哈哈,要像这样写f(x)=f(x−1)+1+f(x−2)+1的话,那当然也可以,但是这样未免不够美观,而且每次都需要进行额外的加2,这显然会造成算法效率的降低。如果是按这样来,我们初始化时的时候,从第零阶开始f(0)=0。
若是按f(x)=f(x−1)+f(x−2)这样来,我们的f(0)=1。可以见到,两种不同的公式,只是缘于初始化时赋的值不同。如果按f(0)=1来,每次调用f(x)时,就已经加上1了,这个1由f(0)=1提供,所以不必每次调用f(x)都像一样f(x)=f(x−1)+1+f(x−2)+1分别加上1。
你无法反驳,显然f(x)=f(x−1)+f(x−2)更加美观与迅速,不是吗?
完成了对于公式的理解后,我们需要理解f(0)的初值设置。如果你认为,f(0)到f(0)本身也算一种方案,以符合(x)=f(x−1)+f(x−2)的运行逻辑,那就应该让f(0)=1。
如果你不认为f(0)到f(0)本身不是一种方案,那他就应该符合f(x)=f(x−1)+1+f(x−2)+1的逻辑。
完成了对于公式的构建与初值的选择后,我们来进行代码的构建
由于他超简单的,所以我一次性把完整代码放出来了。
时间复杂度:O(n)
空间复杂度:O(1)
class Solution {
public:int climbStairs(int n) {int p = 0, q = 0, r = 1;for (int i = 1; i <= n; ++i) {p = q; q = r; r = p + q;}return r;}
};
由于我们只需要记录最终的方案数,且后面的计算无需查询更遥远的结果,所以我们不需要完整的容器来记录所有变量,只需要p和q,就可以分别存储f(i-1)和f(i-2)这些中间变量。
对于输出的结果,我们只需要利用一个变量 r 就可以完成对结果的储存。
对于上面的操作,他有个名称,叫做滚动数组。这是一个很有意思的数据结构,官方题解中有动画演示,没有了解过的小伙伴可以看一看,链接我会按照惯例放到题解的最后。
对于滚动数组的实现,我们通过注释来理解他
p = q; q = r; r = p + q;
/*设当前进行的台阶序号为i*/
/*第一行的 p 代表i-2的阶梯方案数,即f(i-2)*/
/*第一行的 q 代表的是i-2的方案数,即f(i-2),这是因为进入下一台阶序号后, q 的值仍未得到更新,记录的是上一个台阶序号的方案数f(i-1),而进入下一台阶序号后,台阶编号由i-1变为i-2,故此时q记录的值是
f(i-2)*//*第二行的 q 是将上一个台阶序号的方案数,即f(i-1),赋值给q*/
/*第二行的 r 是上一个台阶的方案数*//*求当前台阶序号的方案数,即 r=f(i)=f(i-2)=p + f(i-1)=q*/
最后返回一个答案 r ,没什么好说的。
作者:力扣官方题解
链接:https://leetcode.cn/problems/climbing-stairs/solutions/286022/pa-lou-ti-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。