1. 程式人生 > >演算法入門6:回溯法

演算法入門6:回溯法

一. 回溯法 – 深度優先搜素                       

1. 簡單概述

       回溯法思路的簡單描述是:把問題的解空間轉化成了圖或者樹的結構表示,然後使用深度優先搜尋策略進行遍歷,遍歷的過程中記錄和尋找所有可行解或者最優解。

基本思想類同於:

  • 圖的深度優先搜尋
  • 二叉樹的後序遍歷

      【

         分支限界法:廣度優先搜尋

         思想類同於:圖的廣度優先遍歷

                                二叉樹的層序遍歷

      】

2. 詳細描述

        詳細的描述則為:

        回溯法按深度優先策略搜尋問題的解空間樹。首先從根節點出發搜尋解空間樹,當演算法搜尋至解空間樹的某一節點時,先利用剪枝函式

判斷該節點是否可行(即能得到問題的解)。如果不可行,則跳過對該節點為根的子樹的搜尋,逐層向其祖先節點回溯;否則,進入該子樹,繼續按深度優先策略搜尋。

        回溯法的基本行為是搜尋,搜尋過程使用剪枝函式來為了避免無效的搜尋。剪枝函式包括兩類:1. 使用約束函式,剪去不滿足約束條件的路徑;2.使用限界函式,剪去不能得到最優解的路徑。

        問題的關鍵在於如何定義問題的解空間,轉化成樹(即解空間樹)。解空間樹分為兩種:子集樹和排列樹。兩種在演算法結構和思路上大體相同。

3. 回溯法應用

       當問題是要求滿足某種性質(約束條件)的所有解或最優解時,往往使用回溯法。

       它有“通用解題法”之美譽。

二. 回溯法實現 - 遞迴和遞推(迭代)                               

        回溯法的實現方法有兩種:遞迴和遞推(也稱迭代)。一般來說,一個問題兩種方法都可以實現,只是在演算法效率和設計複雜度上有區別。       【類比於圖深度遍歷的遞迴實現和非遞迴(遞推)實現】

1. 遞迴

        思路簡單,設計容易,但效率低,其設計正規化如下:

//針對N叉樹的遞歸回溯方法
void backtrack (int t)
{
	if (t>n) output(x); //葉子節點,輸出結果,x是可行解
	else
		for i = 1 to k//當前節點的所有子節點
		{
			x[t]=value(i); //每個子節點的值賦值給x
			//滿足約束條件和限界條件
			if (constraint(t)&&bound(t)) 
			backtrack(t+1);	//遞迴下一層
		}
}

2. 遞推

      演算法設計相對複雜,但效率高。

//針對N叉樹的迭代回溯方法
void iterativeBacktrack ()
{
	int t=1;
	while (t>0) {
		if(ExistSubNode(t)) //當前節點的存在子節點
		{
			for i = 1 to k  //遍歷當前節點的所有子節點
			{
				x[t]=value(i);//每個子節點的值賦值給x
				if (constraint(t)&&bound(t))//滿足約束條件和限界條件 
				{
					//solution表示在節點t處得到了一個解
					if (solution(t)) output(x);//得到問題的一個可行解,輸出
					else t++;//沒有得到解,繼續向下搜尋
				}
			}
		}
		else //不存在子節點,返回上一層
		{
			t--;
		}
	}
}

三. 子集樹和排列樹                                                        

1. 子集樹

       所給的問題是從n個元素的集合S中找出滿足某種性質的子集時,相應的解空間成為子集樹。 如0-1揹包問題,從所給重量、價值不同的物品中挑選幾個物品放入揹包,使得在滿足揹包不超重的情況下,揹包內物品價值最大。它的解空間就是一個典型的子集樹。

       回溯法搜尋子集樹的演算法正規化如下:

void backtrack (int t)
{
  if (t>n) output(x);
    else
      for (int i=0;i<=1;i++) {
        x[t]=i;
        if (constraint(t)&&bound(t)) backtrack(t+1);
      }
}

2. 排列樹

      所給的問題是確定n個元素滿足某種性質的排列時,相應的解空間就是排列樹。 如旅行售貨員問題,一個售貨員把幾個城市旅行一遍,要求走的路程最小。它的解就是幾個城市的排列,解空間就是排列樹。       回溯法搜尋排列樹的演算法正規化如下:

void backtrack (int t)
{
  if (t>n) output(x);
    else
      for (int i=t;i<=n;i++) {
        swap(x[t], x[i]);
        if (constraint(t)&&bound(t)) backtrack(t+1);
        swap(x[t], x[i]);
      }
} 

四. 經典問題                                    

(1)裝載問題 (2)0-1揹包問題 (3)旅行售貨員問題 (4)八皇后問題 (5)迷宮問題 (6)圖的m著色問題

1. 0-1揹包問題

        問題:給定n種物品和一揹包。物品i的重量是wi,其價值為pi,揹包的容量為C。問應如何選擇裝入揹包的物品,使得裝入揹包中物品的總價值最大?        分析:問題是n個物品中選擇部分物品,可知,問題的解空間是子集樹。比如物品數目n=3時,其解空間樹如下圖,邊為1代表選擇該物品,邊為0代表不選擇該物品。使用x[i]表示物品i是否放入揹包,x[i]=0表示不放,x[i]=1表示放入。回溯搜尋過程,如果來到了葉子節點,表示一條搜尋路徑結束,如果該路徑上存在更優的解,則儲存下來。如果不是葉子節點,是中點的節點(如B),就遍歷其子節點(D和E),如果子節點滿足剪枝條件,就繼續回溯搜尋子節點。

程式碼:

#include <stdio.h>
 
#define N 3         //物品的數量
#define C 16        //揹包的容量
 
int w[N]={10,8,5};  //每個物品的重量
int v[N]={5,4,1};   //每個物品的價值
int x[N]={0,0,0};   //x[i]=1代表物品i放入揹包,0代表不放入
 
int CurWeight = 0;  //當前放入揹包的物品總重量
int CurValue = 0;   //當前放入揹包的物品總價值
 
int BestValue = 0;  //最優值;當前的最大價值,初始化為0
int BestX[N];       //最優解;BestX[i]=1代表物品i放入揹包,0代表不放入
 
//t = 0 to N-1
void backtrack(int t)
{
	//葉子節點,輸出結果
	if(t>N-1) 
	{
		//如果找到了一個更優的解
		if(CurValue>BestValue)
		{
			//儲存更優的值和解
			BestValue = CurValue;
			for(int i=0;i<N;++i) BestX[i] = x[i];
		}
	}
	else
	{
		//遍歷當前節點的子節點:0 不放入揹包,1放入揹包
		for(int i=0;i<=1;++i)
		{
			x[t]=i;
 
			if(i==0) //不放入揹包
			{
				backtrack(t+1);
			}
			else //放入揹包
			{
 				//約束條件:放的下
				if((CurWeight+w[t])<=C)
				{
					CurWeight += w[t];
					CurValue += v[t];
					backtrack(t+1);
					CurWeight -= w[t];
					CurValue -= v[t];
				}
			}
		}
		//PS:上述程式碼為了更符合遞歸回溯的正規化,並不夠簡潔
	}
}
 
int main(int argc, char* argv[])
{
	backtrack(0);
 
	printf("最優值:%d\n",BestValue);
 
	for(int i=0;i<N;i++)
	{
	   printf("最優解:%-3d",BestX[i]);
	}
	return 0;
}

2. 旅行售貨員問題

/****************************************************************
 *問  題:旅行售貨員
 *算  法:回溯法
 *描  述:解空間為 排列樹
 ****************************************************************/
#include <stdio.h>
#define N 4				   //城市數目
#define NO_PATH -1		   //沒有通路
#define MAX_WEIGHT 4000
int City_Graph[N+1][N+1];  //儲存圖資訊
int x[N+1];                //x[i]儲存第i步遍歷的城市
int isIn[N+1];             //儲存 城市i是否已經加入路徑
int bestw;                 //最優路徑總權值
int cw;                    //當前路徑總權值
int bestx[N+1];            //最優路徑
//-----------------------------------------------------------------
void Travel_Backtrack(int t){        //遞迴法
	int i,j;
	if(t>N){                         //走完了,輸出結果
		for(i=1;i<=N;i++)            //輸出當前的路徑
			printf("%d ",x[i]);
		printf("/n");
		if(cw < bestw){              //判斷當前路徑是否是更優解
			for (i=1;i<=N;i++){
				bestx[i] = x[i];
			}
			bestw = cw;
		}
		return;
	}
	else{
		for(j=1;j<=N;j++){           //找到第t步能走的城市
			if(City_Graph[x[t-1]][j] != NO_PATH && !isIn[j]){ //能到而且沒有加入到路徑中
				isIn[j] = 1;
				x[t] = j;
				cw += City_Graph[x[t-1]][j];
				Travel_Backtrack(t+1);
				isIn[j] = 0;
				x[t] = 0;
				cw -= City_Graph[x[t-1]][j];
			}
		}
	}
}
void main(){
	int i;
	City_Graph[1][1] = NO_PATH;
	City_Graph[1][2] = 30;
	City_Graph[1][3] = 6;
	City_Graph[1][4] = 4;
	
	City_Graph[2][1] = 30;
	City_Graph[2][2] = NO_PATH;
	City_Graph[2][3] = 5;
	City_Graph[2][4] = 10;
	City_Graph[3][1] = 6;
	City_Graph[3][2] = 5;
	City_Graph[3][3] = NO_PATH;
	City_Graph[3][4] = 20;
	
	City_Graph[4][1] = 4;
	City_Graph[4][2] = 10;
	City_Graph[4][3] = 20;
	City_Graph[4][4] = NO_PATH;
	//測試遞迴法
	for (i=1;i<=N;i++){
		x[i] = 0;               //表示第i步還沒有解
		bestx[i] = 0;           //還沒有最優解
		isIn[i] = 0;            //表示第i個城市還沒有加入到路徑中
	}
	 
	x[1] = 1;                   //第一步 走城市1
	isIn[1] = 1;                //第一個城市 加入路徑
	bestw = MAX_WEIGHT;
	cw = 0;
	Travel_Backtrack(2);        //從第二步開始選擇城市
	printf("最優值為%d/n",bestw);
	printf("最優解為:/n");
	for(i=1;i<=N;i++){
		printf("%d ",bestx[i]);
	}
	printf("/n");
}

3. 詳細描述N皇后問題

       問題:在n×n格的棋盤上放置彼此不受攻擊的n個皇后。按照國際象棋的規則,皇后可以攻擊與之處在同一行或同一列或同一斜線上的棋子。

       N皇后問題等價於在n×n格的棋盤上放置n個皇后,任何2個皇后不放在同一行或同一列或同一斜線上。

      分析:從n×n個格子中選擇n個格子擺放皇后。可見解空間樹為子集樹。

      使用Board[N][N]來表示棋盤,Board[i][j]=0 表示(I,j)位置為空,Board[i][j]=1 表示(I,j)位置擺放有一個皇后。

      全域性變數way表示總共的擺放方法數目。

      使用Queen(t)來擺放第t個皇后。Queen(t) 函式符合子集樹時的遞歸回溯正規化。當t>N時,說明所有皇后都已經擺   放完成,這是一個可行的擺放方法,輸出結果;否則,遍歷棋盤,找皇后t所有可行的擺放位置,Feasible(i,j) 判斷皇后t能否擺放在位置(i,j)處,如果可以擺放則繼續遞迴擺放皇后t+1,如果不能擺放,則判斷下一個位置。

       Feasible(row,col)函式首先判斷位置(row,col)是否合法,繼而判斷(row,col)處是否已有皇后,有則衝突,返回0,無則繼續判斷行、列、斜方向是否衝突。斜方向分為左上角、左下角、右上角、右下角四個方向,每次從(row,col)向四個方向延伸一個格子,判斷是否衝突。如果所有方向都沒有衝突,則返回1,表示此位置可以擺放一個皇后。

        程式碼:

/************************************************************************ 
 * 名  稱:NQueen.cpp
 * 功  能:回溯演算法例項:N皇后問題 
 * 作  者:JarvisChu 
 * 時  間:2013-11-13 
 ************************************************************************/ 
 
#include <stdio.h>
 
#define N 8
 
int Board[N][N];	//棋盤 0表示空白 1表示有皇后
int way;		//擺放的方法數
 
 
//判斷能否在(x,y)的位置擺放一個皇后;0不可以,1可以
int Feasible(int row,int col)
{
	//位置不合法
	if(row>N || row<0 || col >N || col<0)
		return 0;
 
	//該位置已經有皇后了,不能
	if(Board[row][col] != 0)
	{	//在行列衝突判斷中也包含了該判斷,單獨提出來為了提高效率
		return 0;
	}
 
	//////////////////////////////////////////////////
	//下面判斷是否和已有的衝突
 
	//行和列是否衝突
	for(int i=0;i<N;++i)
	{
		if(Board[row][i] != 0 || Board[i][col]!=0)
			return 0;
	}
 
	//斜線方向衝突
 
	for(int i=1;i<N;++i)
	{
/* i表示從當前點(row,col)向四個斜方向擴充套件的長度
 
左上角 \  / 右上角   i=2
        \/           i=1
        /\           i=1
左下角 /  \ 右下角   i=2
*/
		//左上角
		if((row-i)>=0 && (col-i)>=0)    //位置合法
		{
			if(Board[row-i][col-i] != 0)//此處已有皇后,衝突
				return 0;
		}
 
		//左下角
		if((row+i)<N && (col-i)>=0)
		{
			if(Board[row+i][col-i] != 0)
				return 0;
		}
 
		//右上角
		if((row-i)>=0 && (col+i)<N)
		{
			if(Board[row-i][col+i] != 0)
				return 0;
		}
 
		//右下角
		if((row+i)<N && (col+i)<N)
		{
			if(Board[row+i][col+i] != 0)
				return 0;
		}
	}
 
	return 1; //不會發生衝突,返回1
}
 
 
//擺放第t個皇后 ;從1開始
void Queen(int t)
{
	//擺放完成,輸出結果
	if(t>N)
	{
		way++;
		/*如果N較大,輸出結果會很慢;N較小時,可以用下面程式碼輸出結果
		for(int i=0;i<N;++i){
			for(int j=0;j<N;++j)
				printf("%-3d",Board[i][j]);
			printf("\n");
		}
		printf("\n------------------------\n\n");
		*/
	}
	else
	{
		for(int i=0;i<N;++i)
		{
			for(int j=0;j<N;++j)
			{
				//(i,j)位置可以擺放皇后,不衝突
				if(Feasible(i,j))
				{
					Board[i][j] = 1;  //擺放皇后t
					Queen(t+1);       //遞迴擺放皇后t+1
					Board[i][j] = 0;  //恢復
				}
			}
		}
	}
}
 
//返回num的階乘,num!
int factorial(int num)
{
	if(num==0 || num==1)
		return 1;
	return num*factorial(num-1);
}
 
 
int main(int argc, char* argv[])
{
	//初始化
	for(int i=0;i<N;++i)
	{
		for(int j=0;j<N;++j)
		{
			Board[i][j]=0;
		}
	}
 
	way = 0;
 
	Queen(1);  //從第1個皇后開始擺放
 
	//如果每個皇后都不同
	printf("考慮每個皇后都不同,擺放方法:%d\n",way);//N=8時, way=3709440 種
 
	//如果每個皇后都一樣,那麼需要除以 N!出去重複的答案(因為相同,則每個皇后可任意調換位置)
	printf("考慮每個皇后都不同,擺放方法:%d\n",way/factorial(N));//N=8時, way=3709440/8! = 92種
 
	return 0;
}

PS:該問題還有更優的解法。充分利用問題隱藏的約束條件:每個皇后必然在不同的行(列),每個行(列)必然也只有一個皇后。這樣我們就可以把N個皇后放到N個行中,使用Pos[i]表示皇后i在i行中的位置(也就是列號)(i = 0 to N-1)。這樣程式碼會大大的簡潔,因為節點的子節點數目會減少,判斷衝突也更簡單。

4. 迷宮問題

        問題:給定一個迷宮,找到從入口到出口的所有可行路徑,並給出其中最短的路徑

        分析:用二維陣列來表示迷宮,則走迷宮問題用回溯法解決的的思想類似於圖的深度遍歷。從入口開始,選擇下一個可以走的位置,如果位置可走,則繼續往前,如果位置不可走,則返回上一個位置,重新選擇另一個位置作為下一步位置。

        N表示迷宮的大小,使用Maze[N][N]表示迷宮,值為0表示通道(可走),值為1表示不可走(牆或者已走過);

        Point結構體用來記錄路徑中每一步的座標(x,y)

       (ENTER_X,ENTER_Y) 是迷宮入口的座標

       (EXIT_X, EXIT _Y)    是迷宮出口的座標

       Path容器用來存放一條從入口到出口的通路路徑

       BestPath用來存放所有路徑中最短的那條路徑

       Maze()函式用來遞迴走迷宮,具體步驟為:

       1. 首先將當前點加入路徑,並設定為已走        2. 判斷當前點是否為出口,是則輸出路徑,儲存結果;跳轉到4        3. 依次判斷當前點的上、下、左、右四個點是否可走,如果可走則遞迴走該點        4. 當前點推出路徑,設定為可走

       程式碼:

/************************************************************************ 
 * 名  稱:Maze.cpp
 * 功  能:回溯演算法例項:迷宮問題
 * 作  者:JarvisChu 
 * 時  間:2013-11-13 
 ************************************************************************/ 
#include <iostream>
#include <vector>
 
using namespace std;
 
typedef struct
{
	int x;
	int y;
}Point;
 
#define N 10         //迷宮的大小
#define ENTER_X 0    //入口的位置(0,0)
#define ENTER_Y 0
#define EXIT_X N-1   //出口的位置(N-1,N-1)
#define EXIT_Y N-1 
 
 
int Maze[N][N];		//定義一個迷宮,0表示通道,1表示不可走(牆或已走)
int paths;		//路徑條數
vector<Point> Path;	//儲存一條可通的路徑
vector<Point> BestPath;	//儲存最短的路徑
 
bool First = true;	//標誌,找到第一條路徑
 
//初始化迷宮
void InitMaze()
{
	//簡單起見,本題定義一個固定大小10*10的迷宮
	//定義一個迷宮,0表示通道,1表示牆(或不可走)
	int mz[10][10]={
	{0,0,1,1,1,1,1,1,1,1}, //0
	{1,0,0,1,1,0,0,1,0,1}, //1
	{1,0,0,1,0,0,0,1,0,1}, //2
	{1,0,0,0,0,1,1,0,0,1}, //3
	{1,0,1,1,1,0,0,0,0,1}, //4
	{1,0,0,0,1,0,0,0,0,1}, //5
	{1,0,1,0,0,0,1,0,0,1}, //6
	{1,0,1,1,1,0,1,1,0,1}, //7
	{1,1,0,0,0,0,0,0,0,0}, //8
	{1,1,1,1,1,1,1,1,1,0}  //9
    //   0 1 2 3 4 5 6 7 8 9
	}; 
 
	//複製到迷宮
	memcpy(Maze,mz,sizeof(mz));
 
	paths = 0;
}
 
//從(x,y)位置開始走;初始為(0,0)
void MazeTrack(int x,int y)
{
	///////////////////////////////////////
	//當前點加入到路徑
	Point p={x,y};
	Path.push_back(p);
	Maze[x][y] = 1;         //設定為已走,不可走
 
	//cout<<"來到("<<x<<","<<y<<")"<<endl;
 
	///////////////////////////////////////
	//如果該位置是出口,輸出結果
	if(x == EXIT_X && y== EXIT_Y)
	{
		cout<<"找到一條道路"<<endl;
		paths++;
		
		//輸出路徑
		vector<Point>::iterator it;
		for(it=Path.begin();it!=Path.end();++it)
		{
			cout<<"("<<it->x<<","<<it->y<<") ";
		}
		cout<<endl;
 
		//判斷是否更優
		if(First)//如果是找到的第一條路徑,直接複製到最優路徑
		{
			for(it=Path.begin();it!=Path.end();++it)
			{
				BestPath.push_back(*it);
			}
			First = false;
		}
		else //不是第一條,則判斷是否更短
		{
			//更短,複製到最優路徑
			if(Path.size()<BestPath.size())
			{
				BestPath.clear();
				for(it=Path.begin();it!=Path.end();++it)
				{
					BestPath.push_back(*it);
				}
			}
		}
	}
 
	///////////////////////////////////////
	//判斷(x,y)位置的上、下、左、右是否可走
 
	if((x-1)>=0 && Maze[x-1][y]==0)//上(x-1,y);存在且可走
	{
		MazeTrack(x-1,y);
	}
 
	if((x+1)<N && Maze[x+1][y]==0)//下(x+1,y);存在且可走
	{
		MazeTrack(x+1,y);
	}
 
	if((y-1)>=0 && Maze[x][y-1]==0)//左(x,y-1);存在且可走
	{
		MazeTrack(x,y-1);
	}
 
	if((y+1)<N && Maze[x][y+1]==0)//右(x,y+1);存在且可走
	{
		MazeTrack(x,y+1);
	}
 
	///////////////////////////////////////
	//返回上一步
	Path.pop_back();
	Maze[x][y] = 0;         //設定為未走
}
 
 
int main(int argc, char* argv[])
{
	//初始化迷宮
	InitMaze();
		
/*	//顯示迷宮
	for(int i=0;i<N;++i){
		for(int j=0;j<N;++j)
			cout<<Maze[i][j]<<"  ";
		cout<<endl;
	}*/
 
	//回溯法走迷宮
	MazeTrack(ENTER_X,ENTER_Y);
 
	//顯示最優的路徑
	cout<<"可行路徑總條數為"<<paths<<";最優路徑為"<<endl;
	vector<Point>::iterator it;
	for(it=BestPath.begin();it!=BestPath.end();++it)
	{
		cout<<"("<<it->x<<","<<it->y<<") ";
	}
	cout<<endl;
	return 0;
}

PS:用WPF實現了一個簡單的圖形化迷宮程式。白色表示通道,紅色表示牆,最短的路徑用黃色顯示。目前實現了一個10*10的迷宮自動搜素最短通路,右側顯示搜尋過程中得到的每一個可行通路。 由於構造一個迷宮比較複雜,所以暫時“迷宮設定”功能沒有做實現,至於手動一步步檢視搜素過程的動畫也沒有做實現。

實現的大致思路如下:迷宮的資料使用二維資料mazeData表示。迷宮的顯示使用Grid控制元件表示,每個方格處新增一個Rectangle控制元件,如果該方格mazeData值為0,則填充白色值為1,則填充紅色,值為2則填充黃色。

XAML程式碼為:

<Window x:Class="MazeAnimation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="迷宮" Height="496" Width="673" Loaded="Window_Loaded">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="120"></RowDefinition>
        </Grid.RowDefinitions>
        
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="463"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        
        <DockPanel Name="dpTips" Grid.Row="0" Grid.ColumnSpan="2" Background="AliceBlue" >
            <Label FontSize="16" Foreground="#FFAD1616" HorizontalAlignment="Center">迷宮的動態演示</Label>
        </DockPanel>
        
        <Grid Name="gdMaze" Grid.Row="1" Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
            
        </Grid>
 
        <ScrollViewer Grid.Row="1" Grid.Column="1"  Margin="5" HorizontalAlignment="Stretch" HorizontalScrollBarVisibility="Auto">
            <TextBox Name="tbLog" Background="Beige"></TextBox>
        </ScrollViewer>
        
        <DockPanel Name="dpSetting" Grid.Row="2" Grid.Column="0"  VerticalAlignment="Stretch">
 
            <TabControl Name="tcMazeSetting"  Background="#FFE5D9D9" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
                <TabItem Header="迷宮設定" Name="tabItemMaze">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*"></RowDefinition>
                            <RowDefinition Height="*"></RowDefinition>
                            <RowDefinition Height="*"></RowDefinition>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="60"></ColumnDefinition>
                            <ColumnDefinition Width="*"></ColumnDefinition>
                        </Grid.ColumnDefinitions>
                        
                        <Label Content="大小:"  Name="label1" Grid.Row="0" Grid.Column="0"/>
                        <Label Content="入口:"  Name="label2" Grid.Row="1" Grid.Column="0"/>
                        <Label Content="出口:"  Name="label3" Grid.Row="2" Grid.Column="0"/>
                                                                   
                        <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal">
                            <Label Content="高:"></Label>
                            <TextBox Name="tbMazeHeight" HorizontalAlignment="Left" MinWidth="40"></TextBox>
                            <Label Content="寬:"></Label>
                            <TextBox Name="tbMazeWidth" HorizontalAlignment="Left" MinWidth="40"></TextBox>
                        </StackPanel>
 
                        <StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal">
                            <Label Content="X="></Label>
                            <TextBox Name="tbEnterX" HorizontalAlignment="Left" MinWidth="40"></TextBox>
                            <Label Content="Y="></Label>
                            <TextBox Name="tbEnterY" HorizontalAlignment="Left" MinWidth="40"></TextBox>
                        </StackPanel>
 
                        <StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
                            <Label Content="X="></Label>
                            <TextBox Name="tbExitX" HorizontalAlignment="Left" MinWidth="40"></TextBox>
                            <Label Content="Y="></Label>
                            <TextBox Name="tbExitY" HorizontalAlignment="Left" MinWidth="40"></TextBox>
                        </StackPanel>
                    </Grid>
                    
                </TabItem>
                <TabItem Header="演示設定" Name="tabItemDemo">
                    <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
                        <CheckBox Name="cbAutoRun" Content="自動執行" Margin="10"></CheckBox>
                        <StackPanel Orientation="Horizontal">
                            <Label Content="執行速度:" Margin="10"></Label>
                            <TextBox Name="tbAutoRunSpeed" MinWidth="50" Margin="10"></TextBox>
                            <Label Content="毫秒" Margin="0,10,0,10"></Label>
                        </StackPanel>
                    </StackPanel>
                </TabItem>
            </TabControl>
        </DockPanel>
        
        <StackPanel Orientation="Horizontal" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Center">
            <Button Name="btnStart" Content="自動開始" Height="40" Width="70" Margin="5" Click="btnStart_Click"></Button>
            <Button Name="btnNext" Content="手動下一步" Height="40" Width="70" Margin="5" Click="btnNext_Click"></Button>
        </StackPanel>
    </Grid>
</Window>

對應的MainWindow.xaml.cs程式碼為:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
 
namespace MazeAnimation
{
 
 
 
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public struct Point
        {
            public int x;
            public int y;
            public Point(int a, int b) { x = a; y = b; }
        };
 
        public bool bAutoRun = true;
        public int mazeHeight = 10;
        public int mazeWidth = 10;
 
        int[,] mazeData = new int[10, 10]
            {
                {0,0,1,1,1,1,1,1,1,1}, //0
                {1,0,0,1,1,0,0,1,0,1}, //1
                {1,0,0,1,0,0,0,1,0,1}, //2
                {1,0,0,0,0,1,1,0,0,1}, //3
                {1,0,1,1,1,0,0,0,0,1}, //4
                {1,0,0,0,1,0,0,0,0,1}, //5
                {1,0,1,0,0,0,1,0,0,1}, //6
                {1,0,1,1,1,0,1,1,0,1}, //7
                {1,1,0,0,0,0,0,0,0,0}, //8
                {1,1,1,1,1,1,1,1,1,0}  //9
            //   0 1 2 3 4 5 6 7 8 9
            };
 
        public int enterX = 0;
        public int enterY = 0;
        public int exitX = 9;
        public int exitY = 9;
        public int runSpeed = 100;
 
        public int paths = 0; //總條數
        public Stack<Point> path = new Stack<Point>(); //一條找到的路徑
        public Stack<Point> bestPath = new Stack<Point>();//最優路徑
        public bool bFrist = true;
 
 
        public MainWindow()
        {
            InitializeComponent();
        }
 
        //顯示迷宮,白色0表示通道,紅色1表示不可走,黃色2表示最優的路徑,綠色3表示已經走過的路徑
        private void DisplayMaze()
        {
            gdMaze.Children.Clear();
            //設定可走和不可走
            for (int i = 0; i < mazeHeight; i++)
            {
                for (int j = 0; j < mazeWidth; j++)
                {
                    Rectangle rect = new Rectangle();
                    rect.SetValue(Grid.RowProperty, i);
                    rect.SetValue(Grid.ColumnProperty, j);
 
                    if (mazeData[i, j] == 0)
                    {
                        rect.Fill = Brushes.White;
                    }
                    else if (mazeData[i, j] == 1)
                    {
                        rect.Fill = Brushes.Red;
                    }
                    else if (mazeData[i, j] == 2)
                    {
                        rect.Fill = Brushes.Yellow;
                    }
                    else if (mazeData[i, j] == 3)
                    {
                        rect.Fill = Brushes.Blue;
                    }
                    gdMaze.Children.Add(rect);
                }
            }
        }
 
        //初始化迷宮
        private void InitMaze()
        {
 
            gdMaze.Background = Brushes.LightGray;
            gdMaze.ShowGridLines = true;
 
            for (int i = 0; i < mazeHeight; i++)
            {
                gdMaze.RowDefinitions.Add(new RowDefinition());
            }
 
            for (int i = 0; i < mazeWidth; i++)
            {
                gdMaze.ColumnDefinitions.Add(new ColumnDefinition());
            }
 
            DisplayMaze();
        }
 
        //從(x,y)位置開始走;初始為(0,0)
        private void MazeTrack(int x, int y)
        {
            ///////////////////////////////////////
            //當前點加入到路徑
            Point p = new Point(x, y);
            path.Push(p);
 
            mazeData[x, y] = 3;         //設定為已走,不可走           
            //DisplayMaze();
            //System.Threading.Thread.Sleep(runSpeed);//休眠
 
 
            ///////////////////////////////////////
            //如果該位置是出口,輸出結果
            if (x == exitX && y == exitY)
            {
                string msg = "找到一條道路(逆序)\n";
                tbLog.AppendText(msg);
 
                paths++;
 
                //輸出路徑
                foreach (Point pnt in path)
                {
                    msg = "(" + pnt.x + "," + pnt.y + ")";
                    tbLog.AppendText(msg);
                }
                tbLog.AppendText("\n\n");
 
                //判斷是否更優
                if (bFrist)//如果是找到的第一條路徑,直接複製到最優路徑
                {
                    foreach (Point pnt in path)
                    {
                        bestPath.Push(pnt);
                    }
 
                    bFrist = false;
                }
                else //不是第一條,則判斷是否更短
                {
                    //更短,複製到最優路徑
                    if (path.Count < bestPath.Count)
                    {
                        bestPath.Clear();
                        foreach (Point pnt in path)
                        {
                            bestPath.Push(pnt);
                        }
                    }
                }
            }
 
            ///////////////////////////////////////
            //判斷(x,y)位置的上、下、左、右是否可走
 
            if ((x - 1) >= 0 && mazeData[x - 1, y] == 0)//上(x-1,y);存在且可走
            {
                MazeTrack(x - 1, y);
            }
 
            if ((x + 1) < mazeHeight && mazeData[x + 1, y] == 0)//下(x+1,y);存在且可走
            {
                MazeTrack(x + 1, y);
            }
 
            if ((y - 1) >= 0 && mazeData[x, y - 1] == 0)//左(x,y-1);存在且可走
            {
                MazeTrack(x, y - 1);
            }
 
            if ((y + 1) < mazeWidth && mazeData[x, y + 1] == 0)//右(x,y+1);存在且可走
            {
                MazeTrack(x, y + 1);
            }
 
            ///////////////////////////////////////
            //返回上一步
            path.Pop();
            mazeData[x, y] = 0;         //設定為未走
 
            //DisplayMaze();
            //System.Threading.Thread.Sleep(runSpeed);//休眠
        }
 
 
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            //初始化變數
            tbMazeHeight.Text = mazeHeight.ToString();
            tbMazeWidth.Text = mazeWidth.ToString();
            tbEnterX.Text = enterX.ToString();
            tbEnterY.Text = enterY.ToString();
            tbExitX.Text = exitX.ToString();
            tbExitY.Text = exitY.ToString();
 
            cbAutoRun.IsChecked = bAutoRun;
            tbAutoRunSpeed.Text = runSpeed.ToString();
 
            //初始化迷宮
            InitMaze();
        }
 
        //點選開始
        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            string msg = "開始走迷宮\n";
            tbLog.AppendText(msg);
            MazeTrack(enterX, enterY);
 
            //顯示最優的路徑
            msg = "\n可行路徑總條數為" + paths + "\n最優路徑為\n";
            tbLog.AppendText(msg);
 
            foreach (Point pnt in bestPath)
            {
                msg = "(" + pnt.x + "," + pnt.y + ")";
                tbLog.AppendText(msg);
 
                mazeData[pnt.x, pnt.y] = 2;
 
            }
            DisplayMaze();
        }
 
        //下一步
        private void btnNext_Click(object sender, RoutedEventArgs e)
        {
            string msg = "手動開始走迷宮 暫未實現\n";
            tbLog.AppendText(msg);
        }
 
    }
}