搜索与回溯算法(基础算法)
目录
1.前言
正片开始
2.演练
1.素数环
2.拆分
3.N皇后问题(八皇后plus)
4.最高效益
3.课后练习
1.LETTERS
2.迷宫
4.小结
1.前言
啦啦啦啦我又来啦hhh
咳咳这次还是一笔带过,顺便说一下,接下来每篇文章的最后一行有快捷跳转至本专栏的其他算法,至于前几个已经改好啦~~~
正片开始
好,我们这次呢,讲的是搜索与回溯,其实了解的都知道,递归就是搜索嘛,那这么简单别搞了。
咳咳,开个玩笑不会有人真信了吧,这可是高质量文章,怎么可能不结束......额不,结束了呢。
好,不扯了,来看回搜索与回溯。这东西呢,也是重要的基础算法之一。众所周知,有些题目你根本推不出答案的关系对吧(想必有很多人被坑过),比如1,5,19,234,21241,143315...你看似这个数列有些像递推,但是吧,这是我随便在键盘上打的hh。
既然如此,搜索与回溯就派出了用场。当然,还是声明一下,回溯是搜索的plus版(简化版),∴回溯就是递归的plus版,又∵递归是搜索的一种,∴回溯是搜索的plusplus版......
等等,停下,赶紧回正题,不然都没兴趣了,毕竟是来学的,不是来看相声的。
不方,我们继续看回溯(由于搜索不是本篇重点,所以不看),回溯呢,就是一种位于搜索算法中间的控制策略,它基本是深度优先搜索,只要遇到了死路,就退回去一步,直到找到了出口,也就是找到了答案;或者发现没路,也就是没答案时,便停止运行。
同样的,与递归算法相同,这种算法的时间问题值得研究。当然,这种方法适用于找最优解或找一个解的情况,而找多个解嘛,深度不大好,时间很large,广度优先搜索最好,下章讲。
好,我们看一看回溯算法的基础框架:(未减枝)
int dfs(int x){for(int i = 1;i <= 算符种数;i++){if(判断条件){保存这个结果(令它为①)if(满足终止条件) 输出解(+结束相当于找最优解)else search(x+1);恢复保存①之前的状态,也就是回溯一步}}
}
上面这个框架我不常用,甚至好像没用过...下面这个我比较常用,但看个人喜好哈
int dfs(int x){if(满足终止条件) 输出解(+结束相当于找最优解)else{for(int i = 1;i <= 算符种数;i++){if(判断条件){保存这个结果search(x+1);恢复状态}}}
}
非常看不懂简单,上手来练一练。
2.演练
好,第一题
1.素数环
题目为将1~20这20个数摆成一个圆环,要求任意两个相邻的数的和都是质数。
这题乍一看非常简单,毕竟就一个个相加再判断质数嘛(质数函数没背下来的给我背!!!),so这时又有一个重要的问题:当遍历到第20个的时候如何跟第1个比?
切,特判。
好,再简单理解一下题意:我们可以把这圆环上看作20个空(毕竟给了20个数),也就是说,每个位置都有20种可能,那么这样的话如果用递归...,这long比long还多一亿倍啊,疯了。
所以呢这时就要用回溯。当然,要一边判断一遍填。
来吧。
#include <bits/stdc++.h>
using namespace std;int type;//第x种情况
int a[25];//保险起见+5
int b[25];//判断所选数字是否被选中过bool isprime(int x){ //不会的给我背!!if(x <= 1) return false;for(int i = 2;i * i <= x;i++){if(x % i == 0) return false;}return true;
}void dfs(int step){for(int i = 1;i <= 20;i++){if(isprime(a[step-1]+i) && (!b[i])){ //判断选中数字是否能选中且与前面一位数构成质数a[step] = i;b[i] = 1;//变更为选中if(step == 20){if(isprime(a[20]+a[1])){type++;cout << type << ":";for(int j = 1;j <= 20;j++){cout << a[j] << " ";}cout << endl;}}else dfs(step+1);b[i] = 0;}}
}int main(){dfs(1);cout << type << endl;return 0;
}//电脑性能不好的别运行!!!
我为什么要提醒不要去试图运行呢?因为光是以“2 1 4”开头的素数环就已经达到了恐怖的116084种排列方法,因此千万不要去挑战自己电脑的CPU!!!
(以下是输出部分)
可以看到,这仅仅才列举了一点点,总数估算了一下大概要几亿种吧(也就几亿种...)
没事,好歹这题也算是撒花了。
下一题。
2.拆分
任何一个大于1的自然数都能被拆分成若干个小于它的自然数之和,先给出一个自然数n,求它所能被拆分的所有方法数。
如n=3时:
3=1+1+1
3=2+1
即总数sum=2
还是套回溯,所以我们可以用遍历一层层筛选出可供选择的数,从而得出结果,所以直接看代码:
#include <bits/stdc++.h>
using namespace std;int n,a[25],ans;void dfs(int step,int sum){if(sum > n) return; //当总和过大时就可以直接结束(剪枝)if(sum == n){ //当总和刚好相等时就可以加一种方法ans++;return;}for(int i = a[step - 1];i <= n-1;i++){ //小于用n-1a[step] = i;dfs(step + 1,sum + i); //这里主要回溯在sum+i中,这时sum并未改变,因此再用就算一次回溯,相当于15~17行
// sum += i;
// dfs(step+1,sum);
// sum -= i;}
}int main(){cin >> n;a[0] = 1; //注意赋初值dfs(1,0); //初始的函数步数为1,总和为0cout << ans;return 0;
}
ok十分简单对吧,接下来上难度:
3.N皇后问题(八皇后plus)
八皇后想必学过的大佬们都不陌生,毕竟这可是回溯中极其重要的题目之一,不会有人做回溯不做这题吧?
但是呢,这里本蒟蒻手动加强一下这道题:
————————————————————————————————————
题目描述
对一个如下的8×8的国际象棋棋盘,有八个皇后被放置在棋盘上,使得每行、每列和每条斜线上都至多只有一个皇后,
这就是著名的八皇后问题,下图是其中一个解.
类似地我们可以定义n皇后问题:n×n()的国际象棋棋盘,有n个皇后被放置在棋盘上,使得每行、每列和每条斜线上都至多只有一个皇后.
你需要根据n,输出n皇后问题的第一个解. 容易发现一个解中,每行一定会恰好有一个皇后,我们规定两个解中如果前k−1行的皇后位置相同,则第k行的皇后更靠前(靠左)的排在前面.(或者说,以皇后的每行位置,按字典序排序)
————————————————————————————————————
这题乍一看用递归嘛,可是n别说10了,哪怕是本来的8都要2^64了诶,人家2^31的小身板怎么能撑得住呢。
so,为了电脑的安全,我们用回溯来优化一下,让电脑逍遥地活着更好的撑住。
好,分析哈,首先行和列检测是很容易的,但斜线如何检测呢?
我们发现当填完后每个皇后的位置中的行和列相加的值都是不同的(看上图,从第一列开始每个皇后的行+列的值分别是:2,9,8,12,7,10,13,11,互不相同),那么就好办了。
我们可以推出公式:i+a[i] != j+a[j]
于是,可以变编出如下代码:
#include <bits/stdc++.h>
using namespace std;int n,a[25],vis[25];bool check(){for(int i = 1;i <= n;i++){for(int j = i+1;j <= n;j++){if(a[i] == a[j] || i-a[i] == j-a[j] || i+a[i] == i+a[j])//判断行列斜线的值是否相等,若相等则不成立return false;}}return true;
}void dfs(int step){if(step == n+1){if(check()){for(int i = 1;i <= n;i++){cout << a[i] << " ";}exit(0);//因为要求给出字典序最前的一种,所以结束后立即跳出dfs函数,不再继续}}for(int i = 1;i <= n;i++){if(!vis[i]){a[step] = i; //指皇后位置在第step列第i行vis[i] = 1; //选中时dfs(step+1);vis[i] = 0; //回溯}}
}int main(){cin >> n;dfs(1);return 0;
}
也是十分easy,接下来继续(本文内容稍多,请见谅)
4.最高效益
设有A、B、C、D、E五人分别从事1、2、3、4、5五项工作,每人仅能从事一项,他们工作所得的效益为:
1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|
A | 13 | 11 | 10 | 4 | 7 |
B | 13 | 10 | 10 | 8 | 5 |
C | 5 | 9 | 7 | 7 | 4 |
D | 15 | 12 | 10 | 11 | 5 |
E | 10 | 11 | 8 | 8 | 4 |
(打表给我手打废了)
现在每个人都需要选择一项工作且不能重复选一项,在所有组合中求效益最高的一种组合。
这道题似乎很简单,但如何判断是否选过呢?
设一个bool型a数组就完sir,只要判断a[i]是否被用过(0为没用过,1为已用过),若没用过就继续就可以啦,呵呵。
当然,还要有一个sum来统计效益总和,其实也不复杂。
来代码:(打表最累哈)
#include <bits/stdc++.h>
using namespace std;//打表,这里本蒟蒻喜欢遍历1~5,so外面多了一行0,当然习惯0~4的也可以用d[5][5]
int d[6][6] = { {0,0,0,0,0,0},{0,13,11,10,4,7},{0,13,10,10,8,5},{0,5,9,7,7,4},{0,15,12,10,11,5},{0,10,11,8,8,4}
};
int ans,f[10],h[10];
//这里ans是最高效益,f[10]存储当前组合方式,h[10]储存最优方案
int p[6]; //判断是否用过的数组(回溯时先1后0)void dfs(int step,int sum){if(step > 5){ //5人已全被安排工作if(sum > ans){ //判断效率是否更高//全部更改为最优方案ans = sum; for(int i = 1;i <= 5;i++){h[i] = f[i];}}return; //void要注意返回空值来结束,否则在部分情况下会超时(这里虽不会,但养成好习惯)}for(int i = 1;i <= 5;i++){ //遍历每一项工作if(!p[i]){ //判断工作是否已被占用f[step] = i; //若未使用,则第step人选第i项工作p[i] = 1; //标记为已被占用dfs(step+1,sum+d[step][i]);//往下搜(这里sum的值只在下一层中使用,不用这一层并未改变,不需回溯)p[i] = 0; //回溯}}
}int main(){//本题无输入,直接搜dfs(1,0);for(int i = 1;i <= 5;i++){char t = 64+i; //这里使用了ASCll码,t值分别对应A、B、C、D、Ecout << t; printf(":第%d项\n",h[i]);}printf("total:%d",ans);return 0;
}
so easy。
啥,你想直接要答案?自己去运行。
3.课后练习
经过这一节几乎没用干货满满的小课,想必你肯定有些收获,下面来手动练习一下:
1.LETTERS
给出一个R*C的大写字母矩阵,你一开始位于左上角,并且可以通过上下左右走动来收集字母,但每个字母只能收集一次,而往后就不能再经过收集过的字母的格子。求你最多能收集多少个字母呢。
样例输入:
3 6
HFDFFB
AJHGDH
DGAGEH
样例输出:
6
样例解释:
收集H(1,1),A(1,2),D(1,3),G(2,3),J(2,2),F(2,1)六个字母
2.迷宫
今天你在丛林中探险时不幸进入了一个迷宫,迷宫由n*n的矩阵组成,其中“.”为路,是可以走的;“#”为墙,是不能走的(你不能瞬移和挖穿墙壁)。而你每次只能移动到上下左右的相邻格子上。
现在经过你一顿勇猛的乱冲后,你到了(hb,lb)的位置,出口则在(he,le)的位置,这时,你获得了一个地图,但身为程序员的你看不懂这诡异的地图,于是将其导入了你随身携带的电脑中(电脑里没地图,想得美),先请你写出一段程序,帮你自己看看有没有出去的路,如果出口的路被封死了也就只能够等待救援,而如果未被封死,就可以继续试探来寻找出路。
输入:
第一行是需解决的迷宫数k。
后面是k组输入,每组输入的第1行是n,表示迷宫大小为n*n,后2~n+1行是一个n*n的矩阵,矩阵中为“.”或者“#”,代表这个格子是路还是墙,n+2行则是四个数hb,lb,he,le,代表你的位置和出口的位置。
输出:
输出k行,每行输出对应一个输入。如果可以逃出,则输出“You can find the way.”;否则输出“Please wait for support.”
输入样例:
2
3
. # #
. . #
# . .
0 0 2 2
5
. . . . .
# # # . #
. . # . .
# # # . .
. . . # .
0 0 4 0
样例输出:
You can find the way.
Please wait for support.
4.小结
这次我们学习了搜索与回溯的算法,可以说你应对考试的方法又多了一种,如果学会了,你可以去一些平台上(不打推销)来练练手,我们《广度优先搜索算法(基础算法)》见,86!
上一章:递归算法 下一章:广度优先搜索算法(未完工)