[Python] 算法心得—动态规划

1. 硬币组合

如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?

参考资料

假设d[i]为凑满i元所需最少的硬币数,那么:

d[i] 解释
d[0] = 0 0
d[1] = 1 需要从面值不大于1元的硬币中选择,结果为1
d[2] = d[2-1] + 1 = 2 从面值不大于2元的硬币中选择,符合要求的硬币面值为:1元
d[3] = d[3-3] + 1 = 1 符合要求的硬币面值为:1元/3元,含有3元的硬币d[3]=d[3-3]+1=1,不含3元的硬币d[3]=d[3-1]+1=d[2]+1 =3
... ...

可以得出状态转移方程:dp[i] = min(d[i-value[j]) + 1,参考代码:

import sys

# 需要用硬币凑满的钱数
amount = 12
# 硬币的种类
coins = [1, 3, 5]

def coin_dynamic(amount, coins):
    dp = [0]
    for i in range(1, amount + 1):
        dp.append(sys.maxsize)
        for j in range(len(coins)):
            if coins[j] <= i and dp[i - coins[j]] + 1 < dp[i]:
                dp[i] = dp[i - coins[j]] + 1
    return dp

2. 0-1背包问题

假设我们有n件物品,分别编号为1, 2...n。其中编号为i的物品价值为value[i],它的重量为weight[i]。为了简化问题,假定价值和重量都是整数值。现在,假设我们有一个背包,它能够承载的重量是capacity。现在,我们希望往包里装这些物品,使得包里装的物品价值最大化,那么我们该如何来选择装的东西呢?

参考资料

首先我们先构建一个表格dp[i][j],横轴为背包的容纳重量(从1到背包的实际最大容纳),纵轴为各个可选择的物品。而表格中的每个单元格表示的是使用i与前的物品、且保证总重量不大于j情况下背包能容纳物品的最大价值。

尝试填充完毕后,我们可以得到一个结论:在i行j列的最大值可以说是(i-1行[即不取i物品]j列的值) 和 (i物品的价值 + i-1行j-i物品价值列的值[即取了i物品的价值]),写成状态转移方程即为:`dp[i][j] = max{dp[i-1][j], dp[i-1][j-value[i]] + value[i]},对应的代码如下:

# n个物体的重量(w[0]无用)
weight = [1, 4, 3, 1]
# n个物体的价值(p[0]无用)
value = [1500, 3000, 2000, 2000]
# 计算n的个数
n = len(weight) - 1
# 背包的载重量
capacity = 5
# 装入背包的物体的索引
x = []

def bag_dynamic(weight, value, capacity, x):
    n = len(weight)
    weight.insert(0, 0)
    value.insert(0, 0)

    dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]

    for i in range(1, n + 1):
        for j in range(1, capacity + 1):
            if j >= weight[i]:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
            else:
                dp[i][j] = dp[i - 1][j]
    j = capacity
    for i in range(n, 0, -1):
        if dp[i][j] > dp[i - 1][j]:
            x.append(i)
            j = j - weight[i]

    return dp[n][capacity]

3. CPU双核问题

双核CPU的两个核能够同时的处理任务,现在有n个已知数据量的任务需要交给CPU处理,假设已知CPU的每个核1秒可以处理1kb,每个核同时只能处理一项任务。n个任务可以按照任意顺序放入CPU进行处理,现在需要设计一个方案让CPU处理完这批任务所需的时间最少,求这个最小的时间。

要使CPU处理时间最少,就要充分利用其双核,使其达到负载均衡。所以该问题的实质就是将数组分成两部分,使得两部分的和的差最小。

weight = [0, 3072, 3072, 7168, 3072, 1024]  # 假设进入处理的的任务大小
weight = list(map(lambda x: int(x / 1024), weight))  # 转化下
value = weight  # 这题的价值和任务重量一致
capacity = int(sum(weight) / 2 + 1)  # 背包承重为总任务的一半


def dynamic_cpu(weight, value, capacity):
    dp = [[0 for _ in range(capacity + 1)] for _ in range(len(weight))]

    for i in range(1, len(value)):
        for j in range(1, capacity + 1):
            if j >= value[i]:
                dp[i][j] = max(dp[i - 1][j], value[i] + dp[i - 1][j - weight[i]])
            else:
                dp[i][j] = dp[i - 1][j]
    return dp[-1][-1]


print(dynamic_cpu(weight, value, capacity))

4. LIS-最长非降子序列

一个序列有N个数:A[1],A[2],…,A[N],求出最长非降子序列的长度(子序列不必连续,但不能破坏原序列的顺序,子串必须要连续)。

参考文章

定义d[i]为A[i]位置上的最长非降子序列长度,所以:

 d[0] = 1  
 d[1] = d[0] + 1 (A[1] >= A[0])  
      = 1 (A[1] < A[0])
 d[2] = d[1] + 1 (A[2] >= A[1])
      = d[0] + 1 (A[0] <= A[2] <= A[1])
      = 1 (A[2] < A[0], A[2] < A[1])
 ......

所以可以得到状态转移方程:d[i] = max{1, d[j]+1}(其中j<i, A[j]<=A[i]),代码如下:

def lis(arr):
    num = len(arr)
    d = [0] * num
    max_len = 1
    for i in range(num):
        d[i] = 1
        for j in range(i):
            if arr[j] <= arr[i] and d[i] < d[j] + 1:
                d[i] = d[j] + 1
            if d[i] > max_len:
                max_len = d[i]
    return max_len


print(lis([2, 1, 5, 3, 6, 4, 8, 9, 7]))

此时的时间复杂度为O(N^2),还有一种巧妙的方法可以将复杂度降为O(n * logN)。

定义suq为最长递增子序列,它是一个有序数组。

step 1,将arr[0]有序放入suq,则suq=[2];

step 2,将arr[1]有序放入,因为arr[1]<suq[-1](小于有序数组最后一个数),所以将suq[-1]替换为arr[1],suq=[1];

step 3,将arr[2]有序放入,此时arr[2]>suq[-1](大于有序数组最后一个数),所以将suq尾部添加arr[2],suq=[1,5];

step 4,将arr[3]有序放入,此时如step 2情况,suq=[1,3];

step 5,同step 3,suq=[1,3,6];

step 6,同step 2,suq=[1,3,4];

step 7,同step 3,suq=[1,3,4,8];

step 8,同step 3,suq=[1,3,4,8,9];

step 9,同step 2,将arr[8]替换suq[3],suq=[1,3,4,7,9]

所以最长递增子序列长度为len(suq)=5。

上述的插入分两种情况,第一种:arr[i]>=suq[-1],尾插;第二种:arr[i]<suq[-1],将arr[i]替换掉suq中比其大的第一个元素(此时可以用二分法查找),每一个插入复杂度为O(logN),总复杂度为O(N * logN)。

def binary_search(suq, l, h, key):
    if suq[h] <= key:
        return h+1
    while l < h:
        mid = (l+h) // 2
        if suq[mid] <= key:
            l = mid + 1
        else:
            h = mid
    return l

def lis_bs(arr):
    num = len(arr)
    suq = arr[0:1]
    max_len = 1
    for i in range(1, num):
        point = binary_search(suq, 0, max_len-1, arr[i])
        if point <= max_len - 1:
            suq[point] = arr[i]
        else:
            suq.insert(point, arr[i])
            max_len += 1
    return max_len

print(lis_bs([2, 1, 5, 3, 6, 4, 8, 9, 7]))

5. LCS-最长公共子序列

给定序s1=[1,3,4,5,6,7,7,8],s2=[3,5,7,4,8,6,7,8,2],s1和s2的相同子序列,且该子序列的长度最长,即是LCS。

参考文章

可以得出以下结论:

如果s1和s2的最后一个元素相等,那么s1和s2的LCS就等于除去最后一个元素的s1和除去最后一个元素的s2的LCS再加上它们相等的最后一个元素;

如果s1和s2最后一个元素不想等,那么其LCS就等于(除去最后一个元素的s1和s2的LCS)和(除去最后一个元素的s2和s1的LCS)中更大的一个。

代码如下:

s1 = [1, 3, 4, 5, 6, 7, 7, 8]
s2 = [3, 5, 7, 4, 8, 6, 7, 8, 2]


def lcs(s1, s2):
    dp = [[0] * (len(s2) + 1) for _ in range(len(s1) + 1)]

    for i in range(1, len(s1) + 1):
        for j in range(1, len(s2) + 1):
            if s1[i - 1] == s2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[-1][-1]


print("max LCS number:", lcs(s1, s2))

6. 组成SUM的方案数

给定一个有n个正整数的数组A和一个整数sum,求选择数组A中部分数字和为sum的方案数。
当两种选取方案有一个数字的下标不一样,我们就认为是不同的组成方案。

同样,我们用dp[i][j]表示用前i个数字凑到j最多有多少种方案,那么可得:

# 当不用i个数字时能凑到j的最多情况
dp[i][j] = dp[i-1][j]
# 用了i时,只需看原来凑到j-arr[i]的最多情况
dp[i][j] += dp[i-1][j-arr[i]]

代码如下:

def sum_combination(arr, sum):
    length = len(arr)

    dp = [[1] + [0] * sum for _ in range(length + 1)]

    for i in range(1, length + 1):
        for j in range(1, sum + 1):
            if j - arr[i - 1] >= 0:
                dp[i][j] = dp[i - 1][j] + dp[i - 1][j - arr[i - 1]]
            else:
                dp[i][j] = dp[i - 1][j]
    return dp[length][sum]


print(sum_combination([5, 5, 10, 2, 3], 10))

7. 连续子数组的最大和

上一章数组篇谈到过这个问题,下面我们看一下如何能用一种更简便更易理解的方式来解决吧。

定义dp[i]表示以第i个元素为结尾的连续子数组的最大和,则有状态方程 dp[i]=max{dp[i-1]+a[i], a[i]}

代码如下:

def max_subarray(arr):
    dp = [arr[0]] + [0] * (len(arr)-1)

    for i in range(1, len(arr)):
        dp[i] = max(dp[i-1]+arr[i], arr[i])

    return max(dp)

print(max_subarray([-1,2, 1]))

8. 暗黑数

这道题超酷的!

一个只包含’A’、’B’和’C’的字符串,如果存在某一段长度为3的连续子串中恰好’A’、’B’和’C’各有一个,那么这个字符串就是纯净的,否则这个字符串就是暗黑的。例如:

BAACAACCBAAA 连续子串”CBA”中包含了’A’,’B’,’C’各一个,所以是纯净的字符串

AABBCCAABB 不存在一个长度为3的连续子串包含’A’,’B’,’C’,所以是暗黑的字符串

你的任务就是计算出长度为n的字符串(只包含’A’、’B’和’C’),有多少个是暗黑的字符串。

从第一位开始不断往后推算暗黑数的排列总数,定义f(n)为n位上暗黑数的排列总数,假设S(n)为n与n-1相同的排列数,D(n)为n与n-1不同的排列数。

那么,可以得到f(n-1) = S(n-1) + D(n-1) (1)。

如果n-1与n-2相同,那么新增字母ABC都可以,有3*S(n-1)种可能;如果n-1与n-2不同,那么新增字母只能是n-1与n-2中的一个,有2*D(n-1)种可能;

由此可得:f(n) = 3\*S(n-1) + 2\*D(n-1) = 2\*f(n-1) + S(n-1) (2)

我们继续分析,如果n-1与n-2相同,那么之后有S(n-1)种可能是n与n-1相同的,有2*S(n-1)种可能是n与n-1不同的;如果n-1与n-2不同,有D(n-1)种可能是n与n-1相同的,D(n-1)种可能是n与n-1不同的。

由此又可得 S(n) = S(n-1) + D(n-1),结合上述式(1) f(n-1) = S(n-1) + D(n-1)可得: S(n) = f(n-1)

代入式(2)可得:f(n) = 2*f(n-1) + f(n-2)

代码水到渠成:

# 递推
def dark_num(arr):
    dp = []
    dp[0], dp[1] = 3, 9
    if len(arr) < 3:
        return dp[len(arr)-1]
    dp += [0] * (len(arr) - 2)
    for i in range(2, len(arr)):
        dp[i] = 2 * dp[i-1] + dp[i-2]
    return dp[len(arr)-1]
    
 # 递归
 def dark_num(arr):
    if arr == 1:
        return 3
    elif arr==2:
        return 9
    else:
        return 2*dark_num(arr-1) + dark_num(arr-2)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容

  • 动态规划(Dynamic Programming) 本文包括: 动态规划定义 状态转移方程 动态规划算法步骤 最长...
    廖少少阅读 3,268评论 0 18
  • 在C语言中,五种基本数据类型存储空间长度的排列顺序是: A)char B)char=int<=float C)ch...
    夏天再来阅读 3,333评论 0 2
  • 回溯算法 回溯法:也称为试探法,它并不考虑问题规模的大小,而是从问题的最明显的最小规模开始逐步求解出可能的答案,并...
    fredal阅读 13,632评论 0 89
  • 《浪淘沙》 唐 · 白居易 借问江潮与海水,何似君情与妾心? ...
    zxz缘来缘尽阅读 518评论 0 0
  • 学校的教学楼门洞底下有一只狗,这只个狗和别的狗不太一样。我观察了很久,它总是成天的趴在门洞底下,天气热了就趴在阴影...
    盐是甜的阅读 305评论 0 0