减治法利用了一个问题给定实例的解和同样问题较小实例的解之间的某种关系,一旦建立了这种关系,我们既可以从顶向下,也可以从底向上地来运用该关系。自顶向下会自然导致出递归算法,但还是非递归实现较好,自底向上版本往往是迭代实现的,从求解问题的一个较小实例开始,该方法有时也称为增量法。
(递归和迭代:迭代就是求解当前,如果无法得到解再逐层深入;递归就是从大问题逐渐化小,但不求解,直到化到最小问题,直接整合结果)
减治法有三种主要的变化形式:减去一个常量,减去一个常量因子,减去的规模是可变的。
在减常量变化形式中,每次算法迭代哦那个是从实例中减去一个相同的常量,一般为1。
减常因子意味着在算法的每次迭代中,总是从实例的规模中减去一个相同的常数因子,一般为2(例如减半思想)。
减可变规模的变化形式中,每次迭代时规模减小的模式都是不同的。
示例1:深度优先查找DFS
从任意顶点开始访问图的顶点,然后把该顶点标记为已访问,在每次迭代的时候,紧接着处理与当前顶点邻接的未访问顶点(这里任意选择一个顶点,实际上是由图的数据结构决定,要注意的是,不同的选择顺序会导致最大查找深度不同),直到该顶点的所有邻接顶点都已被访问过,此时沿着来路后退一条边,并试着从那里访问未访问的顶点。在后退到起始顶点,并且起始顶点也是一个终点时,最终停下来。可以用一个栈来跟踪深度优先查找的操作。在第一次访问一个顶点时,把该顶点入栈,当它成为一个终点时,将它出栈。
用一个邻接矩阵来表示图,从顶点a开始访问:
![]() |
a b c d e f e 0 0 1 1 0 0 f 0 0 1 0 0 0 |
def find_children(neigh_mat,curr):
children=[]
for i, reachable in enumerate(neigh_mat[curr]):
if reachable:
children.append(i)
return children
def df_search_recurrent(neigh_mat,curr,isvisit,name):
print(name[curr])
isvisit[curr]=1
children=find_children(neigh_mat,curr)
for child in reversed(children):
if not isvisit[child]:
df_search_recurrent(neigh_mat,child,isvisit,name)
def df_search_cycle(neigh_mat,track_stack,isvisit,name):
curr=track_stack.pop()
if not isvisit[curr]:
print(name[curr])
isvisit[curr]=1
children=find_children(neigh_mat,curr)
for child in children:
track_stack.append(child)
if __name__=="__main__":
neigh_mat=[[0,1,1,1,0,0],
[1,0,1,1,0,0],
[1,1,0,0,1,1],
[1,1,0,0,1,0],
[0,0,1,1,0,0],
[0,0,1,0,0,0]]
name=["a","b","c","d","e","f"]
isvisit=[0,0,0,0,0,0]
df_search_recurrent(neigh_mat,0,isvisit,name)
isvisit = [0, 0, 0, 0, 0, 0]
track = [0]
while len(track)>0:
df_search_cycle(neigh_mat, track, isvisit, name)
注意:这里给出的递归代码为了和循环保持结果一致,采用了子节点的倒序。
示例2:广度优先查找BFS
首先访问所有和初始顶点邻接的顶点,然后是离它两条边的所有未访问顶点,以此类推,直到所有与初始顶点同在一个连通分量重的顶点都访问过了为止,如果仍然存在未被访问的顶点,该算法必须从图的其他连通分量重的任意顶点重新开始。可以用队列来跟踪广度优先查找的操作,该队列先从遍历的初始顶点开始,将该顶点标记为已访问,在每次迭代的时候,找出所有和队头顶点邻接的未访问顶点,将它们标记为已访问,再把它们入队,然后将队头顶点从队列中移除。
def find_children(neigh_mat,curr):
children=[]
for i, reachable in enumerate(neigh_mat[curr]):
if reachable:
children.append(i)
return children
def bf_search_cycle(neigh_mat,track_queue,isvisit,name):
children=find_children(neigh_mat,track_queue[0])
for child in children:
if not isvisit[child]:
print(name[child])
isvisit[child]=1
track_queue.append(child)
track_queue.pop(0)
if __name__=="__main__":
neigh_mat=[[0,1,1,1,0,0],
[1,0,1,1,0,0],
[1,1,0,0,1,1],
[1,1,0,0,1,0],
[0,0,1,1,0,0],
[0,0,1,0,0,0]]
name=["a","b","c","d","e","f"]
track = [0]
isvisit=[1,0,0,0,0,0]
print(name[0])
while len(track) > 0:
bf_search_cycle(neigh_mat,track,isvisit,name)
示例3:拓扑排序
一个有向图的节点可以按次序列出,使得对于图中的每一条边来说,边的起始顶点总是排在边的结束顶点之前。
为了使得拓扑排序成立,图必须是一个有向无环图。
一种方法是执行一次DFS遍历,记住顶点出入栈的顺序,将该次序反过来就得到拓扑排序的一个解。当一个顶点出栈时,在比它更早出栈的顶点中,不可能有到该顶点的边,否则就是一条回边。
另一种方法基于“减一”,不断在余下的有向图中求一个“源”,它是一个没有输入边的顶点,然后把它和所有从它触发的边都删除,顶点被删除的次序就是拓扑排序的一个解。
这里实现第二种方法,第一种可以参考DFS部分。
def find_source(link_mat,nodes):
node_in_count={}
node_out_count={}
for n in nodes:
node_in_count[n]=0
node_out_count[n]=0
for start_node in nodes:
for end_node in nodes:
if start_node==end_node:
continue
if link_mat[start_node][end_node]:
node_out_count[start_node]+=1
node_in_count[end_node]+=1
sources=[]
for n in node_in_count:
if node_in_count[n]==0:
sources.append(n)
return sources
def topo_sort(link_mat,nodes,sort_list):
sources=find_source(link_mat,nodes)
sort_list.extend(sources)
for source in sources:
for i in range(len(link_mat[source])):
link_mat[source][i]=0
nodes.remove(source)
if __name__=="__main__":
neigh_mat=[[0,1,1,1,0,0],
[0,0,1,1,0,0],
[0,0,0,0,1,1],
[0,0,1,0,1,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0]]
names = ["a", "b", "c", "d", "e", "f"]
nodes = [0,1,2,3,4,5]
# edges=[["a","b"],["a","c"],["a","d"],["b","c"],["b","d"],["c","f"],["c","e"],["d","e"]]
sort_list=[]
length=len(nodes)
while len(sort_list)!=length:
topo_sort(neigh_mat,nodes,sort_list)
print(sort_list)
这里每次将所有“源”取完,确定了顺序,如果要找所有排序结果,每次取一个源再做拓扑排序,递归求解。
示例4:组合对象
组合对象中最重要的类型就是排列、组合和给定集合的子集,一般这种情况出现在要对不同的选择进行考虑的问题中。
python中的排列方法:itertools.permutations;组合方法:itertools.combinations
生成排列的算法:将问题的规模减一,从n!个排列到(n-1)!个排列,再将n插入到n-1个元素的每一种排列中的n个可能的位置中去。
生成子集的算法:集合A的所有子集可以分为两组,不包含an的子集和包含an的子集,前一组就是{a1,……an-1}的所有子集,后一组中的每一个元素都可以通过把an添加到{a1,……an-1}的一个子集中来获得。
import itertools
import copy
def example():
result_com = itertools.combinations('abcd', 4)
result_per = itertools.permutations('abcd', 4)
for r in result_com:
print(r)
print()
for r in result_per:
print(r)
def permutations(wait_list):
if len(wait_list)==1:
return [wait_list]
front_lists=combinations(wait_list[:-1])
result_lists=[]
for front_list in front_lists:
result_list=[]
for i in range(len(front_list)+1):
result_list = front_list[:i]
result_list.append(wait_list[-1])
result_list.extend(front_list[i:])
result_lists.append(result_list)
return result_lists
def subsets(wait_list):
if len(wait_list)==0:
return [[]]
front_subset_list=subsets(wait_list[:-1])
results=[]
results.extend(front_subset_list)
for front_subset in front_subset_list:
result=copy.deepcopy(front_subset)
result.append(wait_list[-1])
results.append(result)
return results
if __name__=="__main__":
result_per=permutations([1,2,3])
for r in result_per:
print(r)
result_sub=subsets([1,2,3])
for r in result_sub:
print(r)
生成排列的Johnson-Trotter算法:
将数字赋予方向属性,在讨论的元素上面画一个小箭头表示,如果元素k的箭头指向一个相邻的较小元素,我们说它在这个以箭头标记的排列中是移动的。
class Element():
def __init__(self,value,direction):
self.value=value
self.direction=direction
def change_direction(self,direction):
self.direction=direction
def __str__(self):
return str(self.value)
def exist_mobile(elements):
mobiles=[]
for i,e in enumerate(elements):
if e.direction<0:
if i==0:
continue
if elements[i-1].value<e.value:
mobiles.append((i,e))
else:
if i==len(elements)-1:
continue
if elements[i+1].value<e.value:
mobiles.append((i,e))
return mobiles
def johnson_trotter(n):
pers=[]
elements=[]
for i in range(1,n+1):
elements.append(Element(i,-1))
pers.append(copy.deepcopy(elements))
mobiles=sorted(exist_mobile(elements),key=lambda x:x[1].value,reverse=True)
while len(mobiles)>0:
k=mobiles[0]
i=k[0]
e=k[1]
if e.direction<0: # <-
elements[i-1],elements[i]=elements[i],elements[i-1]
else: # ->
elements[i+1],elements[i]=elements[i],elements[i+1]
for x in elements:
if x.value > e.value:
x.direction = x.direction * -1
pers.append(copy.deepcopy(elements))
mobiles=sorted(exist_mobile(elements),key=lambda x:x[1].value,reverse=True)
return pers
if __name__=="__main__":
result_john=johnson_trotter(4)
for r in result_john:
for e in r:
print(e,end=" ")
print()
生成子集的反射格雷码:
n个元素集合A={a1,a2,……,an}的所有2^n个子集和长度为n的所有2^n个位串之间有一一对应关系,建立这样一种对应关系的最简单方法是为每一个子集指定一个位串,如果a1属于该子集,bi=1;如果ai不属于该子集,bi=0。如n=3时,{a1,a3}对应101。当该算法以字典序(两个符号0和1构成的字母表)生成位串时,子集的排列次序是不自然的。考虑得到挤压串,其中所有包含aj的子集必须紧排在所有包含a1,……aj-1的子集后面,很容易就能对基于位串的算法做调整,让他生成相关子集的挤压序。
BRGC是一种生成位串的最小变化算法,使得每一个位串和它的直接前驱之间仅相差一位(我们希望每一个子集和它的直接前驱之间的区别,要么是增加了一个元素,要么是删除了一个元素,但两者不能同时发生)。n=3时:00 001 011 010 110 111 101 100
def subsets_position(n):
if n==1:
return [[0],[1]]
else:
position_list=subsets_position(n-1)
position_list_copy=copy.deepcopy(position_list)
for l in position_list:
l.insert(0,0)
for l in position_list_copy:
l.insert(0,1)
position_list.extend(position_list_copy)
return position_list
def subsets_squeeze(n):
position=[]
for i in range(n):
position.append(0)
position_list=[]
position_list.append(position)
for i in range(n):
position_list_i=copy.deepcopy(position_list)
for position in position_list_i:
position[i]=1
position_list.extend(position_list_i)
return position_list
def gray_code(n):
if n==1:
return [[0],[1]]
else:
l1=gray_code(n-1)
l2=copy.deepcopy(list(reversed(l1)))
for l in l1:
l.insert(0,0)
for l in l2:
l.insert(0,1)
l1.extend(l2)
return l1
if __name__=="__main__":
print(subsets_position(3))
print(subsets_squeeze(3))
print(gray_code(2))
示例5:假币问题(常因子)
在n枚外观相同的硬币中,有一枚是假币,在一架天平上,可以比较任意两组硬币的轻重,假币较轻。
将n枚硬币分为两堆,如果n为奇数,留下一枚额外的硬币,如果天平上的硬币重量相同,则额外的硬币是假币,否则对轻的那堆硬币再做同样处理。