public class NQueen { //N皇后:
/*
N皇后问题:给定一个正整数N,在N*N的棋盘上摆N个皇后,返回有多少种摆法
要求:任意二个皇后不能同行、同列、同斜线
*/
public static void main(String[] args) {
int n=14;
long start1 = System.currentTimeMillis(); //System.currentTimeMillis() 在java中是最常用的获取系统时间的方法---单位:毫秒(秒=毫米/1000)
System.out.println(nqueen1(n));
long end1 = System.currentTimeMillis(); //start:开始时间, end:结束时间, 差值:方法系统运行时间(start 和 end 可以修改命名)
System.out.println("cost timr:" + (end1 - start1) + "ms"); //可以将 差值/1000 ,ms -换- s ,以获得秒的单位
/*
365596
cost timr:6008ms
结论:结果一样,时间复杂度没变,但(常量改进)方法快近25倍
365596
cost timr:272ms
*/
long start2 = System.currentTimeMillis(); //System.currentTimeMillis() 在java中是最常用的获取系统时间的方法---单位:毫秒(秒=毫米/1000)
System.out.println(nqueen2(n));
long end2 = System.currentTimeMillis(); //start:开始时间, end:结束时间, 差值:方法系统运行时间(start 和 end 可以修改命名)
System.out.println("cost timr:" + (end2 - start2) + "ms"); //可以将 差值/1000 ,ms -换- s ,以获得秒的单位
}
//调用实现方法
public static int nqueen1(int n) {
if(n<1) {
return 0; //n<1,没必要执行
}
int[] arr = new int[n]; //创建n长度的数组---arr[4]=6: 第五个皇后-5行6列
return queen1(arr,0,n); //调用实现方法
}
//第一种方法
//实现方法: arr[4]=6: 第五个皇后-5行6列 i:目前在第i行
public static int queen1(int[] arr,int i,int n) {
if(i==n) { //n(0~n-1),越界-即已经找完一种方法
return 1;
}
int res=0; //初始化
for(int j=0;j<arr.length;j++) { //尝试每一行的皇后摆在 j 列上是否可行(会尝试所有列-j不可行就尝试j+1)
if(asd(arr,i,j)) { //调用分支界限:true则运行---判断当前 i行j列的皇后 是否跟之前的皇后相冲突(行、列、斜线)
arr[i]=j; //满足条件,则能在j列摆上i行的皇后
res+=queen1(arr,i+1,n); //于是进行i+1皇后摆的列(重新尝试0~n-1个列)
}
}
return res;
}
//内部方法-分支限界:排除出现同列、同行、同斜线的情况
public static boolean asd(int[] arr,int i,int j) {
for(int k=0;k<i;k++) { //判断所有情况
if(j==arr[k] || Math.abs(arr[k]-j)==Math.abs(k-i) ) { //当列相同、同斜线时-输出false(不执行)
//Math.abs() :返回值的绝对值,注意:当输入值为最小值,依旧返回最小值(int:-2147483648~2147483647 最小值的相反数已溢出,绝对值+1-变-最小值)
//这里同斜线(即 |j-arr[k]| == |i-k|,如同斜线 arr[8]=10,arr[5]=7, |10-8|=|7-5|)
return false;
}
}
return true;
}
//第二种方法---更快(位运算)
//调用实现方法
public static int nqueen2(int n) {
if(n<1 || n>32) { //二进制方法,(字符32位)
return 0; //n<1,没必要执行
}
int limit = n == 32?-1 : (1<<n)-1; //创建变量:limit,当N=32时,32个位都是1,即limit=-1,否则将1右移n位数-1(创建出最后有n个1的二进制数)
//N=8,即 00000000000000000000000011111111, [1<<8-1 : 100000000 -1 : 11111111]
return queen2(limit,0,0,0); //调用实现方法
}
/* limit:对n的取值范围进行限制,n=5, limit=11111
colLim:列的限制(哪个行、列有皇后+1,然后这个列其他行就不能+1)
leftDiaLim:左斜线的限制(当第一行其中一个列+1,它的左下所有行的斜线都+1,其他行同理)
实现方法:如第一行(00010000),左n行:(00010000)<<n
rightDiaLim:右斜线的限制(当第一行其中一个列+1,它的右下所有行的斜线都+1,其他行同理)
*/
public static int queen2(int limit,int colLim,int leftDiaLim,int rightDiaLim) {
if(colLim == limit) { //即所有列都成功完成皇后选择递归就退出
return 1;
}
// 它= colLim | leftDiaLim | rightDiaLim :即将所有位为1的集合在一起, |(或):有1即为1 ,&(并):都为1即为1 ,~(非):0则为1
//limit & 它 ,即只保留最后n个位置的1
int pos = limit &( ~(colLim | leftDiaLim | rightDiaLim) ); //取所有位上不为1的位置-(不在同列、不在斜线上)(作为候补皇后位置)
int mostRightOne = 0;
int res = 0;
while(pos != 0) { //列上还有皇后位置可选
mostRightOne = pos & (~pos +1); //只提取出Pos最右1的位
pos = pos - mostRightOne; //提取出最右1之后的pos
res +=queen2(limit,colLim | mostRightOne,(leftDiaLim | mostRightOne)<<1, (rightDiaLim | mostRightOne)>>>1);
// colLim | mostRightOne: 原来列限制 或(+) 现在放皇后列的限制
//(leftDiaLim | mostRightOne)<<1:原来左斜线限制 或 现在放皇后之后左斜线限制
// (rightDiaLim | mostRightOne) >>>1:原来右斜线限制 或 现在放皇后之后右斜线限制
}
return res;
}
}