算法思维 ---- 回溯算法

回溯算法

回溯实际上是一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

说到底,实际上回溯算法就是一个N叉树前序遍历加上后序遍历

//二叉树遍历框架
func traverse(root) {
	if root is nil {
		return
	}
	//前序遍历代码
	traverse(root.left)
	//中序遍历代码
	traverse(root.right)
	//后序遍历代码
	
}


//N叉树遍历框架
func traverse(root) {
	if root is nil {
		return
	}
	
	for child := range root.children {
		//前序遍历代码
		traverse(child)
		//后序遍历代码
	}
}

回溯的框架

回溯算法就是N叉树的遍历,这个N等于当前可做的选择(choices)的总数,同时,在前序遍历的位置作出当前选择(choose过程),然后开始递归,最后在后序遍历的位置取消当前选择(unchoose过程)。伪代码如下:

/**
 * choiceList:当前可以进行的选择列表
 * track : 可以理解为决策路径,就是已经作出一系列的选择
 * answer : 用来存储我们符合条件的决策路径
 */
 
 func backtrack(choiceList, track, answer) {
 	if track is OK {
		answer.add(track)
	} else {
		for choice as range choiceList {
			// choose: 选择一个choice加入track
			backtrack(choices, track, answer)
			// unchoose: 从track中撤销上面的选择
		}
	}
 }

可以看出,回溯算法的核心其实就是一个N叉树的遍历,回溯算法相当于一个决策过程,递归的遍历一颗决策树,穷举所有的决策,同时把符合条件的决策挑出来

一个例子

如果你要吃饭,但是要去哪里吃呢,具体吃什么呢?这就是一个决策问题。
在这里插入图片描述

那么计算机会如何选择呢?计算机处理问题的方法就是穷举,这里计算机会穷举并比较所有的决策,然后选出某个最优的策略,比如计算机会选择:吃饭-》外卖-》肯德基-》全家桶。

如何让计算机正确的穷举并比较所有的决策,就是回溯算法的设计技巧了。回溯算法的核心就是在于如何设计choose和unchoose部分的逻辑

全排列问题

在这里插入图片描述
全排列问题就是典型的回溯算法问题。我们的思路是,先把第一个数固定为 1,然后全排列 2,3;再把第一个数固定为 2,全排列 1,3;再把第一个数固定为 3,全排列 1,2 。这就是个决策的过程。转化成决策树表示一下:
在这里插入图片描述

可以发现每向下走一层就是在「选择列表」中挑一个「选择」加入「决策路径」,然后把这个选择从「选择列表」中删除(以免之后重复选择);当一个决策分支探索完成后,我们就要向上回溯,要把该分支的「选择」从「决策列表」中取出,然后把这个「选择」重新加入「选择列表」(供其他的决策分支使用)。

上面的,就是模版中choose和unchoose的过程,choose过程是向下探索,进行一个选择;unchoose过程是向上回溯,撤销刚刚的选择。
在这里插入图片描述
现在我们可以针对全排列问题来具体化回溯算法的模版:

/**
 * choiceList:选择列表,当前可以进行的选择列表
 * track : 决策路径,已经作出一系列的选择
 * answer : 用来存储完成的全排列
 */
 
 func backtrack(choiceList, track, answer) {
 	if track is OK {
		answer.add(track)
	} else {
		for choice as range choiceList {
			// choose过程
			// 把choice加入track
			// 把choice移出choiceList
			backtrack(choices, track, answer)
			// unchoose过程
			// 把choice移出track
			// 把choice重新加入choiceList
		}
	}
 }

下面是具体代码(golang):

func permute(nums []int) [][]int {
    track := []int{}
    ans := [][]int{}
    backTrack(nums, &track, &ans)
    return ans
}

func backTrack(nums []int, track *[]int, ans *[][]int) {
    if len(*track) == len(nums) {
        tmp := make([]int, len(*track))
        copy(tmp, *track)
        *ans = append(*ans, tmp)
    } else {
        for _, v := range nums {
            if isIn(v, *track) {
                continue
            }
            *track = append(*track, v)
            backTrack(nums, track, ans)
            *track = (*track)[:len(*track)-1]
        }
    }
}

func isIn(i int, arr []int) bool {
    for _, v := range arr {
        if v == i {
            return true
        }
    }

    return false
}

在这里插入图片描述

N皇后问题

在这里插入图片描述
也是回溯的思路,代码如下:

func solveNQueens(n int) [][]string {
    board := []string{}
    ans := [][]string{}
    initBoard(&board, n)
    backTrack(0, n, &board, &ans)
    return ans

}

//初始化棋盘
func initBoard(board *[]string, n int) {
    var s string
    for i := 0; i < n; i++ {
        s += "."
    }
    for i := 0; i < n; i++ {
        *board = append(*board, s)
    }
}

func backTrack(row int, n int, board *[]string, ans *[][]string) {
    if row == len(*board) {
        tmp := make([]string, len(*board))
        copy(tmp, *board)
        *ans = append(*ans, tmp)
    } else {
        for col := 0; col < n; col++ {
        	//如果选择这个位置会被攻击,跳过这个位置
            if !isPlace(board, row, col, n) {
                continue
            }
            //choose过程
            (*board)[row] = strRex((*board)[row], col, 'Q')
            //进行下一行的选择
            backTrack(row + 1, n, board, ans)
            //unchoose过程
            (*board)[row] = strRex((*board)[row], col, '.')
        }
    }
}

func strRex(s string, i int, b byte) string {
    bytes := []byte(s)
    bytes[i] = b
    s = string(bytes)
    return s
}

//判断board[row][col]是否可以放置Q
func isPlace(board *[]string, row, col, n int) bool {
	//检查正上方
    for i := 0; i < row; i++ {
        if (*board)[i][col] == byte('Q') {
            return false
        }
    }
	//检查右斜上方
    for i, j := row - 1, col + 1; i >= 0 && j < n; i, j = i-1, j+1 {
        if (*board)[i][j] == byte('Q') {
            return false
        }
    }
    //检查左斜上方
    for i, j := row - 1, col - 1; i >= 0 && j >= 0; i, j = i-1, j-1 {
        if (*board)[i][j] == byte('Q') {
            return false
        }
    }
    //不用检查下方,因为下方还没有放置皇后
    return true
}

「决策路径」:就是棋盘board,每一步决策即是board的每一行,row变量记录着当前决策路径走到了哪一步。
「选择列表」:对于给定的一行(即决策中的每一步),每一列都可能放置一个‘Q’,这就是所有的选择,代码中的for循环就是在穷举「选择列表」判断是否可以放置皇后。
在这里插入图片描述
在这里插入图片描述

总结

重新复习一下回溯的算法框架吧

 func backtrack(choiceList, track, answer) {
 	if track is OK {
		answer.add(track)
	} else {
		for choice as range choiceList {
			// choose: 选择一个choice加入track
			backtrack(choices, track, answer)
			// unchoose: 从track中撤销上面的选择
		}
	}
 }

参考自https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484523&idx=1&sn=8c403eeb9bcc01db1b1207fa74dadbd1&chksm=9bd7fa63aca07375b75e20404fde7f65146286ef5d5dea79f284b8514fa1adb294e389518717&scene=21#wechat_redirect

图片来源自https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484523&idx=1&sn=8c403eeb9bcc01db1b1207fa74dadbd1&chksm=9bd7fa63aca07375b75e20404fde7f65146286ef5d5dea79f284b8514fa1adb294e389518717&scene=21#wechat_redirect

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值