暴力搜索 枚举
暴力搜索的核心就是暴力,即把所有的可能性都列出来,然后一一试验,最后得到解。
下面来看看一些暴力搜索的例子:
1. 除法
输入正整数n,从小到大输出所有形如abcde / fghij = n 的表达式,其中a~j 是 数字0~9的一个排列,2<= n <=79
样例输入:
62
样例输出:
79546 / 01283 = 62
94736 / 01528 = 62
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int main() {
int n;
cin >> n;
char buf[99];
for(int fghij = 1234; ; fghij++) {
int abcde = fghij * n;
/*把abcde 和 fghij按照 "%05d%05d" 的格式输入到buf中,
按照%05d的格式 fghij前一定会有一个0,
例如1283 * 62 = 79546,则buf为 7954601283*/
sprintf(buf, "%05d%05d", abcde, fghij);
if(strlen(buf) > 10) {
break;
}
sort(buf, buf+10);
bool ok = true;
/*算法的关键点,前面进行了排序,因此第一位肯定是0,
abcde fghij各不相同,把该排列排序之后,顺序一定是0123456789,
只需要判断这10位中存不存在相同的数字 */
for(int i = 0; i < 10; i++) {
if(buf[i] != '0' + i) ok = false;
}
if(ok) {
cout << abcde << " / " << fghij << endl;
}
}
}
2.求最大乘积
输入n个元素组成的序列S,找出一个乘积最大的连续子序列。如果这个最大乘积不是正数,输出0
样例输入:
3
2 4 -3
5
2 5 -1 2 -1
样例输出:
8
20
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int main(){
int n, i;
int max = 0;
//输入数据
cin >> n;
int buf[1000];
for(i = 0; i < n; i++){
cin >> buf[i];
}
//处理数据
if(n == 1){
cout << buf[0];
}
int tmp = 1;
//算法核心
for(i = 0; i < n; i++){//取第一个.....到最后一个
tmp = buf[i];
for(int j = i+1; j < n; j++){//取第二个......到最后一个
tmp *= buf[j];
if(tmp > max){
max = tmp;
}
}
tmp = 1;
}
cout << max;
}
3. 枚举排列:生成1~n的排列
输入一个数n,输出所有1~n的排列
样例输入:
3
样例输出:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
分析实现:
可以把它想象成一棵树的结构,按照dfs算法递归来填第1个位置,第2个位置,第3个位置……….第n个位置
一共有两种实现方法:
(1)自己实现
#include<iostream>
using namespace std;
int A[101];
void printQueue(int n, int cur) {
if(cur == n) {//到达了边界,即到达了叶子节点,输出
for(int i = 0; i < n; i++) {
cout << A[i] << " ";
}
cout << endl;
} else {
for(int i = 1; i <= n; i++) {
bool ok = true;
/*看cur的前的几位有没有出现i,保证当前所填的位置的前几个位置不出现i
例如:当cur=2时,A[0] = 1, A[1]=2, 0和1的位置已经填了数值1和2,
当前2的位置,就不能填1和2了,所以会循环到i=3的情况
*/
for(int j = 0; j < cur; j++) {
if(A[j] == i) ok = false;
}
if(ok){
A[cur] = i;
printQueue(n, cur+1);
}
}
}
}
int main(){
int n;
cin >> n;
printQueue(n, 0);
}
(2)使用c++提供的next_permutation(p,p+n)函数,此函数能够按顺序生成1~n的所有排列
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int n, p[10];
cin >> n;
//首先要给定初始队列
for(int i = 0; i < n; i++){
cin >> p[i];
}
sort(p, p+n);
do{
for(int i = 0; i < n; i++){
cout << p[i] << " ";
}
cout << endl;
} while(next_permutation(p, p+n));
}
总结:枚举的思想非常重要,回溯法就是结合枚举来实现的,特别是枚举排列,和回溯法的算法框架非常相似
回溯法
1. 回溯法的描述
回溯法是暴力搜索的一种,回溯法在递归过程中,把生成和检查过程结合起来,避免了无意义的枚举。
回溯法:探索到某一步时,发现原先选择并不优或达不到目标,则返回到上一层调用(即回溯到上一层)
2. 回溯法的原理
回溯法在问题的解空间树中,按dfs策略,从根结点出发搜索解空间树
算法搜索至解空间树的任意一点时,先利用 剪枝函数 判断该节点是否可行(即能得到问题的解)。如果不可行,则跳过对该节点为根的子树的搜索,逐层向其祖先节点回溯;否则,进入该子树,继续按深度优先策略搜索
3. 子集树和排列数
使用回溯法求解的问题,解一般求子集或者排列,因此回溯法有两个概念:子集树和排列树
子集树:
当所给的问题是从n个元素的集合S中找出满足某种性质的 子集 时,相应的解空间称为子集树。
这类子集问题通常有2^n个叶节点,其节点总个数为2^(n+1)-1。遍历子集树的任何算法均需要O(2^n)的计算时间。
如0-1背包问题,从所给重量、价值不同的物品中挑选几个物品放入背包,使得在满足背包不超重的情况下,背包内物品价值最大。它的解空间就是一个典型的子集树
归纳得讲,就是选还是不选
算法框架:
void backtrack (int cur)
{
if (cur>n) output(x);
else
for (int i=0;i<=1;i++) {
x[cur]=i;
if (legal(cur)) backtrack(cur+1); //legal(cur)是剪枝函数,满足剪枝函数,进入子树dfs
}
}
排列树:
当所给问题是确定n个元素满足某种性质的 排列 时,相应的解空间树称为排列树。
排列树通常有n!个叶子节点。因此遍历排列树需要O(n!)的计算时间。
如旅行售货员问题,一个售货员把几个城市旅行一遍,要求走的路程最小。它的解就是几个城市的排列,解空间就是排列树
算法框架:(排列树的算法可以有两种写法)
(1)第一种:用visted[]数组来记录已经遍历过的结点,1表示已选取,其它位置不能再选,0表示未选取,此时for循环从1~n
void backtrack (int cur)
{
if (cur>n) output(x);
else
for (int i=1;i<=n;i++) { //注意这里是从1~n
if(!visted[i]){
visted[i] = 1;//表示该节点已被选取
if (legal(cur)) backtrack(cur+1); //legal(cur)是剪枝函数,满足剪枝函数,进入子树dfs
visted[i] = 0;
}
}
(2)第二种:使用swap函数交换(没有第一种写法好理解)
void backtrack (int cur)
{
if (cur>n) output(x);
else
for (int i=cur;i<=n;i++) {
swap(x[cur], x[i]);
if (legal(cur)) backtrack(cur+1); //legal(cur)是剪枝函数,满足剪枝函数,进入子树dfs
swap(x[cur], x[i]);
}
}
无论什么时候,使用回溯法时,记住:
cur:代表当前层,当前行,cur+1表示去到下一层或下一行;
for中的i:代表当前列,i++表示横移到右一列或是右一兄弟节点
要分清用什么填什么:一般是用 i 填 cur
4. 回溯法实例
1. 八皇后问题
在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
问题分析:
(1)如果我们逐行放置皇后则肯定没有任意两个皇后位于同一行,只需要判断列和对角线即可,因此按行来遍历则可以排除掉行的冲突
(2)使用一个二维数组vis[3][],其中vis[0][i]表示列,vis[1][i]和vis[2][i]表示对角线。因为(x,y)的y-x值标识了主对角线,x+y值标识了副对角线(由于y-x可能为负,所以存取时要加上n)。vis[0][i] = 1表示当前列存在皇后,vis[1][i]和vis[2][i] = 1 则分别表示主对角线和副对角线存在皇后
(3)再使用一个数组C,来记录皇后的排列
例如(3,2),主对角线的值是-1,图a中所有-1的格子都是坐标的主对角线,副对角线的值是5,图b中所有5的格子都是坐标的副对角线
列出四皇后问题的解:(由于八皇后的解太多,无法列举,因此列出死皇后的解,原理是一样的)
代码实现:
#include<cstdio>
#include<cstring>
using namespace std;
int C[50];//用于记录每一行的皇后的列
int vis[3][50];//用于记录当前坐标的列,主、副对角线是否存在皇后
int tot = 0;//用于记录解的数量
int n = 8;//皇后的数量
void search(int cur) {
int i, j;
if(cur == n) {
for(int i = 0; i < n; i++) {
printf("%d ", C[i]);
}
printf("\n");
tot++;
} else for(i = 0; i < n; i++) {
//结合上两个表分析,cur表示所在行,i表示所在列,例如cur=1,i=2,则表示坐标点(1,2)
if(!vis[0][i] && !vis[1][cur+i] && !vis[2][cur-i+n]) {
C[cur] = i;
vis[0][i] = vis[1][cur+i] = vis[2][cur-i+n] = 1;
search(cur+1);
vis[0][i] = vis[1][cur+i] = vis[2][cur-i+n] = 0;
}
}
}
int main() {
memset(vis, 0, sizeof(vis));
search(0);
printf("%d\n", tot);
return 0;
}
流程分析:(要结合上面两张对角线图)
cur表示行,i表示列
当cur = 0,i =0:表示坐标(0,0),此时第一个,无论vis[0][0],vis[1][0],vis[2][8]都是0,所以可以将第一个皇后放在(0,0),并分别为三个vis赋值1,第一次递归:(去到下一行)
- 当cur = 1,i = 0:表示坐标(1,0),vis[0][0] 已经为1,表示当前列有皇后了,i+1,横移列;
- 当cur = 1,i = 1:表示坐标(1,1),但是v[2][8] 为 1,表示当前坐标的副对角线已经有皇后了,i+1,横移列;
- 当cur = 1,i = 2:表示坐标(1,2),此时v[0][2],v[1][3],v[2][7] = 0,表示当前坐标的列,主对角线,副对角线都不存在皇后,此坐标可以放置皇后,然后递归,去到下一行
…以此类推,可以得出解