由排列组合来看回溯剪枝

全排列

由m个数字里面选n个进行全排列,(0,1)和(1,0)不互斥:

def n_of_m_circle(total, num):
    count = 0
    time = 0
    
    def circle(n, begin, queue):

        if len(queue) == num:
            nonlocal time
            time = time + 1
            print(queue)

        # print(queue)

        if n < 0:
            return

        nonlocal count
        count += 1

        for i in [k for k in range(begin, total) if k not in queue]:
        	# 每次递归的传参begin都等于0
            circle(n - 1, 0, queue + [i])
            
    circle(num, 0, [])

上面的代码每次递归的范围都是从0开始到total,除去已经遍历的数字。相当于不放回的取数字。示意图如下:
在这里插入图片描述

组合

组合也是不放回的取数字,但是类似(0,1)和 (1,0)是互斥的关系。
代码如下:

    def circle(n, begin, queue):

        if len(queue) == num:
            nonlocal time
            time = time + 1
            print(queue)

        # print(queue)

        if n < 0:
            return

        nonlocal count
        count += 1

        for i in range(begin, total):
        	# 下次递归从i+1开始。这样选过的数字就不会再选。
            circle(n - 1, i + 1, queue + [i])

    circle(num, 0, [])

组合是全排列的子集,可以考虑在全排列的基础上剪枝。假设我们每次递归都从比当前点更大的点选取元素,那后加入的点必定比先加入的点大。这样就能保证出现(1,2,3)的序列后就不会出现(1,3,2),(3,2,1),(3,1,2),(2,1,3),(2,3,1)。其递归示意图如下:
在这里插入图片描述

回溯

回溯算法和枚举算法有一定的关联性。但是回溯算法都有类似树的性质。其回溯过程类似二叉树后续遍历和树的深度优先遍历(这里说的是递归调用栈总是调用到最底层,然后再调用上层)。假设我们在程序开始的时候用一个栈记录遍历的节点,然后在遍历完后退出。那栈里面存的序列总是从根节点到当前节点的序列(有点类似先序遍历的意思,但是遍历是使用的意思,例如print,不能把节点入栈作为遍历来看)。加入有一个按着层序排列的二叉树:
在这里插入图片描述

queue = []

def pre_order(root: Node):
    if root is None:
        return

    # do something
    queue.append(root)
	# queue里面存的是根节点到当前节点的路径
	
 	# do something 先序遍历回溯法上来就从根节点开始
    # print(self.circle)

    # [0]
    # [0, 1]
    # [0, 1, 3]
    # [0, 1, 3, 7]
    # [0, 1, 3, 8]
    # [0, 1, 4]
    # [0, 1, 4, 9]
    # [0, 2]
    # [0, 2, 5]
    # [0, 2, 6]

    pre_order(root.left)
    pre_order(root.right)
  	
    # do something 后序遍历回溯法上来就从叶节点开始
    print(self.circle)

    # [0, 1, 3, 7]
    # [0, 1, 3, 8]
    # [0, 1, 3]
    # [0, 1, 4, 9]
    # [0, 1, 4]
    # [0, 1]
    # [0, 2, 5]
    # [0, 2, 6]
    # [0, 2]
    # [0]    
    queue.pop()

根据上面的输出可以看出,如果采用无论是先序遍历还是后续遍历,queue里存的都是根节点到当前节点的序列。但是先序更适合做加法操作,而后续更适合做减法操作。
所以从上面的例子可以总结出回溯的几个要点:

  • 要在程序开始的时候入栈,然后结束的时候出栈。
  • 先序做加法,后续做减法。
  • 如果做剪枝操作的话,显然在递归前效果更好。

此外要注意的是这里用一个全局栈来做记录。可以考录用传参的方式,把已经走过的路径传给下一层,这样就不需要显示的出栈了。

发布了85 篇原创文章 · 获赞 21 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/weixin_37275456/article/details/101172586