算法进阶
贪心算法
贪⼼算法(⼜称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪⼼算法并不保证会得到最优解,但是在某些问题上贪⼼算法的解就是最优解。要会判断⼀个问题能否⽤贪⼼算法来计算。
贪心算法没有固定的框架,算法设计的关键是贪婪策略的选择。贪心策略要无后向性,也就是说某状态以后的过程不会影响以前的状态,只与当前状态有关。
找零问题
假设商店⽼板需要找零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)区间占⽤场地。
问:安排哪些活动能够使该场地举办的活动的个数最多?
贪⼼结论:最先结束的活动⼀定是最优解的⼀部分。(思路就是每次都找最先结束的活动)
证明:假设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())
钢条切割问题
某公司出售钢条,出售价格与钢条⻓度之间的关系如下表:
问题:现有⼀段⻓度为n的钢条和上⾯的价格表,求切割钢条 ⽅案,使得总收益最⼤。
⻓度为4的钢条的所有切割⽅案如下:(c⽅案最优)
思考:⻓度为n的钢条的不同切割⽅案有⼏种?
递推式
最优子结构
可以将求解规模为n的原问题,划分为规模更⼩的⼦问题:完成⼀次切割后,可以将产⽣的两段钢条看成两个独⽴的钢条切个问题。
组合两个⼦问题的最优解,并在所有可能的两段切割⽅案中选取组合收益最⼤的,构成原问题的最优解。
钢条切割满⾜最优⼦结构:问题的最优解由相关⼦问题的最优解组合而成,这些⼦问题可以独⽴求解。
钢条切割问题还存在更简单的递归求解⽅法
从钢条的左边切割下⻓度为i的⼀段,只对右边剩下的⼀段继续进⾏切割,左边的不再切割
递推式简化为:
不做切割的⽅案就可以描述为:左边⼀段⻓度为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))
重构解
如何修改动态规划算法,使其不仅输出最优解,还输出最优切割⽅案?
对每个⼦问题,保存切割⼀次时左边切下的⻓度
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相似度对比
例如:要求出a="ABCBDAB"与b="BDCABA"的LCS:
由于最后⼀位"B"≠"A": 因此LCS(a,b)应该来源于LCS(a[:-1],b)与LCS(a,b[:-1])中更⼤的那⼀个
"""最长公共子序列"""
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⾮对称加密系统:
- 公钥:⽤来加密,是公开的
- 私钥:⽤来解密,是私有的
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