浅说深度优先搜索(中)——回溯

写在最前

相信在你们不懈的努力之下,基本的递归一定可以写出来了,那么我们现在就来看看递归的升级版——回溯怎么写吧!

简说回溯

递归是一种特别重要的解题策略。大部分题目需要找到最优解,而这个求解过程可能存在一定的规律性,比如贪心,递推等,但是大部分情况可能只能暴力枚举所有情况找到答案,而暴力枚举这个过程简单的可以用循环实现,但是对于一些比较复杂的情况,需要用递归来实现。
比如数独游戏,在最开始的时候,每个空格子有很多种可能,我们需要先尝试填入一个数,在此基础上再去尝试填入其他格子,在填数过程中,我们会发现当前这种方案可能是错误的,那么我们就需要返回上一步,重新尝试其他可能,这种解题办法我们称为回溯法。

还是老样子,直接用例题来提高熟练度

全排列问题

全排列问题

题目描述

按照字典序输出自然数 1 1 1 n n n 所有不重复的排列,即 n n n 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。

输入格式

一个整数 n n n

输出格式

1 ∼ n 1 \sim n 1n 组成的所有不重复的数字序列,每行一个序列。

每个数字保留 5 5 5 个场宽。

样例 #1

样例输入 #1

3

样例输出 #1

    1    2    3
    1    3    2
    2    1    3
    2    3    1
    3    1    2
    3    2    1

提示

1 ≤ n ≤ 9 1 \leq n \leq 9 1n9

全排列问题是数学上的一类经典问题,在数学上存在排列公式可以直接快速求解。
但是在实际问题中可能存在一些其他的限制条件,比如某个位置不能放某个数,某个数必须放在某个位置,这种情况下用公式就比较麻烦,所以先考虑一种比较效率低下但是比较万能的方法。
简单来说就是对于每一个位置,依次放入一个之前未使用过的数,当把所有数都放入之后,就表示存在一种答案,当把每个位置的所有情况都尝试之后,累加得到的答案就是正确答案。
我们可以把该题看做有n个格子,依次要把n个数放入这些格子中。对于每一个格子而言,放数的方法都是相同的——找到一个之前未放入的数。
那么我们如何才能知道这个数是否已经使用过了呢?比较简单的办法就是开一个book数组,标记1—n中每一个数的状态。
每次放数的时候,需要考虑两个问题。
第一:所有情况都不能遗漏,即从1枚举到n。
第二:之前放过的数不能重复放,即未被标记。
当n个格子中都有数的时候表示放数结束,得到一种正确方案,记录答案并返回。
前面的做法只考虑了一种情况,比如n=3的时候,找到的第一种排列为1 2 3,接下来我们应该先考虑第3个格子是否还可以放其他数,发现不可以放其他数,那么应该继续返回上一次,即回到第二个格子,第二个格子之前放了数字2,我们考虑是否可以放下一个数字3,发现可以,当第二个格子放了数字3之后,继续考虑第三个格子,发现可以放数字2,此时又找到一种可能的情况。接着继续返回尝试其他可能,当返回到第一个格子的时候,发现可以放数字2,继续到第二个格子,可以放数字1,到第三个格子,可以放数字3,又是一种情况…一次尝试和返回,直到返回主函数为止。

ACcode

#include <bits/stdc++.h>
using namespace std;

int tmp[100];
bool used[100];
void dfs(int n,int step){
	if (step==n){
		for (int i=0;i<n;i++){
			printf("%5d",tmp[i]);
		}
		cout<<endl;
		return;
	}
	for (int i=1;i<=n;i++){
		if (used[i]==0){
		tmp[step]=i;
		used[i]=1;
		dfs(n,step+1);
		used[i]=0;//回溯
		}
	}
}

int main() {
	int n;
	cin>>n;
	dfs(n,0);
	return 0;
}

借书问题

在这里插入图片描述
对于输入的每个同学喜欢的书的情况,可以开一个二维数组来表示,a[i][j]表示第i位同学是否喜欢第j本书。
对于每一个同学是否可以借这一本书,考虑的情况都是相同的:
(1)这本书是否存在
(2)第i个同学是否喜欢这本书
(3)这本书是否已经在前面被借走了。
当这n位同学都可以借到一本书,那么就表示这种方案是合理的,如果某位同学无书可以借,那么就说明这种方案是错误的,就需要返回上一位同学重新借其他书。
该题还需要输出借书的方案,即我们需要记录每位同学借了哪一本书,再开一个b数组,b[i]=j表示第i位同学借了第j本书。
由于方案不止一种,输出的方案需要按照字典序排列。即前面的同学先借编号小的书,那么我们在枚举的时候就只能从1开始枚举到n,这样得到的可行方案就是按照字典序从小到大排列的。
还有一些题目要求按照字典序从大到小排列,即前面的同学先借编号大的书,这种情况我们需要从n到1枚举。

ACcode

#include<bits/stdc++.h>
using namespace std;

int n,a[15][15],cnt;
int ans[1000][15],b[15],used[20];

inline int read(){
	int x=0;
	char ch=0;
	while (ch>'9'||ch<'0')ch=getchar();
	while (ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar();
	return x;
}

void dfs(int k){
	if (k==n){
		for (int i=0;i<n;i++){
			ans[cnt][i]=b[i];
		}
		cnt++;
	}else {
		for (int i=0;i<n;i++){
			if (used[i]==0&&a[k][i]==1){
				used[i]=1;
				b[k]=i+1;
				dfs(k+1);
				used[i]=0;
			}
		}
	}
}
int main(){
	n=read();
	for (int i=0;i<n;i++){
		for (int j=0;j<n;j++){
			cin>>a[i][j];
		}
	}
	dfs(0);
	cout<<cnt<<endl;
	for (int i=0;i<cnt;i++){
		for (int j=0;j<n;j++){
			cout<<ans[i][j]<<" ";
		}
		cout<<endl;
	}
	return 0;
}

好了,基础的部分已经搞完了,现在我们来看看组合问题
前面我们遇到的问题都是用回溯法求解排列类问题,即选择相同的元素,但是顺序不同算不同的方案。很多时候还会出现另一类问题——组合数问题。即从n个元素中选择m个元素的一个组合,组合和排列的区别在于,组合和顺序没有关系只要每个元素都相同,顺序不同也只算作一种方案,即1,1,5和5,1,1是一个相同的组合。

组合的输出

题目描述

排列与组合是常用的数学方法,其中组合就是从 n n n 个元素中抽出 r r r 个元素(不分顺序且 r ≤ n r \le n rn),我们可以简单地将 n n n 个元素理解为自然数 1 , 2 , … , n 1,2,\dots,n 1,2,,n,从中任取 r r r 个数。

现要求你输出所有组合。

例如 n = 5 , r = 3 n=5,r=3 n=5,r=3,所有组合为:

123 , 124 , 125 , 134 , 135 , 145 , 234 , 235 , 245 , 345 123,124,125,134,135,145,234,235,245,345 123,124,125,134,135,145,234,235,245,345

输入格式

一行两个自然数 n , r ( 1 < n < 21 , 0 ≤ r ≤ n ) n,r(1<n<21,0 \le r \le n) n,r(1<n<21,0rn)

输出格式

所有的组合,每一个组合占一行且其中的元素按由小到大的顺序排列,每个元素占三个字符的位置,所有的组合也按字典顺序。

注意哦!输出时,每个数字需要 3 3 3 个场宽。以 C++ 为例,你可以使用下列代码:

cout << setw(3) << x;

输出占 3 3 3 个场宽的数 x x x。注意你需要头文件 iomanip

样例 #1

样例输入 #1

5 3

样例输出 #1

1  2  3
  1  2  4
  1  2  5
  1  3  4
  1  3  5
  1  4  5
  2  3  4
  2  3  5
  2  4  5
  3  4  5

该题的解题策略和前面类似,都是用回溯法尝试所有可能性,找出符合条件的所有方案,但是该题也有和前面不一样的地方,如果按照前面的方式枚举,那么最终得到的答案肯定是有重复的,而且重复了很多次,那么有没有办法避免这种重复的情况呢?要怎么样枚举才能避免重复情况呢?
根据题目的输出我们可以发现,下一个数一定是大于前一个数,这就是一种很好的避免重复的办法,只要我们保证了下一个数大于前一个数,这样一个组合的满足要求的排列有且仅有一个,而且刚好满足字典序最小的要求。

ACcode

#include <bits/stdc++.h>
using namespace std;
int r, n;
int temp[21];

void print () {
	for (int i = 0; i < r; i++) {
		printf("%3d", temp[i]);
	}
	cout << endl;
}

void f(int start, int k ) { //k 还有多少个数字没有选
	if (k == 0) {
		print();//打印结果
		return;
	}
	for (int i = start; i <= n ; i++) {
		temp[r - k] = i;
		f(i + 1, k - 1);
	}
}


int main() {
	cin >> n >> r;
	f(1, r);
	return 0;
}

放苹果

题目描述

m m m 个同样的苹果放在 n n n 个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法。( 5 , 1 , 1 5,1,1 5,1,1 1 , 1 , 5 1,1,5 1,1,5 是同一种方法)

输入格式

第一行是测试数据的数目 t t t,以下每行均包括二个整数 m m m n n n,以空格分开。

输出格式

对输入的每组数据 m m m n n n,用一行输出相应的结果。

样例 #1

样例输入 #1

1
7 3

样例输出 #1

8

样例 #2

样例输入 #2

3
3 2
4 3
2 7

样例输出 #2

2
4
2

提示

对于所有数据,保证: 1 ≤ m , n ≤ 10 1\leq m,n\leq 10 1m,n10 0 ≤ t ≤ 20 0 \leq t \leq 20 0t20

根据题目中的要求,该题仍然是一道组合数问题,即跟选中元素的顺序无关,所以枚举的时候保证下一个元素大于等于下一个元素即可,注意这里可以等于。
但是该题的结束条件有点不一样,当n个盘子都放了苹果之后是不是就一定是一种正确方案了呢?并不是,因为还要保证苹果必须要放完,所以我们还需要记录剩余的苹果数量(或者已经放了的苹果数量)。
同样,当前盘子可以放的苹果数量的范围为k到剩余苹果的数量,甚至还可以精确到剩余苹果数量除以剩余盘子数量。

#include <bits/stdc++.h>
using namespace std;
int t;

int sol(int m, int n) { //m为苹果,n为盘子
	if (n == 1 || m == 0)
		return 1;
	if (m < n)
		return sol(m, m);
	if (m >= n)
		return sol(m, n - 1) + sol(m - n, n);
}

int  main() {
	cin >> t;
	int m, n;
	for (int i = 1; i <= t; i++) {
		cin >> m >> n;
		cout << sol(m, n) << endl;
	}
	return 0;
}

烤鸡

题目背景

猪猪 Hanke 得到了一只鸡。

题目描述

猪猪 Hanke 特别喜欢吃烤鸡(本是同畜牲,相煎何太急!)Hanke 吃鸡很特别,为什么特别呢?因为他有 10 10 10 种配料(芥末、孜然等),每种配料可以放 1 1 1 3 3 3 克,任意烤鸡的美味程度为所有配料质量之和。

现在, Hanke 想要知道,如果给你一个美味程度 n n n ,请输出这 10 10 10 种配料的所有搭配方案。

输入格式

一个正整数 n n n,表示美味程度。

输出格式

第一行,方案总数。

第二行至结束, 10 10 10 个数,表示每种配料所放的质量,按字典序排列。

如果没有符合要求的方法,就只要在第一行输出一个 0 0 0

样例 #1

样例输入 #1

11

样例输出 #1

10
1 1 1 1 1 1 1 1 1 2 
1 1 1 1 1 1 1 1 2 1 
1 1 1 1 1 1 1 2 1 1 
1 1 1 1 1 1 2 1 1 1 
1 1 1 1 1 2 1 1 1 1 
1 1 1 1 2 1 1 1 1 1 
1 1 1 2 1 1 1 1 1 1 
1 1 2 1 1 1 1 1 1 1 
1 2 1 1 1 1 1 1 1 1 
2 1 1 1 1 1 1 1 1 1

提示

对于 100 % 100\% 100% 的数据, n ≤ 5000 n \leq 5000 n5000

该题的解题思路比较简单,对于每一种调料,考虑[1,4]两种情况,主要在于时间复杂度的优化。
因为每种调料的范围是[1,4],在考虑当前第i种调料的时候,之前调料和为sum,需要满足第一个条件是sum+i+(10-step)<=n,即后面每种调料至少要放1g,一共是(10-step)g,还需要满足第二个条件是sum+i+4*(10-step)>=n,即后面每种调料最多放4g。
对于回溯类题目,由于比赛中爆搜一般都不是正解,所以我们要尽可能优化搜索的效率,这样才能得到更高的分数,需要考虑前面是否合理以及后面是否合理。
但是这里作者偷了个懒,你看代码就知道了

#include <bits/stdc++.h>  
using namespace std;  

int main()  {  
    int a,b,c,d,e,f,g,h,i,j,in,x=0;  
    cin>>in;  
    for (a=1;a<=4;a++)  {  
        for (b=1;b<=4;b++)  {  
            for (c=1;c<=4;c++)  {  
                for (d=1;d<=4;d++)  {  
                    for (e=1;e<=4;e++)  {  
                        for (f=1;f<=4;f++)  {  
                            for (g=1;g<=4;g++)  {  
                                for(h=1;h<=4;h++)  {  
                                    for (i=1;i<=4;i++)  {  
                                        for (j=1;j<=4;j++)  {  
                                            if (a+b+c+d+e+f+g+h+i+j==in){  
                                                x++;  
                                            }  
                                        }  
                                    }  
                                }  
                            }  
                        }  
                    }  
                }  
            }  
        }  
    }  
    cout<<x<<endl;  
    for (a=1;a<=4;a++){  
        for (b=1;b<=4;b++){  
            for (c=1;c<=4;c++){  
                for (d=1;d<=4;d++){  
                    for (e=1;e<=4;e++)  {  
                        for (f=1;f<=4;f++)  {  
                            for (g=1;g<=4;g++)  {  
                                for(h=1;h<=4;h++)  {  
                                    for (i=1;i<=4;i++)  {  
                                        for (j=1;j<=4;j++)  {  
                                            if (a+b+c+d+e+f+g+h+i+j==in){  
                                                cout<<a<<" ";  
                                                cout<<b<<" ";  
                                                cout<<c<<" ";  
                                                cout<<d<<" ";  
                                                cout<<e<<" ";  
                                                cout<<f<<" ";  
                                                cout<<g<<" ";  
                                                cout<<h<<" ";  
                                                cout<<i<<" ";  
                                                cout<<j<<endl;  
                                            }  
                                        }  
                                    }  
                                }  
                            }  
                        }  
                    }  
                }  
            }  
        }  
    } 
	return 0; 
}

OK哈,这就是所有的回溯算法,再找个时间吧最后一部分写了就可以完结了!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

排序算法之基数排序

目录 一、简介二、代码实现三、应用场景 一、简介 算法平均时间复杂度最好时间复杂度最坏时间复杂度空间复杂度排序方式稳定性基数排序O(n*k)O(n*k)O(n*k)O(nk)Out-place稳定 稳定&#xff1a;如果A原本在B前面&#xff0c;而AB&#xff0c;排序之后A仍然在B的前面&#xff1b…

03-为啥大模型LLM还没能完全替代你?

1 不具备记忆能力的 它是零状态的&#xff0c;我们平常在使用一些大模型产品&#xff0c;尤其在使用他们的API的时候&#xff0c;我们会发现那你和它对话&#xff0c;尤其是多轮对话的时候&#xff0c;经过一些轮次后&#xff0c;这些记忆就消失了&#xff0c;因为它也记不住那…

笔记本电脑坏了硬盘数据会丢失吗 笔记本电脑坏了如何取出硬盘的资料 数据恢复软件

笔记本电脑对我们真的非常重要了&#xff0c;是实现无纸化办公和学习的重要工具&#xff0c;但是如果笔记本电脑坏了我们存储在电脑里的资料该怎么办&#xff1f;笔记本电脑坏了硬盘数据会丢失吗&#xff1f;相信有许多朋友都会有这样的担忧。本文今天就为大家解决笔记本电脑坏…

Docker 的基本管理

一. 云的相关知识 1. 关于云 云端服务器都有哪些提供商&#xff1a; 国内&#xff1a; 阿里云&#xff08;Alibaba Cloud&#xff09;&#xff1a; 提供ECS&#xff08;Elastic Compute Service&#xff09;弹性计算服务&#xff0c;包括通用型、计算型、内存型等多种实例…

CodeGemma初探

什么是 CodeGemma CodeGemma是一系列强大而轻量级的模型的集合&#xff0c;可以执行各种编码任务&#xff0c;包括填充中间代码补全、代码生成、自然语言理解、数学推理和指令跟随。 版本&#xff1a; instruct&#xff1a;7B, 这个版本专门针对自然语言到代码聊天和指令跟随…

【Linux高性能服务器编程】——高性能服务器框架

hello &#xff01;大家好呀&#xff01; 欢迎大家来到我的Linux高性能服务器编程系列之高性能服务器框架介绍&#xff0c;在这篇文章中&#xff0c;你将会学习到高效的创建自己的高性能服务器&#xff0c;并且我会给出源码进行剖析&#xff0c;以及手绘UML图来帮助大家来理解&…

解锁EDM设计秘籍:关键要素一览,邮件如何设计?

一个成功的EDM邮件需要包含多个关键元素&#xff0c;从内容、设计到呼唤行动&#xff0c;每个环节都至关重要。今天&#xff0c;我们就来探讨EDM邮件中应包含的关键元素&#xff1f;以及如何设计邮件&#xff1f; 一、EDM必备关键要素 1、吸引眼球的主题行 主题行应该简短明了…

NC398 腐烂的苹果

腐烂的苹果 一个腐烂的苹果每分钟可以向上下左右四个方向扩展&#xff0c;扩展之后&#xff0c;又会有新的腐烂的苹果&#xff0c;一直去腐蚀好的苹果&#xff0c;求多少分钟后&#xff0c;网格中全是烂苹果。 第一次做这道题的时候&#xff0c;想到这道题考察的其实是多源BFS…

C#版Facefusion:让你的脸与世界融为一体!-04 人脸替换

C#版Facefusion&#xff1a;让你的脸与世界融为一体&#xff01;-04 人脸替换 目录 说明 效果 模型信息 项目 代码 下载 说明 C#版Facefusion一共有如下5个步骤&#xff1a; 1、使用yoloface_8n.onnx进行人脸检测 2、使用2dfan4.onnx获取人脸关键点 3、使用arcface_w60…

网络基础之-IP地址

文章目录 1. IP地址&#xff1a;网络和主机1.1 A类IP地址1.2 B类IP地址1.3 C类IP地址1.4 D类和E类IP地址 2.几个特殊的IP地址2.1 私有地址2.2网关 1. IP地址&#xff1a;网络和主机 IP地址是用于在计算机网络中唯一标识设备的一组数字。它由32位&#xff08;IPv4&#xff09;或…

05_Flutter屏幕适配

05_Flutter屏幕适配 一.屏幕适配方案 通过指定基准屏宽度&#xff0c;进行适配&#xff0c;基准屏宽度取决于设计图的基准宽度&#xff0c;以iphone 14 pro max为例&#xff0c; devicePixelRatio 物理宽度 / 逻辑宽度(基准宽度) iphone 14 pro max的物理尺寸宽度为1290&…

创新入门|解锁您的潜在市场:探秘付费点击广告(PPC)的秘密武器

在我们的营销领域&#xff0c;按点击付费 &#xff08;PPC&#xff09; 广告是增加流量、提高知名度并最终将点击转化为客户的基石策略。这种有针对性的广告模式&#xff0c;即企业只在点击广告时付费&#xff0c;彻底改变了公司投资在线推广的方式。尽管它看起来很简单&#x…

手写Promise实现

手写Promise实现 一、前言二、代码三、测试四、测试结果 一、前言 阅读参考资料&#xff0c;本文整理出使用 构造函数 手撕出 Promise 的方法&#xff0c;在整理过程中不断添加注解以示思路。有错请指出哟&#xff0c;一起进步&#xff01;&#xff01;&#xff01;class 实现 …

2024接口自动化测试入门基础知识【建议收藏】

接口自动化测试是指通过编写测试脚本和使用相关工具&#xff0c;对软件系统的接口进行自动化测试的过程。 今天本文从4个方面来介绍接口自动化测试入门基础知识 一、接口自动化测试是什么&#xff1f; 二、接口自动化测试流程&#xff1f; 三、接口自动化测试核心知识点有那些…

开始Java之旅

1.Java语言 java是一门优秀的程序设计语言&#xff0c;并且是一种半编译型&#xff0c;半解释型语言。 Java 语言源于 1991 年 4 月&#xff0c;Sun 公司 James Gosling博士 领导的绿色计划(Green Project) 开始启动&#xff0c;此计划最初的目标是开发一种能够在各种消费性电…

Threejs绘制传送带

接下来会做一个MES场景下的数字孪生&#xff0c;所以开始做车间相关的模型&#xff0c;不过还是尽量少用建模&#xff0c;纯代码实现&#xff0c;因为一方面可以动态使用&#xff0c;可以调节长度和宽度等&#xff0c; 下面这节就做一个简单的传送带&#xff0c;这是所有车间都…

学之思考试系统环境启动QA

学之思考试系统环境启动Q&A 目录 学之思考试系统环境启动Q&A后台代码启动失败:前台代码启动失败常见解决方式参考资料后台代码启动失败: 后端代码启动不成功,不能够自动导入maven,配置依赖; 使用idea打开到:\xzs-master\xzs-mysql-master\source\xzs这个路径下;…

函数的创建和调用及删除

Oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 函数和存储过程非常类似&#xff0c;也是可以存储在 Oracle 数据库中的 PL/SQL代码块&#xff0c;但是有返回值。 可以把经常使用的功能定义为一个函数&#xff0c;就像系统…

使用Flask部署ppocr模型_3

PaddleOCR环境搭建、模型训练、推理、部署全流程&#xff08;Ubuntu系统&#xff09;_1_paddle 多进程推理-CSDN博客 PP-Structure 文档分析-CSDN博客 Pycharm的Terminal进入创建好的虚拟环境 有时候Pycharm的terminal中显示的是硬盘中的项目路径&#xff0c;但没有进入创建好…

Python 开发实现登陆和注册模块

Python 开发实现登陆和注册模块 一、案例介绍 本例设计一个用户登录和注册模块&#xff0c;使用Tkinter框架构建界面&#xff0c;主要用到画布、文本框、按钮等组件。涉及知识点&#xff1a;Python Tkinter界面编程、pickle数据存储。本例实现了基本的用户登录和注册互动界面…