演算法入門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);
}
}
}