算法进阶

贪心算法

定义

是指在对问题求解时,总是做出当前看来是最好的选择,着眼于眼前(做出目前对自己好的:贪心),不从整体最优上考虑,做出某种意义上的局部最优解。但有时贪心算法的解就是最优解。要会判断一个问题是否用贪心算法来计算。

例题

  1. 找零问题:假设商店老板需要找零n元钱,钱币的面额有:100元、50元、20元、5元、1元,如何找零使得所需钱币的数量最少?
  2. 代码求解
li = [i for i in map(int,input().split())]
money = int(input())
def pay_money(li,money):
    m = [0 for _ in range(len(li))]
    li = sorted(li,reverse=True)
    for index,val in enumerate(li):
        m[index] = money//val
        money = money % val
    return m,money
print(pay_money(li,money))

背包问题

0-1背包

有时需要判断0-1背包是否能用贪心算法来做:有时局部的最优解没能装满背包,且导致不是整体最优解

分数背包

对于分数背包而言,使用贪心算法来做一定是整体最优解:背包一定是满的,一点都不剩

举例

一个小偷在某个商店发现有n个商品,第i个商品价值vi元,重wi千克。他希望拿走的价值尽量高,但他的背包最多只能容纳W千克的东西。他应该拿走哪些商品?

image

  • 贪心算法:找到当前的最优解:每个单位商品的价值:商品1、2、3的价值分别为6,5,4

  • 0-1背包对于一个商品,小偷要么把它完整拿走,要么留下。不能只拿走一部分,或把一个商品拿走多次。(商品为金条)

    因此,若用贪心算法求解,0-1背包只能拿贪心算法中价值最大的前两个,即商品1和商品2,共计160元,30千克,背包容量为50,50-30<30,不能再拿商品3。而不使用贪心算法,拿商品2和商品3,共计220元,50千克,该决策优于贪心算法决策。

  • 分数背包对于一个商品,小偷可以拿走其中任意一部分。(商品为金沙)

    无论是否使用贪心算法,都能使得决策为最优解(该例子分数背包在使用贪心算法下,可拿10千克商品1,20千克商品2和20千克商品3,共计240元)

    # 分数背包问题贪心算法求解
    goods = [(60,10),(100,20),(120,30)]     # 每个商品对应的价值即重量
    W = 50                                  # 背包容量
    goods.sort(key=lambda x: x[0] / x[1], reverse=True)     # 贪心算法思想(对当前每个商品的价值进行排序,从大到小排序)
    def fractional_package(goods,W):
        m = [0 for _ in range(len(goods))]          # 用于存放对应每个商品(从大价值到小价值)拿走的数量
        total_price = 0                 # 用于存放拿走的价值总数
        for index,(price, weight) in enumerate(goods):
            if W >= weight:             # 如果当前背包容量大于或等于大价值的重量时,可都拿走
                m[index] = 1            # 1代表都拿走
                total_price += price
                W -= weight             # 背包总量要改变
            else:                       # 当前背包容量小于大价值的重量时,拿走部分
                m[index] = W/weight     # 表示拿走商品总重量的几分之几
                total_price += m[index] * price
                W = 0                   # 当前无背包空间
                break
        return total_price,m
    print(fractional_package(goods,W))
    

拼接最大数字问题

  1. 题目

image

  1. 思想:比首位,首位大的先排,当首位一样,比下一位,以此类推,当都一样只是长度不同,则两数相加进行比较,如何大的数进行存放(两数已排好序)

  2. 代码实现***(待搞懂cmp_to_key用法)

li = [32,94,128,1286,6,71]
from functools import cmp_to_key
def num_cmp(x,y):           # 自定义排序规则
  if x + y > y + x:
      return 1
  elif x + y < y + x:
      return -1           # 当返回一个负数时,改变原本的排序顺序(由后往前进行比较,插入)
  else:
      return 0
def num_join(li):
  li = list(map(str,li))
  li.sort(key=cmp_to_key(num_cmp),reverse=True)
  return li
print("".join(num_join(li)))

活动选择问题

  1. 题目
    image

  2. 思想:贪心算法结论:最早结束的活动一定是最优解的一部分。最早结束的先举办。结束时间与开始时间不相撞

  3. 代码实现

activities = [(1,4),(3,5),(0,6),(5,7),(3,9),(5,9),(6,10),(8,11),(8,12),(2,14),(12,16)]
def activity_choose(li):
    li.sort(key=lambda x:x[1])      # 以结束时间为比较对象,按最先结束的先安排为原则进行活动安排
    activities_final_choose = [li[0]]   # 最先结束一定存在在活动的最先选择中
    for i in range(1,len(li)):
        if li[i][0] >= activities_final_choose[-1][1]:   # 判断活动开始时间是否大于已排好的最后一个活动的结束时间
            activities_final_choose.append(li[i])
    return activities_final_choose
print(activity_choose(activities))

贪心算法总结

需要get到该问题是贪心的,知道贪啥,按啥来贪

上述问题采用的贪心算法的共同点:

  • 都是最优化问题(最多/大,最少/小)

动态规划

思想

动态规划(DP)思想 = 递推式(问题的求解需要前一个问题求解出来) + 重复子问题(将子问题进行事先存储)

  1. 每个子问题只求解一次,保存求解结果
  2. 之后需要此问题时,只需查找保存的结果

例题引入

image

解法:

# 使用递归方法(出现重复计算子问题)
# 递归思想
def fib(n):
    if n == 1 or n == 2:
        return 1
    else:
        return fib(n-1) + fib(n-2)
print(fib(6))

# 非递归思想,动态规划(DP)思想 = 递推式(问题的求解需要前一个问题求解出来) + 重复子问题(将子问题进行事先存储)
def fib_no_rec(n):
    li = [0,1,1]    # 按照以n为下标进行存放在数组中
    if n > 2:
        for i in range(n-2):    # 重复循环添加n-2次
            num = li[-1] + li[-2]       # 取数组后两项相加
            li.append(num)
    return li[n]       # 返回求的第n项
print(fib(6))

钢条切割问题

  1. 背景:

image

  1. 分析:r[i]为最优解
    image

递推式

rn = max(pn,r1+rn-1,r2+rn-2,···,rn-1+r1)(动态规划思想中的递推式,rn为长度为n的钢条的最优解)
image

最优子结构(小的问题的最优解可以构成大的问题的最优解)

image

image

简化方法的理解:例如n = 9:9可以分为2、3、2、2,也可以分为5、4还可以分为7、2,而4的最优解是分为2和2,对应7、2中的2;而7可以分成2、3、2,因此我们可以切割后的左边不继续切割,对切割后的右边进行切割(即左边为原始长度的价值,找到右边的最优价值)

image

自顶向下递归算法的时间复杂度为:O(2^n)较慢:递归存在计算重复问题。

钢条切割问题——自底向上动态规划(循环/迭代)解法

时间效率为:O(n^2)

p = [0,1,5,8,9,10,17,17,20,24,30]
def cut_rod_dp(p, n):   #动态规划:找到递归式(该问题的递推式为简化的递推式);存放前个问题的结果
    r = [0]
    for i in range(1,n+1):  # 长度为n的钢条的最优解是在前n-1钢条最优解的情况下求出的
        rec = 0             # 定义一个初始比较数,一步步找到最大(最优)解
        for j in range(1,i+1):      # 对每个长度可能的情况进行考虑
            rec = max(rec,p[j] + r[i - j])      # 找到每个长度的最优解
        r.append(rec)           # 将0-n的每个长度的最优解进行存放
    return r[n]
print(cut_rod_dp(p,10))

钢条切割问题——重构解

image

  1. 思想:保存(s[i])最优方案切割后左边的长度(该长度不再进行切割),保存的长度出去,找原始长n去除掉左边长的长度(即切割后右边的长度)的最优解,以此类推,直到右边长度为0停止。

  2. 代码实现

# 1.存储每个部分长度最优解切割的左半部分不再进行切割的长度
def cut_node_extend(p,n):
    r = [0]     # 用于存放最优解的价格
    s = [0]     # 用于存放切割最优解的左半部分
    for i in range(1,n+1):
        rec_r = 0     # 储存目前价格比较高的值
        rec_s = 0       # 储存目前最优解的左半部分
        for j in range(1,i+1):  # 循环1-n每个长度的每种情况
            if p[j] + r[i - j] > rec_r:
                rec_r = p[j] + r[i - j]     # 找到最优解的价格
                rec_s = j                   # 找到最优解的左半部分
        r.append(rec_r)
        s.append(rec_s)
    return r[n],s
# 2.找回n的切割方法
def cut_rod_solution(p,n):
    rec,s = cut_node_extend(p,n)    # 存放总价值以及切割列表方法
    solution = []           # 用于存放长度为n的钢条的切割方法
    while s[n] > 0:         # 当切割后的左半部分值为0时,说明已切割完毕
        solution.append(s[n])   # 存放不再切割的部分,依次将所有不能再切割的长度进行存放
        n -= s[n]           # 依次找到切割的右半部分
    return rec,solution
price,solution = cut_rod_solution(p,9)
print(price)
print(solution)

最长公共子序列(LCS)问题

字串与子序列

  • 字串是连续的子序列,顺序不能改变,且连续
  • 子序列是可以不连续,但相对顺序不可变(空序列是任意一个序列的子序列)

问题

image

思考

  • 若用暴力穷举法的时间复杂度为O(2^(m+n)):假设m与n是各字符串的长度

  • 是否可用动态规划来做?(是否具有最优子结构性质)

    1、首先它是一个最优化问题(最长)

    image

    2、该递推式为上图的c[i,j]

    原理:image

    ​ 3、代码实现该问题的动态规划算法:

    def LCS(x,y):
        m = len(x)          # m为总行数
      n = len(y)          # n为1行的总元素个数
        c = [[0 for _ in range(n+1)]for _ in range(m+1)]    # c用于存储每个字长对应的公共最长子序列
        d = [[0 for _ in range(n+1)]for _ in range(m+1)]    # d用于存储每个字长对应的公共最长子序列的来源(1表示左上,2表示左边,3表示右边)
        for i in range(1,m+1):
            for j in range(1,n+1):
                if x[i-1] == y[j-1]:
                    c[i][j] = c[i-1][j-1] + 1
                    d[i][j] = "1"       # "1"表示从左上来
                elif c[i-1][j] > c[i][j-1]:
                    c[i][j] = c[i-1][j]
                    d[i][j] = "2"       # "2"表示从上面来
                else:
                    c[i][j] = c[i][j-1]
                    d[i][j] = "3"       # "3"表示从左边来
        return  c[m][n],d
    def lcs_trackback(x,y):
        c,d = LCS(x,y)      # c表示两字符串的最长公共子序列长度,d表示存储
        li = []             # 用来存储两个序列的公共子序列(回溯)
        m = len(x)  # m为总行数
        n = len(y)  # n为1行的总元素个数
        while m>0 and n>0:      # 当有一个为0时,说明,已达到二维列表的最顶端(m表示行数,n表示列数)
            if d[m][n] == "1":  # 当为1时,表示当前两字符相等
                li.append(x[m-1])     # 存放任意一个字符串的相等字符
                m -= 1
                n -= 1              # 分别对数组下标进行更改
            elif d[m][n] == "2":    # 当为2时,说明当前下标元素值是由上边元素来的
                m -= 1
            else:                   # 当为3时,说明当前下标元素值是由左边元素来的
                n -= 1
        return c,"".join(reversed(li))
    print(lcs_trackback("ABCBDAB","BDCABA"))
    

欧几里得算法

image

最大公约数(GCD)问题

约数与倍数

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

最大公约数的理解

给定两个整数a,b,两个数的所有公共约数中的最大值即为最大公约数,例:12与16的最大公约数为4

计算方法

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

代码实现

from tim import cal_time
# 递归算法
def gcd(a, b):
    if b == 0:
        return a
    else:
        return gcd(b, a % b)
@cal_time
def c(a, b):
    return gcd(a, b)
# 非递归算法
@cal_time
def gcd1(a, b):
    while b > 0:
        a, b = b, a % b
    return a
print(c(60,21))
print(gcd1(60,21))
# 时间效率差不多,递归只调用了一次

最小公倍数(MCM)问题

求出两数的最大公约数a,再两数分别除以最大公约数a得出b,c结果,最小公倍数等于abc

eg:两数分别为12,16,两数最大公约数为4,a = 4,b = 12/4 = 3,c = 16/4 = 4,最小公倍数为abc = 48

# 求最大公约数
def MCD(a,b):
    while b > 0:
        a, b = b, a % b
    return a
def MCM(a,b):
    x = MCD(a,b)
    return a * b / x
print(MCM(12,16))

RSA 加密算法(非对称加密)(用到了扩展欧几里得算法)

密码与加密

  • 传统密码:加密算法是秘密的(凯撒密码:每个字母往后移三位)
  • 现代密码系统:加密算法是公开的,密钥(随机/指定生成)是秘密的
    • 对称加密:只需一个密钥,该密钥既是加密密钥,也是解密密钥
    • 非对称加密:需要两个密钥,一个密钥用来加密,一个密钥用来解密,两个密钥不相等

RSA加密算法流程

image

image

image

目前RSA破解不了的原因:两个质数很难选取,大数拆分很难(公钥里的n很难拆分为p和q)

posted @ 2023-11-05 17:37  byyya  阅读(46)  评论(0)    收藏  举报