这篇文章将介绍回溯算法与分支限界算法的有关概念、具体应用及代码等内容。
一、回溯法
1.1 概念
回溯法是一种试探法,所以它也叫试探算法。它尝试构建问题的解,并且在发现解不满足条件的时候撤销选择(即“回溯”),尝试其他路径来寻找可行解。
1.2 举例
有一不等式如下:
求符合条件的所有整数解。
你们对于这个问题我们如果采用回溯的算法则可以有如下的代码:
def save(x, y, z):if 3 * x + 4 * y + 2 * z > 12:return Falseelse:print(x, y, z)# 处理 x, y, z 的递增情况,并递归调用if x < 5:save(x + 1, y, z)if y < 4:save(x, y + 1, z)if z < 7:save(x, y, z + 1)if __name__ == '__main__':save(0, 0, 0)
关于代码的输出因为太多了,所以就不给出了。不过在这里可以给出它的搜索树:
(0, 0, 0)/ | \/ | \(1,0,0) (0,1,0) (0,0,1)/ \ / \ / \
(2,0,0)(1,1,0)(0,2,0)(0,1,1)(0,0,2)/ \ ... ...
我们分析这个代码,发现它与遍历有些相似,但不同的是,在遍历中,如果遇到了不符合的结果时代码还将以往之前,不撞南墙不后悔,但回溯不同,它在遇到不符合条件的情况时就直接停止了,不再继续浪费资源去运算了。 即它在发现总和在大于目标总和时会及时停止,而在达到目标要求时会输出结果,同时在每个解上进行递增以至到达解允许达到的最大边界。
我们再给出一个例子,这是一个关于4*4的拉丁方(Latin)的例子,要求在一个4*4的格子中填入1,2,3,4以使得每一行每一列都没有重复的数字。例如如下的这个拉丁方:
1 2 3 4
3 4 1 2
4 3 2 1
2 1 4 3
那么我们可以给出如下的代码:
def is_valid(square, row, col, num):# 检查同一行是否有重复for i in range(4):if square[row][i] == num:return False# 检查同一列是否有重复for i in range(4):if square[i][col] == num:return Falsereturn Truedef backtrack(square, row, col):if col == 4:backtrack(square, row + 1, 0)returnif row == 4:print_square(square) # 打印解决方案returnfor num in range(1, 5):if is_valid(square, row, col, num):square[row][col] = numbacktrack(square, row, col + 1)square[row][col] = 0 # 回溯def print_square(square):path=r"C:\Users\20349\Desktop\Studing\studying\算法设计与分析\homework\Latin.txt"with open(path,'a') as result:for row in square:result.write(" ".join(map(str, row))+"\n")result.write("\n")square = [[0]*4 for _ in range(4)]
backtrack(square, 0, 0)
1.3 算法步骤总结
我们可以总结出步骤如下:
1.定义状态表示,即如何表示当前问题的部分解;
2.定义终止条件,确定何时到达问题的解空间边界,即何时停止搜索;
3.定义递归的框架,并确定如何对解进行扩展;
4.检查状态的有效性,即检查新生成的状态是否符合给定的约束条件,若不符合则及时回溯;
5.记录解,通过输出、储存等方式将当前得到的解及时记录下来;
6.回溯,当搜索无法前进时(即达到叶子节点或发现当前路径不可行),回溯到上一个状态;
7.剪枝,在搜索过程中,尽可能早地剪除那些不可能导致最优解的分支,这可以通过设定上下界、预估解的质量等方式实现。
1.4 决策树
在回溯算法中,我们是可以通过决策树来展示整个算法求解的过程的。决策树是一种监督学习方法,它通过一系列的判断(节点)来做出分类或回归决策。每一个节点代表一个判断标准,而每条分支代表了基于这一标准的不同结果。最终,沿着决策树的路径,我们会到达叶节点,即最终的决策结果。
我在之前曾经写过关于决策树的文章,不了解的可以回看那篇文章,此外,我再在这里给出一个决策树的代码:
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, plot_tree
import matplotlib.pyplot as plt# 加载数据集
iris = load_iris()
X = iris.data
y = iris.target
feature_names = iris.feature_names
target_names = iris.target_names# 创建决策树分类器
clf = DecisionTreeClassifier(random_state=42)
clf.fit(X, y)# 可视化决策树
plt.figure(figsize=(40, 20))
plot_tree(clf, filled=True, feature_names=feature_names, class_names=target_names, fontsize=10)
plt.title("Decision Tree for Iris Dataset")
plt.show()
代码绘制的决策树如下:
不过机器学习的决策树与回溯算法里的决策树还是存在一些不同之处的:
首先在机器学习中,决策树是为了分类和回归任务,通过对数据进行分割,找到最优的决策规则,从而对新数据进行预测。而在回溯算法中,它用于搜索问题的解空间,通过回溯法逐步尝试不同的决策路径,直到找到满足条件的解或确定没有解;
此外在构建上也有所不同,机器学习中,决策树是通过递归地选择最佳特征进行分割,通常使用信息增益、基尼不纯度等指标来评估特征的重要性。而回溯算法中则是通过递归地尝试不同的决策路径,如果某条路径不满足条件,则回溯到上一个节点,尝试其他路径;
然后在停止条件上,机器学习中通常基于数据集的纯度(如所有样本属于同一类)、树的深度、最小样本数等。而在回溯算法里通常基于问题的具体要求,如找到一个解、遍历完所有可能的路径等。
最后,我在这里给出上述的Latin方的绘制决策树的代码:
import networkx as nx
import matplotlib.pyplot as plt# 记录决策路径的字典
decision_tree = {}
current_path = []def is_valid(square, row, col, num):# 检查同一行是否有重复for i in range(4):if square[row][i] == num:return False# 检查同一列是否有重复for i in range(4):if square[i][col] == num:return Falsereturn Truedef backtrack(square, row, col, parent=None):if col == 4:backtrack(square, row + 1, 0, parent)returnif row == 4:print_square(square) # 打印解决方案returnfor num in range(1, 5):if is_valid(square, row, col, num):square[row][col] = numcurrent_path.append((row, col, num))node_id = hash(tuple(map(tuple, square)))decision_tree[node_id] = (tuple(map(tuple, square)), parent)backtrack(square, row, col + 1, node_id)current_path.pop()square[row][col] = 0 # 回溯def print_square(square):path = r"C:\Users\20349\Desktop\Studing\studying\算法设计与分析\homework\Latin.txt"with open(path, 'a') as result:for row in square:result.write(" ".join(map(str, row)) + "\n")result.write("\n")# 初始化拉丁方阵
square = [[0] * 4 for _ in range(4)]
backtrack(square, 0, 0)# 构建树结构
G = nx.DiGraph()
for node_id, (state, parent) in decision_tree.items():G.add_node(node_id, state=state)if parent is not None:G.add_edge(parent, node_id)# 绘制树
plt.figure(figsize=(12, 8))
pos = nx.spring_layout(G, k=0.5, iterations=20)
node_labels = {node: str(state) for node, state in nx.get_node_attributes(G, 'state').items()}
nx.draw(G, pos, with_labels=True, labels=node_labels, node_size=3000, node_color='lightblue', font_size=10, font_weight='bold')
plt.title("Decision Tree for Latin Square Backtracking")
plt.show()
因为这个的绘制太过复杂,最后的结果不易展示,所以就不给出图像了。
二、分支限界
分支限界法和回溯法都是可以用于进行组合优化求解的,在某种程度上,可以说分支限界法是回溯法的一种变种,不过也存在一些不同之处。
首先我们来看它的相似之处:它们都通过树形结构来表示问题的解空间,然后也都通过递归来探索解空间,此外,它们都可以用剪枝技术来减少需要的解空间,以此节约资源。
接下来是不同之处,在具体的剪枝方法上,回溯是依靠是否满足约束条件来进行剪枝,而分支限界法则在考虑是否满足约束条件的同时还通过界限检查来剪枝。计算每个子问题的界限(通常是下界或上界),如果子问题的界限超出已知最优解的界限,则剪枝该子问题;
在探索策略上,回溯一般采用深度优先搜索(DFS)策略,逐层深入,直到找到一个解或确定没有解。而分支限界则考虑多种算法,比如广度优先搜索(BFS)、深度优先搜索(DFS)和优先队列式搜索(Best-First Search);
在寻找解上,回溯更多的是寻找可行解,而分支限界是去寻找最优解。
在这里给出一个分支限界的代码:
class Item:def __init__(self, weight, value):self.weight = weightself.value = valueself.ratio = value / weightdef knapsack_branch_and_bound(capacity, items):items.sort(key=lambda x: x.ratio, reverse=True)best_value = 0queue = [(0, 0, 0)] # (index, current_weight, current_value)while queue:index, current_weight, current_value = queue.pop(0)if index < len(items):item = items[index]# 如果当前物品可以加入背包if current_weight + item.weight <= capacity:new_value = current_value + item.valuenew_weight = current_weight + item.weightif new_value > best_value:best_value = new_valuequeue.append((index + 1, new_weight, new_value))# 不选择当前物品的情况queue.append((index + 1, current_weight, current_value))return best_value# 测试代码
if __name__ == "__main__":items = [Item(10, 60), Item(20, 100), Item(30, 120)]capacity = 50max_value = knapsack_branch_and_bound(capacity, items)print("The maximum value that can be put in the knapsack is", max_value)
代码的运行结果为:
The maximum value that can be put in the knapsack is 220
这些就是关于回溯与分支限界的一些简单的介绍,因为在这里分支限界仅是对于回溯的一个简单拓展,所以没有过多叙述,但实际上关于分支限界的介绍还应有许多,以后也许会再有所介绍。
此上