算法分析入门

举报
子都爱学习 发表于 2021/08/14 20:21:39 2021/08/14
【摘要】  题目:现有一个m * n的网格,从最左上角出发,每次只能向右或者向下移动一格,问有多少种不同的方法可以到达最右下角的格子?   见下图一个m*n的格子,从A走到B:分析1,数学表达式这道题就是一个很明显的的排列组合问题,我们可以推导出它的数学表达式   要从A到B,必须向左走m步,向下走n步,什么时候走可以不关心,一共m+n步,很明显,分解出来,m+n步中,任选m向下走步,排列组合解就是一...

 题目:现有一个m * n的网格,从最左上角出发,每次只能向右或者向下移动一格,问有多少种不同的方法可以到达最右下角的格子?
   见下图一个m*n的格子,从A走到B:

分析

1,数学表达式

这道题就是一个很明显的的排列组合问题,我们可以推导出它的数学表达式   

要从A到B,必须向左走m步,向下走n步,什么时候走可以不关心,一共m+n步,很明显,分解出来,m+n步中,任选m向下走步,排列组合解就是一共有C(m+n,m)种所以,对于m*n的格子,一样的,就是从m+n步中选出m步向下或n步向右,因此为C(m+n,m)=C(m+n,n)种。

表达式就是   f(m,n) = C(m+n,m)

def f(self, m: int, n: int) -> int:
        return int(math.factorial(m+n)/math.factorial(m)/math.factorial(n))


2.回溯(递归思想)

但是大多数时候,一个问题是不能用标准的数学表达式的,比如这个问题如果说中间有个节点不能经过,这时候就不适用了,这时候我们就要用到编程的思想。下面谈的就是递归思想
我们先来了解什么是递归:

递归,就是在运行的过程中调用自己。构成递归需具备的条件:
1. 子问题须与原始问题为同样的事,且更为简单;
2. 不能无限制地调用本身,须有个出口,化简为非递归状况处理。

递归的三大要素

第一要素:明确你这个函数想要干什么

对于递归,我觉得很重要的一个事就是,这个函数的功能是什么,他要完成什么样的一件事,而这个,是完全由你自己来定义的。也就是说,我们先不管函数里面的代码什么,而是要先明白,你这个函数是要用来干什么。

例如,我定义了一个函数

// 算 n 的阶乘(假设n不为0)
int f(int n){

}

这个函数的功能是算 n 的阶乘。好了,我们已经定义了一个函数,并且定义了它的功能是什么,接下来我们看第二要素。

第二要素:寻找递归结束条件

所谓递归,就是会在函数内部代码中,调用这个函数本身,所以,我们必须要找出递归的结束条件,不然的话,会一直调用自己,进入无底洞。也就是说,我们需要找出当参数为啥时,递归结束,之后直接把结果返回,请注意,这个时候我们必须能根据这个参数的值,能够直接知道函数的结果是什么。

例如,上面那个例子,当 n = 1 时,那你应该能够直接知道 f(n) 是啥吧?此时,f(1) = 1。完善我们函数内部的代码,把第二要素加进代码里面,如下

// 算 n 的阶乘(假设n不为0)
int f(int n){
    if(n == 1){
        return 1;
    }
}

有人可能会说,当 n = 2 时,那我们可以直接知道 f(n) 等于多少啊,那我可以把 n = 2 作为递归的结束条件吗?

当然可以,只要你觉得参数是什么时,你能够直接知道函数的结果,那么你就可以把这个参数作为结束的条件,所以下面这段代码也是可以的。

// 算 n 的阶乘(假设n>=2)
int f(int n){
    if(n == 2){
        return 2;
    }
}

注意我代码里面写的注释,假设 n >= 2,因为如果 n = 1时,会被漏掉,当 n <= 2时,f(n) = n,所以为了更加严谨,我们可以写成这样:

// 算 n 的阶乘(假设n不为0)
int f(int n){
    if(n <= 2){
        return n;
    }
}

第三要素:找出函数的等价关系式

第三要素就是,我们要不断缩小参数的范围,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。

例如,f(n) 这个范围比较大,我们可以让 f(n) = n * f(n-1)。这样,范围就由 n 变成了 n-1 了,范围变小了,并且为了原函数f(n) 不变,我们需要让 f(n-1) 乘以 n。

说白了,就是要找到原函数的一个等价关系式,f(n) 的等价关系式为 n * f(n-1),即

f(n) = n * f(n-1)。

这个等价关系式的寻找,可以说是最难的一步了,如果你不大懂也没关系,因为你不是天才,你还需要多接触几道题,我会在接下来的文章中,找 10 道递归题,让你慢慢熟悉起来

找出了这个等价,继续完善我们的代码,我们把这个等价式写进函数里。如下:

// 算 n 的阶乘(假设n不为0)
int f(int n){
    if(n <= 2){
        return n;
    }
    // 把 f(n) 的等价操作写进去
    return f(n-1) * n;
}

至此,递归三要素已经都写进代码里了,所以这个 f(n) 功能的内部代码我们已经写好了。

这就是递归最重要的三要素,每次做递归的时候,你就强迫自己试着去寻找这三个要素。


回到我们的问题:
第一步,求解的是达到f(m,n)路径的总数
第二步,子问题与关系式:到达f(m,n)的情况有两种,一种是从f(m-1,n)过来,一种是从f(m,n-1)过来
                                        定义关系式:f(m,n) = f(m-1,n) + f(m,n-1)
第三步, 终止条件   f(0,0) = 1   (这里有些特殊,边界上都是1)

代码

def f(m: int, n: int) -> int:
    if m == 0 or n == 0:
        return 1
    return f(m-1,n) + f(m, n-1)



3.回溯2(递归 + 缓存)
递归是最直观的解法,但是递归过程中会有重复计算问题。比如:我们计算2和3这个点都会计算1这个点的值,计算1的时候又会重复计算,

当我们的维度很小时,差别不大,但是当维度很大时,开销就会变成指数级增长。
下图是f(5,5), f(10,10),f(15,15)的结果及运行时间

既然重复计算了,那我们是不是可以考虑将计算的结果保存下来呢,如果有这个值就直接取,没有就计算出来并保存
我们用列表cache来保存

cache = [[0]*15 for i in range(15)]
def f1(m: int, n: int) -> int:
    if m == 1 or n == 1:
        return 1
    if cache[m][n] != 0:
        return cache[m][n]
    cache[m][n] = f(m-1, n) + f(m, n-1)
    return cache[m][n]



3.dp
我们考虑了cache[i][j]来保存在第i,j时所有路径的值,显而易见cache[i][j] = cache[i-1][j] + cache[i][j-1],即cache[2][2] = cache[1][2] +cache[2][1] = 2
所以可以正向赋值cache,返回cache[m][n]即可

直接上代码

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[1]*n for _ in range(m)]
        #print(dp)  dp[0][j]或者dp[i][0]取不到,所以可以是任意值
        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
        return dp[-1][-1]


4.dp空间优化
令m为行、n为列 优化1:行列两层循环中的循环体cur[j] = pre[j] + cur[j-1] ,cur[j] 表示遍历到的从起点到第i行第j列的路径数,它等于当前第i行第j-1列即 cur[j-1]的值 加上 上一行第j列的值pre[j] 内层循环一次后即计算完了第i行各列的值,在计算下一行第i+1行之前执行pre = cur.clone(); 即第i行的值就是第i+1行的前一行,两层循环完以后最后要到达的终点的行的值存于pre数组中,所以取出 pre[n-1]即可

优化2:相比优化1,少了pre数组,cur[j] += cur[j-1] 即 cur[j] = cur[j-1] + cur[j] 未赋值之前右边的cur[j] 始终表示当前行第i行的上一行第j列的值,赋值之后左边的cur[j]表示当前行第i行第j列的值,cur[j-1] 表示当前行第i行第j-1列的值(cur[j-1] 在计算cur[j]之前就已经计算了,所以表示的是当前行而不是上一行 ), 思路跟优化1是一样的,除了少用了一个数组

代码

# 思路一
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        pre = [1] * n
        cur = [1] * n
        for i in range(1, m):
            for j in range(1, n):
                cur[j] = pre[j] + cur[j-1]
            pre = cur[:]
        return pre[-1]



#  思路二
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        cur = [1] * n
        for i in range(1, m):
            for j in range(1, n):
                cur[j] += cur[j-1]
        return cur[-1]


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。