因为以下算法基于局部搜索思想,在此先介绍与其相关的一些概念。
局部搜索算法是从爬山算法进化而来。设想我们需以最快的速度(最优解)爬上一座我们事先没有半点信息的大山,那么沿着最陡的方向爬是一不错的选择。这就是局部搜索算法的最基本思想,即在求解问题的过程中始终向着可能最优的方向搜索。
描述算法时需用到领域的概念,所谓领域,简单的说即是给定点附近其他点的集合。在距离空间中,领域一般被定义为以给定点为圆心的一个圆;而在组合优化问题中,领域一般定义为由给定转化规则对给定的问题域上每结点进行转化所得到的问题域上结点的集合。公式描述如下:
设D为问题定义域,若存在一映射N,使得:
N:S∈D→N(S)∈2^D
则称N(S)为N的领域,A∈2^D为N的邻居。
局部搜索(Local Search)的一般过程是:
原始的局部搜索算法存在三个改进的算法,分别从三个不同的侧面对其进行了优化。
即考虑到算法在搜索过程中陷入到局部极值点而结束的情况。设想我们去攀登一座山群的最高峰,而此山群有很多的小山峰,且我们对此山群一无所知,那么当我们按照算法的步骤来到一座小山峰(局部极值点)时,我们会错误的判断这就是最高峰,事实上这有可能是一个很糟糕的解(即与最高峰还差很远)。
为便于理解,我们考虑用此局部搜索算法寻找一开口向下的抛物线的顶点:设此顶点的x坐标为10,求领域的映射N定义为N:x∈R,N(x)=x±3(即给定点x的领域仅有在其两边相距为3的两个点),指标函数f(x)=-y(x)(为使指标函数值小的解为较优解,我们让其取相反数);那么当我们所选取的初始解为3时,无论如何算法都将不能在顶点(最优解)处结束。根本原因就是我们的步长固定,所以能够搜索到的也仅为一些固定的点。解决此问题可以在搜索的过程中改变步长(本质为改变映射函数N)。
在上面步长问题的讨论中,我们发现起始点的选择也对问题的求解有很大的影响,选择不好可能会导致得不出最优解的情况。一种很自然的解决方案就是随机的选择一些可能解,分别以这些可能解为初始解进行搜索,最后将这些搜索得到的解进行比较从而选出最优解。
笔者按照上面的算法思想,用C++实现了八皇后问题的一个求解算法,八皇后问题既是:在一个8×8的国际象棋盘上,有八个皇后,每个皇后占一格,要求八个皇后之间不能够出现相互“攻击”的现象。即不能够有两个皇后同时出现的同一行、同一列、同一对角线上。具体的算法如下:
为方便朋友们的试验,在此提供源文件下载地址:QueenSearch.cpp
隐藏源文件
/***********************************************************************************
模块:QueenSearch.cpp
目的:解决经典的八皇后问题
思想:局部搜索
作者:SageMiner
***********************************************************************************/
#include <Windows.h>
#include <stdio.h>
#include <tchar.h>
#define N 8 // 皇后数目,
////////////////////////////////////////////////////////////////////////////////////
// 皇后
typedef struct _Queen
{
unsigned int x;
unsigned int y;
}QUEEN,*pQUEEN,**ppQUEEN;
// 棋格
typedef struct _Grid
{
pQUEEN pQueen;
}GRID,*pGRID,**ppGRID;
////////////////////////////////////////////////////////////////////////////////////
/*
* 函数声明提前
*/
// 初始化棋盘,使每行、每列上仅出现一位皇后
bool InitChessBroad(GRID ppChessBroad[][N],pQUEEN pN_Queen,unsigned n);
// 两皇后冲突则返回true,否则返回false
bool isConflict(pQUEEN pQueen_1,pQUEEN pQueen_2);
// 计算给定一组皇后中发生冲突的数目
unsigned CountOfConflict(pQUEEN pN_Queen,unsigned n);
// 随机交换两位皇后的行、列号,若能使冲突数减少则返回true,若任意可能交换都不能够使冲突数目减少则返回false
bool ChangeTwoQueen(GRID ChessBroad[][N],pQUEEN pN_Queen,unsigned n);
// 打印输出
void PrintChessBroad(GRID ppChessBroad[][N],unsigned n);
////////////////////////////////////////////////////////////////////////////////////
bool InitChessBroad(GRID ppChessBroad[][N],pQUEEN pN_Queen,unsigned n)
{
int *pavCols; // 可用的行列
unsigned i,j;
int r;
pavCols=new int[n];
/*
考虑到rand()函数的特性,即以静态变量为基础按照一定的规则变化此变量,然而每当我们
程序启动时静态变量的数值是一样的,因此不能说成是随机的,只能说是以特定初始值按照
特定规则变化的一组数值,在此,我们以系统当前启动时间为基准让rand()运行一段时间。
*/
DWORD dwTickCount=GetTickCount()%100;
while(dwTickCount--)
rand();
for(i=0;i<n;i++)
pavCols[i]=i;
// 扫描每一行,在每一行的合适位置放入一皇后
for(i=0;i<n;i++)
{
for(j=0;j<n;j++)
ppChessBroad[i][j].pQueen=NULL;
while(1)
{
r=rand()%n;
if(pavCols[r]!=-1)
{
pavCols[r]=-1;
break;
}
}
//
ppChessBroad[i][r].pQueen=pN_Queen+i;
pN_Queen[i].x=i;
pN_Queen[i].y=r;
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////
bool isConflict(pQUEEN pQueen_1,pQUEEN pQueen_2)
{
if(pQueen_1==pQueen_2)
return false;
if((pQueen_1->x==pQueen_1->y) && (pQueen_2->x==pQueen_2->y) // 同在主对角线上
|| ((pQueen_1->x+pQueen_1->y==N-1) &&(pQueen_2->x+pQueen_2->y==N-1))) // 同在副对角线上
return true;
return (pQueen_1->x==pQueen_2->x || pQueen_1->y==pQueen_2->y) true:false; // 判定同行同列
}
////////////////////////////////////////////////////////////////////////////////////
unsigned CountOfConflict(pQUEEN pN_Queen,unsigned n)
{
unsigned i,j,Count=0;
for(i=0;i<n-1;i++)
for(j=i+1;j<n;j++)
if(isConflict(pN_Queen+i,pN_Queen+j))
Count++;
return Count;
}
////////////////////////////////////////////////////////////////////////////////////
bool ChangeTwoQueen(GRID ChessBroad[][N],pQUEEN pN_Queen,unsigned n)
{
int tmp,h1,h2;
unsigned i,j,Count=0; // 冲突数目
bool *isavH_1,*isavH_2; // 指向一维数组,用来标识两层循环中可用的皇后
isavH_1=new bool[n];
isavH_2=new bool[n];
for(i=0;i<n;i++)
isavH_1[i]=isavH_2[i]=true; // 初始均可用
Count=CountOfConflict(pN_Queen,n);
// 两层循环产生从n个元素中选择个的组合数
for(i=0;i<n-1;i++)
{
//随机选择一个可用的皇后作为第一组合数h1
while(1)
{
h1=rand()%n;
if(isavH_1[h1])
{
isavH_1[h1]=false;
// 第二组合数从尚未选为第一组合数集合中选择
for(j=0;j<n;j++)
isavH_2[j]=isavH_1[j];
break;
}
}
for(j=i+1;j<n;j++)
{
//随机选择一个可用的皇后作为第二组合数h2
while(1)
{
h2=rand()%n;
if(isavH_2[h2])
{
isavH_2[h2]=false;
break;
}
}
// 交换标号为h1,h2皇后的x坐标
tmp=pN_Queen[h1].x;
pN_Queen[h1].x=pN_Queen[h2].x;
pN_Queen[h2].x=tmp;
if(Count>CountOfConflict(pN_Queen,n))
{
// 更改相应的棋格
ChessBroad[pN_Queen[h2].x][pN_Queen[h1].y].pQueen=NULL;
ChessBroad[pN_Queen[h1].x][pN_Queen[h2].y].pQueen=NULL;
ChessBroad[pN_Queen[h1].x][pN_Queen[h1].y].pQueen=pN_Queen+h1;
ChessBroad[pN_Queen[h2].x][pN_Queen[h2].y].pQueen=pN_Queen+h2;
return true;
}
else
{
tmp=pN_Queen[h1].x;
pN_Queen[h1].x=pN_Queen[h2].x;
pN_Queen[h2].x=tmp;
}
// 交换标号为h1、h2皇后的y坐标
tmp=pN_Queen[h1].y;
pN_Queen[h1].y=pN_Queen[h2].y;
pN_Queen[h2].y=tmp;
if(Count>CountOfConflict(pN_Queen,n))
{
// 更改相应的棋格
ChessBroad[pN_Queen[h1].x][pN_Queen[h2].y].pQueen=NULL;
ChessBroad[pN_Queen[h2].x][pN_Queen[h1].y].pQueen=NULL;
ChessBroad[pN_Queen[h1].x][pN_Queen[h1].y].pQueen=pN_Queen+h1;
ChessBroad[pN_Queen[h2].x][pN_Queen[h2].y].pQueen=pN_Queen+h2;
return true;
}
else
{
tmp=pN_Queen[h1].y;
pN_Queen[h1].y=pN_Queen[h2].y;
pN_Queen[h2].y=tmp;
}
}
}
// 不存在这样的交换则返回false
return false;
}
////////////////////////////////////////////////////////////////////////////////////
void PrintChessBroad(GRID ppChessBroad[][N],unsigned n)
{
unsigned i,j;
printf("\n");
for(i=0;i<n;i++)
{
for(j=0;j<n;j++)
{
if(ppChessBroad[i][j].pQueen==NULL)
printf("*\t");
else
printf("H\t");
}
printf("\n\n\n");
}
}
////////////////////////////////////////////////////////////////////////////////////
int _tmain(int argc, _TCHAR* argv[])
{
GRID ChessBroad[N][N];
QUEEN nQueen[N];
while(1)
{
InitChessBroad(ChessBroad,nQueen,N);
while(1)
{
// 完成皇后的布局
if(0==CountOfConflict(nQueen,N))
{
PrintChessBroad(ChessBroad,N);
return 1;
}
if(!ChangeTwoQueen(ChessBroad,nQueen,N))
break;
}
}
return 0;
}
隐藏源文件
笔者运行程序时的一次截图如下(因为这是个随机算法,各次运行时的输出结构可能不一样):
最后,简略的讨论此算法的一些特性:
首先,此算法运行结束是一定会得到一个正确的解,因为算法仅有一个必须使解满足条件才能够通过的出口。其次,相对于回溯那样的暴力搜索算法而言,此算法在时间复杂度、空间复杂度上均是可以接受的。时间上,不需要像回溯法那样按部就班诸个解诸个解的验证;空间上,也不需要像回溯法那样为保存回溯点而占据大量的内存。特别是当问题规模比较大时,就更能显示回溯法的局限性,一般认为当皇后数目达到100时回溯法已经很难求解了。以上所说的均相对于平均复杂度而言,原则上讲,一个随机算法只能够以加权(发生概率)平均复杂度去度量,否则就没有意义。比如说这个算法,其最好时间复杂度为:
O(InitChessBroad)+O(CountOfConflict)+O(PrintChessBroad)
而最坏的时间复杂度将不可度量,但事实上最坏情况很难发生。可见单纯考虑最好与最坏的情况已无多大意义,因为在此还有一个很重要的因素——发生的概率。
这么好的算法笔者也是最近在学习人工智能时才知道的,敬仰为此算法做出贡献的前辈们。
我相信站在这类巨人的肩上,我们定会看的更远......
参考文献:
***********************************************************************************************************
By SageMiner
2010-2-1