如何理解“回溯算法”?
回溯算法是一种通过尝试和错误来解决问题的算法设计方法。它通常用于解决组合问题、排列问题和其他需要探索所有可能解的情境。
基本概念
-
搜索树:回溯算法可以视为在一个搜索树中进行深度优先搜索。每个节点代表一个状态,算法会尝试从当前状态扩展到下一个状态。
-
选择和约束:在每一步,算法会做出一个选择,然后检查该选择是否满足约束条件。如果满足,则继续深入;如果不满足,则回溯到上一步,尝试其他选择。
-
剪枝:回溯算法的一个重要特点是可以通过剪枝来减少不必要的搜索。例如,当发现某个选择不可能导致有效解时,可以立即停止该路径的探索。
过程
- 选择:在当前状态下,选择一个选项。
- 约束:检查选择是否满足问题的约束条件。
- 递归:如果满足条件,递归调用回溯算法;如果不满足,则回退到上一步。
- 结果:当找到一个解时,可以选择继续搜索以找到所有可能的解,或者停止。
应用场景
回溯算法常用于解决需要试探多个可能解的复杂问题,尤其是那些可以用递归方式解决的问题。常见的应用包括
- 八皇后问题:在棋盘上放置八个皇后,使得它们互不攻击。
- 组合问题:从一组元素中选择特定数量的组合。
- 排列问题:生成给定元素的所有排列。
示例1
八皇后问题简介
八皇后问题是经典的回溯算法应用。问题要求在8x8的棋盘上放置8个皇后,使得她们之间互不攻击。这意味着任何两个皇后不能在同一行、同一列或同一对角线上。
回溯算法原理
八皇后问题的回溯算法通过逐行放置皇后,检查每个放置是否安全(即不会与之前放置的皇后冲突),如果安全则继续在下一行放置皇后;如果不安全,则回退到上一行,尝试在其他列放置皇后。
算法步骤:
- 初始化棋盘:创建一个8x8的棋盘或者用一个数组来表示每一行中的皇后位置。
- 递归放置皇后:
- 从第一行开始,尝试在每一列放置皇后。
- 对于每一列,检查是否与之前放置的皇后冲突。
- 如果安全,则递归地尝试在下一行放置皇后。
- 如果在某一行找不到合适的位置,则回溯到上一行,改变之前的放置位置。
- 终止条件:当所有8个皇后都成功放置时,记录一种解法。
- 输出结果:输出所有可能的解法。
Swift代码实现
//
// ViewController_Queen.swift
// Test
//
// Created by Yin123456 on 2024/8/29.
//
import UIKit
class EightQueens {
// 存储所有找到的解决方案
private var solutions: [[Int]] = []
// 用于跟踪每一行皇后的列位置
private var queens: [Int] = []
// 皇后的数量,默认为 8
private let n: Int
// 初始化方法
init(n: Int = 8) {
self.n = n
// 初始化 queens 数组,初始值为 -1,表示未放置皇后
self.queens = Array(repeating: -1, count: n)
}
// 解决八皇后问题
func solve() -> [[Int]] {
placeQueen(row: 0) // 从第 0 行开始放置
return solutions // 返回所有找到的解决方案
}
// 递归方法,尝试在给定行放置皇后
private func placeQueen(row: Int) {
// 如果已放置 n 个皇后,记录解决方案
if row == n {
solutions.append(queens)
return
}
// 遍历当前行的每一列
for col in 0..<n {
// 检查该位置是否安全
if isSafe(row: row, col: col) {
queens[row] = col // 放置皇后
// 递归调用,尝试放置下一行的皇后
placeQueen(row: row + 1)
queens[row] = -1 // 回溯,撤销选择
}
}
}
// 检查在给定位置放置皇后是否安全
private func isSafe(row: Int, col: Int) -> Bool {
// 遍历之前放置的皇后,检查是否有冲突
for i in 0..<row {
let queenCol = queens[i]
// 检查同一列、主对角线和副对角线的冲突
if queenCol == col || // 同一列
queenCol - i == col - row || // 主对角线
queenCol + i == col + row { // 副对角线
return false // 不安全
}
}
return true // 安全
}
// 打印所有找到的解决方案
func printSolutions() {
for solution in solutions {
for row in 0..<n {
var rowString = ""
for col in 0..<n {
// 如果该列有皇后,加入 "Q",否则加入 "."
if solution[row] == col {
rowString += "Q "
} else {
rowString += ". "
}
}
print(rowString) // 打印当前行
}
print("\n") // 每个解之间空一行
}
}
}
class ViewController_Queen: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 使用例子
let eightQueens = EightQueens() // 创建八皇后实例
let solutions = eightQueens.solve() // 解决问题并获取解决方案
eightQueens.printSolutions() // 打印解决方案
print("总共有 \(solutions.count) 种解法") // 输出解的数量
}
/*
// MARK: - Navigation
// 在基于故事板的应用程序中,通常需要在导航前进行一些准备
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// 获取新的视图控制器
// 将选定的对象传递给新的视图控制器
}
*/
}
代码解释
EightQueens
类:封装了八皇后问题的解决方案。solve()
方法:开始求解八皇后问题,并返回所有可能的解法(每个解法是一个包含8个整数的数组,表示每行皇后所在的列号)。placeQueen(row:)
方法:递归地尝试在指定行放置皇后,若成功放置,则继续下一行,否则回溯。isSafe(row:col:)
方法:检查在指定位置放置皇后是否安全,即不会与之前放置的皇后冲突。printSolutions()
方法:打印所有找到的解法,每个解法以棋盘形式展示。
示例2
0-1 背包问题简介
0-1 背包问题是一种经典的优化问题,其描述如下:给定一个容量为 W
的背包和 n
个物品,每个物品有一个重量 w_i
和价值 v_i
。在不超过背包容量的情况下,选择最优的物品组合使得背包中物品的总价值最大。0-1 背包的“0-1”指的是每个物品只能选择一次(要么选中,要么不选)。
回溯算法原理
回溯算法通过递归遍历所有可能的物品组合,逐步构建可能的解,并在找到不符合条件的组合时回溯,探索其他可能的组合。
import Foundation
// 背包问题求解类
class KnapsackSolver {
private var maxProfit = 0 // 当前最大收益
private var bestItems: [Int] = [] // 存储最佳物品的索引
// 主方法,接受物品的重量、价值和背包的容量
func knapsack(weights: [Int], values: [Int], capacity: Int) -> (Int, [Int]) {
maxProfit = 0 // 初始化最大收益
bestItems = [] // 初始化最佳物品
var currentItems: [Int] = [] // 当前选择的物品
// 开始解决背包问题
solveKnapsack(weights: weights, values: values, capacity: capacity, currentIndex: 0, currentProfit: 0, currentItems: ¤tItems)
return (maxProfit, bestItems) // 返回最大收益和最佳物品
}
// 递归方法,尝试选择或不选择当前物品
private func solveKnapsack(weights: [Int], values: [Int], capacity: Int, currentIndex: Int, currentProfit: Int, currentItems: inout [Int]) {
// 终止条件:遍历完所有物品或背包容量为 0
if currentIndex == weights.count || capacity <= 0 {
// 更新最大收益和最佳物品
if currentProfit > maxProfit {
maxProfit = currentProfit
bestItems = currentItems
}
return
}
// 选择当前物品
if weights[currentIndex] <= capacity {
currentItems.append(currentIndex) // 记录选择的物品
solveKnapsack(weights: weights,
values: values,
capacity: capacity - weights[currentIndex], // 减去当前物品的重量
currentIndex: currentIndex + 1, // 移动到下一个物品
currentProfit: currentProfit + values[currentIndex], // 增加当前物品的价值
currentItems: ¤tItems)
currentItems.removeLast() // 回溯,撤销选择
}
// 不选择当前物品
solveKnapsack(weights: weights,
values: values,
capacity: capacity, // 不改变容量
currentIndex: currentIndex + 1, // 移动到下一个物品
currentProfit: currentProfit, // 保持当前收益不变
currentItems: ¤tItems)
}
}
// 使用例子
let weights = [1, 2, 3, 5] // 物品的重量
let values = [20, 30, 50, 60] // 物品的价值
let capacity = 5 // 背包的容量
let solver = KnapsackSolver() // 创建背包求解器实例
let (maxProfit, bestItems) = solver.knapsack(weights: weights, values: values, capacity: capacity) // 计算最大收益和最佳物品
print("最大收益是: \(maxProfit)") // 输出: 最大收益是: 80
print("选择的物品是: \(bestItems)") // 输出: 选择的物品是: [1, 2]
代码说明
-
KnapsackSolver 类:
- 负责解决背包问题,计算最大收益和选择的物品。
-
属性:
maxProfit
: 当前的最大收益。bestItems
: 存储最佳物品的索引。
-
方法
knapsack
:- 接受物品的重量、价值和背包的容量作为输入。
- 初始化相关属性,调用
solveKnapsack
方法开始递归计算。
-
方法
solveKnapsack
:- 递归方法,尝试选择或不选择当前物品。
- 终止条件:
- 如果遍历完所有物品或背包容量为 0,检查并更新最大收益和最佳物品。
- 选择当前物品:
- 如果当前物品的重量不超过剩余容量,记录选择的物品,递归调用。
- 回溯时撤销选择。
- 不选择当前物品:
- 直接递归到下一个物品,保持当前收益不变。
复杂度分析
- 时间复杂度:最坏情况下是
O(2^n)
,因为对于每个物品都有两种选择(选择或不选择),导致指数级的组合数量。 - 空间复杂度:主要是递归调用栈的空间,最坏情况下为
O(n)
,其中n
是物品数量。