本文共 10809 字,大约阅读时间需要 36 分钟。
###### 背包问题# 一个小偷在某个商店发现有N个商品,第i个商品价值Vi元,重Wi千克。他希望拿走的价值尽量高,但他的背包最多只能容纳W千克的东西。他应该拿走哪些商品?
#注:背包问题 往下细分 有2种不太一样的问题:0-1背包 和 分数背包# ·0-1背包:对于一个商品,小偷要么把它完整拿走,要么留下。不能只拿走一部分,或把一个商品拿走多次。(商品为金条)# ·分数背包:对于一个商品,小偷可以拿走其中任意一部分。(商品为金砂)
### 举例# 商品 1:V1=60 W1=10 1千克 6块钱# 商品 2:V2=100 W2=20 1千克 5块钱# 商品 3:V3=120 W3=30 1千克 4块钱 降序# 背包容量:W=50# 对于0-1背包和分数背包,贪心算法是否都能得到最优解?为什么?
#注:怎么贪心?算单位重量的商品 分别值多少钱,先把单位重量的 更贵的 先拿走,然后如果包里还有地方 拿剩下的#注:这个贪心算法 对于 分数背包 一定是 最优的 ,生活常识。包一定是满的,一点都不剩
#注:对于0-1背包,贪心的来做,拿走的价值是 160 (商品1和商品2 都拿走),是最好的吗?不是最好的,比如 只拿商品1和商品2#注:显然 0-1背包,不能用贪心算法来做。因为 分数背包装的时候,最后肯定是满的,但是 0-1背包不一定,按贪心算法来拿 最后可能剩下很多容量
goods = [(60, 10), (100, 20), (120, 30)] # 每个商品元组表示 (价格,重量)goods.sort(key=lambda x: x[0] / x[1], reverse=True) # 按照单位价格降序排列def fractional_backpack(goods, w): # 参数 商品、w背包重量 # 贪心拿商品,拿单位重量更值钱的商品,所以先对goods进行排序 m = [0 for _ in range(len(goods))] # m表示每个商品拿多少,存结果的 total_v =0 # 存结果的 拿走的总价值 for i, (price,weight) in enumerate(goods): if w >= weight: m[i] = 1 # 这个商品拿多少。拿1整个,或者小数 total_v += price # 更新拿走的 价值 w -= weight # 并更新w的值 (背包重量) else: m[i] = w / weight total_v += m[i] * price # 更新拿走的 价值 w = 0 # 背包满了 break return total_v, m # 最后返回 存结果的m、total_vprint(fractional_backpack(goods, 50))#结果为# (240.0, [1, 1, 0.6666666666666666])
#注:背包问题,贪心算法,先拿单位重量里最值钱的
###### 拼接最大数字问题 (面试经常问)# 切有N个非负整数,将其按照字符串拼接的方式拼接为一个整数。 如何拼接可以使得得到的整数最大?
# 例:32,94,128,1286,6,71可以拼接的最大整数为 94716321286128#注:按照字符串比较的方式 来排序(先比首位,首位大的最大,首位一样的 再往后走……直到有一个最大,那个最大;或者直到有 一个有 一个没有,没有的最大)# (整数比较:32和6,32大;字符串比较:32和6,6大)
#注:128 和 1286 怎么排?(2个数拼接) # 1286128 1281286,1286128大 # 7286728 7287286,7287286大#注:这个问题怎么解决?贪心的去做,贪头位。但是一个串长、一个串短,并且短串还是长串的子串 (128\1286) 怎么办?
#------------------------------a = "96" # a和b长度一样时,表达式没问题b = "87"a + b if a>b else b + a#------------------------------a = "128"b = "1286"a + b = "1281286" # 长度一样b + a = "1286128" # b + a 大a + b if a+b>b+a else b+a # 这样写#---------a = "728"b = "7286"a + b = "7287286"b + a = "7286728"#------------------------------
from functools import cmp_to_key # 传一个老式的cmp函数,转换成1个key函数li = [32, 94, 128, 1286, 6, 71]# Python2的cmp函数 传2个参数的一个函数,然后根据比较函数返回它们的比较结果 (xy返回1)def xy_cmp(x, y): # 写的cmp函数 if x+y < y+x: return 1 # x>y 让大的去前面 x和y交换,因为默认小数在前面 elif x+y > y+x: return -1 else: return 0def number_join(li): li = list(map(str, li)) # 把数字变成字符串 # 接下来 对li 进行一个排序(2个值对比 做交换) 94在最前面 # li.sort(cmp=lambda x,y: ) # Python2的cmp函数 传2个参数的一个函数,然后根据比较函数返回它们的比较结果 (x y返回1) li.sort(key=cmp_to_key(xy_cmp)) # 执行后 li已经是排好序的了 return "".join(li) # 返回字符串print(number_join(li))# 结果为 94716321286128
#注:li.sort(key=cmp_to_key(xy_cmp)) 看不懂的话,可以写个快排或者冒泡,冒泡:看2个元素是否交换,看a+b>b+a 还是 a+b
###### 活动选择问题 (贪心算法)# ·假设有n个活动,这些活动要占用同一片场地,而场地在某时刻只能供一个活动使用。# ·每个活动都有一个开始时间Si和结束时间fi(题目中时间以整数表示),表示活动在[Si, Fi)区间占用场地。 #注:Fi这一刻 它不占用,避免问题# ·问:安排哪些活动能够使该场地举办的活动的个数最多?
#注:11个活动,# Si 开始时间,# fi 结束时间,# 怎么安排?贪心的问题
### 贪心结论:最先结束的活动一定是最优解的一部分。 贪最早的点,接下来在剩下的 贪 最早的点……#注:第一个结束的活动,一定在最优解里面#注:最早结束,后面剩的时间越长
### 证明:假设a是所有活动中最先结束的活动,b是最优解中最先结束的活动。# 如果a=b,结论成⽴。# 如果a≠b,则b的结束时间⼀定晚于a的结束时间,则此时用a替换掉最优解中的b,a⼀定不与最优解中的其他活动时间重叠,因此替换后的解也是最优解。#注:为什么 a⼀定不与最优解中的其他活动时间重叠? 因为a比b结束的早 那些活动都在b后面,所以不重叠
activities = [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14), (12,16)] # 活动,1个元组表示1个活动.开始时间、结束时间# 保证活动是按照结束时间排好序的。 因为 贪心 是贪 最早结束的活动,排好序后 直接选第一个activities.sort(key=lambda x:x[1]) # 按结束时间排序def activity_selection(a): # 传参a 活动 res = [a[0]] # 最后返回的结果,一开始肯定有a[0]活动,a[0]是最早结束的 #接下来 按照结束时间往后看,是否与前面时间冲突,不冲突 加进去 for i in range(1,len(a)): # 从1 开始,因为a[0] 已经进去了 # 如果a[i]活动的开始时间>=res最后一个活动的结束时间 if a[i][0] >= res[-1][1]: # 当前活动的开始时间大于等于最后一个入选活动的结束时间 # 不冲突 res.append(a[i]) return resprint(activity_selection(activities))#结果为 [(1, 4), (5, 7), (8, 11), (12, 16)] 4个活动,一定是最大的
活动选择问题 -- 精简代码
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])def activity_selection(a): res = [a[0]] for i in range(1, len(a)): if a[i][0] >= res[-1][1]: # 当前活动的开始时间小于等于最后一个入选活动的结束时间 # 不冲突 res.append(a[i]) return resprint(activity_selection(activities))#结果为 [(1, 4), (5, 7), (8, 11), (12, 16)]
#贪心算法4个例子:找零问题、背包问题、拼接数字、活动选择#注:知道按什么来贪心
#注:4个问题 公共特点:# 首选 都是 最优化问题 (什么什么什么 最多、最少、最大、最小)# 不是所有最优化问题,都能用贪心算法来做, 比如背包问题中的0-1背包,贪心 不是最优的,这类问题 可以用动态规划做
#注:贪心算法 代码比较好写,思路比较简单,速度比较快#注:比如活动选择问题,不用贪心,穷举所有情况,需要穷举很多情况 ,n个活动 要看2**n 种方案。但是贪心算法 代码复杂度 O(n)级别,很快
#注:动态规划 是种思想,在各个算法领域都有深入研究:基因测序、基因比对、序列的相似程度、hmm……,很多问题 都可以用动态规划解决
###### 从斐波那契数列看动态规划# ·斐波那契数列:Fn = Fn-1 + Fn-2 #注:递推式# ·练习:使用递归和非递归的方法来求解斐波那契数列的第n项#注:斐波拉契数列:后一项是前2项的和
#注:递归写法# 子问题的重复计算
def fibnacci(n): if n == 1 or n == 2: # 终止条件 return 1 else: return fibnacci(n-1) + fibnacci(n-2) # 递归条件print(fibnacci(10))#结果为 第5项斐波拉契数列# 55
#注:第100项斐波拉契数列 用递归写法 时间很长很长。数不大,但是计算机很慢,为什么?#原因:子问题的重复计算,递归执行效率低# f(6) = f(5) + f(4)# f(5) = f(4) + f(3)# f(4) = f(3) + f2)# f(4) = f(3) + f(2)# ………… #注:相同的问题算了很多遍# f(3) = f(2) + f(1)# f(3) = f(2) + f(1)# f(2) = 1
非递归方法, 动态规划(DP)的思想 = 最优子结构(递推式) + 重复子问题
def fibnacci_no_recurision(n): f = [0,1,1] # 下标是1的第一项是1,下标是2的第二项是1 if n > 2: for i in range(n-2): # 求第n项,往里追加,追加n-2次 (求n=3,追加1次;求n=4,追加2次) num = f[-1] + f[-2] # 算这个数,再append到f里去。这个数 = f最后一项 + f倒数第2项 f.append(num) return f[n] # 因为按照下标来的 所以第n项就是f[n]print(fibnacci_no_recurision(100))#结果为 354224848179261915075#注:虽然这个数很大,但是运算很快
#注:为什么递归方法很慢?#注:因为递归执行效率低。为什么递归执行效率低?#注:子问题的重复计算
#注:为什么 不用递归 算的比递归快#注:因为把之算过的问题 存到了列表里,只需要调用就行了 加一次。#注:因为 不递归的 写法里 ,避免 子问题的重复计算,所以效率比 递归写法 高一点#注:不是说 所有的递归 慢问题 都是因为 子问题的重复计算
#注:DP思想 = 最优子结构(递推式) + 重复子问题#注:不想用递归来做,所以用循环方式,把需要的子问题 存起来,只算一遍,后边的值从列表里取这个值#Python里 取巧方法,递归函数前加装饰器 @lru_cache,就能自动缓存重复子问题的@functools.lru_cache
精简代码
# 子问题的重复计算 #注:递归写法def fibnacci(n): if n == 1 or n == 2: return 1 else: return fibnacci(n-1) + fibnacci(n-2)# 动态规划(DP)的思想 = 递推式 + 重复子问题def fibnacci_no_recurision(n): #注:DP思想 f = [0,1,1] if n > 2: for i in range(n-2): num = f[-1] + f[-2] f.append(num) return f[n]print(fibnacci_no_recurision(100))#结果为# 354224848179261915075
###### 钢条切割问题# ·某公司出售钢条,出售价格与钢条长度之间的关系如下表:
#注:一般来说,长的钢条贵点,而且和长度不是倍数关系
# ·问题:现有一段长度为n的钢条和上面的价格表,求切割钢条方案,使得总收益最大。#注:即 卖出去的钱最多# ·长度为4的钢条的所有切割方案如下:(c方案最优)
# ·思考:长度为n的钢条的不同切割方案有几种?
#注:2**(n-1) (2的(n-1)次方)种方案。长度为n的钢条,有n-1个切割的位置,可以切可以不切,每个位置2种选择,所以2**(n-1)种可能#注:如果相同的结果位置不同 看成同一种方案,那么问题很难,在组合数学里叫做整数切割问题#注:一个一个列出来 不太合理
#注:r[i] 最优解,最优解,最优的情况下 能拿到多少钱
#注:长度为2时 5、1+1#注:长度为3 8、6 不切 8块;切1刀变成2和1或1和2 但不考虑 2不管切不切 最多能卖5块钱,所以6块钱#注:长度为4 9、10 不切 9块;切1刀 3和1 不管3切不切这个问题 给个3价格是8,所以9块;切成2和2 ,2价格是5,所以 10# ………………#注:长度为9 不切 24块;切1刀,1和8,1+22=23 不用管8切不切;切2刀,2和7,5+18=23 不用管7切不切;3和6,17+8=25;4和5,23;5和4………… #注:所以最后 3和6 最大,17+8=25,所以 9 最大值是25#注:动态规划 需要 1个递推式 (即 最优子结构)
###### 钢条切割问题 -- 递椎式# ·设⻓度为n的钢条切割后最优收益值为rn,可以得出递推式:
# ·第⼀个参数Pn表示不切割# ·其他n-1个参数分别表示另外n-1种不同切割方案,对方案i=1,2,...,n-1# ·将钢条切割为⻓度为i和n-i两段# ·⽅案i的收益为切割两段的最优收益之和#注:比如说长度为9。9 、1+8 、2+7 、……、8+1 ,这些所有情况 要一个最大值 max# ·考察所有的i,选择其中收益最大的方案#注:切一刀 (n-1)种方案 和 不切,每一个方案的 收益 是多少。n-1种方案的收益 和不切割的 收益 做对比,选最大值 就是结果
#注:为什么 这是对的? 最优子结构
###### 钢条切割冋题 -- 最优子结构# 可以将求解规模为n的原问题,划分为规模更小的子问题:完成⼀次切割后,可以将产生的两段钢条看成两个独⽴的钢条切割问题。# 组合两个子问题的最优解,并在所有可能的两段切割⽅案中选取组合收益最⼤的,构成原问题的最优解。# 钢条切割满足最优子结构:问题的最优解由相关⼦问题的最优解组合而成,这些子问题可以独⽴求解。#注:最优子结构:子问题的最优解 能够算大的问题的最优解#注:切1刀,只要算 2部分的最优解,不管这2部分 怎么切的,所有方案中,取最大值max,就是最优解#注:最优子结构 就是一个 递推式,DP中 最重要
###### 钢条切割问题 -- 最优子结构# 钢条切割问题还存在更简单的⽅法# 从钢条的左边切割下⻓度为i的⼀段,只对右边剩下的⼀段继续进⾏切割,左边的不再切割# 递推式简化为
# 不做切割的⽅案就可以描述为:左边⼀段⻓度为n,收益为Pn,剩余⼀段⻓度为0,收益为r0=0。#注:切1刀,但是左边不切了,只切右边。把右边r n-1 换成P n-1#注:即 如果长度为9,9不切 24; 1+8 1不切的价格 + 8最优解22; 2+7 2不切的价格 + 7最优解17……#注:左边不切,右边继续切 即左边原价,右边选择最优解#注:为什么可以这样算?为什么这样做最简单? (即p + r)
#注:假如说 9 ,切成2 3 2 2 价格最大。#注:在原来的 递推式里 可以看成 5 + 4 价格最大#注:左边 5 再切成 2和3,右边 4 再切成 2和2#注:但是 这个问题 也可以看成 2 3 2 2 ,切成 2 和 7,在 7 里面再接着切#注:所以 递推式 看成一边切、一边不切 相当于看成 一边是左边一段,右边 可以接着切,没有漏的情况#注:而 之前的 递推式 可能会算重复行为。如果 9 切成 2 3 2 2 最优,那么 看成 5 + 4 最优,2 + 7 最优,重复了#注:第2个递推式,看成2 部分 ,只对 右边的 进行切割,左边不切
写法1 递推式
写法2 递推式
import timedef cal_time(func): def wrapper(*args, **kwargs): t1 = time.time() result = func(*args, **kwargs) t2 = time.time() print("%s running time: %s secs." % (func.__name__, t2 - t1)) return result return wrapper# 写法1# 价格表 长度1 卖1块钱;长度2 卖5块钱p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]def cut_rod_recurision_1(p, n): # 参数n 钢条长度 ; p 价格表 if n == 0: # 递归 终止项 (长度0 卖 0块钱) return 0 else: # 接下来 学会看公式 res = p[n] for i in range(1, n): # 从1 到 n-1 的 n-1 种情况 res = max(res, cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n-i)) # 函数是求r n 的,递归 # 每次循环 都是res自己跟另一个取最大,取完了n-1次方案,res就是所有方案中最大的那个数 return res#注:没有问题 因为每次循环的时候 res已经取了最大值print(cut_rod_recurision_1(p, 9))#结果为 25#注:第一种写法 肯定很慢:重复计算 ,而且重复计算两# 写法2 还是递归来写,左边不切割 右边切割的式子def cut_rod_recurision_2(p, n): if n == 0: # 终止条件,= 0时返回0元 return 0 else: res = 0 for i in range(1, n+1): # 递推式下标告诉了范围 从1到n res = max(res,p[i] + cut_rod_recurision_2(p, n-i)) #p[i] 代表左边 不切割的部分,r n-i 这个函数就是求r的 # for循环完了 res就是最大值 return resprint(cut_rod_recurision_2(p, 9))#结果为 25
#注:给递归函数 加 装饰器 ,会递归的装饰#注:解决方法:套一层马甲 return 原函数
#注:方法1比方法2慢,因为方法1递归了2次,而且n需要算 比n小的所有子问题,子问题都会重复计算。这2个 算法 实际上复杂度很高,慢#注:方法2 只算1个,还是会 重复子问题,最后问题会越来越细#注:递归算法的问题 自顶向下的递归实现,会出现效率差的问题
###### 钢条切割问题 -- 自顶向下递归实现# 为何⾃顶向下递归实现的效率会这么差?# 时间复杂度 O(2**n)
#注:那怎么办?答:动态规划的解法
###### 钢条切割问题 -- 动态规划解法# 递归算法由于重复求解相同子问题,效率极低# 动态规划的思想:# 每个子问题只求解一次,保存求解结果段之后需要此问题时,只需查找保存的结果#注:动态规划需要2点:1、最优子结构(递推式) 2、重复子问题#注:自底向上的算 ,就不会重复求解;而 递归是 直接来r(n)来算,不好。# 算r(1),算完存到列表里不需要重复算,算r(2)……#注:这就是 不用递归,用 循环、迭代、动态规划的思想 解决问题
递归写法 精简代码
写法1 递推式
写法2 递推式
def cut_rod_recurision_1(p, n): if n == 0: return 0 else: res = p[n] for i in range(1, n): res = max(res, cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n-i)) return resdef cut_rod_recurision_2(p, n): if n == 0: return 0 else: res = 0 for i in range(1, n+1): res = max(res, p[i] + cut_rod_recurision_2(p, n-i)) return res
转载地址:http://xctuk.baihongyu.com/