从哈密尔顿路径谈NP问题


       1859年,爱尔兰数学家哈密尔顿(Hamilton)提出下列周游世界的游戏:在正十二面体的二十个顶点上依次标记伦敦、巴黎、莫斯科等世界著名大城市,正十二面体的棱表示连接这些城市的路线。试问能否在图中做一次旅行,从顶点到顶点,沿着边行走,经过每个城市恰好一次之后再回到出发点。这就是著名的哈密尔顿问题,见图8-18。

从哈密尔顿路径谈NP问题_第1张图片

       哥尼斯堡七桥问题是在寻找一条遍历图中所有边的简单路径,而哈密尔顿的周游世界问题则是在寻找一条遍历图中所有点的基本路径。在无向图G=<V,E>中,遍历G中每个顶点一次且仅一次的路径称为哈密尔顿路径,遍历G中每个顶点一次且仅一次的回路称为哈密尔顿回路。具有哈密尔顿回路的图称为哈密尔顿图。哈密尔顿问题是一类问题的总称。

       问题提出没过多久,哈密尔顿就收到许多来自世界各地的表明成功周游世界的答案。然而,有没有一个一般的方法来判定一个图是或者不是哈密尔顿图呢? 一个半世纪过去了,这个问题即一个图是否为哈密尔顿图的判定问题至今悬而未决。哈密尔顿问题一直是图论中的世界性难题,到目前为止仍然没有找到一个像欧拉图那样简单的充分必要条件。目前的研究结果仅仅可以分别给出哈密尔顿回路存在的必要条件和充分条件。


       如果图G=<V,E>是哈密尔顿图,则它必然具备下述性质:

       对V的每个非空真子集 S均有w(G-S)<=|S| ,其中|S|是S中的顶点数, w(G-S)表示G删去顶点集S后得到的图的连通分图个数。

       上述条件是一个必要条件,但不是一个充分条件。因此,可以运用它来判定某些图不是哈密尔顿图。即哈密尔顿图都满足这样的条件,但符合该条件的图不一定是哈密尔顿图,彼得森图就不是一个哈密尔顿图,但它却满足上述条件。

       下面给出一个判定哈密尔顿回路存在的充分条件:

       如果图G=<V,E>是具有n>=3个顶点的简单无向图,且在图G中每一对顶点的度数和都不小于n ,那么 G中必然存在一条哈密尔顿回路。

       上述条件是一个充分条件,但不是一个必要条件。也就是说满足该条件的图必然是哈密尔顿图,但不满足该条件的图也有可能是哈密尔顿图。

       在计算理论中,我们定义P类是确定形 单带图灵机在多项式时间内 可以判定的 语言类,即


P大致对应于在计算机上实际可以解的问题类。与之相对应的,还有NP类,NP类是具有多项式时间验证机的语言类,其中验证机的定义如下:语言A的验证机是一个算法V,其中A = {w | 对某个字符串c, V 接受<w,c>}

       因为只根据w的长度来度量验证机的时间 ,所以多项式时间验证机在w的长度的多项式时间内运行。如果语言A有一个多项式时间验证机,我们就称它是多项式时间可验证的。

       验证机利用额外的信息(即上述定义中的符号c)来验证字符串w 是A 的成员。该信息称为A 的成员资格证书.或证明。注意,对于多项式验证机,证书具有多项式的长度( w的长度),因为这是该验证机在它的时间界限内所能访问的全部信息长度。

       NP的意思就是非确定型多项式时间,这也是使用非确定型多项式时间图灵机的一个特征。一个非常重要的定理就是:一个语言在NP 中,当且仅当它能被某个非确定型多项式时间图灵机判定。

       回到我们所谈论的哈密尔顿路径问题,考虑验证一个有向图是否包含连接两个指定节点的哈密尔顿路径问题,我们用HAMPATH来表示这个问题,即

HAMPATH = {<G, s, t> | G是包含从s到t 的哈密尔顿路径的有向图}

HAMPATH是一个典型的NP问题。我们把验证机的定义应用到HAMPATH 上。对于HAMPATH 问题,字符串<G, s, t>∈HAMPATH的证书就是只有一条从s到t 的哈密尔顿路径。

       下面给出一个在非确定型多项式时间内判定HAMPATH 问题的非确定型图灵机

从哈密尔顿路径谈NP问题_第2张图片

       如前所述, NP 是在非确定型图灵机上多项式时间内可解的语言类,或者等价地说,是成员资格可以在多项式时间内验证的语言类。P 是成员资格可以在多项式时间内判定的语言类。把这些内容总结如下,其中,把多项式时间可解的粗略地称为“快速地”可解的。
P = 成员资棉可以快速地判定的语言类
NP =成员资格可以快速地验证的语言类
       我们已经给出了语言的例子,如HAMPATH  , 它是NP 的成员.但不知道是否属于P。多项式可 验证性的能力似乎比多项式可判定性的能力大得多。但是, P 和 NP 也可能相等,虽然这一点可能难以想象。还不能证明在NP 中存在一个不属于P 的语言。
       P = NP 是否成立的问题是理论计算机科学和当代数学中最大的悬而未决的问题之一。如果这两个类相等,那么所有多项式可验证的问题都将是多项式可判定的。 大多数研究人员相信这两个类是不相等的,因为人们已经投入了大量精力为NP中的某些问题寻找多项式时间算法,但皆以失败告终。

       研究人员还试图证明这两个类是不相等的,但是这要求证明不存在快速算法来代替蛮力搜索(Brute Force Search)。目前, 科学研究还无法做到这步。

       2000年5月24日,美国克雷数学研究所(Clay Mathematics Institute,CMI) 指出新世纪亟待解决的七大数学难题,又称千禧年大奖难题(Millennium Prize Problems),P=NP?问题位列其中。根据克雷数学研究所订定的规则,任何一个猜想的解答,只要发表在数学期刊上,并经过两年的验证期,解决者就会被颁发一百万美元奖金。就目前来看,这一世纪难题似乎还没有任何将要被破解的迹象。


       在有向图中也可以定义哈密尔顿有向路径和哈密尔顿有向回路。最后,我们来考虑哈密尔顿回路的一个特例——骑士周游问题。

       骑士周游问题(又称骑士漫游或马踏棋盘问题)是算法设计的经典问题。现在已知的关于该问题的最早记录可追溯到公元九世纪,印度诗人和文艺理论家楼陀罗托(Rudrata)在其著作《诗庄严论》中对此给出了直观的描述。欧拉是西方最早研究骑士周游问题的数学家之一。但第一个系统化解决骑士周游问题的方法则是由德国学者冯·恩斯多夫(H. C. von Warnsdorf)于1823年提出的,这个方法现在被称为恩斯多夫规则或恩斯多夫算法。

从哈密尔顿路径谈NP问题_第3张图片

       国际象棋棋盘横向8 列,纵向8行,总计64个格子。假设有一个马从棋盘上任意起点出发,要求经过63步之后,不重复地遍历整个棋盘上除初始点以外的每个格子。这个问题用现代语言描述的话,其实就是在一个具体的图中找寻哈密尔顿路径。这里的哈密尔顿路径指的是经过图中每个顶点且只经过一次的一条轨迹。如果该轨迹是一条闭合路径,即从起点出发不重复的遍历所有点后仍能回到起始点,那么这条路径则称为哈密尔顿回路。图6-18显示的是一个马所能跳走的八个位置。需要说明的是对于棋盘上任意初始点该问题都有解,且解不唯一。有文献中指出对于一个 8*8的棋盘,它拥有的不同巡游路径的个数大约是1.305*10的35次方 。

       下面给出一些关于骑士周游问题的定理。

       定理1:在 8*8棋盘中,令 aij表示第 i行第 j列的棋格,其中1<=i, j<=8 ; akl表示第k 行第l 列的棋格,其中 1<=k,l<=8。由移动法则可知,从aijakl是合法的移动,当且仅当 |i-k|=1,|j-l|=2  或者|i-k|=2|j-l|=1

       定理2:当 |(i+j)-(k+l)|为偶数时,从 aij akl绝不是合法移动;也就是说,只有当 i+j与k+l 的奇偶性不同时, aijakl才可能是合法的移动。

考虑将骑士周游问题拓展到 n*n的棋盘上,则有以下定理:

       定理3:对于骑士周游问题,当n>=5 ,且为偶数时,以任意点作为初始点都有解。

       骑士周游问题在求解过程中需要用到前面介绍的递归,而且事实上,这还是一个分治与回溯相结合的典型例子。从图8-19可知,马每次跳走的选择可能有八种,按着其中一种进行尝试,当发现一种情况走不通时再回溯到先前的某一种情况继续尝试其它的选择。如此继续下去即可得到最终的结果。图8-20给出了骑士周游问题的两种解的情况,特别地其中右图给出的是一个哈密尔顿回路解,即巡游路径的起始点和终结点为同一个棋格。

从哈密尔顿路径谈NP问题_第4张图片

       下面考虑编程解决骑士周游问题。前面提到过,欧拉曾经对该问题进行了研究。欧拉把 棋盘上的骑士周游问题转化为在 的表格上能否按一定规则排列数字 的数学问题。我们的程序也基于这种方法来实现。另外,易知该算法的时间复杂度为 ,有鉴于如果单纯使用回溯方法而不进行优化,程序效率将是非常低的,于是不妨考虑采用一种启发式的遍历规则:即向前看两步,当每准备跳一步时,设准备跳到 点,计算 这一点可能往几个方向跳,将这个数目设为 点的权值,并将所有可能的 按权值进行排序,从最小的开始,循环遍历所有可能的 ,回溯求出结果。该算法可以求出所有可能的骑士周游路径,算出一个可行的结果很快。当然,其不足在于时间复杂度本质上并没有改变,所以要求出所有可能结果时,仍然很慢。后面的示例程序正是基于这种思想设计完成的。


#include "stdafx.h"
#include "conio.h"
#include <iostream>

using namespace std;

class Board
{
private:
	int board[8][8];  //棋盘
	int step;         //当前走的步数
	int No;	          //当前解的编号
	int direct[8][2]; //各前进方向的坐标偏移
	int wayCount[8][8];  //棋盘上每个位置可跳出的方向数目
	int startX;          //起始点坐标x
	int startY;			//起始点坐标y
	int dir[8][8][8];   //保存最优的搜索方向顺序
	

	void init() 
	{ 
		int i,j,k;
		int x,y;
		//确定从棋盘上每个位置可跳出的方向数目
		for(j=0;j<8;j++)
		{
			for(i=0;i<8;i++) 
			{
				wayCount[j][i]=0;
				for(k=0;k<8;k++) 
				{  
					x=i+direct[k][0]; 
					y=j+direct[k][1];
					if(check(x,y)) 
						wayCount[j][i]++; 
				} 
			}
		}

		//为棋盘上每个位置确定搜索的方向顺序
		for(y=0;y<8;y++)
		{
			for(x=0;x<8;x++) 
			{
				//默认搜索顺序为顺时针方向
				for(k=0;k<8;k++)
				{
					dir[y][x][k]=k;
				}
				//寻找最优搜索顺序
				for(i=0;i<7;i++)
				{
					k=i;
					int x1=x+direct[dir[y][x][k]][0];
					int y1=y+direct[dir[y][x][k]][1];
					//为各搜索方向按规则排序
					//希望搜索时优先到达下一步可能性较少的位置
					for(j=i+1;j<8;j++)
					{
						int x2=x+direct[dir[y][x][j]][0];
						int y2=y+direct[dir[y][x][j]][1];
						//如果从当前位置出发 方向j优于方向k 则将k替换为j
						if( (!check(x1,y1) && check(x2,y2))
							|| ( check(x1,y1) && check(x2,y2) &&
							wayCount[x1][y1]>wayCount[x2][y2]) )
						{
							k=j;
							x1=x+direct[dir[y][x][k]][0];
							y1=y+direct[dir[y][x][k]][1];
						}
					}
					j=dir[y][x][k];
					dir[y][x][k]=dir[y][x][i];
					dir[y][x][i]=j;
				}
			}
		}
	}

	//检查x,y是否为合法位置
	int check(int x,int y) 
	{ 
		if(x<0||x>7||y<0||y>7)
		{
			return 0;
		}
		else
		{
			return 1;
		}
	}

	//从指定位置(x,y)出发寻找路径
	void dg(int x, int y)
	{
		int i,nx,ny;
		//如果当前为最后一步 则终止递归
		if(step==64)
		{
			printPath();
			return;
		}

		//按照最优的搜索方向顺序 依次向各可能方向搜索
		for(i=0;i<8;i++)
		{
			nx=x+direct[dir[y][x][i]][0];
			ny=y+direct[dir[y][x][i]][1];
			if(nx>=0 && nx<8 && ny>=0 && ny<8)
			{
				//如果成功到达下一位置 则从新位置开始继续搜索
				if(board[ny][nx]<0)
				{
					board[ny][nx]=step;
					step++;
					dg(nx,ny);
					board[ny][nx]=-1;
					step--;
				}
			}
		}
	}

	void printPath()
	{
		int i,j;
		No++;
		cout<<"No"<<No<<":"<<endl;
		for(j=0;j<8;j++)
		{
			for(i=0;i<8;i++)
			{
				cout<<board[j][i]<<" ";
				if(board[j][i]<10) 
					cout<<" "; 
			}
			cout<<endl;
		}
		cout<<"Press anykey to continue...";
		getch();
		cout<<endl;
	}
	void printwc()
	{
		int i,j;
		No++;
		cout<<"No"<<No<<":"<<endl;
		for(j=0;j<8;j++)
		{
			for(i=0;i<8;i++)
			{
				cout<<wayCount[j][i]<<" ";
				if(wayCount[j][i]<10) 
					cout<<" "; 
			}
			cout<<endl;
		}
		cout<<"Press anykey to continue...";
		getch();
		cout<<endl;
	}

public:
	Board(int x, int y)
	{
		int i,j;
		startX=x;
		startY=y;
		direct[0][0]=1;		direct[0][1]=-2;
		direct[1][0]=2;		direct[1][1]=-1;
		direct[2][0]=2;		direct[2][1]=1;
		direct[3][0]=1;		direct[3][1]=2;
		direct[4][0]=-1;	direct[4][1]=2;
		direct[5][0]=-2;	direct[5][1]=1;
		direct[6][0]=-2;	direct[6][1]=-1;
		direct[7][0]=-1;	direct[7][1]=-2;
		step=1;
		No=0;
		for(j=0;j<8;j++)
		{
			for(i=0;i<8;i++)
			{
				board[j][i]=-1;
			}
		}
		board[y][x]=0;
	}

	void GetPath()
	{
		init();
		dg(startX,startY);
		if(No==0)
		{
			cout<<"no result"<<endl;
		}
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	int x,y;
	cout<<"Please input the start point (x,y)."
		<<endl<<"x=";
	cin>>x;
	getchar();
	cout<<"y=";
	cin>>y;
	getchar();
	Board board(x,y);
	board.GetPath();

	return 0;
}

        编译并运行程序,当输入初始坐标 (0,0)时,问题的前两个解如图8-21所示。仔细观察这两个解的结果。原来,当经过第 63步之后,棋子可以再跳回初始点0 的位置,也就是说这两个解不仅仅是找到了哈密尔顿通路,而是找到了哈密尔顿回路。这个问题请读者思考,即如何在 的棋盘上找到骑士周游问题的哈密尔顿回路解。而且这个结果也是基于前面讨论的启发式方法得到的,观察最初的几个遍历点0->1->2->3->4->5->6 ,刚好是沿着矩形棋盘的两条边走下来的。这是因为2 、4 、6 这些位于边上的位置的试探可能性结果较内部的点少,因此程序会优先遍历它们。

从哈密尔顿路径谈NP问题_第5张图片

你可能感兴趣的:(骑士巡游,NP问题,哈密尔顿路径,马踏棋盘)