QS2算法求解N-皇后问题

介绍一种名为QS2的高效随机化贪心算法,用于快速求解N皇后问题。该算法通过随机生成初始状态并不断尝试交换皇后位置来减少碰撞次数,最终找到至少一个可行解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这些天逛论坛,忽然发现可以用QS2算法求解N-皇后问题,而且效果比较好。之前学《人工智能》曾经用爬山法解决过,但当N上千时,效果很差。论坛里介绍QS2算法效果很好。自己便按照楼主给的思路和伪代码写了一遍,果然很厉害啊。在此先赞一个。(以下蓝色部分来自论坛楼主帖子)

        8皇后问题是一个广为人知的问题:将8个皇后放在8×8的棋盘上,皇后之间不能互相攻击,求各种放法。更一般的,把8换成n,其解法个数是随n成几何级增长的,因此程序运行时间也是几何级别的。现在我们关注这样一个问题,既然不能很快的把所有解都枚举出来,那么我们能不能很快的求出一个解来呢?这就是n皇后问题。

有人说,我就用纯搜索来搜第一个解,会不会快呢?你可以试一试,当n增大的时候会变的非常非常慢。除了搜索,还能有什么更好的办法呢?下面介绍一个QS2算法,可以在当n大到500000的时候仍能在几分钟之内得到解。描述算法前,先定义一些数据结构:

1.  状态表示。用queen[i] (1<=i<=n)存储1..n个一个排列,用它表示第i列的皇后所放的行数。这样表示的作用是,同一行同一列都不会有互相攻击的情况发生,发生攻击只可能是对角线。

2.  对角线标记。对于第i行第j列的元素,它有两个方向的对角线。假设以左下角为坐标(1,1)点,那么一条对角线是斜率为正的,另外一条是斜率为负的。对于斜率为正的对角线上的元素,i-j为定值,对斜率为负的对角线上的元素,i+j为定值,因此用i+j来标记该元素所处的负对角线,用i-j来标记其所处的正对角线。用数组dn[ ]来表示第i+j条负对角线上有多少个皇后,用dp[ ]来表示第i-j条正对角线上的皇后的个数。定义在某条对角线上,碰撞的个数为这条对角线上皇后的个数减1。

3.  被攻击的皇后。用一个数组attack[ ]来存储所有被攻击的皇后的行数。此数组用于加速程序的运行。 有了这些数据结构,就可以开始介绍QS2算法了。

算法如下:

1.  repeat

2.    随机产生初始状态queen[i]

3.    collisions = compute_collisions(queen, dn, dp)  (计算每个对角线上皇后的个数,dp[]和dn[],并计算总的碰撞次数collisions)

4.    设limit = C1 * collisions (该变量用于界定程序中每种状态产生的碰撞次数,C1是一个参数,这里设为0.45)

5.    number_of_attacks = compute_attacks(queen, dn, dp, attack)  (计算被攻击的皇后的个数)

6.    loopcount = 0

7.    repeat

8.      for k <- 1 to number_of_attacks do

9.        i <- attack[k]

10.       随机选择一个1..n之间的数j

11.       if 交换queen[i]和queen[j]可以减少总的碰撞次数collisions then

12.         进行这次交换,并重新计算dn[], dp[]和collisions

13.         if collisions = 0 then 找到解,结束。

14.         if collisions < limit then do

15.           limit <- C1*collisions

16.           重新计算number_of_attacks = compute_attacks(queen, dn, dp, attack)

17.         end do

18.       end do

19.     end do

20.     loopcount <- loopcount + number_of_attacks

21.   until loopcount > C2 * n  (C2是一个常量,这里设为32)

22. until collisions = 0

        通观这个算法,发现其实很简单,就是先随机产生了一种排列方案,然后找一个被攻击的皇后,随机选择另一个皇后,让她们交换位置,如果这个交换可以使总的碰撞次数减少,那么就进行交换。由于这个算法是随机的,不能保证从最开始随机产生的queen[i]状态一定可以得到一组解,因此,程序在运行一段时间之后,如果没有解产生,就重新产生一组初始值(这个过程是通过loopcount变量来控制的)。再看看这个算法的性能。由于程序里面充满了随机化,对这个程序的性能的分析也无法直接从代码中来分析其复杂度。它的性能是通过实验来分析的。下面这个表列出了一些统计数字:

皇后个数n                         1000          10000        100000        500000

被测试的皇后数               14742        138762      1386974     6981193

进行交换的皇后对个数    436            4333           43256         216407

        表中第2行被测试的皇后对个数,是指程序第11行对两个皇后交换后是否可以减少碰撞个数的测试。表中第3行进行交换的皇后对个数,是指程序第12行交换两个皇后的次数。

 从表中我们不难发现,随着n增大,测试皇后对和交换皇后对这两个操作都是呈线性增加的。也就是说,对由一个初始状态出发到产生解,所用时间是线性的。但是,如果不能从某个随机的初始状态产生出解呢?虽说若不能出解,程序运行一段时间后会重新产生初始状态,但如果不能产生解的初始状态很多,那么程序的性能也会大大下降。事实上这个顾虑是多余的,实验验证,当n超过1000的时候,几乎总可以从第一个初始值出发找到解!

        这个算法介绍完了,最后,我想说说我对这个算法的感觉。首先,这个算法是实验性的。其性能的评估没法用传统的复杂度分析方法来进行,必须通过实验来验证。当我看到了这个算法,如果不是亲自去写一遍,我根本无法相信它会这么快的出解,这也是它神奇的地方。其次,这个算法的思路跳出了解决n皇后问题一般所能想到的回溯搜索方法,取而代之的是一个随机化贪心算法。虽然说它是不确定的,可是它却快速的解决了问题。这为人们提供了一个思路,当遇到传统的搜索很难奏效的时候,随机化贪心也许会另辟捷径,收到意想不到的效果。

        最后在贴上自己写的code

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <vector>
using namespace std;

const double C1 = 0.45;
const double C2 = 32;

void initQueen(int N , vector <int> &queen) // 初始化Queen
{
	vector <int> array(N);
	for(int i = 0; i < N ; i++)
		array[i] = i;
    queen.clear();
	while(N)
	{
		double t = rand()%N;
		queen.push_back(array[t]);
		array[t] = array[--N];
	}
}

__int64 compute_collisions(const vector<int> &queen,vector<int> &dn,vector<int> &dp)
{
	for(int t =0 ; t < 2*queen.size() ; t++)
	{
		dn[t] = dp[t] = 0;		
	}

	for(int i = 0 ; i < (int)queen.size() ; i++)
	{
		int j = queen.at(i);
	    dn[i+j]++;
		dp[queen.size()-1+i-j]++;
	}

    __int64 collisions = 0;
	for(int k =0 ; k < 2*queen.size()-1;k++)
	{
		collisions +=dn[k]*(dn[k]-1)/2;
		collisions +=dp[k]*(dp[k]-1)/2;
	}
	return collisions;
}

int compute_attacks(const vector<int> &queen,vector<int> &dn,vector<int> &dp,vector<int>&attack)
{
	attack.clear();
    int attackedLines = 0;
	for(int i =0 ; i<(int)queen.size();i++)
	{
		int j = queen.at(i);
		if(dn.at(i+j) > 1 || dp.at(queen.size()-1+i-j) > 1)
		{
			attack.push_back(i);
			attackedLines++;
		}
	}
	return attackedLines;
}

int randInt(int k ,int size)
{
	int val =rand()%size;
	while(val == k)
		val = rand()%size;

	return val;
}


int cacl_collisions(const vector<int> &queen,vector<int> &dn,vector<int> &dp,int a,int b)
{
	int i,j,counts = 0 ;
	
	i = queen[a];
    j = queen[b];

    counts -= dn[a+i] - 1;
    counts -= dn[b+j] - 1;
    if(a+i == b+j) counts++;

    counts -= dp[queen.size()-1+a-i] - 1;
    counts -= dp[queen.size()-1+b-j] - 1;
    if(a-i == b-j) counts++;

    counts += dn[a+j] ;
    counts += dn[b+i] ;
    if(a+j == b+i) counts++;
	
    counts += dp[queen.size()-1+a-j] ;
    counts += dp[queen.size()-1+b-i] ;
    if(a-j == b-i) counts++;

	return counts;
}

void updateQueen(vector<int> &queen,vector<int> &dn,vector<int> &dp,int a,int b)
{
	int i = queen.at(a);
	int j = queen.at(b);

	dn[a+i]--;
	dn[b+j]--;
    dp[queen.size()-1+a-i]--;
    dp[queen.size()-1+b-j]--;

	int t = queen[a];
	queen[a] = queen[b];
	queen[b] = t;

	dn[a+j]++;
	dn[b+i]++;
    dp[queen.size()-1+a-j]++;
    dp[queen.size()-1+b-i]++;
}

void printQueen(vector<int>&queen)
{
	for(int i = 0 ; i < (int)queen.size(); i++)
	{
		for(int j = 0;j < (int)queen.size();j++)
		{
			if(j == queen.at(i))
				cout<<"0";
			else cout<<"*";
		}
		cout<<endl;
	}
}
void pashan(int N)
{
	__int64 collisions = N;
	int num_of_attack;
    int swapCount = 0;

	vector<int> queen;
    vector<int> attack;

	vector<int> dn(2*N);
	vector<int> dp(2*N);

	if(N == 2||N == 3)
	{
		cout<<"无解"<<endl;
		return ;
	}
	
	while(collisions)
	{
		initQueen(N,queen);
		collisions = compute_collisions(queen, dn, dp);
		num_of_attack = compute_attacks(queen, dn, dp, attack);

		int limit = C1*collisions;
		int loopSteps = 0;
		while(loopSteps < C2*N)
		{ 
			for(int k = 0 ; k < num_of_attack ; k++)
			{
				int i = attack.at(k);
				int j = randInt(i,N);

				int c;
				if(( c = cacl_collisions(queen,dn,dp,i,j)) < 0)
				{
					collisions += c;
					updateQueen(queen,dn,dp,i,j);
                    swapCount++;

					if(collisions == 0)break;
					if(collisions < limit)
					{
						limit = C1*collisions;
//						k = 0;
						num_of_attack = compute_attacks(queen, dn, dp, attack);
					}
				}
			}
			if(collisions == 0)break;
			loopSteps +=num_of_attack;
		}
	}
	cout<<"交换次数: "<<swapCount<<endl;
//	printQueen(queen);
}

int main()
{
	int N=100000;
	clock_t startTime,endTime;

	cout<<"请输入皇后的个数:";
	cin>>N;

	srand((unsigned)time(NULL));

	startTime = clock();
	pashan(N);
	endTime = clock();

	double totalTime = 1.0*(endTime - startTime)/CLOCKS_PER_SEC;
	cout<<"共耗时:"<<totalTime<<"s"<<endl;

	return 0;
}





评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值