算法入门到进阶(三)——搜索技术(递归和排列)


前言

搜索技术是基本的编程技术,在算法竞赛学习中是基础的基础。搜索使用的算法是BFS和DFS,BFS用队列,DFS用递归来具体实现。在BFS和DFS的基础上可以扩展出A算法,双向广搜算法,迭代加深搜索,IDA等技术。第三阶段都将详细介绍这些知识点

基本概念

搜索技术是“暴力法”算法思想的具体实现。

人们常说:“要利用计算机强大的计算能力。”如果答案在一大堆的数字里面,让计算机一个个去试,符合条件的不就是答案了吗?

没错最基本的算法思想"暴力法"就是这样。例如,银行卡密码是6位数字,共100万个,对于计算机来说,尝试100万次只需要一瞬间。不过计算机也不是无敌的。为了应对计算机强大的计算能力,可以对密码进行强化设计。例如网络账号密码,大部分网站都要求长度在8位以上,并且混合数字,字母,标点等。从40多个符号中选8个组成密码,数量有40x39x38x37x36x35x34x33>3万亿,即使使用计算机也不能很快算出来。

暴力法(Brute force,又称为蛮力法):把所有可能的情况都罗列出来,然后逐一检查,从中找到答案。这种方法简单,直接,不玩花样,利用了计算机强大的计算能力。

虽然暴力法常常是低效的代名词,但是它仍然很有用,原因如下:

  1. 很多问题只能用暴力法解决,例如猜密码。
  2. 对于小规模的问题,暴力法完全够用,而且避免了高级算法需要的复杂编码,在竞赛中可以加快解题速度。在竞赛中也可以用暴力法来构造测试数据,以验证高级算法的正确性。
  3. 把暴力法当作参照(benchmark)。既然暴力法是“最差”的,那么可以把它当作一个比较衡量另外的算法有多“好”。不过在具体编程的时候通常需要对暴力法进行优化,以减少搜索空间,提高效率。例如利用剪枝技术跳过不符合要求的情况,从而减少复杂度。

虽然暴力搜索的思路简单,但是操作起来并不简单。一般有以下操作:

  1. 找到所有可能的数据,并且用数据结构表示和存储
  2. 剪枝。尽量多的排除不符合条件的数据,以减少搜索的空间
  3. 用某个算法快速检索这些数据。
    其中第一步就可能很不容易。例如迷宫问题,如和列举从起点到终点的所有可能的路径?再如图论中的“最短路径问题”,在地图上任取两个点,它们之间所有可行的路径可能是一个天文数字,以至于根本无法一一列举出来。所以计算最短路径的Dijkstra算法使用贪心法,进行从局部扩散到全局的搜索,不能列举所有可能的路径。

暴力法的主要操作是搜索,搜索的主要技术是BFS和DFS。掌握搜索技术是学习算法的基础。在搜索时,具体问题会有相应的数据结构,例如队列,栈,图,树等。读者应该能熟练的在这些数据结构上进行搜索的操作。

本阶段主要讲解BFS和DFS,以及基于它们的优化技术,并以一些经典的搜索问题为例讲解算法思想,例如排列组合,生成子集,八皇后,八数码,图遍历等。

递归和排列

排列和组合问题是在暴力枚举的时候经遇到的,一般有3中常见情况。
在这里插入图片描述本篇博客用递归程序来实现问题1和2,问题3留在下一篇博客.
在计算机编程教材中都会提到递归的概念和应用,一般会用数学中的递推方程来讲解递归的概念,例如f(n)=f(n-1)+f(n-2)。在计算机系统中,递归是通过嵌套来实现的,设计指针,地址,栈的使用。

从算法的思想上看,递归是把大问题逐步缩小,直到变成最小的同类问题的过程。例如n->n-1->n-2->…->1,最后的小问题的解是已知的,一般是给定的初始初始条件。在递归的过程中,由于大问题和小问题的解决方法完全一样,那么大家自然可以想到,大问题的程序和小问题的可以写成一样。一个递归函数直接调用自己,就实现了程序的复用。

递归和分治法的思路非常相似,分治是把一个大问题分解为多个类型相同的子问题,事实上,一些涉及分治法的问题可以用递归来编程,典型的有快速排序,归并排序等。

对于编程初学者来说,递归是一个难以理解的编程概念,很容易绕晕。为了帮助理解,可以一步步打印出递归函数的输出,看它从大到小解决问题的过程

编程竞赛中的暴力法常常需要考虑所有可能的情况,用递归编程可以轻松,方便的实现对搜索空间所有状态的遍历

案例1:采用STL库中的next_permutation()函数实现全排列

题目

打印n个数的全排列,一共n!个。

在用递归解决这个问题之前,先给出STL的实现方法

思路

如果用全排列的场景比较简单,可以直接用C++STL的库函数next_permutation(),按字典序输出下一个排列。在使用之前,先用sort()给数据排序,得到最小排列,然后每调用next_permutation()一次,就得到一个大一点的排列

next_permutation()优点是能按从小到大的顺序输出排列。

源码

#include<iostream>
#include<algorithm>  //包含sort和next_permutation()函数的头文件
using namespace std;
int main() {
	int data[4] = { 5,2,4,1 };
	sort(data, data + 4);//默认从小到大,得到最小排列
	do {
		for (int i = 0; i < 4; i++) {//输出每个排列的值
			cout << data[i] << " ";
		}
		cout << endl;
	} while (next_permutation(data, data + 4));//把下一个排列放在data中
	return 0;

}

运行结果

在这里插入图片描述

分析

在这里插入图片描述

案例1:采用递归求全排列

题目

打印n个数的全排列,一共n!个。

思路

下面用递归求全排列,代码很短,但是理解起来不太容易。认真理解

在递归调用之前,为了对比,先给出一个简单,粗暴的方法:以10个数的全排列为例,用排列组合的思路写一个10级的for循环,在每一个for中选一个和前面的for用过的都不同的数。当n=10时,一共有10!=362800个排列

验证

在这里插入图片描述

运行结果

在这里插入图片描述

显然这个程序很笨,运行需要的时间很长,下面我们用递归来写,能感受到明显的差别。

递归思路

假定数字是{1,2,3,4,5,…,n};

  1. 让第一个数不同得到n个数列。其办法是把第1个和后面的每个数交换
    1,2,3,4,5,…,n
    2,1,3,4,5,…,n
    3,1,2,4,5,…,n
    .
    .
    .
    n,2,3,4,5,…,1
    以上n个数列,只要第一个数不同,不管后面的n-1个数是怎样排列的,这n个数列都不同。
    这是递归的第一层。
  2. 继续:在上面的每个数列中去掉第一个数,对后面n-1个数进行同样的方法进行排列。例如从上面的第二行的{2,1,3,4,5,…,n}进入第二层(去掉首位2);
    1,3,4,5,…,n
    3,1,4,5,…,n
    .
    .
    .
    n,3,4,5,…,1
    以上n-1个数列,只要第一个数不同,不管后面的n-2个数是怎么排列的,这n-1个数列都不同
    这是递归的第二层
  3. 重复上面的代码,直到用完所有的数字。

因此上面的结果就是n!个

源码

#include<iostream>
#include<algorithm>
using namespace std;

void Swap(int &a,int &b){//也可以用STL库中的swap函数,只是速度慢些
	int temp = a;
	a = b;
	b = temp;

}

int Data[] = { 1,2,3,4,5,6,7,8,9,11,12 };//只会用到前10个

int num = 0;

void Perm(int begin, int end) {
	int i;
	if (begin == end) {
		
		num++;//表示一个全排列,到最底层,递归结束
	}
	else {
		for (i = begin; i <= end; i++) {
			//将第一个位置的数和后面每一个数进行交换,那么后面的每个数都出现在了第一个位置,加上自己,就有n个不同的数列
			Swap(Data[begin], Data[i]); 
			Perm(begin + 1, end);  //得到的这个数列,继续递归,进入第二层
			Swap(Data[begin], Data[i]);  //将位置换回来,交换后面的那个数。
		}
	}
}
int main() {
	Perm(0, 9);
	cout << "num:" << num << endl;
	return 0;
}

运行结果

在这里插入图片描述
刚好就是10的阶乘。
如果你每次都打印了结果的话,这个结果就很慢,因为cout打印这屏幕上也是需要时间的。

分析

在这里插入图片描述

案例2,打印n个数中任意m个数全排列

就把上面的Perm函数改一下就行,
在这里插入图片描述

总结

相信经过上面的例题你已经明白了递归的思想,那我们就可以继续学习下面的内容了
子集生成和组合问题,BFS和队列

加油,每天进步一点点!一步一个jio印。

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jacky~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值