数据结构和算法是计算机技术的基本功之一,北京大学的课程深入浅出,使用Python作为载体简化了编程难度。最近浏览了31-44,主要内容是递归算法介绍,动态规划算法介绍,并且以汉诺塔、谢宾斯基三角形、找零优化、0-1背包问题等经典问题讲解了算法的应用。
概括讲,递归算法适用于自相似的问题,动态规划算法要求问题最优解可以由子问题最优解发展得到,很多时候可以相互转化。
递归-将大规模问题分解为小规模问题,在算法流程中调用自身
def listsum(numlist):
if len(numlist)==1:
return numlist[0]
else:
return numlist[0]+listsum(numlist[1:])
print(listsum([1,2,3,4]))
基本结束条件必须有,递归要能减少问题规模,调用自身-递归三定律
1 任意进制转换
基本结束条件:小于n进制的整数,进制基求余数,整数除大化小
def toStr(n,base):
convertString="0123456789ABCDEF"
if n<base:
return convertString[n]
else:
return toStr(n//base,base)+convertString[n%base]
print(toStr(6,2))
递归调用的实现:当一个函数被调用的时候,系统会把调用时的现场数据压入到系统调用栈
压入栈的【现场数据】称为【栈帧】。当函数返回时,从栈顶取得返回地址,恢复现场,弹出
栈帧,按地址返回。调用顺序和返回顺序相反。
Recursion Error:可能是忘设结束条件,或者溢出系统调用栈容量
import sys
print(sys.getrecursionlimit())
sys.setrecursionlimit(2000)
sys.getrecursionlimit()
#|递归可视化,分形树
#turtle模块,海龟作图系统Python内置
import turtle
t=turtle.Turtle()
"""
#draw a five-angles star
t.pencolor('blue')
t.pensize(8)
for i in range(5):
t.forward(100)
t.right(144)
t.hideturtle()#hide the arow
turtle.done()
Fractal分形,几何形状的局部和整体自相似
自然现象中的分形特征-二叉树
2 绘制分形树
def tree(branch_len):
if branch_len>1:
t.forward(branch_len)
t.right(20)
tree(branch_len-10)
t.left(40)
tree(branch_len-10)
t.right(20)
t.backward(branch_len)
t=turtle.Turtle()
t.left(90)
t.penup()
t.backward(100)
t.pendown()
t.pencolor('grey')
t.pensize(4)
tree(100)
turtle.done()
Sierpinski Triangle谢尔宾斯基三角形
每一次迭代边长减半,degree=0的时候就是一个等边三角形
def sierpinski(degree,points):
colormap=['blue','red','green','white','yellow','orange']
drawTriangle(points,colormap[degree])
if degree>0:
sierpinski(degree-1,{'left':points['left'],
'top':getMid(points['left'],points['top']),
'right':getMid(points['left'],points['right'])})
sierpinski(degree-1,{'top':points['top'],
'left':getMid(points['left'],points['top']),
'right':getMid(points['top'],points['right'])})
sierpinski(degree-1,{'right':points['right'],
'top':getMid(points['right'],points['top']),
'left':getMid(points['left'],points['right'])})
def drawTriangle(points,color):
t.fillcolor(color)
t.penup()
t.goto(points['top'])
t.pendown()
t.begin_fill()
t.goto(points['left'])
t.goto(points['right'])
t.goto(points['top'])
t.end_fill
def getMid(p1,p2):
return ((p2[0]+p1[0])/2,(p2[1]+p1[1])/2)
points={'left':(-200,-100),'top':(0,200),'right':(200,-100)}
#sierpinski(5,points)
#turtle.done()
3 汉诺塔
N个盘子一堆要从1号柱移到3号柱,一次只能移动1层,且上层不能低于下层
将问题拆解成第n个盘子与前n-1个盘子的移动,实现问题的缩小和函数自调用
def mveTower(height,fromPole,withPole,toPole):
if height>=1:
mveTower(height-1,fromPole,toPole,withPole)
moveDisk(height,fromPole,toPole)
mveTower(height-1,withPole,fromPole,toPole)
def moveDisk(disk,fromPole,toPole):
print(f"Moving disk[{disk}] from {fromPole} to {toPole}.")
mveTower(2,"#1","#2","#3")
4 海龟探索迷宫
迷宫数据结构,字符列表的列表,+墙壁, 通道,s起点
定义递归算法:海龟从原位置向北移动一步,以新位置递归调用探索迷宫寻找出口,如果这样找
不到出口,那么将海龟从原位置向南移动一步,以新位置递归调用探索迷宫
南也找不到,则回到原位置,向西移动一步,递归继续,同理向东。
无限递归问题:向北一步之后再向北是墙,则回到原点再向北,进入无限循环
面包屑机制:留下记录,一旦走回头路则换下一个方向调用递归。
结束条件:墙壁-返回失败,面包屑-返回失败,四个方向都失败-没有出路-失败,出口-成功
核心探索函数-“好文共欣赏,疑义相与析。”
def searchFrom(maze,startRow,StartColumn):
maze.updatePosition(startRow,startColumn)
if maze[startRow][StartColumn]== OBSTACLE:
return False
if maze[startRow][StartColumn]==TRIED or maze[startRow][StartColumn]==DEAD_END:
return False
#结束条件-出口-返回True
if maze.isExit(startRow,StartColumn):
maze.updatePosition(startRow,StartColumn,PART_OF_PATH)
return True
maze.updatePosition(startRow,StartColumn,TRIED)
#短路机制,or会先检查前面的路径,Fasle之后才会尝试后面的Alternatives
found=searchFrom(maze,searchFrom-1,StartColumn) or \
searchFrom(maze,startRow+1,StartColumn) or \
searchFrom(maze,startRow,StartColumn+1) or \
searchFrom(maze,startRow,StartColumn-1)
if found:#根据True返回值更新路径上节点的状态
maze.updatePosition(startRow,StartColumn,PART_OF_PATH)
else:
maze.updatePosition(startRow,StartColumn,DEAD_END)
return found#返回True/False,True则继续作路线图,False则根据短路代码调用其他方向的探索函数
5 分治策略
分而治之,将一个大问题分解成若干个更小规模的部分,并将结果汇总得到原问题的解
分治策略的应用相当广泛,排序、查找、遍历、求值
6 优化问题和贪心算法
优化问题示例: 找零个数最少化问题
贪心策略,最大化大面值代币数量,类推。
递归解法-一定能找到最优解的方法
确定基本结束条件:面值等于某种硬币
确定如何减少-减去各种面值,求兑换硬币最少数量-选择最小的一个
def recMC(coinValueList, change):
minCoins=change
if change in coinValueList:
return 1#终止条件
else:
for i in [c for c in coinValueList if c<=change]:
numCoins=1+recMC(coinValueList,change-i)#问题缩小
if numCoins<minCoins:#求最小值
minCoins=numCoins
return minCoins
递归的问题是低效,需要重复计算
改进-消除重复计算,使用一个表将中间结果保存起来,在计算之前查表
这个算法的中间结果就是部分找零的最优解,如果计算过,则直接调用
def recDC(coinValueList,change,knownResults):
minCoins=change
if change in coinValueList:
knownResults[change]=1
return 1#终止条件
elif knownResults[change]>0:
return knownResults[change]
else:
for i in [c for c in coinValueList if c<=change]:
numCoins=1+recMC(coinValueList,change-i)#问题缩小
if numCoins<minCoins:#求最小值
minCoins=numCoins
knownResults[change]=minCoins#每一级余额都存在这一级余额的最优解
return minCoins
print(recDC([1,5,10,25],63,[0]*63))#运算量减少30万倍
#中间记录称作记忆化,函数值缓存,Memoization提高了递归解法的性能
7 找零问题的动态规划解法
动态规划算法更有条理,从1分钱找最优解开始,使得每一步都是最优解
依赖于更少钱数最优解的简单计算,这就有了【最优化问题能够用动态规划解决】的前提:问题最优解包含子问题最优解
#numCoins=min(1+numCoins(originalamount-coini))
def dpMakeChange(coinValueList,change,minCoins,coinsUsed):
for cent in range(1,change+1):#输入金额格式转换为最小单位
coinCounts=cent
newCoin=1#记录值:初始化
#从1分代币开始,逐个检查更多代币的最优组合
for j in [c for c in coinValueList if c<= cent]:
if minCoins[cent-j]+1<coinCounts:
coinCounts=minCoins[cent-j]+1
newCoin=j
minCoins[cent]=coinCounts#建立一个最优解查询表
coinsUsed[cents]=newCoin#记录对应某个特定余额,最后一个加入的币值
return minCoins[change]
print(recDC([1,5,10,25],63,[0]*63))
#思想:从最简单开始寻找答案
#怎样记录所用硬币找零的值?coinsUsed[]
def printCoins(coinsUsed,change):
coin=change
while coin>0:
thisCoin=coinsUsed[coin]
print(thisCoin)
coin-=thisCoin
8 01背包问题
解法1 动态规划
如何选择物品偷盗使得价值最高
m(i,w)前i个宝物中,重量不超过W,得到的最大价值
一定是max(m(i-1,w),vi+m(i-1),w-wi)
tr=[None,{'w':2,'v':3},{'w':3,'v':4},{'w':4,'v':8},{'w':5,'v':8},{'w':9,'v':10}]
max_w=20
缓存-前i个物品,最大重量w下的最大价值
m={(i,w):0 for i in range(len(tr)) for w in range(max_w+1)}
for i in range(1,len(tr)):
for w in range(1,max_w+1):
if tr[i]['w']>w:#若第i个物品的重量超过最大限重,则m(i,w)必等于i-1的状态
m[(i,w)]=m[(i-1,w)]
else:#【核心】:dp状态转移公式,使用/不使用第i个物品,是决定前后最优状态的唯一因素
m[(i,w)]=max(m[(i-1,w-tr[i]['w'])]+tr[i]['v'],m[(i-1,w)])
print(m[len(tr)-1,max_w])
解法2 递归
m={}#递归缓存字典
tr={(2,3),(3,4),(4,8),(5,8),(9,10)}
def thief(tr,w):
#如果待选物品集合已经取空,或者最大限重为0,则m为0,此为结束条件
if tr==set() or w==0:#set()创建无序不重复元素集合
m[(tuple(tr),w)]=0#tuple()针对字典,返回key值元组
return 0
elif (tuple(tr),w) in m:#根据key值()查找最优解
return m[tuple(tr),w]
else:
vmax=0
for t in tr:
if t[0]<= w:#要求待评价物品重量低于限重
#求的是i种不同的i-1再加1个物品的方案,在当前w下的最大值
v=thief(tr-{t},w-t[0])+t[1]#减少规模的递归调用自身,i-2问题,类推直到0结束
vmax=max(vmax,v)
m[(tuple(tr),w)]=vmax
return vmax
print(thief(tr,max_w))
对比两种方法,计算都要从单取开始逐渐增多,动态规划的核心是状态转移公式,递归的核心是缩小问题范围,本质相通。
递归小结:
适用于自相似性问题,递归三定律:基本结构条件,减小问题规模,调用自身
递归算法能够于问题表达自然契合,但有时会引发巨量的重复计算,记忆化/函数值缓存可以有效减少重复计算
如果一个问题最优解包括规模更小相同问题的最优解,就可以用动态规划来解决
网友评论