Loading

算法进阶

贪心算法

贪⼼算法(⼜称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。

贪⼼算法并不保证会得到最优解,但是在某些问题上贪⼼算法的解就是最优解。要会判断⼀个问题能否⽤贪⼼算法来计算。

贪心算法没有固定的框架,算法设计的关键是贪婪策略的选择。贪心策略要无后向性,也就是说某状态以后的过程不会影响以前的状态,只与当前状态有关。

找零问题

假设商店⽼板需要找零n元钱,钱币的⾯额有:100元、50元、 20元、5元、1元,如何找零使得所需钱币的数量最少?

def func(li, n):
    li.sort(reverse=True)
    m = [0 for _ in range(len(li))]
    for index, value in enumerate(li):
        m[index] = n // value
        n = n % value
    return m,n


if __name__ == '__main__':
    li = [100, 50, 20, 5, 1]
    print(func(li, 373))

背包问题

⼀个⼩偷在某个商店发现有n个商品(假设每个商品都不同,且只有一个),第i个商品价值vi元,重wi千克。他希望拿⾛的价值尽量⾼,但他的背包最多只能容纳W千克的东⻄。他应该拿⾛哪些商品?

  • 0-1背包:对于⼀个商品,⼩偷要么把它完整拿⾛,要么留下。不能只拿⾛⼀部分,或把⼀个商品拿⾛多次。(商品为⾦条)

  • 分数背包:对于⼀个商品,⼩偷可以拿⾛其中任意⼀部分。(商品为⾦砂)

举例:

  • 商品1:v1=60 w1=10 (每千克6元)

  • 商品2:v2=100 w2=20 (每千克5元)

  • 商品3:v3=120 w3=30 (每千克4元)

  • 背包容量:W=50

对于0-1背包和分数背包,贪⼼算法是否都能得到最优解?为什么?

  • 0-1背包:拿商品1、商品2,商品3拿不了,重量超出,所以只能拿160元的;这显然不是最优解,背包剩下20千克没有装。所以0-1背包是无法使用贪心算法得到最优解的。

  • 分数背包可以。

def fractional_backpack(goods, total_capacity):
    """
    :param goods: 商品信息
    :param total_capacity: 背包总容量
    :return:
    """
    total_price = 0
    li = [0 for _ in range(len(goods))]
    for index, (price, weight) in enumerate(goods):
        if total_capacity >= weight:
            total_capacity -= weight
            total_price += price
            li[index] = 1
        else:
            total_price += total_capacity * (price / weight)
            li[index] = total_capacity / weight
            break
    # 返回拿走的商品信息和总共得到的金额
    return li, total_price


if __name__ == '__main__':
    goods = [(60, 10), (100, 20), (120, 30)]
    goods.sort(key=lambda x: x[0] / x[1], reverse=True)
    total_capacity = 50
    li, get_price = fractional_backpack(goods, total_capacity)
    print(li, get_price)

拼接最大数字问题

有n个⾮负整数,将其按照字符串拼接的⽅式拼接为⼀个整数。 如何拼接可以使得得到的整数最⼤?

例:32,94,128,1286,6,71可以拼接除的最⼤整数为 94716321286128(考虑128和1286哪个在前)

from functools import cmp_to_key


def xy_cmp(x, y):
    if x + y > y + x:
        return -1
    elif x + y < y + x:
        return 1
    return 0


def number_join(number_str_list):
    number_str_list.sort(key=cmp_to_key(xy_cmp))
    return "".join(number_str_list)


if __name__ == '__main__':
    number_list = [32, 94, 128, 1286, 6, 71]
    number_str_list = list(map(str, number_list))
    last_number = number_join(number_str_list)
    print(last_number)

活动选择问题

假设有n个活动,这些活动要占⽤同⼀⽚场地,⽽场地在某时刻只能供⼀个活动使⽤。

每个活动都有⼀个开始时间si和结束时间fi(题⽬中时间以整数表示),表示活动在[si, fi)区间占⽤场地。

问:安排哪些活动能够使该场地举办的活动的个数最多?

image-20230506205100560

贪⼼结论:最先结束的活动⼀定是最优解的⼀部分。(思路就是每次都找最先结束的活动)

证明:假设a是所有活动中最先结束的活动,b是最优解中最先结束的活动。

如果a=b,结论成⽴。

如果a≠b,则b的结束时间⼀定晚于a的结束时间,则此时⽤a替换掉最优解中的b,a⼀定不与最优解中的其他活动时间重叠,因此替换后的解也是最优解。

def activity_selection(activities):
    tmp_list = [activities[0]]  # 因为排序了,
    for i in range(1, len(activities)):
        if activities[i][0] >= tmp_list[-1][1]:
            # 如果当前的活动开始时间大于或等于已经开始的活动截至时间,时间就不冲突,加入到活动列表中
            tmp_list.append(activities[i])
    return tmp_list


if __name__ == '__main__':
    # 每个小元组表示活动开始时间和截止时间
    activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)]
    # 保证活动是按照结束时间排好序的
    activities.sort(key=lambda x: x[1])
    activities_plan = activity_selection(activities)
    print(activities_plan)

动态规划

动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算

网上比较流行的一个例子:

A :"1+1+1+1+1+1+1+1 =?"
A :"上面等式的值是多少"
B :计算 "8"
A :在上面等式的左边写上 "1+" 呢?
A :"此时等式的值为多少"
B :很快得出答案 "9"
A :"你怎么这么快就知道答案了"
A :"只要在8的基础上加1就行了"
A :"所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"

从斐波那契数列看动态规划

斐波那契数列:Fn = Fn−1 + Fn−2

练习:使⽤递归和⾮递归的⽅法来求解斐波那契数列的第n项

"""斐波那契"""


def fibonacci(n=20):
    """递归版本(非常慢),存在大量的重复计算"""
    if n == 1 or n == 2:
        return 1
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)


def fibonacci_no_recursion(n=20):
    """非递归版本(更快)"""
    tmp_list = [0, 1, 1]
    if n > 2:
        for i in range(n - 2):
            tmp_list.append(tmp_list[-1] + tmp_list[-2])
        return tmp_list[n]
    return tmp_list[n]


if __name__ == '__main__':
    # print(fibonacci())
    print(fibonacci_no_recursion())

青蛙跳阶问题

Leetcode原题:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个10级的台阶总共有多少种跳法。

PS:答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

本题可转化为 求斐波那契数列第 n 项的值

有些小伙伴第一次见这个题的时候,可能会有点蒙圈,不知道怎么解决。其实可以试想:

  • 要想跳到第10级台阶,要么是先跳到第9级,然后再跳1级台阶上去;要么是先跳到第8级,然后一次迈2级台阶上去。
  • 同理,要想跳到第9级台阶,要么是先跳到第8级,然后再跳1级台阶上去;要么是先跳到第7级,然后一次迈2级台阶上去。
  • 要想跳到第8级台阶,要么是先跳到第7级,然后再跳1级台阶上去;要么是先跳到第6级,然后一次迈2级台阶上去。

假设跳到第n级台阶的跳数我们定义为f(n),很显然就可以得出以下公式:

f(10) = f(9)+f(8)
f (9)  = f(8) + f(7)
f (8)  = f(7) + f(6)
...
f(3) = f(2) + f(1)

即通用公式为: f(n) = f(n-1) + f(n-2)
"""青蛙跳阶问题"""


def frog_jump(n=10):
    a, b = 1, 1
    for _ in range(n):
        a, b = b, a + b
    return a % 1000000007


if __name__ == '__main__':
    print(frog_jump())

钢条切割问题

某公司出售钢条,出售价格与钢条⻓度之间的关系如下表:

image-20230511094804633

问题:现有⼀段⻓度为n的钢条和上⾯的价格表,求切割钢条 ⽅案,使得总收益最⼤。

⻓度为4的钢条的所有切割⽅案如下:(c⽅案最优)

image-20230511094822474

思考:⻓度为n的钢条的不同切割⽅案有⼏种?

image-20230511094846655

递推式

image-20230511094921238

最优子结构

可以将求解规模为n的原问题,划分为规模更⼩的⼦问题:完成⼀次切割后,可以将产⽣的两段钢条看成两个独⽴的钢条切个问题。

组合两个⼦问题的最优解,并在所有可能的两段切割⽅案中选取组合收益最⼤的,构成原问题的最优解。

钢条切割满⾜最优⼦结构:问题的最优解由相关⼦问题的最优解组合而成,这些⼦问题可以独⽴求解。

钢条切割问题还存在更简单的递归求解⽅法

从钢条的左边切割下⻓度为i的⼀段,只对右边剩下的⼀段继续进⾏切割,左边的不再切割

递推式简化为:image-20230511095022548

不做切割的⽅案就可以描述为:左边⼀段⻓度为n,收益为pn,剩余⼀段⻓度为0,收益为r0=0。

自顶向下递归实现

时间复杂度 O(2n)

def cut_recurision(li, n):
    """
    递归方式一
    """
    if n == 0:
        return 0
    else:
        res = li[n]
        for i in range(1, n):
            res = max(res, cut_recurision(li, i) + cut_recurision(li, n - i))
        return res


def cut_recurision_2(li, n):
    """
    递归方式二
    """
    if n == 0:
        return 0
    else:
        res = 0
        for i in range(1, n + 1):
            res = max(res, li[i] + cut_recurision_2(li, n - i))
        return res


if __name__ == '__main__':
    li = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40]
    # li = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]

    print(cut_recurision(li, 15))
    print(cut_recurision_2(li, 15))

动态规划解法

递归算法由于重复求解相同⼦问题,效率极低

动态规划的思想: 每个⼦问题只求解⼀次,保存求解结果;之后需要此问题时,只需查找保存的结果

时间复杂度 O(n2)

def cut_no_recurision(li, n):
    """非递归方式(速度快)"""
    tmp_list = [0, ]
    for i in range(1, n + 1):
        res = 0
        for j in range(1, i + 1):
            res = max(res, li[j] + tmp_list[i - j])
        tmp_list.append(res)
    return tmp_list[n]

if __name__ == '__main__':
    li = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40]
    # li = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]

    print(cut_no_recurision(li, 15))

重构解

如何修改动态规划算法,使其不仅输出最优解,还输出最优切割⽅案?

对每个⼦问题,保存切割⼀次时左边切下的⻓度

image-20230511100257739

def cut_no_recurision_extend(li, n):
    r_list = [0, ]
    s_list = [0, ]
    for i in range(1, n + 1):
        r = 0  # 价格的最大值
        s = 0  # 价格最大值对应方案的左边不切割部分的长度
        for j in range(1, i + 1):
            if li[j] + r_list[i - j] > r:
                r = li[j] + r_list[i - j]
                s = j
        r_list.append(r)
        s_list.append(s)
    return r_list[n], s_list


def cut_solution(li, n):
    """输出切割方案"""
    r, s = cut_no_recurision_extend(li, n)
    print(r, s)
    res = []
    while n > 0:
        res.append(s[n])
        n -= s[n]
    return res


if __name__ == '__main__':
    li = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40]
    # li = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]

    # print(cut_no_recurision_extend(li, 15))
    print(cut_solution(li, 20))

动态规划问题关键特征

什么问题可以使⽤动态规划⽅法?

  • 最优⼦结构

    • 原问题的最优解中涉及多少个⼦问题

    • 在确定最优解使⽤哪些⼦问题时,需要考虑多少种选择

  • 重叠⼦问题

比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景。

最⻓公共子序列

⼀个序列的⼦序列是在该序列中删去若⼲元素后得到的序列。

  • 例:“ABCD”和“BDF”都是“ABCDEFG”的⼦序列(位置可以跳跃,但不能颠倒)

最⻓公共⼦序列(LCS)问题:给定两个序列X和Y,求X和Y⻓度最⼤的公共⼦ 序列。

  • 例:X="ABBCBDE" Y="DBBCDB" LCS(X,Y)="BBCD"

应⽤场景:字符串相似度⽐对;DNA相似度对比

image-20230511083300416

例如:要求出a="ABCBDAB"与b="BDCABA"的LCS:

由于最后⼀位"B"≠"A": 因此LCS(a,b)应该来源于LCS(a[:-1],b)与LCS(a,b[:-1])中更⼤的那⼀个

image-20230511083533205

"""最长公共子序列"""


def lcs_length(x, y):
    """返回最长公共子序列的长度"""
    m = len(x)
    n = len(y)
    c = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if x[i - 1] == y[j - 1]:
                # i,j位置上的字符匹配时,当前位置的数值来源于左上角的数值加1
                c[i][j] = c[i - 1][j - 1] + 1
            else:
                # i,j位置上的字符不匹配时,当前位置的数值来源于 左方或上方的最大数值
                c[i][j] = max(c[i - 1][j], c[i][j - 1])
    return c[m][n]


def lcs(x, y):
    """标出最长公共子序列的匹配箭头"""
    m = len(x)
    n = len(y)
    c = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
    d = [[0 for _ in range(n + 1)] for _ in range(m + 1)]  # 标出箭头
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if x[i - 1] == y[j - 1]:
                # 匹配用左上方箭头,数字1表示
                c[i][j] = c[i - 1][j - 1] + 1
                d[i][j] = 1
            elif c[i - 1][j] > c[i][j - 1]:
                # 来源于上方,数字2表示
                c[i][j] = c[i - 1][j]
                d[i][j] = 2
            else:
                # 来源于左边,数字3表示
                c[i][j] = c[i][j - 1]
                d[i][j] = 3
    return c[m][n], d


def lcs_back(x, y):
    """回溯,生成最长公共子序列字符串"""
    i = len(x)
    j = len(y)
    length, d = lcs(x, y)
    tmp_list = []
    while i > 0 and j > 0:
        if d[i][j] == 1:
            # 左上方的箭头(两个字符匹配时)
            tmp_list.append(x[i - 1])
            i -= 1
            j -= 1
        elif d[i][j] == 2:
            i -= 1
        else:
            j -= 1
    return "".join(reversed(tmp_list))


if __name__ == '__main__':
    x = "ABCBDAB"
    y = "BDCABA"

    print(lcs_length(x, y))
    print("".center(50, "-"))

    c, d = lcs(x, y)
    for line in d:
        print(line)
    print("".center(50, "-"))

    print(lcs_back(x, y))

欧⼏⾥得算法

最大公约数

约数:如果整数a能被整数b整除,那么a叫做b的倍数,b叫做a的约数。

给定两个整数a,b,两个数的所有公共约数中的最⼤值即为最⼤公约数(Greatest Common Divisor, GCD)。

例:12与16的最⼤公约数是4

如何计算两个数的最⼤公约数:

  • 欧⼏⾥得:辗转相除法(欧⼏⾥得算法)
  • 《九章算术》:更相减损术

最大公约数---欧几里得算法

欧⼏⾥得算法:gcd(a, b) = gcd(b, a mod b)

例:gcd(60, 21) = gcd(21, 18) = gcd(18, 3) = gcd(3, 0) = 3

"""最大公约数---欧几里得算法"""


def gcd(a, b):
    """递归求法"""
    if b == 0:
        return a
    else:
        return gcd(b, a % b)


def gcd_no_recursion(a, b):
    """非递归求法"""
    while b > 0:
        tmp = a % b
        a = b
        b = tmp
    return a


if __name__ == '__main__':
    print(gcd(12, 16))
    print(gcd_no_recursion(12, 16))

应用:实现分数计算

利⽤欧⼏⾥得算法实现⼀个分数类,⽀持分数的四则运算。

"""用欧几里得算法实现⼀个分数类,支持分数的四则运算。"""


class Fraction:
    def __init__(self, a, b):
        self.a = a
        self.b = b
        # 简化a,b,约去最小公倍数
        x = self.gcd(a, b)
        self.a /= x
        self.b /= x

    def gcd(self, a, b):
        """最大公约数"""
        while b > 0:
            tmp = a % b
            a = b
            b = tmp
        return a

    def greatest_common_divisor(self, a, b):
        """
        最小公倍数
        例子:12 16 -> 4 --> 3*4*4=48(init中已经简化了a,b)
        """
        x = self.gcd(a, b)
        return a * b / x

    def __add__(self, other):
        """加法"""
        a = self.a
        b = self.b
        c = other.a
        d = other.b
        # 分母通分
        denominator = self.greatest_common_divisor(b, d)
        # 分子
        numerator = a * denominator / b + c * denominator / d

        return Fraction(numerator, denominator)

    def __sub__(self, other):
        """减法"""
        a = self.a
        b = self.b
        c = other.a
        d = other.b
        # 分母通分
        denominator = self.greatest_common_divisor(b, d)
        # 分子
        numerator = a * denominator / b - c * denominator / d
        return Fraction(numerator, denominator)

    def __mul__(self, other):
        """乘法"""
        a = self.a
        b = self.b
        c = other.a
        d = other.b
        # 分母
        denominator = b * d
        # 分子
        numerator = a * c
        return Fraction(numerator, denominator)

    def __truediv__(self, other):
        """除法(不是整除)"""
        a = self.a
        b = self.b
        c = other.a
        d = other.b
        # 分子分母颠倒
        c, d = d, c
        # 分母
        denominator = b * d
        # 分子
        numerator = a * c
        return Fraction(numerator, denominator)

    def __str__(self):
        return "%d/%d" % (self.a, self.b)


if __name__ == '__main__':
    a = Fraction(1, 3)
    b = Fraction(1, 2)
    print(a + b)
    print(a - b)
    print(a * b)
    print(a / b)

RSA加密算法简介

密码与加密

传统密码:加密算法是秘密的

现代密码系统:加密算法是公开的,密钥是秘密的

对称加密和⾮对称加密

RSA加密算法

RSA⾮对称加密系统:

  • 公钥:⽤来加密,是公开的
  • 私钥:⽤来解密,是私有的
  • image-20230511103340832

RSA加密算法过程

1. 随机选取两个质数p和q
2. 计算n=pq
3. 选取⼀个与φ(n)互质的⼩奇数e,φ(n)=(p-1)(q-1)
4. 对模φ(n),计算e的乘法逆元d,即满⾜ (e*d) mod φ(n) = 1
5. 公钥(e, n) 私钥(d, n)

加密过程:c = (m^e) mod n
解密过程:m = (c^d) mod n
posted @ 2023-05-11 14:57  hkwJsxl  阅读(21)  评论(0编辑  收藏  举报