使我对动态规划的理解发生转折的算法的解决过程的思想变化的过程

本文通过解析 Unique Paths 问题,介绍了如何运用动态规划解决路径计数问题,并详细阐述了从递归到动态规划的转换过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

事情发生在今天这个夜黑风高的夜晚。

我遇到一道题目

62. Unique Paths

A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).

How many possible unique paths are there?

走方格,只能向下和向右。

首先,一看就是有数学规律的,于是我开始上演一部大戏。(m,n)= x 代表m行,n列,x种走法。

(4,1)=1(4,2)=4(4,3)=10(4,4)=20(4,5)=35(4,6)=56(4,7)=84

强迫症的我似乎找到了一点点规律。

1,4,10,20,35,56,84  一种平方式增长。

(3,1)=1(3,2)=3(3,3)=6(3,4)=10(3,5)=15(3,6)=21

1,3,6,10,15,21  一种线性增长。

当m=2时那就是非零常数式增长,

当m=1时就是常数。

逆推得到当m值越大随n的增长方式为 x^(m-1)式。

而且总结归纳出,高层次的相邻值相加结果等于低成次的结果。

即 a(m,n)-a(m,n-1)=a(m-1,n) 

从而得到 a(m,n)=a(m-1,n)+a(m,n-1);

有了递推公式我就开始想装逼了!!!!

谁都不要拦我!!

直接递归搞定,

然而,事与愿违。时间超时,超时!!

是的啊,这个和斐波那契数列一样啊,使用递归的时候函数调用会成指数式增长,而且这个肯定比斐波那契更厉害。

好吧,那就想斐波那契那样从前往后加吧。

从哪开始加呢???无从下手啊!!

再推倒看看

a(m,n)-a(m,n-1)=a(m-1,n)

这样能写出 a(m,n-1)-a(m,n-2)=a(m-1,n-1),

我靠这样写过之后就可以推出来什么了

a(m,n)-a(m,n-1)=a(m-1,n)

 a(m,n-1)-a(m,n-2)=a(m-1,n-1)

 a(m,n-2)-a(m,n-3)=a(m-1,n-2)

 a(m,n-3)-a(m,n-4)=a(m-1,n-3)

...

 a(m,2)-a(m,1)=a(m-1,2)

相加之后得到:

a(m,n)=a(m-1,n)+a(m-1,n-1)+a(m-1,n-2)+a(m-1,n-3)......... a(m-1,2)+a(m-1,1)

好像很有用了,等等,好像陷入一个超级爆炸的递归中了。右边的a(m-1,n)等还能分身很多,每个分身又能分身,最后会变成一个明确的数,但是分身太爆炸了,m分成n个m-1,每个又能分成n-x个m-2;同时n能分成m个n-1,每个又能分成m-x个n-2。双变量的指数增长!!大学里的高等数学没有学过这种。。。。

好吧不解数学了,怎么写成代码呢?

首先必要的a(m,n)=a(m-1,n)+a(m,n-1)实质是不变的,

在代码中直接 return a(m-1,n)+a(m,n-1);但是这样会带来爆炸式的函数调用开销。

其实类似斐波那契数列一样,递归的时候计算了太多已经计算过的内容了。

我们如果能够在第一次计算某个数时保存值,这样下次再需要的时候就不需要计算了,直接提取就好了。

if( 已经计算过){ 从某个地方拿出来} 

这样就ok了啊,这样计算值最多就是m*n个,剩下的就是每当需要的时候拿出来相加就可以了。

肯定避免不了一个m*n的二维数组,我们记作dp[m][n],

数组有了,怎么放进去呢?

寻找初始值,无论是任何(m,1)、(1,m),都是有一种可能,一直走。就是d[x][1]=1,d[1][x] =1;

其他的数都是这两加上去的。

如下,最简单的递归,就是加了dp的递归!!当我们已经有了值时直接return值,不用再调用函数了!!!

    int helper(int m, int n, vector< vector<int> > &dp){
        if(m==1 || n==1){
            return 1;
        }
        if(dp[m][n]==0){
            dp[m][n]= helper(m-1, n, dp) + helper(m, n-1, dp);   
        }
        return dp[m][n];
    }
在主函数中调用

    int uniquePaths(int m, int n) {
        vector< vector<int> > dp(m+1, vector<int> (n+1, 0) );
        return helper(m, n, dp);
    }
就这样超时变成accept!!!成功解决了问题!


在这整个的函数推导中,我们试图得到一个直接答案,却陷入一个爆炸增长危机中,我们每一步去分解问题总是出现很多很多小的子问题,这些小问题存在量多、重复的问题。

为了解决大量的重复问题,我们使用数组对每个子问题存储值和分辨是否为重复计算。解决问题!

能使用动态规划的问题:

可由子问题构成的,能拆解成子问题,且子问题总体上在缩小问题,最终能拆成一个可预知的值。

这样我们使用递归+数组就解决了问题。

然而这不是彻底的解决问题的办法,递归和动态规划并不是一种类型。

重点!!递归是由大分小,动态规划是由小构成大。

我们由一个最小子问题得到一个中型子问题,再慢慢构成一个大问题,最后达到目标。这就是动态规划!,每向前走的一步(小问题)都可能对将来的步伐(大问题)带来解决方法,动态的自我走向答案。

解决代码,简洁优雅!

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector< vector<int> > dp(m, vector<int> (n, 1) );
        for(int i=1; i<m; i++)
            for(int j=1; j<n; j++)
                dp[i][j] = dp[i][j-1] + dp[i-1][j];
        return dp[m-1][n-1];
    }

};

虽然是个比较简单的问题,但是对理解动态规划有很大的帮助,其他的动态规划都是万变不离其中。

在这之前我都是一个迷糊的状态,说清楚也不太清楚,说不明白也知道解决动态问题的方法。通过自己对公式的推导(虽然没推导出来),熟悉了问题分成子问题的过程。正是由于分成子问题后数量过大无法解决大量子问题,促使我的解决问题的方法由“化大为小”转化成了“积小成大”。

一切算法的学习皆该如此,深入的剖析,返璞归真,从外看到里,再从里看到外。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值