算法进阶
贪心算法
定义
是指在对问题求解时,总是做出当前看来是最好的选择,着眼于眼前(做出目前对自己好的:贪心),不从整体最优上考虑,做出某种意义上的局部最优解。但有时贪心算法的解就是最优解。要会判断一个问题是否用贪心算法来计算。
例题
- 找零问题:假设商店老板需要找零n元钱,钱币的面额有:100元、50元、20元、5元、1元,如何找零使得所需钱币的数量最少?
- 代码求解
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千克的东西。他应该拿走哪些商品?

-
贪心算法:找到当前的最优解:每个单位商品的价值:商品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))
拼接最大数字问题
- 题目

-
思想:比首位,首位大的先排,当首位一样,比下一位,以此类推,当都一样只是长度不同,则两数相加进行比较,如何大的数进行存放(两数已排好序)
-
代码实现***(待搞懂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)))
活动选择问题
-
题目
![image]()
-
思想:贪心算法结论:最早结束的活动一定是最优解的一部分。最早结束的先举办。结束时间与开始时间不相撞
-
代码实现
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)思想 = 递推式(问题的求解需要前一个问题求解出来) + 重复子问题(将子问题进行事先存储)
- 每个子问题只求解一次,保存求解结果
- 之后需要此问题时,只需查找保存的结果
例题引入

解法:
# 使用递归方法(出现重复计算子问题)
# 递归思想
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))
钢条切割问题
- 背景:

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

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


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

自顶向下递归算法的时间复杂度为: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))
钢条切割问题——重构解

-
思想:保存(s[i])最优方案切割后左边的长度(该长度不再进行切割),保存的长度出去,找原始长n去除掉左边长的长度(即切割后右边的长度)的最优解,以此类推,直到右边长度为0停止。
-
代码实现
# 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)问题
字串与子序列
- 字串是连续的子序列,顺序不能改变,且连续
- 子序列是可以不连续,但相对顺序不可变(空序列是任意一个序列的子序列)
问题

思考
-
若用暴力穷举法的时间复杂度为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"))
欧几里得算法

最大公约数(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加密算法流程



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





浙公网安备 33010602011771号