回溯算法解决八皇后问题、0-1背包问题(附代码实现)

324 阅读8分钟

如何理解“回溯算法”?

回溯算法是一种通过尝试和错误来解决问题的算法设计方法。它通常用于解决组合问题、排列问题和其他需要探索所有可能解的情境。

基本概念

  1. 搜索树:回溯算法可以视为在一个搜索树中进行深度优先搜索。每个节点代表一个状态,算法会尝试从当前状态扩展到下一个状态。

  2. 选择和约束:在每一步,算法会做出一个选择,然后检查该选择是否满足约束条件。如果满足,则继续深入;如果不满足,则回溯到上一步,尝试其他选择。

  3. 剪枝:回溯算法的一个重要特点是可以通过剪枝来减少不必要的搜索。例如,当发现某个选择不可能导致有效解时,可以立即停止该路径的探索。

过程

  1. 选择:在当前状态下,选择一个选项。
  2. 约束:检查选择是否满足问题的约束条件。
  3. 递归:如果满足条件,递归调用回溯算法;如果不满足,则回退到上一步。
  4. 结果:当找到一个解时,可以选择继续搜索以找到所有可能的解,或者停止。

应用场景

回溯算法常用于解决需要试探多个可能解的复杂问题,尤其是那些可以用递归方式解决的问题。常见的应用包括

  • 八皇后问题:在棋盘上放置八个皇后,使得它们互不攻击。
  • 组合问题:从一组元素中选择特定数量的组合。
  • 排列问题:生成给定元素的所有排列。

示例1

八皇后问题简介

八皇后问题是经典的回溯算法应用。问题要求在8x8的棋盘上放置8个皇后,使得她们之间互不攻击。这意味着任何两个皇后不能在同一行、同一列或同一对角线上。

回溯算法原理

八皇后问题的回溯算法通过逐行放置皇后,检查每个放置是否安全(即不会与之前放置的皇后冲突),如果安全则继续在下一行放置皇后;如果不安全,则回退到上一行,尝试在其他列放置皇后。

算法步骤:

  1. 初始化棋盘:创建一个8x8的棋盘或者用一个数组来表示每一行中的皇后位置。
  2. 递归放置皇后
    • 从第一行开始,尝试在每一列放置皇后。
    • 对于每一列,检查是否与之前放置的皇后冲突。
    • 如果安全,则递归地尝试在下一行放置皇后。
    • 如果在某一行找不到合适的位置,则回溯到上一行,改变之前的放置位置。
  3. 终止条件:当所有8个皇后都成功放置时,记录一种解法。
  4. 输出结果:输出所有可能的解法。

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: &currentItems)
        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: &currentItems)
            currentItems.removeLast()  // 回溯,撤销选择
        }
        
        // 不选择当前物品
        solveKnapsack(weights: weights,
                      values: values,
                      capacity: capacity,  // 不改变容量
                      currentIndex: currentIndex + 1,  // 移动到下一个物品
                      currentProfit: currentProfit,  // 保持当前收益不变
                      currentItems: &currentItems)
    }
}

// 使用例子
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]

代码说明

  1. KnapsackSolver 类:

    • 负责解决背包问题,计算最大收益和选择的物品。
  2. 属性:

    • maxProfit: 当前的最大收益。
    • bestItems: 存储最佳物品的索引。
  3. 方法 knapsack:

    • 接受物品的重量、价值和背包的容量作为输入。
    • 初始化相关属性,调用 solveKnapsack 方法开始递归计算。
  4. 方法 solveKnapsack:

    • 递归方法,尝试选择或不选择当前物品。
    • 终止条件:
      • 如果遍历完所有物品或背包容量为 0,检查并更新最大收益和最佳物品。
    • 选择当前物品:
      • 如果当前物品的重量不超过剩余容量,记录选择的物品,递归调用。
      • 回溯时撤销选择。
    • 不选择当前物品:
      • 直接递归到下一个物品,保持当前收益不变。

复杂度分析

  • 时间复杂度:最坏情况下是 O(2^n),因为对于每个物品都有两种选择(选择或不选择),导致指数级的组合数量。
  • 空间复杂度:主要是递归调用栈的空间,最坏情况下为 O(n),其中 n 是物品数量。