文章目录

  • 八皇后问题
  • 全排列(排列组合)
  • 仿照八皇后解决全排列
  • 优化
  • Python中的itertools
  • 括号生成
  • 全排列(回溯)+剪枝
  • 回溯-双指针



回溯法又叫试探法,算法核心是深度优先搜索,就是假设第一步,再假设第二步一直死磕到头,看能不能走通,走不通咱就回去,重新走,可以退一步重新走,也可以一直退到第一步。直到找到可以走通的路线。如果小时候玩过迷宫游戏就能深刻体会到这个算法就是你心中所想的过程。

八皇后问题

八皇后问题是有一个8X8的棋盘,要把八个皇后放上去,条件是,每一个皇后的米子区域都不能有其他皇后,如下图,假如放进去了第一个皇后,灰色区域就不能再放任何皇后。




python回溯算法求解n皇后问题 python 八皇后回溯算法_leetcode

我们现在的需求是,找出所有可以放下八皇后的可能性。 首先,创造一个棋盘,假设已经放了皇后的位置为1,可放位置为0:

def generate_board(n):
	return [[0 for ii in range(n)] for jj in range(n)]
board=generate_board(8)
# 展示一个棋盘
def print_board(board):
    n=len(board)
    for ii in range(n):
        for jj in range(n):
            if board[ii][jj]==1:
                print("* ",end="")
            else:
                print("o ",end="")
        print()
    print("----------------------")

首先,需要找到限制条件,也就是假如放了第一个皇后,怎么判断后面的皇后能放还是不能放某个位置。

对于任何的一个位置(x,y),如果想放一个皇后,以下位置是禁区:
同列: y相等
同行: x相等
斜向正方向(""):(x+n, y+n)或者(x-n, y-n)
斜向反方向("/"):(x+n, y-n)或者(x-n, y+n)

变成python代码如下

def can_place(x,y,board):
	n=len(board)
    for ii in range(n):
        for jj in range(n):
            if ii==x and board[ii][jj]==1: # 同行排除
                return False
            if jj==y and board[ii][jj]==1: # 同列排除
                return False
            if ii+jj==x+y and board[ii][jj]==1: # 斜向反方向("/")排除
                return False
            if ii-x==jj-y and board[ii][jj]==1: # 斜向正方向("\")排除
                return False
    return True

然后开始放皇后:

total=0
def nQueens(row,board):
	if row==len(board): ## 按照行放皇后,如果递归到了第8行也就是第9个皇后,就说明跑通了,所以递归停止
		print_borad(board) ## 展示棋盘
		global total
		total+=1 ## 想看一共有多少种可能性
	for column in range(len(board): ## row这一行的哪一个列可以放皇后
		if can_place(step,ii):
			board[step][ii]=1
			nQueens(step+1,board) ## 如果找到了可以放的位置,进入到下一行继续找可以放的位置
			board[step][ii]=0

全排列(排列组合)

问题:全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

仿照八皇后解决全排列

这个问题乍眼看去,好像和八皇后问题八竿子打不到一撇,实际上分析分析是很相似的问题,可以这么类比,给第一个皇后在第一行找一个位置,第二个皇后在第二行找一个位置只要和第一个皇后不同列就行,最后8个数找到自己的位置,也就是索引错开即可,这样的可能性有多少。也就是放宽了八皇后的条件(只要不在同一行同一列即可)。
按照这个思路,我们把全排列问题转换为八皇后问题,看看怎么写?

def generate_board(n):
    return [[0 for ii in range(n)] for jj in range(n)] 

def can_place(x,y,board):
    for ii in range(len(board)):
        for jj in range(len(board)):
            if ii==x and board[ii][jj]!=0: # 同行排除
                return False
            if jj==y and board[ii][jj]!=0: # 同列排除
                return False
    return True

def generate_results(board):
    res=[0 for ii in range(len(board))]
    for ii in range(len(board)):
        for jj in range(len(board)):
            if board[ii][jj]!=0:
                res[jj]=board[ii][jj]
    return res
                
def nQueens(row,board,lis):
    if row==len(board):
        print(generate_results(board))
    else:
        for column in range(len(board)):
            if can_place(row,column,board):
                board[row][column]=lis[row]
                nQueens(row+1,board,lis)
                board[row][column]=0
if __name__ == "__main__":
    lis=[1,2,3]
	board=generate_board(len(lis))
	nQueens(0,board,lis)

优化

只要已经理解了八皇后问题的解决思想,以上的代码其实很容易明白。但是还能优化,如下:

def permute(nums):
    def back(current_pos,lis): 
        if current_pos == len(nums): ## 现在要做的事情是从第0个位置开始填数,当填到了超出列表长度的位置,说明填完了,递归结束
            results.append(res[:])
        else:
            for ele in lis: ## 从第一个数开始填
                res.append(ele)
                new_lis=lis[:]
                new_lis.remove(ele)
                back(current_pos+1,new_lis) # 第一个数填完了,从剩下的数里面挑一个继续填第二个数,第三个数。。。。
                res.pop()
	res=[]
	results=[]
	back(0, nums)
	return results

Python中的itertools

关于全排列问题,其实python里面有个内建包叫做itertools。来感受一下他的强大,如果就是解决以上的问题

import itertools
lis=[1,2,3]
list(itertools.permutations(lis,3)) # 从lis中抽三个数
## [(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

除此以外,前段时间遇到一个排列组合问题也用到了这个包。生成一个长度为nd的new_list,第一个位置只能放list1里面的元素,第二个位置只能放list2里面的元素,以此类推,问有多少种可能性?

import itertools
list1 = [1, 2]
list2 = ['A', 'B', 'C']
list3 = [7, 8, 9]
list(itertools.product(list1,list2,list3))

括号生成

题目如下:https://leetcode-cn.com/problems/generate-parentheses/

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例:
输入:n = 3
输出:[
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”
]

全排列(回溯)+剪枝

可以很容易想到全排列,然后再去除重复项和不符合要求的。代码如下

def generateParenthesis(n):
    def back(current_pos, lis):
        if current_pos==(n-1)*2+1:
            results.append("".join(res[:])+")")
        for ele in lis:
            if Counter(res+[ele])[")"]>Counter(res+[ele])["("]: ## 剪枝
                continue
            res.append(ele)
            new_lis=lis[:]
            new_lis.remove(ele)
            back(current_pos+1,new_lis)
            res.pop()

    lis=["(",")"]*(n-1)
    res=["("]
    results=[]
    back(1,lis)

    return set(results)

可惜这个全排列+剪枝的方法无法通过leetcode,时间复杂度太高,超出限制。

回溯-双指针

附上一个比较简单的方案,双指针法,只要”(“的数目比n小,就可以一直放”(“,只要”)“的数目一直比”(“小,就可以放”(“。

def generateParenthesis(n):
    def back(tmp,left,right):
        if len(tmp) == 2 * n:
            results.append(tmp)
            return
        if left < n:
            back(tmp + '(', left + 1, right)
        if right < left:
            back(tmp + ')', left, right + 1)
    results = []
    back('',left = 0,right = 0)
    return results