vlambda博客
学习文章列表

动态规划算法入门练习 (python实现)

动态规划(英语:Dynamic programming,简称DP)是一种在数学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。在生物信息领域,比如在序列比对的时候,就用到了动态规划的思想。在隐马尔科夫模型中的维特比 (Viterbi) 算法也使用了动态规划算法。

  1.  爬楼梯 


【题目】有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。求出一共有多少种走法。



【思路】这个问题仔细想想其实与斐波那契数列很像,同样可用递归求解。

假如最后我们已经爬上了10层,那么最后一个步骤走了要么一步, 要么两步。也就是说,我们到10层的走法,其实就是我们走到八层和九层的方法的和。即F(n) = F(n-2) + F(n-1)。

【解法1】递归


def climb(n):
    if n == 1:
        return 1
    elif n == 2:
        return 2
    else:
        return climb(n - 1) + climb(n - 2)

climb(10)


【解法2】动态规划


def climb(n):
    way = [0] * n
    way[0] = 1
    way[1] = 2
    for i in range(2, n):
        way[i] = way[i - 1] + way[i - 2]
    return way[n - 1]

# 或者
def climb=(n):
    if n <= 2:
        return n
    else:
        a, b = 1, 2
        for i in range(n - 2):
            a, b = b, a + b
        return b

climb(10)



 2.  打家劫舍 


【题目】你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

【例子】

动态规划算法入门练习 (python实现)

【思路】当我们偷窃到第i家的时候,所能偷到的就是一直到第i - 1家所偷到的与第i - 2家和第i家偷到的钱的和的最大值,即dp[i] = max(nums[i] + dp[i -2], dp[i-1])。 

【解法1】


def rob(nums):
    size = len(nums)
    
    if size == 1:
        return nums[0]
    elif size == 2:
        return max(nums[0], nums[1])
    elif size == 0:
        return 0     
    else:
        dp = [0] * size
        dp[0], dp[1] = nums[0], max(nums[0], nums[1])

        for i in range(2, size):
            dp[i] = max(nums[i] + dp[i - 2], dp[i - 1])

        return dp[-1]


【解法2】优化 只用到了dp[i-2]与dp[i-1]的值,不需要列表


def rob(nums):
    now = last = 0
    for i in range(len(nums)):
        now, last = max(last + nums[i], now), now
    return now



3.  除数博弈


【题目】爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。

最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作:

选出任一 x,满足 0 < x < N 且 N % x == 0 。

用 N - x 替换黑板上的数字 N 。

如果玩家无法执行这些操作,就会输掉游戏。

只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 false。假设两个玩家都以最佳状态参与游戏。

【例子】

【思路1】动态规划

可以分析一步步地先分析一下,找一下其中规律:

当N = 1时,Alice没有选择,输;

当N = 2时,Alice选1,Bob没有选择, 赢。

当N = 3时,Alice选1,Bob选的时候N=2,根据上一个结果,先手赢,所以Bob赢,Alice输。

当N = 4时,Alice选1,Bob选的时候N=3,根据上一个结果,先手输,Bob会输,Alice赢。

……

以此类推,可以看出,假如alice选的k,那么N%k==0和当N-k的时候必须输(即Bob的时候必输)这两个条件必须满足。


【解法1】

def divisorgame(N):
    if N <= 1:
        return False
    else:
        dp = [False] * N
        dp[0] = False
        dp[1] = True

        for i in range(3, N + 1): # N的实际取值
            for j in range(1, i // 2 + 1):
                if i % j == 0 and dp[i-1-j] == False:
                    dp[i-1] = True
                    break
        return dp[-1]


【思路2】数学归纳

上面的动态归纳法需要两层循环,效率较低,仔细分析一下这个题,还有额外的解法。看一下结果,可以发现,当N为奇数时,Alice(先手)必输;当N为偶数时,Alice必赢。因为:

  • 最后一步中,拿到2的一定会赢,拿到1的会输。

  • 当N为奇数时,其约数一定是奇数,所以Bob拿到的一定会是偶数,Bob拿1,这样Alice拿到的还是奇数,这样一直到Bob拿到2,Alice就会输掉。

  • 当N为偶数时,偶数的约数可奇可偶,Alice可以选1或者一个奇数,Bob拿到的就是一个奇数,从上面可知奇数必输。Alice则会赢。

所以此题就会转化为一个数学问题。


【解法2】

def divisorgame(N):
    return N % 2 == 0