简单地说,将一个问题A简化为另一个问题B涉及到某种形式的转换,然后解决方案 给B(直接或通过一些按摩)给A一个解决方案。一旦你学会了一系列标准算法 (在这本书中你会遇到很多),这是你遇到新问题时通常会做的事情。你能 以某种方式改变它,以便用你知道的方法之一来解决它?在许多方面,这是核心 解决所有问题的过程。 让我们举个例子。你有一个数字列表,你想找到两个不相同的数字 彼此最接近(即绝对差最小的两个):
from random import rangdrange
seq = [randrange(10**10) for i in range(100)]
dd = float("inf")
for x in seq:
for y in seq:
if x == y:continue
d = abs(x-y)
if d < dd:
xx,yy,dd = x,y,d
xx, yy
(15743, 15774)
两个嵌套循环,都在seq上;很明显这是二次循环,这通常不是一件好事。 假设你已经研究过一些算法,你知道如果序列 排序。您还知道排序通常是对数线性的,或q(n lg n)。明白了吗?这里的见解是 在排序顺序中,两个最近的数字必须相邻:
seq.sort()
dd = float("inf")
for i in range(len(seq)-1):
x, y = seq[i],seq[i-1]
if x == y:continue
d = abs(x-y)
if d < dd:
xx,yy,dd=x,y,d
更快的算法,同样的解决方案。新的运行时间是对数线性的,以排序为主。我们的原创 问题是“在一个序列中找到两个最接近的数字”,我们将其简化为“在 排序序列,”通过排序序列在这种情况下,我们的减少(排序)不会影响我们得到的答案。 一般来说,我们可能需要改变答案,使其符合原始问题。
注意,在某种程度上,我们只是将问题分为两部分,对排序的序列进行排序和扫描。你也可以 说扫描是一种减少原始问题到排序序列的问题的方法。一切都是关于 观点。
把A减到B有点像说“你想解A吗?”哦,这很容易,只要你能解出b。”见图4-1 为了说明减少是如何工作的。
图4-1。使用从A到B的约简,用B的算法求解A。B的算法(中心,内部 circle)可以转换输入b吗?输出B!,而还原包含两个转换(较小的 圆圈)从A开始?到B?从B!A!,一起形成主算法,哪个转换输入A?到 输出A!
一,二,很多 我已经用归纳法解决了第3章中的一些问题,但是让我们回顾一下,并通过以下几个步骤来解决 例子。在抽象地描述归纳法时,我们说我们有一个命题或陈述,p(n),并且我们 想证明对任何自然数n都是正确的。例如,假设我们正在研究第一个n的和 奇数;p(n)可以是以下语句:
这是非常熟悉的,几乎和我们在上一章中讨论过的握手和一样。你 通过调整握手公式可以很容易地得到这个新结果,但是让我们看看我们如何通过归纳法证明它。 相反。归纳法的思想是让我们的证明“扫过”所有自然数,有点像一排多米诺骨牌。 坠落。我们首先建立P(1),这在本例中非常明显,然后我们需要展示每个Domino, 如果它掉下来,下一个就会倒掉。换句话说,我们必须证明,如果p(n-1)是真的,那么p(n)就是 也是如此。 如果我们能证明这一含义,即p(n–1)p(n),结果将扫过n的所有值,从 P(1),用P(1)P(2)建立P(2),然后转到P(3)、P(4)等。换句话说,关键是 以确定让我们更进一步的含义。我们称之为归纳步骤。在我们的示例中,这意味着 我们假设如下(p(n-1)):
镜子,镜子 在他出色的网络视频节目中,泽弗兰克曾经说过:“你知道没有什么可以害怕的,但是 恐惧本身。“是的,这叫做递归,这会导致无限的恐惧,所以谢谢你。”4 建议是,“为了理解递归,我们必须首先理解递归。” 的确。尽管无限递归是一种相当病态的行为,但递归很难让人信服。 在某种程度上,递归实际上只有作为归纳的镜像才有意义(见图4-3)。在归纳法中,我们 (概念上)从一个基本情况开始,展示归纳步骤是如何使我们更进一步,直至完整的问题。 大小,n.对于弱诱导,6我们假设(诱导假设)我们的解适用于n-1,由此,我们 推断它适用于n。递归通常更像是分解事物。从一个完整的问题开始, 大小为n。将大小为n–1的子问题委托给递归调用,等待结果,然后扩展子解决方案 找到一个完整的解决方案。我相信你能明白这是怎么回事。在某种程度上,归纳法告诉我们 为什么递归有效,递归给了我们一个简单的方法(直接)实现归纳思想。
图4-3。归纳(在左边)和递归(在右边),作为彼此的镜像
例如,以前一节中的跳板问题为例。制定解决方案的最简单方法 对此(至少在我看来)是递归的。你放置一个L形件,这样你得到四个等价的子问题,和 然后你递归地解决它们。通过归纳,解决方案是正确的。
实现棋盘式覆盖
尽管棋盘覆盖问题在概念上有一个非常简单的递归解决方案,但是实现它可以 需要一些思考。实现的细节对示例的要点并不重要,所以感觉 如果你愿意,可以跳过这个侧边栏。实现解决方案的一种方法如下所示:
def cover(board,lab=1,top=0,left=0,side=None):
if side is None: side = len(board)
## side length of subboard:
s = side//2
## offsets foe outer/inner squares of subboards:
offstes = (0,-1),(side-1,0)
for dy_outer,dy_inner in offstes:
for dx_outer,dx_inner in offsets:
# if the outer corner is not set..
## ... label the inner corner:
board[top+s+dy_inner][left+s_dx-inner] = lab
# next label:
lab +=1
if s > 1:
for dy in [0,s]:
#
for dx in [0,s]:
lab = cover(board,lab,top+dy,left+dx,s)
return lab
##
board = [[0]*8 for i in range(8)] ## Eight by eight checkerboard
board[7][7] = -1
cover(board)
for row in board:
print("%si*8) % tuple(row))
def ins_sort_rec(seq,i):
if i ==0:return
ins_sort_rec(seq,i-1)
j = i
while j > 0 and seq[j-1] >seq[j]:
seq[j-1],seq[j] =seq[j],seq[j-1]
j -=1
这些算法并没有那么有用,但它们通常是被教授的,因为它们是很好的例子。而且,他们是 经典,所以任何一个算法师都应该知道它们是如何工作的。
再一次,你可以看到这两者非常相似。递归实现显式表示 归纳假设(作为递归调用),而迭代版本显式表示重复执行 归纳步骤。两者都是通过找到最大的元素(for循环查找max_j)并将其交换到 正在考虑的序列前缀的结尾。注意,您也可以在 此节从开始,而不是从结束(在插入排序中,将所有对象排序到右侧,或查找 选择排序中的最小元素)
def sel_sort_rec(seq,i):
if i == 0:return
max_j = i
for j in range(i):
if seq[j] > seq[max_j]:max_j = j
seq[i],seq[max_j] = seq[max_j],seq[i]
sel_sort_rec(seq,i-1)
# Selection Sort
def sel_sort(seq):
for i in range(len(seq)-1,0,-1):
max_j = i
for j in range(i):