算法基础之回溯法

news2024/11/14 13:51:03

本文将详细介绍回溯法的基本原理和适用条件,并通过经典例题辅助读者理解回溯法的思想、掌握回溯法的使用。本文给出的例题包括:N皇后问题、子集和问题。

算法原理

在问题的解空间树中,回溯法按照深度优先的搜索策略,从根结点出发搜索解空间树,回溯法实际上一个类似穷举的搜索尝试过程。由于采用回溯法求解时存在退回到祖先结点的过程,所以需要保存搜索过的结点,可以自定义栈来保存祖先结点,也可以采用递归。

在搜索解空间时,通常采用两种策略避免无效搜索,以提高搜索效率。一是使用约束函数在拓展结点处剪除不满足约束条件的路径,二是使用限界函数剪去得不到问题解或最优解的路径,这两类函数统称为剪枝函数。

回溯法的一般非递归设计如下:

int x[n];	// 解向量
void backtrack(int n){
    int i=1;								// 根结点层次
    while(i>=1){
        if(ExistSubNode(t)){				// 当前结点存在子结点
            x[i]取一个可能的值;
            if(constraint(i)&&bound(i)){	// 满足约束条件和限界函数
                if(x为可行解)  输出x;			// 输出解
                else i++;					// 下一层次
            }
        }
        else i--;							// 回溯
    }
}

回溯法的一般递归设计如下:

int x[n];	// 解向量
void backtrack(int i){
    if(i>n) 输出结果;							// 到达叶子结点
    else {
        for(j=下界;j<=上界;j++){				// 枚举x[i]的所有可能
            x[i]=j;
            ...
            if(constraint(i)&&bound(i)) 		// 满足约束条件和限界函数
                backtrack(i+1);					// 下一层次
        }
    }
}

N皇后问题

题目描述

N N N 皇后问题要求在一个 N × N N×N N×N 的棋盘上放置 N N N 个皇后,使得它们彼此不受“攻击”。观察表明 N N N 皇后问题的解存在对称性,求其中不对称的那些解的数量。

输入输出

输入:皇后个数N。

输出:不对称的那些解。

解题思路

仔细观察 N N N 皇后的解,发现一种方案可以通过“对称”得到另一种方案。以“左右对称”为例,当 N = 5 N=5 N=5,限定第一行皇后在左边一半区域时,方案数为 6 6 6,如下图所示。

在这里插入图片描述

通过“左右对称”可以获得另一方案,同时发现,后面有两种方案重复,去除重复方案后,剩下的刚好是 N = 5 N=5 N=5 时的全部方案,如下图所示。

在这里插入图片描述

N N N 为偶数时关于中间那条线对称,当 N N N 为奇数时关于中间那一列对称。利用对称性可以使得工作量减少一半,为此,在放置皇后时,增加两条限制

  • 第一行的皇后只放在左边一半区域,也即位置小于等于 ( n + 1 ) / 2 (n+1)/2 (n+1)/2
  • N N N 为奇数且第一行皇后刚好放在 ( n + 1 ) / 2 (n+1)/2 (n+1)/2 位置(即中间)时,为避免重复,第二行皇后必须放在左边一半区域。
代码实现
int *Q, N, ANS; // 棋盘 棋盘大小 解的数量
int main() {
    printf("请输入棋盘大小N: ");
    while (cin >> N) {
        ANS = 0, Q = new int[N + 1], Q[1] = 0;
        // 求解
        queen();
        // N=1时无第二行,无法施加两条限制,特殊处理
        printf("解的总数为: %d\n", N!=1?ANS * 2:ANS); 
        printf("\n请输入棋盘大小N: ");
    }
}
/**
 * N皇后非递归回溯求解-基础实现
 * 所有下标均从1开始
 */
void queen() {
    // 第一个皇后
    int k = 1;
    // N是否为奇数、中间位置、当前行最多能放到第几列
    int odd = N & 1, M = (N + 1) >> 1, L;

    // 开始放置皇后
    while (k > 0) {
        // 第k个皇后尝试下一个位置
        Q[k]++;

        // 第一行放置的皇后不能超过中间
        if (k == 1)L = M;
        // N为奇数且第一行放在中间时,第二行不能超过中间
        else if (k == 2 && odd && Q[1] == M)L = M - 1;
        // 其它情况可以放到中间的右边
        else L = N; 

        // 寻找第k行的下一个可以放置的位置
        while (Q[k] <= L && !place(k))Q[k]++;

        // 已超过当前行的上限L,回溯,返回上一行
        if (Q[k] > L)--k;
        // 如果放置所有皇后,则打印结果,否则放置下一行
        else k == N ? (showRes(),ANS++) : Q[++k] = 0;
    }
}
/**
 * 判断第k个皇后当前位置是否合适 Q[k]是第k个皇后放置的位置
 * @param k 第k个皇后
 * @return 是否可以放置
 */
bool place(int k) {
    for (int i = 1; i < k; ++i)
        // 同列、同斜线已存在皇后
        if (Q[i] == Q[k] || abs(Q[i] - Q[k]) == abs(i - k))return 0;
    return 1;
}
/**
 * 打印可行解
 */
void show() {
    printf("(");
	for (int i = 1; i <= N; ++i)printf("%d,", Q[i]);
    printf("\b)\n");
}

时间复杂度: O ( n n ) O(n^n) O(nn)

空间复杂度: O ( n ) O(n) O(n)

子集和问题

题目描述

已知包含 n n n 个不同正整数 w i w_i wi 的集合, ( 0 ≤ i ≤ n − 1 ) (0≤i≤n-1) (0in1),求该集合的所有满足条件的子集,使得每个子集中的正整数之和等于另一个给定的正整数 W W W

输入输出

输入:一行输入 n n n W W W 的值,第二行输入 n n n 个不同的正整数 w i w_i wi

输出:如果有答案,则输出所有满足条件的子集(用固定长度 n n n 元组 x x x 表示, x i x_i xi 0 0 0 1 1 1)。如果没有答案,则输出 n o   s o l u t i o n ! no\ solution! no solution!

解题思路

N = 4 N=4 N=4 时,解空间树如下图所示。其中,结点中的数字为结点的编号,规定往结点左边“走”,对应 x i x_i xi 记为 1 1 1,即表示选取第 i i i 个正整数,往结点右边“走”,对应 x i x_i xi 记为 0 0 0,即表示不选取第 i i i 个正整数。

在这里插入图片描述

集合中的正整数按输入顺序从 0 0 0 开始编号,设数组 x x x x [ i ] = 1 x[i]=1 x[i]=1 表示选择第 i i i 个正整数, x [ i ] = 0 x[i]=0 x[i]=0 表示不选择第 i i i 个正整数。 s w sw sw 记录尝试选取第 i i i 个正整数时,下标为 0 0 0 i − 1 i-1 i1 的正整数中已选取的正整数的和, u w uw uw 记录尝试选取第 i i i 个正整数时,下标为 i + 1 i+1 i+1 n − 1 n-1 n1 的正整数的和。当面对第 i i i 个正整数时,需要依次尝试选取第 i i i 个和不选取第 i i i 个。

尝试选取第 i i i 个时,先判断第 i i i 个是否可选。若 s w + w [ i ] ≤ W sw+w[i]≤W sw+w[i]W,即,在假定选取第 i i i 个的情况下,已选正整数的和没有超过给定正整数 W W W,则第 i i i 个可选,否则,不可选,剪掉左枝。

例如,当前处于上图中的结点 4 4 4,考虑选取第 3 3 3 个正整数,到达结点 8 8 8,如果已选取的正整数的和超过给定正整数 W W W,则剪去以结点 8 8 8 为根结点的二叉树,因为,结点 8 8 8 往下,无论做何选择,已选正整数的和都不可能为 W W W(在结点 8 8 8 时就已经超过 W W W)。

尝试不选取第 i i i 个时,先判断此后是否存在解。若 s w + u w ≥ W sw+uw≥W sw+uwW,即,在假定不选取第 i i i 个的情况下,此后已选正整数的和仍有可能达到 W W W,则此后存在解,否则,此后不存在解,剪掉右枝。

例如,当前处于上图中的第 4 4 4 个节点,考虑不选取第 3 3 3 个正整数,到达结点 9 9 9,如果从结点 9 9 9 开始,一直往左“走”也无法达到给定正整数 W W W,则剪去以结点 9 9 9 为根节点的子树,因为,此后最大和都已不可能达到 W W W

代码实现
int N, W;      // 正整数个数 指定和
int *w, *x;    // 正整数 解
int main() {
    int rw = 0;
    // 输入正整数个数N
    cin >> N >> W;                   
    w = new int[N], x = new int[N];
    // 输入N个正整数
    for (int i = 0; i < N;rw += w[i++])cin >> w[i]; 
    // 求解
    solve(0, 0, rw);
}
/**
 * 尝试选取第i个正整数(i从0开始)
 * 面对第i个正整数,需要依次尝试选取第i个和不选取第i个
 *  1.选第i个: 选第i个前判断第i个是否可选,sw+W[i]<=W即为可选
 *  2.不选第i个: 不选第i个前,判断此后是否存在解,sw+uw>=W即为存在解
 * @param i 第i个正整数
 * @param sw [0,i-1]已选正整数的和
 * @param uw [i+1,n-1]的和
 */
void solve(int i, int sw, int uw) {
    // 已达叶子结点
    if (i >= N) {
        if (sw == W)ANS++,show(); //找到一个解
        return;
    }
    // 选取第i个,未超过W(超过则剪掉左枝)
    if (sw + w[i] <= W) { 
        x[i] = 1;								// 选取第i个
        solve(i + 1, sw + w[i], uw - w[i]);		// 尝试选取第i+1个
    }
    // 不选第i个,此后存在解(不存在则剪掉右枝)
    if (sw + uw >= W) {   
        x[i] = 0;							// 不选取第i个
        solve(i + 1, sw, uw - w[i]);		// 尝试选取第i+1个
    }
}
/**
 * 输出解
 */
void show() {
    printf("(%d", x[0]);
    for (int i = 1; i < N;) printf(",%d", x[i++]);
    printf(")\n");
}

时间复杂度: O ( 2 n ) O(2^n) O(2n)

空间复杂度: O ( n ) O(n) O(n)

经验总结

回溯法与深度优先遍历非常相似,剪枝是回溯法的一个明显特征,但并不是任何回溯法都包含剪枝,因而很难区分回溯法与深度优先遍历,广义来讲,带回退的算法都是回溯算法。

如果仅仅采用深度优先,那么需要遍历整个解空间,与穷举并无太大区别,此时的回溯法可看做按深度优先+穷举,穷举的时间复杂度无疑是较高的。为了提高搜索的效率,在搜索解空间时,需要在拓展结点处剪除不满足约束条件的路径和得不到问题解或最优解的路径。不满足约束条件是指,当前路径已经不满足题目对解的要求,说明此后含有该路径的路径也必然不符合要求(算法开始前可能需要对元素进行排序才能满足这一点),因此,没必要在此基础上继续尝试。得不到问题解或最优解是指,虽然当前路径目前来说是合法的,但此后即使“使尽全力“也无法达到要求,此时也没必要在此基础上继续尝试。不满足约束条件和得不到问题解或最优解,前者侧重考虑当前,后者侧重考虑将来。

回溯法常采用递归来实现,在递归调用返回时会自动回退和恢复,但也可采用非递归实现,如本实验的N皇后问题的实现,此时需要编码实现回退和恢复。采用递归实现,代码简洁,采用非递归实现,需要处理的问题较多,逻辑稍微更加复杂,代码量相对而言可能更多。

END

文章文档:私信回复关键字可获取本文文档。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1935933.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LDR6020:重塑iPad一体式有线键盘体验的创新力量

在移动办公与娱乐日益融合的时代&#xff0c;iPad凭借其强大的性能和便携性&#xff0c;成为了众多用户不可或缺的生产力工具。然而&#xff0c;为了进一步提升iPad的使用体验&#xff0c;一款高效、便捷的键盘成为了不可或缺的配件。今天&#xff0c;我们要介绍的&#xff0c;…

TYPE-C接口PD取电快充协议芯片ECP5701:支持PD 2.0和PD 3.0(5V,9V,12V,15V,20V)

随着智能设备的普及&#xff0c;快充技术成为了越来越多用户的刚需。而TYPE-C接口作为新一代的USB接口&#xff0c;具有正反插、传输速度快、充电体验好等优点&#xff0c;已经成为了快充技术的主要接口形式。而TYPE-C接口的PD&#xff08;Power Delivery&#xff09;取电快充协…

【数据结构】线性结构——数组、链表、栈和队列

目录 前言 一、数组&#xff08;Array&#xff09; 1.1优点 1.2缺点 1.3适用场景 二、链表&#xff08;Linked List&#xff09; 2.1优点 2.2缺点 2.3适用场景 三、栈&#xff08;Stack&#xff09; 3.1优点 3.2缺点 3.3适用场景 四、队列&#xff08;Queue&#xff09; 4.1优点…

【python】Python高阶函数--reduce函数的高阶用法解析与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Redis常用的5大数据类型

Reids字符串&#xff08;String&#xff09; 设置相同的key&#xff0c;之前内容会覆盖掉 Redis列表&#xff08;List&#xff09; 常用命令 从左往右放值 数据结构 Redis集合&#xff08;set&#xff09; sadd<key><value1><value2>...... 数据结构 Set数据…

前端组件化开发:以Vue自定义底部操作栏组件为例

摘要 随着前端技术的不断演进&#xff0c;组件化开发逐渐成为提升前端开发效率和代码可维护性的关键手段。本文将通过介绍一款Vue自定义的底部操作栏组件&#xff0c;探讨前端组件化开发的重要性、实践过程及其带来的优势。 一、引言 随着Web应用的日益复杂&#xff0c;传统的…

「豆包Marscode体验官」 | 云端 IDE 启动 Rust 体验

theme: cyanosis 我正在参加「豆包MarsCode初体验」征文活动 MarsCode 可以看作一个运行在服务端的远程 VSCode开发环境。 对于我这种想要学习体验某些语言&#xff0c;但不想在电脑里装环境的人来说非常友好。本文就来介绍一下在 MarsCode里&#xff0c;我的体验 rust 开发体验…

Games101学习笔记 Lecture22 Animation(cont.)

Lecture22 Animation(cont. 一、单个粒子模拟Ordinary Differential Equation ODE 常微分方程ODE求解方法——欧拉方法解决不稳定中点法改进欧拉方法自适应步长隐式欧拉方法 二、流体模拟基于位置的方法物质点方法 一、单个粒子模拟 想模拟粒子在场中的运动 Ordinary Differe…

Token Labeling(NeurIPS 2021, ByteDance)论文解读

paper&#xff1a;All Tokens Matter: Token Labeling for Training Better Vision Transformers official implementation&#xff1a;https://github.com/zihangJiang/TokenLabeling 出发点 ViTs的局限性&#xff1a;尽管ViTs在捕捉长距离依赖方面表现出色&#xff0c; 但…

代码随想录算法训练营第五十八天|108.冗余连接、109.冗余连接II

108.冗余连接 题目链接&#xff1a;108.冗余连接 文档讲解&#xff1a;代码随想录 状态&#xff1a;还行 思路&#xff1a; 并查集可以解决什么问题&#xff1a;两个节点是否在一个集合&#xff0c;也可以将两个节点添加到一个集合中。 题解&#xff1a; public class Main {p…

套用BI方案做数据可视化是种什么体验?

在数字化转型的浪潮中&#xff0c;数据可视化作为连接数据与决策的桥梁&#xff0c;其重要性日益凸显。近期&#xff0c;我有幸体验了奥威BI方案进行数据可视化的全过程&#xff0c;这不仅是一次技术上的探索&#xff0c;更是一次对高效、智能数据分析的深刻感受。 初识奥威&a…

.net dataexcel 脚本公式 函数源码

示例如: ScriptExec(""sum(1, 2, 3, 4)"") 结果等于10 using Feng.Excel.Builder; using Feng.Excel.Collections; using Feng.Excel.Interfaces; using Feng.Script.CBEexpress; using Feng.Script.Method; using System; using System.Collections.Gen…

场景分析法挖掘需求的常见4大步骤

场景分析方法&#xff0c;有助于精确定位需求&#xff0c;优化产品设计&#xff0c;促进团队协同&#xff0c;减少项目风险&#xff0c;提升用户满意度与市场竞争力。若场景分析不足&#xff0c;产品可能偏离用户需求&#xff0c;导致功能冗余或缺失&#xff0c;用户体验差&…

java中传引用问题

在 Java 中&#xff0c;所有对象都是通过引用传递的&#xff0c;而基本数据类型是通过值传递的。 引用传递&#xff1a; 当一个对象作为参数传递给方法时&#xff0c;传递的是对象的引用。对这个对象引用进行的修改会影响到原始对象。例如&#xff1a; public class Test {p…

Designing Data-Intensive Applications数据密集型应用系统设计-读书笔记

目录 第一部分可靠性、可扩展性、可维护性硬件故障描述负载 吞吐与延迟可维护性 第二章 数据模型与查询语言第三章索引哈希索引B-tree事务 第三章 编码第二部分、分布式数据系统第五章 数据复制单主从复制节点失效日志实现复制滞后问题 多主节点复制 第六章、数据分区3 第一部分…

10个常见的电缆载流表,值得收藏!

众所周知,电线电缆的载流是所有电工、电气人员都必须具备的基本储备,但是如果要将那么多的“数字”都记得清清楚楚,还是有一点困难的!今天咱们就做了一个电力电缆载流量对照表,速度收藏!下次参考不迷路! 1、0.6/1KV聚氯乙烯绝缘电力电缆载流量 以上电缆载流量计算条件:…

世界启动Ⅳ--利用AI和费曼技巧学习一切

前言 有无数的学习技巧可以帮助你消化复杂的概念&#xff0c;并有信心记住它们。如果你像我一样是一个不断学习的学生&#xff0c;你就会明白有效学习方法的重要性。其中最简单的一种就是费曼技巧。 在本文中&#xff0c;我将解释如何有效地应用费曼学习方法&#xff0c;以及…

应用最优化方法及MATLAB实现——第5章代码实现

一、概述 继上一章代码后&#xff0c;这篇主要是针对于第5章代码的实现。部分代码有更改&#xff0c;会在下面说明&#xff0c;程序运行结果跟书中不完全一样&#xff0c;因为部分参数&#xff0c;书中并没有给出其在运行时设置的值&#xff0c;所以我根据我自己的调试进行了设…

迁移学习在乳腺浸润性导管癌病理图像分类中的应用

1. 引言 乳腺癌主要有两种类型:原位癌:原位癌是非常早期的癌症&#xff0c;开始在乳管中扩散&#xff0c;但没有扩散到乳房组织的其他部分。这也称为导管原位癌(DCIS)。浸润性乳腺癌:浸润性乳腺癌已经扩散(侵入)到周围的乳腺组织。侵袭性癌症比原位癌更难治愈。将乳汁输送到乳…

C++中的new和模版

前言 随着C的学习&#xff0c;讲了C的发展过程、流插入、流提取、函数缺省值、类与构造等等。接下来学习C很方便的 玩意&#xff0c;函数模版。函数模版就像是模具一样&#xff0c;C会自动用模版编译出合适的函数供程序员使用。以前不同类型相同操作的函数都能通过函数模版&…