Codility刷题之旅 - Dynamic programming

903 阅读3分钟

今天继续Codility的Lessons部分的第17个主题——Dynamic programming

Codility - Lessons - Dynamic programming

还是按先总结红色部分PDF,然后再完成蓝色的Tasks部分的节奏来~

image.png

PDF Reading Material

  • 前言: 介绍了动态规划解法的主要思想,即将原始问题拆解为多个相似且更小的问题,然后通过获得这些相似小问题的解,让原始问题的解更容易确定下来。
  • 17.1 - The Coin Changing Problem: 上一章节的贪婪算法无法准确计算的拼硬币问题,在用本章的动态规划思想可以实现全局最优解的计算。整体思路如下,首先定义几个变量的含义,本题中我们每种硬币的供应是无限的,硬币种类数是n种,最终需要凑到的金额是k。
    • PDF Material中给的动态规划思路,和CSDN上这篇中文文章第一部分是相同的:blog.csdn.net/qq_29493173… ,可以参考~
    • 最终在时间复杂度 O(n · k) ,空间复杂度 O(k)下得到了全局最优解
  • 17.2 - Exercise: 一个青蛙跳到position k一共有多少种跳法的问题,青蛙每一步可以跳的长度一共有n种(S(0),S(1),...S(n-1))。运用动规思想,初始化从position 0~position k的跳法序列,序列第一个元素是1,后续全是0。之后就是顺序运用动规思想,向后更新每一个position处的跳法,更新方式利用到了dp[j]=dp[j-S(i)]。

Tasks

NumberSolitaire: In a given array, find the subset of maximal sum in which the distance between consecutive elements is at most 6.

image.png

问题概述:本题的输入是包含N个整数的Array A,代表了连续N个格子的板子,Array中的值代表每个格子上的一个数字。玩家初始起点在第一个格子,不断投掷骰子,并根据投掷得到的点数进行移动。而在最终移动到最后一个格子时,加总之前跳过的每一个格子上的数字。需要返回的结果,是走到终点时,能够得到的max(之前行进过的格子值之和)。

这里要注意的是,当我们已经移动到比较靠近终点的位置时,如果投掷得到的骰子点数是大于此时距离终点距离的话,则并不会移动而是继续投掷,直到满足投掷点数<距离终点格子数。

解法概述:本题解法的动规思想,基本源于PDF Material中17.2部分的解答,因为每次骰子的取值只会在1~6的6种中,所以我们在j从index=1~n-1的格子循环中的每一步,带入s = 1~6,来进行dp(j)=max(dp(j), dp(j-s)+A[j])的判断,完成循环后就可以得到走到终点时可能的最大值。

def solution(A):
    n = len(A)
    # 初始化DP
    dp = [A[0]] + [-10000*n]*(n-1)
    # 动规划
    for j in range(1, n):
        for i, s in enumerate([1,2,3,4,5,6]):
            if s <= j:
                dp[j] = max(dp[j], dp[j-s] + A[j]) #dp(j)=max(dp(j), dp(j-s)+A[j])
    return dp[n-1]

image.png

MinAbsSum: Given array of integers, find the lowest absolute sum of elements.

image.png

问题概述:本题的输入,是包含了N个整数的Array A。需要返回的,是最小的val(A, S)=|sum{ A[i]*S[i] for i=0...N-1}|,其中每个S[i],是1或-1中的任意一个.

解法概述:本题还是有点难,最终参考了以下官方solution PDF中的golden solution:codility.com/media/train…,如果英文理解起来比较困难,还有一篇中文的解答写的不错:shubo.io/min-abs-sum…

参见官网solution Pdf中的slow solution,是比较容易理解本题解答的DP思想的。不过slow solution下的最终performance并不能符合预期。事实上根据题目中对Array长度和枚举值的范围描述,A中整数的的取值范围一共是200个,而总数量可能达到20000。

所以,A中重复元素可能很多,因此选择循环A中每种元素来迭代更新dp[j],会比循环A中每个元素来迭代更新dp[j]更高效。这也就是golden solution的解法,利用了一个count数组来记录A中每种abs(a)出现的次数,后续用O(N*M^2)的复杂度完成了dp的更新。

def solution(A):
    N = len(A)
    M = 0
    for i in range(N):
        A[i] = abs(A[i])
        M = max(A[i], M)
    S = sum(A)

    count = [0] * (M + 1)
    for i in range(N):
        count[A[i]] += 1
    
    dp = [-1] * (S + 1)
    dp[0] = 0
    for a in range(1, M + 1):
        if count[a] > 0:
            for j in range(S):
                if dp[j] >= 0:
                    dp[j] = count[a]
                elif (j >= a and dp[j - a] > 0):
                    dp[j] = dp[j - a] - 1
    
    result = S
    for i in range(S // 2 + 1):
        if dp[i] >= 0:
            result = min(result, S - 2 * i)
    return result

image.png