N皇后问题是一个老掉牙的问题了,随便翻一本算法书籍都能看到对它的介绍,其实N皇后问题可以用非递归方法解决,有效避免N过大时的递归工作栈溢出,且占用的存储空间较小,运行速度也较快,达到运行速度和空间合理利用的两全,代码很简单,并不复杂,有时简单也是一种美,意味着可靠和高效。追求程序的复杂和难以理解的编程者不会认同这一点,但对用简单的设计就可以解决的问题用复杂的数据表示加以描述是没有必要的。
我们可以利用对称性解决N皇后问题:
对于n皇后问题的任意解,把它相对于棋盘中线做对称变换,即让N皇后解中任意一个皇后的列c变为n-c,得到的仍然是n皇后问题的一个解。所以剪枝时对第一行剪枝,让第一行皇后列依次取1, 2,3,–,[n/2],若n为偶数,第一行[n/2]+1,[n/2]+2–n列均不用考虑,直接剪掉,若n为奇数,[n/2]+2,—,n列不用考虑直接剪掉,皇后除放置于第一行1,2–,[n/2]列外还可放置于第一行[n/2]+1列,此时剪掉第二行[n/2]+1,—,n列即可也就是对第二行而言,在第一行已放置在最中间列的皇后限制下第二行1,2–,[n/2]列中所有可放置列关于中线的对称列全部剪掉。
用改进的剪枝策略优化N皇后问题所得代码如下:
#include <iostream>
#include <malloc.h>
#include <math.h>
using namespace std;
int place(int i, int k, int* p, bool* col)
{
if (col[i - 1]) //该列被占用
return 0;
for (int m = 0; m < k; m++)
{
if ((abs(m - k) == abs(p[m] - i))) //k+1行i列不是合法位置
return (0);
}
return (1); //k+1行i列是合法位置
}
int find(int n, int* p, int k, bool n_is_odd, bool* col)
{
int i;
i = p[k];
if (k == 1) //如果当前行为第二行且n为奇数,同时第一行皇后位置为正中间,则第二行最多试探到n/2列终止
{
if (n_is_odd && p[0] == n / 2 + 1)
{
if (i + 1 < n / 2)
return i + 1;
else
{
return 0;
}
}
}
for (i++; i <= n; i++)
{
if (place(i, k, p, col) == 1) //在k+1行找到可放置皇后的列
return (i); //返回该列
}
return (0); //在k+1行没有找到可放置皇后的列
}
void output(int n, int* p)
{
int i, j;
for (i = 0; i < n; i++)
{
for (j = 1; j <= n; j++)
{
if (j == p[i])
printf("1 ");
else
printf("0 ");
}
printf("\n");
}
printf("\n");
}
int main()
{
int k, n;
int m;
int* p;
printf("you want to solve n queens problem\n");
printf("n=");
scanf_s("%d", &n); //输入问题规模
p = (int*)malloc(n * sizeof(int)); //p[k]表示k+1行皇后所在列
for (k = 0; k < n; k++)
{
p[k] = 0; //初始化数组p
}
bool *col = (bool*)malloc(n * sizeof(bool)); //表示列是否被皇后占用的数组
for (k = 0; k < n; k++)
{
col[k] = false;
}
k = 0; //初始化为第一行
m = 0; //记录解的个数变量初始化
bool n_is_odd = false;
if (n % 2 != 0)
{
n_is_odd = true;
}
int max_col;
if (n_is_odd)
{
max_col = n / 2 + 1;
}
else
{
max_col = n / 2;
}
while (true)
{
if (k == 0) //进入或回溯至第一行
{
if (p[k] != 0)
col[p[k] - 1] = false;
col[p[k]] = true;
p[k] = p[k] + 1; //试探下一列
if (p[k] > max_col) //利用max_col剪枝,如果n为奇数, 第一行第n / 2 + 1列试探完毕后终止,否则n / 2列试探完毕后终止
{
break;
}
k++; //试探下一行
}
else
{
int temp;
if ((temp = find(n, p, k, n_is_odd, col)) == 0) //k+1行没有找到可放置皇后的位置
{
if (p[k] != 0)
{
col[p[k] - 1] = false;
p[k] = 0; //必要的清理
}
k--; //回溯
}
else
{
if (p[k] != 0)
col[p[k] - 1] = false;
p[k] = temp; //在k+1行找到的位置赋予p[k]
col[temp - 1] = true;
if (k != (n - 1)) //皇后没有全部放置完毕
{
k++; //试探下一行
}
else //皇后全部放置成功,找到解
{
m++;
printf("The %dnd solution\n", m);
output(n, p); //输出解
}
}
}
}
printf("There are %d solutions in total\n", m);
return 0;
}
运行结果
附n皇后问题的递归代码:
该程序段为自己的创作(教科书上现成的代码没必要复制粘贴),意义不是很大,因为比较繁琐,当n<=8时能输出正确结果,n>8时直接STACKOVERFLOW,所以大家看看就好,并不实用
#include <stdio.h>
#include <math.h>
#include <malloc.h>
#include <stdlib.h>
void print(int n);
void put(int k, int n, int *q, int *p);
void output(int n, int *p);
int law(int k ,int i, int *p);
void main ()
{
int n;
printf ("you want to solve the problem of n queens the n=?\n");
scanf ("%d", &n);
print(n);
}
void print(int n)
{
int number, i;
int *p;
number=0;
p=(int *)malloc(n*sizeof(int));
for (i=0; i<n; i++)
*(p+i)=0;
put(1, n, &number, p);
printf("There are %d solutions in total\n", number);
free(p);
}
void put(int k, int n, int *q, int *p)
{
int i;
if (k==1)
{
(*p)++;
if ((*p)>n)
return;
put(k+1, n, q, p);
}
else
{
i=*(p+k-1);
i++;
while (i<=n)
{
if (law(k, i, p))
{
*(p+k-1)=i;
if (k==n)
{
(*q)++;
printf ("The %dnd solution\n", *q);
output(n, p);
put(k, n, q, p);
}
else
{
put(k+1, n, q, p);
}
break;
}
i++;
}
if (i>n)
{
*(p+k-1)=0;
put(k-1, n, q, p);
}
}
}
void output(int n, int *p)
{
int j, c;
for (j=1; j<=n; j++)
{
for (c=1; c<=n; c++)
{
if (c==*(p+j-1))
printf ("%d", 1);
else
printf ("%d", 0);
}
printf ("\n");
}
printf ("\n");
}
int law(int k ,int i, int *p)
{
int m;
for (m=1; m<k; m++)
{
if ((*(p+m-1)==i)||(abs(m-k)==abs(*(p+m-1)-i)))
return(0);
}
return(1);
}
下面是一个优化过的版本,用left_slope_restrict和right_slope_restrict数组记录被皇后所在的斜线占据的各列,col数组指明那些列已被皇后占用,这样可以使皇后放置位置的判断的时间复杂度达到O(1)
空间复杂度仍为O(n)只有每次抵达新的一行或从一行回溯至上一行时会以O(n)的时间复杂度更新
left_slope_restrict和right_slope_restrict,而未优化的版本每次在一行上多次搜索时每次都要以O(n)的时间复杂度判断位置能否放置皇后,所以每一行的时间复杂度从平方级减到线性,应该是能加速问题求解的
C++代码:
#include <iostream>
#include <malloc.h>
#include <math.h>
using namespace std;
int place(int i, int k, int* p, bool* col, bool* left_slope_restrict, bool* right_slope_restrict)
{
if (col[i - 1] || left_slope_restrict[i - 1] || right_slope_restrict[i- 1]) //该列被占用,//k+1行i列不是合法位置
return 0;
return (1); //k+1行i列是合法位置
}
int find(int n, int* p, int k, bool n_is_odd, bool* col, bool* left_slope_restrict, bool* right_slope_restrict)
{
int i;
i = p[k];
if (k == 1) //如果当前行为第二行且n为奇数,同时第一行皇后位置为正中间,则第二行最多试探到n/2列终止
{
if (n_is_odd && p[0] == n / 2 + 1)
{
if (i + 1 < n / 2)
return i + 1;
else
{
return 0;
}
}
}
for (i++; i <= n; i++)
{
if (place(i, k, p, col, left_slope_restrict, right_slope_restrict) == 1) //在k+1行找到可放置皇后的列
return (i); //返回该列
}
return (0); //在k+1行没有找到可放置皇后的列
}
void output(int n, int* p)
{
int i, j;
for (i = 0; i < n; i++)
{
for (j = 1; j <= n; j++)
{
if (j == p[i])
printf("1 ");
else
printf("0 ");
}
printf("\n");
}
printf("\n");
}
void left_moved(bool* cur, int n)
{
for (int i = 1; i < n; ++i)
{
cur[i - 1] = cur[i];
}
}
void right_moved(bool* cur, int n)
{
for (int i = n - 1; i > 0; --i)
{
cur[i] = cur[i - 1];
}
}
int main()
{
int k, n;
int m;
int* p;
printf("you want to solve n queens problem\n");
printf("n=");
scanf_s("%d", &n); //输入问题规模
p = (int*)malloc(n * sizeof(int)); //p[k]表示k+1行皇后所在列
for (k = 0; k < n; k++)
{
p[k] = 0; //初始化数组p
}
bool *col = (bool*)malloc(n * sizeof(bool)); //表示列是否被皇后占用的数组
for (k = 0; k < n; k++)
{
col[k] = false;
}
bool* left_most_be_removed = (bool*)malloc(n * sizeof(bool)); //保存每次左移时被移出的值
bool* right_most_be_removed = (bool*)malloc(n * sizeof(bool)); 保存每次右移时被移出的值
//从左至右依次从左至右记录对应各列是否在左斜线上被皇后占据
bool* left_slope_restrict = (bool*)malloc(n * sizeof(bool));
for (k = 0; k < n; k++)
{
left_slope_restrict[k] = false;
}
//从左至右依次从左至右对应各列是否咋右斜线上被皇后占据
bool* right_slope_restrict = (bool*)malloc(n * sizeof(bool));
for (k = 0; k < n; k++)
{
right_slope_restrict[k] = false;
}
k = 0; //初始化为第一行
m = 0; //记录解的个数变量初始化
bool n_is_odd = false;
if (n % 2 != 0)
{
n_is_odd = true;
}
int max_col;
if (n_is_odd)
{
max_col = n / 2 + 1;
}
else
{
max_col = n / 2;
}
while (true)
{
if (k == 0) //进入或回溯至第一行
{
if (p[k] != 0)
col[p[k] - 1] = false;
col[p[k]] = true;
p[k] = p[k] + 1; //试探下一列
if (p[k] > max_col) //利用max_col剪枝,如果n为奇数, 第一行第n / 2 + 1列试探完毕后终止,否则n / 2列试探完毕后终止
{
break;
}
k++; //试探下一行
}
else
{
if (p[k] == 0) //抵达新的一行,因此计算出该行对应的left_slope_restrict和right_slope_restrict
{
left_most_be_removed[k] = left_slope_restrict[0];
left_slope_restrict[p[k - 1] - 1] = true;
left_moved(left_slope_restrict, n);
left_slope_restrict[n - 1] = false;
right_most_be_removed[k] = right_slope_restrict[n - 1];
right_slope_restrict[p[k - 1] - 1] = true;
right_moved(right_slope_restrict, n);
right_slope_restrict[0] = false;
}
int temp;
if ((temp = find(n, p, k, n_is_odd, col, left_slope_restrict, right_slope_restrict)) == 0) //k+1行没有找到可放置皇后的位置
{
if (p[k] != 0)
{
col[p[k] - 1] = false;
p[k] = 0; //必要的清理
}
right_moved(left_slope_restrict, n);
left_slope_restrict[0] = left_most_be_removed[k];
left_moved(right_slope_restrict, n);
right_slope_restrict[n - 1] = right_most_be_removed[k];
--k; //回溯,还原出当前行的上一行各列对应的left_slope_restrict和right_slope_restrict
left_slope_restrict[p[k] - 1] = false;
right_slope_restrict[p[k] - 1] = false;
}
else
{
if (p[k] != 0)
col[p[k] - 1] = false;
p[k] = temp; //在k+1行找到的位置赋予p[k]
col[temp - 1] = true;
if (k != (n - 1)) //皇后没有全部放置完毕
{
k++; //试探下一行
}
else //皇后全部放置成功,找到解
{
m++;
printf("The %dnd solution\n", m);
output(n, p); //输出解
}
}
}
}
printf("There are %d solutions in total\n", m);
return 0;
}