动态规划入门之从递推到状态转移

在了解动态规划前,不妨先看看题目 http://acm.hdu.edu.cn/showproblem.php?pid=2046。截图如下.

问你当n为任意数(n>0)时, 骨牌的铺放方案有几种?

咱可以先从归纳法试试,列举出可能出现的解。

当n=1时,只有一种铺放方案,即 |

当n=2时,有两种方案,即 ||  或 =

当n=3时,有||| ,|= ,=|       

当n=4时,有||||,   |=|,  =||, ||=, == 

当n=5时,有。。。8种,我不写了。。

看到这里,你发现这数字有什么规律没有?他们的解序列为1,2,3,5,8 。。。下一个数字是?

智商达到正常人水平的已经看出来下个数字是13了,如果没想出来,面壁思过去(▼ヘ▼#)

设解数列表示为X[n], 则对于任意n>2有 X[n] = X[n-1]+X[n-2],  X[1] = 1, X[2] = 2

但这只是归纳抖机灵,有什么逻辑推理依据使等式成立吗?

当然有!

仔细想想,X[n]是不是就相当于从X[n-1]的最右摆放上一个|,也相当于从X[n-2]的最右摆放上一个=?

那么这个公式就很明显成立, X[n] = X[n-1] + X[n-2] !

啥,你问我为什么不在X[n-2]的最右摆上一个|| ? 嗯,,你可以画一下,在X[n-2]的最右摆上一个||出现的情况都被 在X[n-1]加上一个 | 的情况包含了。

再来看一道题,小明腿短,他一次只能上一步台阶,或者上两步台阶,问他从第0台阶上到第n台阶有多少种走法?

当n=1时,只有一种,小明往上迈一步。

当n=2时,有两种,一种是直接迈两步,一种是迈完一步在迈两步。

当n=3时,有3种,从n=1的情况迈2步,加上n=2的两种情况。

当n=4时,有5种,从_____________, 加上____________  →_→  (填空题)

如果你没看懂。那就重新看一遍吧(笑哭)

以上两道题都是非常经典的斐波那契数列,即下一个数等于前两项的和。

说了这么多还没说代码。接下来我们来实现一下斐波那契数列吧。

我让老铁用递推的方式实现斐波那契,这时候老铁很自信的写出了如下代码:

def Fibonacci(n):
    if n==1:
        return 1
    if n==2:
        return 2
    return Fibonacci(n-1)+Fibonacci(n-2)

我问老铁你这时间复杂度,空间复杂度是多少。老铁还蜜汁自信的回答说O(n), O(n).

我气的差点当场去世。。

可以画一下调用树分析分析。只画了n=5的情况如下:

上图可见n=3的情况下还被多次计算,当n更大时重复多次计算会更明显,时间复杂度O(2^n),当n很大时,计算机就算不动了。(实测n=40就要算几十秒了)

我说老铁,你这写的是递归啊,我要的是递推!

何为递推,就像上面我们在分析问题的时候,我们可以从现有的方案中递推到下一个,即为自底向上的思考方式,而不是自顶向下。

这时候老铁有想法了,很快写出了以下代码。

def Fibonacci(n):
    if n==1:
        return 1
    if n==2:
        return 2
    a, b = 1, 2
    for i in range(2, n):
        b, a = b+a, b
    return b

这下可以了,老铁成功掌握了递推的思想,时间复杂为O(n)

这时候杠精要说了,斐波那契数列有通项公式,你为啥不用通项公式方法去计算呢?这。。我还不是要为了引入下一个关键点嘛。

好,先看例题 https://leetcode-cn.com/problems/longest-increasing-subsequence/

老铁编码能力强悍,直接枚举出所有可能的解,很自信的写下以下代码:

def solve(num: list):
    def dfs(depth: int, count: int, last: int):
        """
        递归遍历所有可能出现的上升序列
        :param depth: 递归深度
        :param count: 当前序列的解,即上升子序列长度
        :param last: 当前序列的最后一个数字,后面的数字必须比这个数字大
        :return: 从数组第depth个元素开头的子序列的解
        """
        if depth >= len(num):
            return count
        t1 = 0
        if num[depth] > last:
            # 当前数字比上一个数字大,可入队
            t1 = dfs(depth + 1, count + 1, num[depth])
        t2 = dfs(depth + 1, count, last)
        return max(t1, t2)

    ans = 0
    for idx, item in enumerate(num):
        ans = max(ans, dfs(idx, 1, item))

    return ans

这个代码基本上就是暴力遍历法的解,但是有个最坏的情况是假如整个数组都是上升序列,那么每种可能出现的排列都会被遍历过,那么整体的时间复杂度就是O(2^n)。有没有什么办法优化呢?

Ok,那么接下来就引入我们本篇的主角了,当当当,动态规划来了。我们想想能否设计一种状态转移方程,就像斐波那契数列那样,问题的解从方程X[n] = X[n-1] + X[n-2]推导得出呢?

我们先从数组长度为1的情况考虑,很明显解为1,我们记下来X[1] = 1.

数组长度为2时呢,当a[2] > a[1] 时,解为2,否则为1.我们记下来X[2]=2 或 X[2]=1

数组长度为3时呢,看看a[3] 是否大于 a[1] 或者a[2],如果都是,则从X[1]中X[2]找到最大值,使X[3] = max(X[1], X[2])。如果a[3] 都比a[1], a[2]小,记下X[3] = 1。 如果a[3]只比a[1]或者a[2]一个大,记下X[3]=2

当数组长度为4时,看看b∈{1, 2, 3}中满足a[b] <a[4]的集合,表示为m,则X[4] = max(X[m])+1

数组长度为n时,看看对于所有的m(m<n且m>0),满足a[n]>a[m], 找到所有的X[m], 则X[n] = max(X[m]+1)

很明显了,我们就可以得到状态转移方程:

X[n] = max(X[m]+1) (a[m] < a[n])

如果你看到这里还不明白的话,可以直接看看代码:

def solve(nums: list):
    x = []
    for idx, num in enumerate(nums):
        tmp = []
        # 找到所有比a[n]小的数组下标
        for i in range(idx):
            if num > nums[i]:
                tmp.append(i)
        if not tmp:
            x.append(1)
        else:
            # 找出符合条件的下标集的最大值
            tmp1 = [x[_] for _ in tmp]
            x.append(max(tmp1)+1)
    return max(x)

这份代码就是按照我上述方法直接写出来的,但是空间复杂度不太美丽,多次申请新数组。

如果直接对着状态转移方程编程,我们可以得出如下代码。

def solve(nums: list):
    dp = []  # dp[i]表示从以第i个元素结尾的最长上升序列长度
    for i, num in enumerate(nums):
        dp.append(1)    # 初始化dp[i] = 1
        for j in range(i):
            if num > nums[j]:
                dp[i] = max(dp[i], dp[j]+1)
    return dp[len(nums)-1]

怎么样,是不是很简短优美?

题目中说到可以优化到O(nlogn)时间复杂度的做法,大致想法就是我们可以改为这样的策略:设dp[i]为上升子序列长度为i,且该上升子序列的末尾最小值为dp[i]。具体做法这里不做展开

再说说经典的背包问题吧。给一个承重量为w的背包,你作为一个职业小偷,面对m个物品,每个物品都有对应的重量u和价值v,问你要怎么选择这n个物品使得你偷到的东西物品价值最多?(w,m,u,v都大于0)

面对这个问题的时候,你可能首先想到贪心策略,即优先拿单位价值(v/u)最高的物品。但这个是不行的,毕竟背包重量有限,举个反例,背包重量为11,有三个物品,对应的(重量,价值)为(8, 16), (4, 7), (7, 10),如果拿了(8,16)的话,只能使最终价值达到16,低于选后两者的总价值17.

咱又想到老方法,当背包重量为1时,背包最大价值为...这里省略了推算过程,我直接上公式 了:

dp[i] = max( dp[i-u]+v, dp[i])   dp[i]代表背包重量为i时能得到的最大价值

写出代码就是

def solve(n, m, u: list, v: list):
    dp = [0 for _ in range(n + 1)]
    for i in range(m):
        ui, vi = u[i], v[i]
        j = n
        while j:
            if j >= ui:
                dp[j] = max(dp[j], dp[j - ui] + vi)
            else:
                break
            j -= 1
    return dp[n]


print(solve(11, 3, [8, 4, 7], [16, 7, 10]))

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值