暴力递归——N皇后详解 && 如何用位运算进行优化

简介: 暴力递归——N皇后详解 && 如何用位运算进行优化


N皇后

N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列,也不在同一条斜线上。给定一个整数n,返回n皇后的摆法有多少种。n=1,返回1。n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0。n=8,返回92

N皇后游戏原则:

  • 不能在同一行
  • 不能在同一列
  • 不能在同一条斜线上(左右斜线都不行)

现在我们来分析题目:

在一行上每次只摆一个皇后,从左往右开始摆,每一行都如此。并且记下这个皇后的位置。后续行数摆的时候只用避免共列和共斜线的问题。如果到某一行发现违反规则,那就退回上一行重新摆,将皇后右移一个位置,后续行数再接着递归。

如何记录皇后的位置,也就是记录皇后的坐标(x,y),只需要用一个数组既可。比如recored[7]=13,代表第7行的皇后在第13列;record[0]=3代表第0行的皇后在第3列

如何检测皇后是否冲突,共行问题不用考虑,因为一开始我们就规定每一行都是只摆放一个。假设有两个皇后的位置如下:

(a,b) 和 (c,d)

不共列:b != d

不共斜线:|a-c| != |b-d|

如果i来到了第n行,那就说明越界了,并且隐含的条件是之前行的摆放都有效,所以可以返回一种摆放结果。

如果没有来到第n行,说明当前行可以摆放皇后,那么就尝试这一行上所有的列,看是否与0~i-1行的皇后冲突。 如果不冲突,就记录好这一行皇后的位置并且去下一行递归;如果冲突就跳到下一列。

// 尝试i行上所有的列(0~n-1列)
    // 如果当前i行的皇后,放在第j列,
    // 不会和之前行(0~i-1行)的皇后冲突(不共行&&不共列&&不公斜线)
    // 那放第j列可以,认为有效,接着去i+1行递归
    // 否则,无效,放下一列
    for(int j=0; j<n; j++) {
      if(isValid(record, i, j)) {// record记录的是0~i-1行皇后的位置
        record[i]=j;// 第i行的皇后在第j列
        ans+=process1(i+1, record, n);
        // 因为在每一行都是直接改要放的列数,所以无需恢复现场
      }
    }

完整代码:

package com.harrison.class12;
public class Code08_NQueens {
  // 0~i-1行的皇后位置都摆放好了,不用再考虑,并且符合要求:不共行、不共列、不共斜线
  // i  表示当前来到第i行摆皇后
  // record[0~i-1]  存放摆好的记录
  // record[0]=3  ->  第0行皇后摆在第3列
  // n  表示一共有多少行(0~n-1行),如果来到n行就越界了
  // 在[0~i-1]行皇后都摆好的情况下,
  // 返回i及其后面行数摆放有效的方式有多少种(整个棋盘都摆好)
  public static int process1(int i,int[] record,int n) {
    // 如果i来到终止行,说明之前行数的摆放都有效,
    // 返回一种合理的摆放方式
    if(i==n) {
      return 1;
    }
    // 如果i没有到终止行,那么当前行可以摆
    int ans=0;
    // 尝试i行上所有的列(0~n-1列)
    // 如果当前i行的皇后,放在第j列,
    // 不会和之前行(0~i-1行)的皇后冲突(不共行&&不共列&&不公斜线)
    // 那放第j列可以,认为有效,接着去i+1行递归
    // 否则,无效,放下一列
    for(int j=0; j<n; j++) {
      if(isValid(record, i, j)) {// record记录的是0~i-1行皇后的位置
        record[i]=j;// 第i行的皇后在第j列
        ans+=process1(i+1, record, n);
        // 因为在每一行都是直接改要放的列数,所以无需恢复现场
      }
    }
    return ans;
  }
  // record[0~i-1]的皇后需要看,第i行的皇后不需要
  // 返回第i行皇后放在了第j列是否有效
  public static boolean isValid(int[] record,int i,int j) {
    for(int k=0; k<i; k++) {// 0~i-1行的某一行的皇后,假设是第k行的皇后
      if(record[k]==j || (Math.abs(record[k]-j)==Math.abs(i-k))) {
        return false;
      }
    }
    return true;
  }
  public static int nums1(int n) {
    if(n<1) {
      return 0;
    }
    int[] record=new int[n];// record[i] i行的皇后 放在了第几列
    return process1(0, record, n);
  }
  public static void main(String[] args) {
    int n=8;
    long start=System.currentTimeMillis();
    System.out.println(nums1(n));
    long end=System.currentTimeMillis();
    System.out.println("cost time:"+(end-start)+"ms");
  }
}

上面这种方法对于解决N皇后问题在如何尝试的方式上已经是最优的了,如果我们不去搞学术的话,对于工作尝试到这里就可以了,但是!!!不知道大家知不知道位运算是比普通运算快很多的呢?所以我们还可以在常数项方面优化一下,但是注意:用位运算优化后的时间复杂度还是跟原来一样,只是在常数项方面进行了优化,但这么一优化还是快了很多的,请接着往下看。

我们把N皇后问题中的列想象成一个正整数的二进制位。什么意思,比如,8皇后问题就只用最后面的八个二进制位;9皇后问题就只用最后面九个二进制位,,,依次类推。如果在哪一列放了皇后,就将该二进制位设为1(一开始全部为0)。也就是说,1代表放了皇后,0代表没有放。

比如8皇后,在第一行的第二列摆了皇后,

那么就是 01000000

当然,在Java里面,int是只占4个字节的整型,所以最多也只有32个二进制位,所以,用这种方式的话,不能超过32个皇后,因为摆不下。

注意:以下文字描述都是以8皇后举例子!!!

那么,当前行如何知道在哪该放皇后呢?主要看两点,第一,当前行是不是跟之前行的皇后共列,第二,之前行的皇后的左右斜线贯穿的位置,当前行不能放。

所以,接下来准备三个变量,列限制columnLimit,左斜线限制leftLimit,右斜线限制rightLimit。

columnLimit:00000000

leftLimit:00000000

rightLimit:00000000

在第一行的时候,是没有任何限制的,那就可以随便放。

假如放在第4列(下标从0开始算),那么上述三个变量就会变成:

columnLimit:00001000

leftLimit:00010000(columnLimit往左移一位)

rightLimit:00000100(columnLimit往右移一位)

ok,第i行上的皇后放好了,那么如何知道第i+1行上哪些列可以放皇后呢?

上述三个变量一起做或运算的结果就是总的限制:假设第i行第4列放了皇后(下标从0开始算),columnLimit | leftLimit |rightLimit 结果就是:00011100,代表第i+1行上第3、4、5列都不可以再放了。

00001000

00010000

00000100

00011100

好的,那么当前情况就是00011100, 0的位置还可以放皇后,所以每个位置都会去尝试一遍。

假设又在第1列放了个皇后(01000000),那么对于i+1行来说,此时的列限制 columnLimit 就是  01001000(之前是00001000),左斜线限制就是 10100000,右斜线限制就是  00100010。不仅受上一行列限制的影响,而且受之前所有行延申的左右斜线的影响。

给大家画个图加深理解:

image.png

那接下来对于i+1行的下一行的限制就是 上面三个变量一起做或运算后的答案——11101010,只能在剩下的三个零上放皇后。

01001000

10100000

00100010

11101010

处理完三个变量后,继续往下这么玩。。。

补充知识:如何提取二进制数中最右侧的1, => int Ans=N&((~N)+1),请参考这篇文章——利用异或运算的性质进行骚操作

两种方法的完整代码:

package com.harrison.class12;
public class Code08_NQueens {
  // 0~i-1行的皇后位置都摆放好了,不用再考虑,并且符合要求:不共行、不共列、不共斜线
  // i  表示当前来到第i行摆皇后
  // record[0~i-1]  存放摆好的记录
  // record[0]=3  ->  第0行皇后摆在第3列
  // n  表示一共有多少行(0~n-1行),如果来到n行就越界了
  // 在[0~i-1]行皇后都摆好的情况下,
  // 返回i及其后面行数摆放有效的方式有多少种(整个棋盘都摆好)
  public static int process1(int i,int[] record,int n) {
    // 如果i来到终止行,说明之前行数的摆放都有效,
    // 返回一种合理的摆放方式
    if(i==n) {
      return 1;
    }
    // 如果i没有到终止行,那么当前行可以摆
    int ans=0;
    // 尝试i行上所有的列(0~n-1列)
    // 如果当前i行的皇后,放在第j列,
    // 不会和之前行(0~i-1行)的皇后冲突(不共行&&不共列&&不公斜线)
    // 那放第j列可以,认为有效,接着去i+1行递归
    // 否则,无效,放下一列
    for(int j=0; j<n; j++) {
      if(isValid(record, i, j)) {// record记录的是0~i-1行皇后的位置
        record[i]=j;// 第i行的皇后在第j列
        ans+=process1(i+1, record, n);
        // 因为在每一行都是直接改要放的列数,所以无需恢复现场
      }
    }
    return ans;
  }
  // record[0~i-1]的皇后需要看,第i行的皇后不需要
  // 返回第i行皇后放在了第j列是否有效
  public static boolean isValid(int[] record,int i,int j) {
    for(int k=0; k<i; k++) {// 0~i-1行的某一行的皇后,假设是第k行的皇后
      if(record[k]==j || (Math.abs(record[k]-j)==Math.abs(i-k))) {
        return false;
      }
    }
    return true;
  }
  public static int nums1(int n) {
    if(n<1) {
      return 0;
    }
    int[] record=new int[n];// record[i] i行的皇后 放在了第几列
    return process1(0, record, n);
  }
  public static int nums2(int n) {
    if(n<1 || n>32) {
      return 0;
    }
    // n==8 limit最右边8个1,其余全是0
    // n==9 limit最右边9个1,其余全是0
    // n==32 最右边32位全是1 -> 十进制-1
    // n!=32 1左移n位再减一
    // 比如n==8 limit=100000000-00000001 -> 11111111
    int limit=n==32?-1:((1<<n)-1);
    // 一开始第0行的时候,没有任何限制
    return process2(limit, 0, 0, 0);
  }
  // limit 划定了问题的规模,是固定不变的参数
  public static int process2(int limit,int columnLimit,int leftLimit,int rightLimit) {
    if(columnLimit==limit) {// base case
      return 1;
    }
    // pos 当前行可以放皇后的位置 1:不能放  0:可以放
    // (columnLimit | leftLimit | rightLimit ) 总限制
    // 为什么要 & limit
    // 1)~(columnLimit | leftLimit | rightLimit ) 左侧的一坨0取反后变成1会干扰
    // 2)左斜线限制会越界,右斜线不会
    int pos=limit&(~(columnLimit | leftLimit | rightLimit ));
    int ans=0;
    int mostRightOne=0;
    // 尝试pos上的每一个1
    while(pos!=0) {
      mostRightOne=pos & ((~pos)+1);
      pos=pos-mostRightOne;
      ans+=process2(limit, 
              columnLimit | mostRightOne,
              (leftLimit | mostRightOne)<<1, 
              (rightLimit | mostRightOne)>>>1);
    }
    return ans;
  }
  public static void main(String[] args) {
    int n=13;
    long start=System.currentTimeMillis();
    System.out.println(nums1(n));
    long end=System.currentTimeMillis();
    System.out.println("cost time:"+(end-start)+"ms");
    start=System.currentTimeMillis();
    System.out.println(nums2(n));
    end=System.currentTimeMillis();
    System.out.println("cost time:"+(end-start)+"ms");
  }
}

两种方法的时间复杂度都是O(N^N)。

但是用位运算优化后,还是肉眼可见的快。

以下是13皇后的测试结果,

image.png

N皇后问题比较难,尤其是用位运算优化比较难理解,博主费心写下这篇文章做个记录,因为有的面试官可能会问到这些经典的问题。

感谢您的三连或点赞评论支持🙇‍🙇‍🙇‍


相关文章
|
存储 算法
蓝桥杯:递归 与 例题:斐波那契数列及优化与应用
蓝桥杯:递归 与 例题:斐波那契数列及优化与应用
68 0
|
3月前
|
算法 数据挖掘 开发者
LeetCode题目55:跳跃游戏【python5种算法贪心/回溯/动态规划/优化贪心/索引哈希映射 详解】
LeetCode题目55:跳跃游戏【python5种算法贪心/回溯/动态规划/优化贪心/索引哈希映射 详解】
|
3月前
|
算法
数据结构和算法学习记录——时间复杂度的计算(嵌套循环、大O的渐进表示法、双重循环、常数循环、strchr、冒泡排序、二分查找、斐波那契数列递归)
数据结构和算法学习记录——时间复杂度的计算(嵌套循环、大O的渐进表示法、双重循环、常数循环、strchr、冒泡排序、二分查找、斐波那契数列递归)
167 1
|
3月前
|
存储 SQL 算法
LeetCode 题目 87:递归\动态规划\哈希实现 扰乱字符串
LeetCode 题目 87:递归\动态规划\哈希实现 扰乱字符串
|
3月前
|
存储
初阶编程题积累(3)——最接近的三数之和(题目描述、示例、题目思路、题解、解析)
初阶编程题积累(3)——最接近的三数之和(题目描述、示例、题目思路、题解、解析)
25 0
|
算法
基础算法(贪心 合并 位运算)
基础算法(贪心 合并 位运算)
54 0
|
存储 人工智能 算法
【双指针、位运算、离散化、区间合并】思路讲解及代码实现
用一篇Blog来讲解下最近学到的双指针、位运算、离散化、区间合并等算法,为日后的刷题打下坚实的基础。
85 0
|
算法 C语言
【C语言】带你玩转经典算法用二分法在一个有序数组中查找某个数
【C语言】带你玩转经典算法用二分法在一个有序数组中查找某个数
181 0
|
人工智能 算法 C++
[**算法**]关于数字反转的两道例题的分析思考
两个题目看着很像,但是细节不太一样,一个是去处理浮点,(其中有用STL大法的和将小数点前后和小数点分开进行输入的还有利用字符串的读写来处理的)还有一个是去处理整数
137 0
|
算法
【每日挠头算法题】Acwing 756. 蛇形矩阵 —— 巧妙解法
【每日挠头算法题】Acwing 756. 蛇形矩阵 —— 巧妙解法
116 0
【每日挠头算法题】Acwing 756. 蛇形矩阵 —— 巧妙解法