动态规划的一般套路

384 阅读3分钟

要想搞清楚动态规划 ,首先要搞清楚递归法解决问题的一般套路。因为动态规划是一类特殊的递归问题, 相较于一般的递归问题,多了选择决策这个过程。 举个简单的例子,从一颗二叉树的根节点出发,直到某一个叶子节点。沿途节点的值之和要怎么才能最大。 最大的路径显然只有一个, 但是从一个节点出发向下有两个选择,我们要选择节点值之和最大的那一条。这就是决策。

递归的一般套路

  • 1 定义状态, 我个人的理解是按我们写的函数来, 入参即为自变量 结果即为因变量,也就是说我们要找到y = f(x)的具体含义,有时可能没这么具体,要是能找到这个公式,那真是太具体了,可能就用不上递归,直接上数学公式。

  • 2 状态定义之后 就可以确定 递推关系,上一阶段和下一阶段的关系, 例如我们的斐波那契 f(n) = f(n-1) + f(n-2) ,实际上很多时候也没这么具体

  • 3 确定边界条件

  • 4开始写代码 如果还记得数学归纳法,基本上可以一一对应。反过来说,我们怎么知道这个递归程序是对的, 我们不知道,但是可以用数学归纳法的思维来考虑。

边界条件是什么? 在代码里这就是递归的结束标志或者迭代开始的地方(已知条件)。 实际上很多时候,这里就是开始,边界条件就是已知正确的,可以说是特例。

还是拿斐波那契来举例子, 已知第一第二个数1, 然后才能用递推公式计算出下一个,只不过解题的时候一般是执果索因。 这个简单例子的状态很容易确定,那就是n 和 f(n)的关系,索引为n的值是多少。我们期望输入一个n得到它对应的元素值,然后我们又找到了n 和n+1的关系那就是 f(n) = f(n-1) + f(n-2) 。

我们假设:我们写的函数是对的, 输入n就会输出n对应的元素,然后又有f(n) = f(n-1) + f(n-2) , 符合递推关系, 又有初始值(边界条件),知道0 、1 就能得出2 ,知道n、 n+1 就能得出n+2这样递推下去, 我们的程序就是正确的。

下面这个代码是迭代版的,也就是正着写的, 因为要知道n 就得知道n-1 和 n-2 ,递归看上去就是这么一种逆向运行的执行 fn(n) 它就会去执行f(n-1)和f(n-2) 一直执行到fn(0) f(1).但是最先弹出执行性上下文的是f(0) f(1), 也就是0 1 的结果依然是最先求出来的, 然后去求2, n是最后被求出来的。这个是比较容易转迭代的。

// 边界条件
if(n < 2) return 1
    let pre1= pre2 =1, res =2;
    while(n --){
        res = pre1 + pre2 ;
        fibArr.push(res)
        pre2 = pre1
        pre1 = res 
    }
    return res

    
}

function  fibonacciArr(n = 0){
// 边界条件
if(n < 2) return 1
 return    fibonacciArr(n -1) + fibonacciArr(n-2)
}

递推问题的求解方向有两种
1 我从哪里来 要得出f(n)的值, 要先解出前面的f(n-1)到f(0) ,例如斐波那契。这也是通常状态定义、 递推关系比较明确时的思考方式。递推求解顺序 就是状态依赖图(肯定有向)的一个拓扑序
2 我到哪里去 计算出f(n-1)时 ,用f(n-1) 去更新fn(n)的值.

突然发现,前端有个过程和递归是一样的。那就是dom事件流,捕获和冒泡。现在统一是先捕获再冒泡,这不就如同,递归的先执行函数执行到最里层, 然后回溯。 回溯就是冒泡啊。

动态规划

上面也说了 动态规划就比普通递归多一个决策的过程。 动态规划中把递推公式称为 状态转移方程,因此 定义状态依然很重要。 状态转移方程中的要素就
状态: 描述变量参数代表的实际意义
决策: 从所有可能产生最优解的状态中,选一个最值。
阶段: 本阶段只依赖于上一阶段, 简单就跟具体的跟数学公式一样, 抽象的有时候就难以把握,实际上大多数情况都是抽象的。
动态规划解题的套路和递归就是一样的:
1 确定动规状态
2 确定状态转移方程
3正确性证明 (数学归纳法)
4 写代码

下面是我写的几道相关题的题解,直接放链接了。

53 题应该是比较简单的动规了,难点在于有没有意识到定义状态的方式,我写的时候还没意识到。连续区间最值问题, 一般定义的状态就是f(i)表示以某个索引i结束的连续区间的最值。以i开头的也行,具体问题具体实现。

53最大子序和

最长递增子序列 ;

最长公共子序列