暴力搜索 回溯法 —— 八皇后问题

暴力搜索 枚举

暴力搜索的核心就是暴力,即把所有的可能性都列出来,然后一一试验,最后得到解。

下面来看看一些暴力搜索的例子:

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,第一次递归:(去到下一行)

  1. 当cur = 1,i = 0:表示坐标(1,0),vis[0][0] 已经为1,表示当前列有皇后了,i+1,横移列;
  2. 当cur = 1,i = 1:表示坐标(1,1),但是v[2][8] 为 1,表示当前坐标的副对角线已经有皇后了,i+1,横移列;
  3. 当cur = 1,i = 2:表示坐标(1,2),此时v[0][2],v[1][3],v[2][7] = 0,表示当前坐标的列,主对角线,副对角线都不存在皇后,此坐标可以放置皇后,然后递归,去到下一行

…以此类推,可以得出解

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值