数据结构与算法九:经典算法面试题--2.八皇后问题

356 阅读14分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

关注我,以下内容持续更新

数据结构与算法(一):时间复杂度和空间复杂度

数据结构与算法(二):桟

数据结构与算法(三):队列

数据结构与算法(四):单链表

数据结构与算法(五):双向链表

数据结构与算法(六):哈希表

数据结构与算法(七):树

数据结构与算法(八):排序算法

数据结构与算法(九):经典算法面试题

八皇后问题

八皇后问题,汉诺塔,阶乘问题,迷宫问题,球和篮子的问题,都可以通过递归(回溯)来解决,除此之外,比如快排,归并排序,二分查找,分治算法等排序算法也可以用递归来解决,本篇主要介绍八皇后问题如何用递归解决

八皇后问题,是回溯算法的典型案例。在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法(92)种

思路分析

1)按行放置皇后,1行只能放1个皇后且只能放1个,例如第1个皇后放第1行,第2个皇后放在第2行,第n个皇后放在第n行;

2)当第1个皇后放下后,再放第2个皇后: 依次尝试第2行的所有列,判断是否与已放好的皇后互相攻击,如果攻击,继续尝试下一列,如果不攻击立即放下,继续放置第3个皇后

3.1)如果第2个皇后可以放下,继续放置第3个皇后,还是依次尝试第3行的所有列,第三个皇后放下后,继续放置第 4、5、6、7、8个皇后,当第8个皇后页放好后,算是找到了一个解

3.2)如果第2个皇后遍历完所有的列无法放下,那么回溯放置第1个皇后:刚才第1个皇后放在第1行第1列,那么回溯的位置从第1行第2列开始尝试放置第1个皇后,第1个皇后放下后继续重复步骤2)和3);

4)当拿到一个解时,逆序回溯其他的解,先回溯"在前边7个皇后放置好的前提下"第8个皇后可以放置的所有解;再回溯"在前边6个皇后放置好的前提下"第7和第8个皇后可以放置的所有解;直至回溯完第1个皇后所有可以放置的列的所有解,就拿到了八皇后所有的解

注意:回溯的情况有2种可能,一种可能是放置皇后的过程中如果某个皇后所有的列都无法放置,就回溯放置上一个皇后;第二种是找到一种解后,逆序回溯寻找其他的解

回溯的规律是这样的,当找到第一种解之后,倒序回溯找下一个解,例如八皇后问题找到一个种解之后先回溯第8个皇后(假设第一种解的第8个皇后放置的位置是queen[k][j]),那么依次尝试放置这一行后边的所有列(就是从queen[k][j+1]到queen[k][n]),如果尝试过程中不与前边的皇后攻击,立即放下,尝试完k行j列后边所有的列后就找到了"在前边7种皇后已放好的前提下"第8个皇后可以放置的所有解;继续回溯下一种解,就是寻找"在前边6个皇后已放置好的前提下",第7和第8个皇后放置的所有解,就是从第7个皇后开始回溯(假设第一种解的第7个皇后放置的位置是queen[k][j]),依次尝试queen[k][j+1]到queen[k][n]的位置,如果尝试过程中不与前边的皇后攻击,立即放下第7个皇后(假设第7个放置的位置是queen[k][i]),继续尝试放置第8个皇后,第8个皇后尝试完从0到n列,,就找到在"前边7个皇后已放置好的前提下",第8个皇后放置的所有解,下次再依次回溯queen[k][i]后边所有的列,如果不攻击,立即放下再尝试放置第8个皇后所有的列,直至第7个皇后尝试完后边所有的列,就找到了"在前边6个皇后已放置好的前提下",第7和第8个皇后放置的所有解;再回溯第6个皇后,以此类推,当尝试完第一个皇后这一行后边所有的列,就找到了八皇后的所有的解法.

完整代码

本篇主要介绍八皇后问题的两种解题思路.

第一种:用2个邻接矩阵分别记录放置的皇后和被攻击的位置,放皇后时先判断这个位置是否是被攻击状态,如果不攻击则放下,每次放下皇后时,就更新被攻击的状态;如果攻击就往上回溯,回溯时要撤回刚才放下的皇后并且回退被攻击的记录. 当放下第8个皇后时就拿到了一种解,再回溯寻找其他的解.

第二种:用一维数组queen记录八皇后放置的位置,queen[i]代表第i+1个皇后放在queen[i]这一列;自定义个方法传入位置的行和列,判断该位置是否与已放好的皇后互相攻击,如果能放下则返回YES否则返回NO,每次放皇后时调用该方法判断该位置是否可以放下,如果可以放下,就继续放下一个皇后;如果无法放下就往上回溯,回溯时要撤回刚才放下的皇后.当放下第8个皇后时就拿到了一种解,再回溯寻找其他的解.

方式一

- (void)viewDidLoad {
    [super viewDidLoad];
    // 8皇后问题
    [self QueenN:8];
}
-(void)QueenN:(int)n{
    //1.初始化n皇后格子和被攻击到的格子

    //1.1 创建n皇后格子
    NSMutableArray<NSMutableArray<NSString*>*>*queen = [NSMutableArray array];

    //1.2. 创建存放8皇后的攻击状态,1代表攻击,0代表不攻击;开始默认都不攻击
    NSMutableArray*attack = [NSMutableArray array];

    //1.3 初始化
    for (int i = 0; i< n; i++) {
        NSMutableArray*arr1 = [NSMutableArray array];
        NSMutableArray*arr2 = [NSMutableArray array];
        for (int j = 0; j< n; j++) {
            [arr1 addObject:@"."];
            [arr2 addObject:@"0"];
        }
        [queen addObject:arr1];
        [attack addObject:arr2];
    }
    
    //2.resolve 存放最后所有的答案
    NSMutableArray *resolve = [NSMutableArray array];

    //3. 解决 8 皇后问题
    [self backtrack:0 n:n queen:queen attack:attack resolve:resolve];
     
    //打印所有的答案
    [self showResolve:resolve];
}

//将皇后所有攻击到的位置进行标记
-(void)updateAttack:(NSMutableArray*)attack x:(int)x y:(int)y{

    //ds存放可以攻击的 8 个方向

    NSArray*ds = @[NSStringFromCGPoint(CGPointMake(0, 1)),NSStringFromCGPoint(CGPointMake(1, 0)),NSStringFromCGPoint(CGPointMake(0, -1)),NSStringFromCGPoint(CGPointMake(-1,0)),NSStringFromCGPoint(CGPointMake(-1, -1)),NSStringFromCGPoint(CGPointMake(1, 1)),NSStringFromCGPoint(CGPointMake(-1, 1)),NSStringFromCGPoint(CGPointMake(1,-1))];

    

    //通过两层循环将将皇后所有攻击到的位置进行标记

    for (int i = 1; i<attack.count; i++) {//从1到 n-1 个距离延伸

        for (int j = 0; j<ds.count; j++) {//遍历 8 个方向

            CGPoint p = CGPointFromString(ds[j]);//取出第j个方向

            int newX = x+ i*p.x; // i*p.x就是距离皇后i个距离;newX就是距离皇后i个距离的x坐标

            int newY = y+ i*p.y; // i*p.y就是距离皇后i个距离;newY就是距离皇后i个距离的y坐标

            if (newX >=0 && newX<attack.count && newY >=0 && newY<attack.count ) {

                attack[newX][newY] = @"1";

            }

        }

    }

}

/**
 k:代表当前处理的行;
 n:代表n皇后问题
 queen:代表皇后的存储状态
 attack:代表皇后的攻击状态
 */
-(void)backtrack:(int)k n:(int)n queen:(NSMutableArray*)queen attack:(NSMutableArray<NSMutableArray<NSString*>*>*)attack resolve:(NSMutableArray*)resolve{

    if (k == n) {//回溯的第一种情况:找到一种解后,逆序回溯寻找其他的解

        [resolve addObject:[self copyQueen:queen]];//当k == n时说明最后一个皇后已放置,已拿到一种解,将其放在resolve里面

        return ;

    }

    //因为第k个皇后放在第k行,所以遍历第k行的所有列,看第k个皇后放在哪一列

    //如果attack[k][i] == 0代表第 k 个皇后可以放在第i列

    //遍历 0到n-1列,在循环中回溯试探皇后可放置的位置

    for (int i = 0; i<n; i++) {

        if ([attack[k][i] intValue] == 0) {//判断k行i列是否可放置皇后

            NSMutableArray*tmpAttack = [self copyAttack:attack];//暂存attack,用于回溯

            queen[k][i] = @"Q";//在该位置放置皇后

            [self updateAttack:attack x:k y:i];//更新attack数组

            [self backtrack:k+1 n:n queen:queen attack:attack resolve:resolve];//递归试探下一行皇后可放置的位置

           

            /**

             1.这里是回溯放置上一个皇后,走到这里有 2 种可能,一种可能是已经拿到一个解,另一种可能是上个皇后所有列无法放置

             2.走到这里说明递归栈里的最后一个backtrack方法已走完,即栈顶的backtrack方法已移除,,这一步的操作是回溯试探递归栈里当前的栈顶的backtrack方法

             */

            attack = tmpAttack;
            queen[k][i] = @".";
        }

        else{
            //else这里是产生回溯的第二种情况:如果某个皇后所有的列都无法放置,就回溯放置上一个皇后;
        }

    }

}

下面这些是辅助方法

-(NSMutableArray<NSMutableArray<NSString*>*>*)copyAttack:(NSMutableArray<NSMutableArray<NSString*>*>*)attack{
    NSMutableArray<NSMutableArray<NSString*>*>*tmp = [NSMutableArray array];
    for (int i = 0; i< attack.count; i++) {
        NSMutableArray*arr1 = [NSMutableArray array];
        for (int j = 0; j< attack[0].count; j++) {
            [arr1 addObject:attack[i][j]];
        }
        [tmp addObject:arr1];
    }
    return tmp;
}

-(NSMutableArray<NSMutableArray<NSString*>*>*)copyQueen:(NSMutableArray<NSMutableArray<NSString*>*>*)queen{
    NSMutableArray<NSMutableArray<NSString*>*>*tmp = [NSMutableArray array];
    for (int i = 0; i< queen.count; i++) {
        NSMutableArray*arr1 = [NSMutableArray array];
        for (int j = 0; j< queen[0].count; j++) {
            [arr1 addObject:queen[i][j]];
        }
        [tmp addObject:arr1];
    }
    return tmp;
}

//打印最后的答案
-(void)showResolve:(NSMutableArray*)resolve{
    printf("打印8皇后的%d种解法: \n\n",(int)resolve.count);
    for (int i = 0; i<resolve.count; i++) {
        printf("第%d种解法: \n",i+1);
        [self showMatex:resolve[i]];
    }
}


//打印邻接矩阵
-(void)showMatex:(NSMutableArray<NSMutableArray<NSString*>*>*)matrix{
    for (int i = 0; i< matrix.count; i++) {
        for (int j = 0; j< matrix[0].count; j++) {
            printf("%s   ",matrix[i][j].UTF8String);
        }
        printf("\n \n");
    }
    printf("\n \n");
}

//打印 queen 和 attack
-(void)showQueen:(NSMutableArray<NSMutableArray<NSString*>*>*)queen attack:(NSMutableArray<NSMutableArray<NSString*>*>*)attack{
    printf("打印queen\n");
    for (int i = 0; i< queen.count; i++) {
        for (int j = 0; j< queen[0].count; j++) {
            printf("%s   ",queen[i][j].UTF8String);
        }
        printf("\n \n");
    }

    printf("打印attack\n");
    for (int i = 0; i< attack.count; i++) {
        for (int j = 0; j< attack[0].count; j++) {
            printf("%s   ",attack[i][j].UTF8String);
        }
        printf("\n \n");
    }

    printf("\n \n \n \n");
}

代码解读

 1.初始化n皇后格子queen和被攻击到的格子attack,queen和attack都用二维数组表示,queen初始化为"."表示 开始状态未放皇后,当某个格子放入皇后,就把这个格子的值变为Q;attack初始化为0表示初始状态没有被攻击到的格子,每次放入皇后之后就更新attack,把所有被攻击到的格子设置为1;还要初始化一个resolve数组,存放每种解法的答案,每次拿到解就把它放到resolve里面

 2.八个皇后的放置必须满足每一行只能放一个,所以第一个皇后从第一行开始放,下一个皇后从下一行开始尝试放,当放下最后一个皇后时,说明已经拿到一种解,将解放入resolve,即当k == n时, 执行[resolve addObject:queen];

 3.那么放下皇后时,如何更新被攻击的格子attack,首先明确一个格子放置皇后以后可能会攻击到的位置,用ds存放可能攻击到的8 个方向:上、下、左、右、左上角、右上角、左下角、右下角,然后延伸可能被攻击到的位置,更新attack

 4. 递归backtrack的过程中,回溯产生的情况有2种,第一种是一种可能是放置皇后的过程中如果某个皇后所有的列都无法放置,就回溯放置上一个皇后;第二种是找到一种解后,逆序回溯寻找其他的解;

方式二

-(void)queenN:(int)n{
    //queen代表皇后放置的位置,queen[i]代表第i+1个皇后放在queen[i]这一列
    NSMutableArray*queen = [NSMutableArray array];
    for (int i = 0; i<n; i++) {
        [queen addObject:@(-1)];
    }

    //resolve存放所有结果
    NSMutableArray<NSMutableArray*>*resolve = [NSMutableArray array];
    
    //解题过程
    [self placeQueen:queen row:0 resolve:resolve];//从第一个皇后开始放

    //打印所有答案
    printf("共%d种解\n",(int)resolve.count);
    for (int i = 0; i<resolve.count; i++) {
        printf("第%d种解:", i+1);
        for (int j = 0; j<n; j++) {
            printf("%4d",[resolve[i][j] intValue]);
        }
        printf("\n\n");
    }

}


/**
 标号①位置后面有注解
*/
-(void)placeQueen:(NSMutableArray*)queen row:(int)row resolve:(NSMutableArray*)resolve{
    int n = (int)queen.count;
    for (int col = 0; col<n; col++) {//遍历所有的列
        if ([self allowPlaceQueen:queen row:row col:col]) {
            queen[row] = @(col);
            if (row < n-1) {//继续递归放下一个皇后
                [self placeQueen:queen row:++row resolve:resolve];
                queen[--row] = @(-1);
            }else{ //标号①
                [resolve addObject:[queen mutableCopy]];
                return;
            }
        }
    }
}

//判断某个皇后是否与已经放下的皇后冲突
-(BOOL)allowPlaceQueen:(NSArray*)queen row:(int)row col:(int)col{
    //row+1个皇后已经放下,所以遍历0-row
    for (int i = 0; i<row; i++) {
        if ([queen[i] intValue] == col) {//在同一列,攻击
            return NO;
        }
        if (abs([queen[i] intValue] - col) == abs(i - row)) {//在同一个对角线,攻击
            return NO;
        }
    }
    return YES;
}

placeQueen方法中标号①的注解

row == n-1是递归出口,堆栈中栈顶的递归函数出桟后,然后逆序出桟下一个递归函数;因为入栈的顺序是row=0、1、2、3、4,所以出桟的顺序为row=4、3、2、1、0;  

递归逻辑:当找到递归出口时(row=7),也就是[self placeQueen:queen row:7 resolve:resolve]执行完后,继续执行栈顶函数[self placeQueen:queen row:6 resolve:resolve],因为入栈阶段[self placeQueen:queen row:6 resolve:resolve]入栈时,col=6,立即暂停入栈接着递归执行了[self placeQueen:queen row:7 resolve:resolve],(暂停入栈时,会暂存本次递归执行到的位置和参数的值),因为for循环中col=6后面的情况没有执行完,当下次递归执行完后,回溯到[self placeQueen:queen row:6 resolve:resolve]时,继续执行暂存的位置,也就是for循环中col=6后面的情况, 如果col=6后面的情况可以遍历完那么函数[self placeQueen:queen row:6 resolve:resolve]执行完返回,就可以出桟,继续回溯[self placeQueen:queen row:5 resolve:resolve];如果没有遍历完就发现可以放下皇后,进入下一个递归[self placeQueen:queen row:7 resolve:resolve],因为没有执行完所以不出桟,堆栈中会暂存此时执行到的for循环的位置;结合下面的打印发现[self placeQueen:queen row:6 resolve:resolve]遍历完了col=6后面的情况,不能放下皇后,因为for循环走完了本次递归已返回,所以[self placeQueen:queen row:6 resolve:resolve]出桟,此时栈顶为[self placeQueen:queen row:5 resolve:resolve], 继续执行栈顶函数;以此类推...

注意:每次出桟堆栈的桟顶函数,逆序执行上一个递归函数时,都要撤销上一步皇后的记录,因为要重新放置,执行queen[--row] = @(-1); 例如出桟<参数row=6的placeQueen函数>后,将要执行<参数row=5的placeQueen函数>时,也就是把之前的queen[5]拿走重新尝试放置queen[5],即queen[5] = @(-1); (这里的row和5是索引)

**共92种解**

**第1种解:   0   4   7   5   2   6   1   3**

**第2种解:   0   5   7   2   6   3   1   4**

**第3种解:   0   6   3   5   7   1   4   2**

**第4种解:   0   6   4   7   1   3   5   2**

**第5种解:   1   3   5   7   2   0   6   4**

**第6种解:   1   4   6   0   2   7   5   3**

**第7种解:   1   4   6   3   0   7   5   2**

**第8种解:   1   5   0   6   3   7   2   4**

**第9种解:   1   5   7   2   0   3   6   4**

**第10种解:   1   6   2   5   7   4   0   3**

**第11种解:   1   6   4   7   0   3   5   2**

**第12种解:   1   7   5   0   2   4   6   3**

**第13种解:   2   0   6   4   7   1   3   5**

**第14种解:   2   4   1   7   0   6   3   5**

**第15种解:   2   4   1   7   5   3   6   0**

**第16种解:   2   4   6   0   3   1   7   5**

**第17种解:   2   4   7   3   0   6   1   5**

**第18种解:   2   5   1   4   7   0   6   3**

**第19种解:   2   5   1   6   0   3   7   4**

**第20种解:   2   5   1   6   4   0   7   3**

**第21种解:   2   5   3   0   7   4   6   1**

**第22种解:   2   5   3   1   7   4   6   0**

**第23种解:   2   5   7   0   3   6   4   1**

**第24种解:   2   5   7   0   4   6   1   3**

**第25种解:   2   5   7   1   3   0   6   4**

**第26种解:   2   6   1   7   4   0   3   5**

**第27种解:   2   6   1   7   5   3   0   4**

**第28种解:   2   7   3   6   0   5   1   4**

**第29种解:   3   0   4   7   1   6   2   5**

**第30种解:   3   0   4   7   5   2   6   1**

**第31种解:   3   1   4   7   5   0   2   6**

**第32种解:   3   1   6   2   5   7   0   4**

**第33种解:   3   1   6   2   5   7   4   0**

**第34种解:   3   1   6   4   0   7   5   2**

**第35种解:   3   1   7   4   6   0   2   5**

**第36种解:   3   1   7   5   0   2   4   6**

**第37种解:   3   5   0   4   1   7   2   6**

**第38种解:   3   5   7   1   6   0   2   4**

**第39种解:   3   5   7   2   0   6   4   1**

**第40种解:   3   6   0   7   4   1   5   2**

**第41种解:   3   6   2   7   1   4   0   5**

**第42种解:   3   6   4   1   5   0   2   7**

**第43种解:   3   6   4   2   0   5   7   1**

**第44种解:   3   7   0   2   5   1   6   4**

**第45种解:   3   7   0   4   6   1   5   2**

**第46种解:   3   7   4   2   0   6   1   5**

**第47种解:   4   0   3   5   7   1   6   2**

**第48种解:   4   0   7   3   1   6   2   5**

**第49种解:   4   0   7   5   2   6   1   3**

**第50种解:   4   1   3   5   7   2   0   6**

**第51种解:   4   1   3   6   2   7   5   0**

**第52种解:   4   1   5   0   6   3   7   2**

**第53种解:   4   1   7   0   3   6   2   5**

**第54种解:   4   2   0   5   7   1   3   6**

**第55种解:   4   2   0   6   1   7   5   3**

**第56种解:   4   2   7   3   6   0   5   1**

**第57种解:   4   6   0   2   7   5   3   1**

**第58种解:   4   6   0   3   1   7   5   2**

**第59种解:   4   6   1   3   7   0   2   5**

**第60种解:   4   6   1   5   2   0   3   7**

**第61种解:   4   6   1   5   2   0   7   3**

**第62种解:   4   6   3   0   2   7   5   1**

**第63种解:   4   7   3   0   2   5   1   6**

**第64种解:   4   7   3   0   6   1   5   2**

**第65种解:   5   0   4   1   7   2   6   3**

**第66种解:   5   1   6   0   2   4   7   3**

**第67种解:   5   1   6   0   3   7   4   2**

**第68种解:   5   2   0   6   4   7   1   3**

**第69种解:   5   2   0   7   3   1   6   4**

**第70种解:   5   2   0   7   4   1   3   6**

**第71种解:   5   2   4   6   0   3   1   7**

**第72种解:   5   2   4   7   0   3   1   6**

**第73种解:   5   2   6   1   3   7   0   4**

**第74种解:   5   2   6   1   7   4   0   3**

**第75种解:   5   2   6   3   0   7   1   4**

**第76种解:   5   3   0   4   7   1   6   2**

**第77种解:   5   3   1   7   4   6   0   2**

**第78种解:   5   3   6   0   2   4   1   7**

**第79种解:   5   3   6   0   7   1   4   2**

**第80种解:   5   7   1   3   0   6   4   2**

**第81种解:   6   0   2   7   5   3   1   4**

**第82种解:   6   1   3   0   7   4   2   5**

**第83种解:   6   1   5   2   0   3   7   4**

**第84种解:   6   2   0   5   7   4   1   3**

**第85种解:   6   2   7   1   4   0   5   3**

**第86种解:   6   3   1   4   7   0   2   5**

**第87种解:   6   3   1   7   5   0   2   4**

**第88种解:   6   4   2   0   5   7   1   3**

**第89种解:   7   1   3   0   6   4   2   5**

**第90种解:   7   1   4   2   0   6   3   5**

**第91种解:   7   2   0   5   1   4   6   3**

**第92种解:   7   3   0   2   5   1   6   4**

关注我

如果觉得我写的不错,请点个赞 关注我 您的支持是我更文最大的动力!