回溯
回溯的思想比较简单,但是却可以解决很多问题。
思想
回溯的思想是穷举所有可能的结果去尝试,如果走的通就继续前进,否则就退回重新选择。在穷举的过程中可以利用剪枝去过滤一部分不可能的结果。
适用范围
回溯法是穷举,那么必然会得到一个问题的全部解,进而又可以得到一个问题的最优解。这两点也正是回溯法的适用范围。
这两种不同的情况又引出了两个属于,约束函数和限界函数。两者都属于剪枝函数,不同点在于约束函数是用来过滤不符合条件的解,限界函数用来过滤非最优解。
抽象 | 具体 | 意义 |
---|---|---|
剪枝函数 | 约束函数 | 得到全部解 |
剪枝函数 | 限界函数 | 得到最优解 |
经典解空间树
经过上述分析,回溯法就是在解空间中不断的剪枝而得到最终的解。最常见的两种解空间就是子集数和排列树。
子集树
假设问题是求字符串"0123"的子集,结合图来讲解子集树,这样更清楚。第1层的节点代表第一个元素,第n层的节点代表第n个元素,任意一个节点node,都有两个路径走向下一个元素的代表节点,权值为0的路径代表不取node,权值为1的路径代表取node。
package main
import "fmt"
func main() {
in := []byte("abc")
out := make([]int, len(in))
subset(in, out, len(in), 0)
}
// in:输入的数据
// out:保存的是对每个元素的选取结果
// size:输入数据的长度,可以不传参,在函数中使用len,这里更方便
// level:层数,本质上是以第几个元素做对象进行选择,编号从0方便编程。
func subset(in []byte, out []int, size, level int) {
//已经访问过叶节点,此时已经生成了一种解
if level == size {
fmtRst(in, out)
return
}
//当i=1递归时,其实就是从左子树回到当前节点然后选择了右子树的过程
for i := 0; i <= 1; i++ {
out[level] = i
subset(in, out, size, level+1)
}
}
// 因为利用out切片保存了对每个元素的选择结果
// 但是最终打印的是源数据的子集
// 这个函数根据源数据和对源数据的每个选择来打印
func fmtRst(in []byte, out []int) {
cnt := 0
for i, v := range out {
if v == 0 {
cnt++
continue
}
fmt.Printf("%c", in[i])
}
if cnt == len(in) {
fmt.Println("{}")
return
}
fmt.Println("")
}
排列树
假设问题是求字符串"123"的全排列,结合图来讲解排列树,这样更清楚。第1层的节点代表所有的源数据,第2层的节点代表的是从第一层的源数据抽取一个元素后剩下的源数据,第n层的节点代表的是从第n-1层的源数据抽取一个元素后剩下的数据。
package main
import "fmt"
func main() {
data := []byte("abc")
arrange(data, len(data), 0)
}
// in:源数据
// size:源数据个数
// level:当前应该处理哪一层
func arrange(data []byte, size, level int) {
//已经访问过叶节点,此时已经生成了一种解
if level == size {
fmt.Println(string(data))
return
}
//level之前都是排列好的
for i := level; i < size; i++ {
data[i], data[level] = data[level], data[i]
arrange(data, size, level+1)
data[i], data[level] = data[level], data[i]
}
}
关于作者
大四学生一枚,分析数据结构,面试题,golang,C语言等知识。QQ交流群:521625004。微信公众号:后台技术栈。