【數據結構課程設計】 迷宮問題
做完課設在B站發了一個演示視頻,一直說寫個博客總結記錄一下但是一直在忙別的課設和考試就一直沒做,現在終於考完試了那就總結一下吧~~
迷宮問題描述:
問題描述
心理學家把一隻老鼠從一個無頂蓋的大盒子的入口處放入,讓老鼠自行找到出口出來。迷宮中設置很多障礙阻止老鼠前行,迷宮唯一的出口處放有一塊奶酪,吸引老鼠找到出口。
迷宮四周設爲牆;無填充處爲可通處。設每個點有四個可通方向,分別爲東、南、西、北。左上角爲入口,右下角爲出口。迷宮有一個入口,一個出口。設計程序求解迷宮的一條通路。
設計要求
(1)設計迷宮的存儲結構。
(2)設計通路的存儲結構。
(3)設計求解通路的算法。
(4)設計迷宮顯示和通路的顯示方式。
(5)輸入:迷宮、入口及出口可在程序中設定,也可從鍵盤輸入。
(6)輸出:迷宮、入口、出口及通路路徑。
思考:
(1)若每個點有8個試探方向(東、東南、南、西南、西、西北、北、東北),如何修改程序?
(2)如何求得所有通路?
(3)如何求得最短通路?
解決方案
這裏要求迷宮的通路,我設計的是求最短通路和所有通路。
求最短通路的算法主要用到廣度優先算法(bfs),由於bfs每次訪問的都是與當前的點相鄰的點,即發散型地訪問遍歷,每次訪問到新的點都將該點距離起點的距離變爲發出點距離起點的距離加一,並且入隊,重複取出隊頭元素執行重複操作直到隊列爲空,即可求出每個點距離起點的最短距離。至於路徑的輸出,先求得所有點距離終點的最短距離dist[i][j](預處理),再從起點開始遍歷,每次遍歷只需要判斷當前點距離終點的最短距離dist[i][j]是否等於下一個點(k,m)距離終點的最短距離dist[k][m] + 1,如果是,則這個點就是起點到終點通路上的一個點,可以用一個容器記錄下來。
第十屆藍橋杯有到差不多的題跟這個求最短路的思路差不多,感興趣的可以去看看。
求所有通路的算法主要用到深度優先算法(dfs), dfs是一種不撞南牆不回頭的方法,當然,撞了南牆就要回頭(回溯),只要下一個點能走就一直走,直到不能走就回頭繼續探索下一個點,是一種比較“暴力”的方法。當然,由於dfs本質上還是遞歸,所以要注意遞歸結束的條件。至於路徑的記錄,可以開一個數組或者vector來記錄路徑,每次走到一個點時,就將這個點添加到數組中,再將記錄路徑長度的變量加一,並將這個點置爲已訪問,當然,遞歸結束開始回溯時要恢復原本的狀態(將記錄路徑長度的變量減一,該點未被訪問)。
至於輸出,我用來座標和圖形化兩種方式輸出,座標就不多說了,圖形化用到了EasyX庫,根據每個點的座標來畫出路徑和迷宮即可。
核心算法都並不困難,但也需要注意細節,理解透徹,比如我寫dfs的時候就因爲沒有注意遞歸出口出了bug,遞歸的結束條件真的是很重要啊!
主要代碼如下:
源.cpp文件:
#include "wlx_Maze.h"
using namespace std;
int main()
{
Wlx_Maze m;
while (true)
{
m.menu();
int choice;
cin >> choice;
switch (choice)
{
case 1:
// 讀入迷宮
m.read_maze();
break;
case 2:
m.second_menu();
break;
case 0:
exit(0);
default:
cout << "輸入有誤請重新輸入!" << endl;
break;
}
system("pause");
system("cls");
}
return 0;
}
wlx_Maze.cpp文件:
#include "wlx_Maze.h"
int dir_row[4] = { -1, 1, 0, 0 },
dir_col[4] = { 0, 0, -1, 1 };// 上下左右四個方向
int dir1_row[8] = { -1, 1, 0, 0, -1, 1, -1, 1 },
dir1_col[8] = { 0, 0, -1, 1, -1, -1, 1, 1 }; // 上下左右,左上、左下、右上、右下八個方向
int mode; // 選擇的模式
// 一級菜單
void Wlx_Maze::menu()
{
cout << "********************* wlx_迷宮求解 ************************" << endl;
cout << "********************* 1、重錄迷宮 *************************" << endl;
cout << "********************* 2、已有迷宮 *************************" << endl;
cout << "********************* 0、退出 **************************" << endl;
cout << "請輸入您的選擇:";
}
// 二級菜單
void Wlx_Maze::second_menu()
{
if (this->col == 0)
{
cout << "暫無迷宮信息!請先錄入!" << endl;
return;
}
bool flag = true;
while (true)
{
system("cls");
cout << "********************** wlx_迷宮求解 *************************" << endl;
cout << "********************* 1、顯示迷宮構造 ***********************" << endl;
cout << "******************* 2、顯示迷宮最短通路 *********************" << endl;
cout << "******************* 3、顯示迷宮所有通路 *********************" << endl;
cout << "************************ 0、退出 ****************************" << endl;
cout << "請輸入您的選擇:";
int op;
cin >> op;
switch (op)
{
case 1:
// 顯示迷宮
this->draw_maze(true, true);
break;
case 2:
// 顯示最短路徑
this->show_min_path();
break;
case 3:
// 顯示所有路徑
this->show_all_path();
break;
case 0:
// 退出
flag = false;
break;
default:
cout << "輸入有誤請重新輸入!" << endl;
break;
}
if (!flag)
break;
system("pause");
}
}
// 初始化
void Wlx_Maze::init()
{
this->col = 0;
this->row = 0;
this->end_x = this->end_y = 0;
this->start_x = this->start_y = 0;
this->path_num = 0;
this->all_path_num = 0;
memset(this->each_path_num, 0, sizeof(this->each_path_num));
memset(this->dist, -1, sizeof(this->dist)); // 初始化每個點到終點的距離都爲-1
memset(this->is_visited, false, sizeof(this->is_visited)); // 初始條件所有點均未被訪問
}
// 構造函數
Wlx_Maze::Wlx_Maze()
{
// 初始化
this->init();
ifstream ifs;
ifs.open(MAZEFILE, ios::in);
// 打開文件失敗
if (!ifs.is_open())
{
ifs.close();
return;
}
// 初始化
int r = 0;
while (ifs >> this->maze[r])
{
this->col = this->maze[r].length();
r++;
}
this->row = r;
//cout << "l = " << this->len << " w = " << this->wid << endl;
ifs.close();
}
// 獲取迷宮
void Wlx_Maze::read_maze()
{
cout << "請輸入迷宮的行數:";
cin >> this->row;
cout << "請輸入迷宮的列數:";
cin >> this->col;
cout << "請輸入迷宮:(提示:0爲可走, 1爲不可走)" << endl;
ofstream ofs;
ofs.open(MAZEFILE, ios::out);
for (int i = 0; i < this->row; i++)
{
cin >> this->maze[i];
ofs << this->maze[i] << endl; // 寫入文件
}
ofs.close();
cout << "錄入迷宮成功! 是否顯示迷宮?(1、顯示 2、不顯示)" << endl;
int sel;
cin >> sel;
if (sel == 1)
this->draw_maze(true, true);
}
// 獲取起點和終點的行號和列號
void Wlx_Maze::get_start_end()
{
while (true)
{
cout << "請輸入迷宮起點的行號[0-" << this->row << "): " << endl;
cin >> this->start_x;
cout << "請輸入迷宮起點的列號[0-" << this->col << "): " << endl;
cin >> this->start_y;
cout << "請輸入迷宮終點的行號[0-" << this->row << "): " << endl;
cin >> this->end_x;
cout << "請輸入迷宮終點的列號[0-" << this->col << "): " << endl;
cin >> this->end_y;
// 起點或終點爲1則不合法
if ( this->start_x < 0 || this->end_x >= this->row
|| this->start_y < 0 || this->end_y >= this->col
|| this->maze[this->start_x][this->start_y] == '1'
|| this->maze[this->end_x][this->end_y] == '1')
{
cout << "終點 / 起點不合法! 請重新輸入!" << endl;
}
else
break;
}
}
// 獲取每個點到終點的最短距離
void Wlx_Maze::get_min_distance()
{
memset(this->dist, -1, sizeof(this->dist));
cout << "您想要選擇:1、模式1(只可走上下左右四個方向)" << endl <<
" 2、模式2(可走上下左右,左上,左下等八個方向, 最多 " << MAXSIZE << "條)" << endl;
cin >> mode;
// 定義隊列
queue< pair<int, int> > q;
this->dist[this->end_x][this->end_y] = 0; // 終點到自己的距離爲0
q.push({ this->end_x, this->end_y }); // 入隊
while (!q.empty())
{
auto t = q.front();
q.pop();
// 模式1
if (mode == 1)
{
for (int i = 0; i < 4; i++)
{
// 新的行列
int new_r = t.first + dir_row[i];
int new_c = t.second + dir_col[i];
// 判斷新的行列是否合法
if (new_r >= 0 && new_r < this->row
&& new_c >= 0 && new_c < this->col // 新點在迷宮範圍內
&& this->dist[new_r][new_c] == -1 // 未被訪問
&& this->maze[new_r][new_c] == '0') // 可走
{
// 新的距離等於當前距離加一
this->dist[new_r][new_c] = this->dist[t.first][t.second] + 1;
q.push({ new_r, new_c }); // 入隊
}
}
}
// 模式2
else
{
for (int i = 0; i < 8; i++)
{
// 新的行列
int new_r = t.first + dir1_row[i];
int new_c = t.second + dir1_col[i];
// 判斷新的行列是否合法
if (new_r >= 0 && new_r < this->row
&& new_c >= 0 && new_c < this->col // 新點在迷宮範圍內
&& this->dist[new_r][new_c] == -1 // 未被訪問
&& this->maze[new_r][new_c] == '0') // 可走
{
// 新的距離等於當前距離加一
this->dist[new_r][new_c] = this->dist[t.first][t.second] + 1;
q.push({ new_r, new_c }); // 入隊
}
}
}
}
}
// 獲取起點到終點的路徑
void Wlx_Maze::get_min_path()
{
int sx = this->start_x, sy = this->start_y; // 起點座標
int ex = this->end_x, ey = this->end_y; // 終點座標
while (sx != ex || sy != ey) // 未到終點
{
if (mode == 1)
{
// 四個方向搜索
for (int i = 0; i < 4; i++)
{
// 新的座標
int nx = sx + dir_row[i];
int ny = sy + dir_col[i];
// 判斷新座標是否合法
if ( nx >= 0 && nx < this->row
&& ny >= 0 && ny < this->col // 新點在地圖範圍內
&& this->maze[nx][ny] == '0' ) // 新點可走
{
// 如果當前點距離終點的距離等於新點距離終點的距離+1則爲該條通路的路徑
if (this->dist[sx][sy] == this->dist[nx][ny] + 1)
{
sx = nx, sy = ny; // 更新
// 記錄路徑
this->min_path[this->path_num].r = nx;
this->min_path[this->path_num++].c = ny;
}
}
}
}
else
{
for (int i = 0; i < 8; i++)
{
// 新的座標
int nx = sx + dir1_row[i];
int ny = sy + dir1_col[i];
// 判斷新座標是否合法
if ( nx >= 0 && nx < this->row
&& ny >= 0 && ny < this->col // 新點在地圖範圍內
&& this->maze[nx][ny] == '0') // 新點可走
{
if (this->dist[sx][sy] == this->dist[nx][ny] + 1)
{
sx = nx, sy = ny;// 更新
// 記錄路徑
this->min_path[this->path_num].r = nx;
this->min_path[this->path_num++].c = ny;
}
}
}
}
}
}
// 畫路徑
void Wlx_Maze::draw_min_path()
{
this->draw_maze(false, true);
// 畫起點
setfillcolor(RGB(212, 68, 68));
solidcircle((2 * this->start_y + 1) * (ROW / 2), (2 * this->start_x + 1) * (ROW / 2), RADIUS);
for (int i = 0; i < this->path_num; i++)
{
int new_r = this->min_path[i].r;
int new_c = this->min_path[i].c;
setfillcolor(RGB(R, G, B));
solidcircle((2 * new_c + 1) * (ROW / 2), (2 * new_r + 1) * (ROW / 2), RADIUS);
Sleep(DELAYTIME);
}
// 畫終點
setfillcolor(RGB(176, 58, 223));
solidcircle((2 * this->end_y + 1) * (ROW / 2), (2 * this->end_x + 1) * (ROW / 2), RADIUS);
_getch();
closegraph();
}
void Wlx_Maze::draw_maze(bool is_quit, bool is_init) // 畫迷宮
{
if (is_init)
{
// 創建繪圖窗口
initgraph(WIDTH, HEIGHT);
// 設置背景色
setbkcolor(RGB(128, 128, 128));
// 用背景色清空屏幕
cleardevice();
}
for (int i = 0; i < this->row; i++)
{
for (int j = 0; j < this->col; j++)
{
if (this->maze[i][j] == '1') // 不可走
{
setfillcolor(RGB(125, 160, 214));
solidrectangle(j * ROW, i * COL, (j + 1) * ROW, (i+1) * COL);
}
else
{
setfillcolor(RGB(127, 243, 204));
solidrectangle(j * ROW, i * COL, (j + 1) * ROW, (i + 1) * COL);
}
}
}
// 按任意鍵退出
if (is_quit)
{
_getch();
closegraph();
}
}
// 顯示迷宮最短通路
void Wlx_Maze::show_min_path()
{
// 獲取起點和終點的座標
this->get_start_end();
// 初始化
this->path_num = 0;
// 獲取每個點到終點的最短距離
this->get_min_distance();
// 無法到達終點
if (this->dist[this->start_x][this->start_y] == -1 )
{
cout << "終點不可達!" << endl;
return;
}
// 獲取最短路徑
this->get_min_path();
cout << "您想要輸出迷宮的圖像or路徑座標?(1、圖像 2、座標)" << endl;
int sel;
cin >> sel;
if(sel == 1) // 顯示圖像
this->draw_min_path();
else
{
cout << "row = " << this->start_x
<< " col = " << this->start_y << endl;
for (int i = 0; i < this->path_num; i++)
cout << "row = " << this->min_path[i].r
<< " col = " << this->min_path[i].c << endl;
}
}
// 獲取所有通路的路徑
Maze_Point p[MAXSIZE]; // 記錄路徑
int p_num = 0;
void Wlx_Maze::get_all_path(int current_row, int current_col)
{
// 到達終點
if (current_row == this->end_x && current_col == this->end_y)
{
if (this->all_path_num >= MAXSIZE) // 路徑個數不要超過記錄路徑數組的最大值
return;
for (int i = 0; i < p_num; i++)
this->all_path[this->all_path_num][i] = p[i];
this->each_path_num[this->all_path_num] = p_num;
this->all_path_num++;
}
// 當前位置合法
if (current_row >= 0 && current_row < this->row
&& current_col >= 0 && current_col < this->col)
{
if (mode == 1) // 模式1
{
for (int i = 0; i < 4; i++)
{
// 新位置
int n_r = current_row + dir_row[i];
int n_c = current_col + dir_col[i];
// 判斷新位置是否合法
if (n_r >= 0 && n_r < this->row
&& n_c >= 0 && n_c < this->col // 在迷宮範圍內
&& !this->is_visited[n_r][n_c] // 未被訪問過
&& this->maze[n_r][n_c] == '0') // 可走
{
this->is_visited[n_r][n_c] = true; // 已走
Maze_Point t;
t.r = n_r;
t.c = n_c;
p[p_num++] = t; // 記錄路徑
this->get_all_path(n_r, n_c); // 遞歸
// 回溯
this->is_visited[n_r][n_c] = false;
p_num--;
}
}
}
else
{
for (int i = 0; i < 8; i++)
{
// 新位置
int n_r = current_row + dir1_row[i];
int n_c = current_col + dir1_col[i];
// 判斷新位置是否合法(在迷宮範圍內、未被訪問過、可走)
if (n_r >= 0 && n_r < this->row
&& n_c >= 0 && n_c < this->col // 在迷宮範圍內
&& !this->is_visited[n_r][n_c] // 未被訪問過
&& this->maze[n_r][n_c] == '0') // 可走
{
this->is_visited[n_r][n_c] = true; // 已走
Maze_Point t;
t.r = n_r;
t.c = n_c;
p[p_num++] = t; // 記錄路徑
this->get_all_path(n_r, n_c); // 遞歸
// 回溯
this->is_visited[n_r][n_c] = false;
p_num--;
}
}
}
}
}
// 顯示迷宮的所有通路
void Wlx_Maze::show_all_path()
{
// 初始化操作
this->all_path_num = 0;
memset(this->each_path_num, 0, sizeof(this->each_path_num));
memset(this->is_visited, false, sizeof(this->is_visited)); // 初始條件所有點均未被訪問
p_num = 0;
// 獲取起點和終點座標
this->get_start_end();
p[p_num].r = this->start_x;
p[p_num++].c = this->start_y;
this->is_visited[this->start_x][this->start_y] = true;
cout << "您想要選擇:1、模式1(只可走上下左右四個方向)" << endl <<
" 2、模式2(可走上下左右,左上,左下等八個方向, 最多 " << MAXSIZE << "條)"<< endl;
cin >> mode;
this->get_all_path(this->start_x, this->start_y);
cout << "您想要輸出迷宮的圖像or路徑座標?" << endl
<< "1、圖像(紅色爲起點,紫色爲終點,按'a'上一頁 、's'直接退出 、回車下一頁) " << endl
<< "2、座標 " << endl;
int sel;
cin >> sel;
if (sel == 1)
{
// 畫路徑
this->draw_all_path();
}
else
{
for (int i = 0; i < this->all_path_num; i++)
{
cout << "第 " << i + 1 << "條通路爲:" << endl;
for (int j = 0; j < this->each_path_num[i]; j++)
cout << "row = " << this->all_path[i][j].r
<< " col = " << this->all_path[i][j].c << endl;
cout << endl;
}
}
}
// 畫出迷宮的所有通路
void Wlx_Maze::draw_all_path()
{
bool flag = true;
while (flag)
{
initgraph(WIDTH, HEIGHT);
setbkcolor(RGB(128, 128, 128)); // 背景顏色
cleardevice();
for (int i = 0; i < this->all_path_num; i++)
{
setbkcolor(RGB(128, 128, 128));
cleardevice();
this->draw_maze(false, false);
// 顯示 當前頁數/總頁數
char page[19];
sprintf(page, "%d / %d", i + 1, this->all_path_num);
RECT r = { WIDTH - 2 * ROW, HEIGHT - COL, WIDTH, HEIGHT };
drawtext(_T(page), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
for (int j = 0; j < this->each_path_num[i]; j++)
{
// 獲取路徑所在的行列
int new_r = this->all_path[i][j].r;
int new_c = this->all_path[i][j].c;
// 畫圖
setfillcolor(RGB(R, G, B));
solidcircle((2 * new_c + 1) * (ROW / 2), (2 * new_r + 1) * (ROW / 2), RADIUS);
// 延時
Sleep(DELAYTIME);
}
// 畫起點和終點
setfillcolor(RGB(212, 68, 68));
solidcircle((2 * this->start_y + 1) * (ROW / 2), (2 * this->start_x + 1) * (ROW / 2), RADIUS);
setfillcolor(RGB(176, 58, 223));
solidcircle((2 * this->end_y + 1) * (ROW / 2), (2 * this->end_x + 1) * (ROW / 2), RADIUS);
char key = _getch();
//cout << " i = " << i << endl;
if (key == 'a' || key == 'A') // 向左翻頁
{
i -= 2;
if (i < 0)
i = -1;
}
else if (key == 's' || key == 'S') // 退出
{
// cout << key << endl;
flag = false;
closegraph();
break;
}
}
closegraph();
}
}
wlx_Maze.h頭文件:
#define _CRT_SECURE_NO_DEPRECATE
#pragma once
#include <cstdio>
#include <cstring>
#include <cmath>
#include <string>
#include <map>
#include <queue>
#include <stack>
#include <iostream>
#include <algorithm>
#include <fstream>
#include <graphics.h>
#include <conio.h>
using namespace std;
const int MAXSIZE = 109;
#define ROW 30
#define COL 30
#define WIDTH 640
#define HEIGHT 480
#define DELAYTIME 200
#define R 212
#define G 144
#define B 186
#define RADIUS 10
#define MAZEFILE "maze_file.txt"
class Maze_Point // 迷宮上每個點的座標
{
public:
int r;
int c;
};
class Wlx_Maze // 迷宮
{
public:
int col; // 迷宮的列
int row; // 迷宮的行
int end_x, end_y; // 終點座標
int start_x, start_y; // 起點座標
string maze[MAXSIZE]; // 存放迷宮
int path_num; // 最短路徑的長度
int all_path_num; // 通路的個數
Maze_Point all_path[MAXSIZE][MAXSIZE]; // 第i條通路對應的每個點的座標
bool is_visited[MAXSIZE][MAXSIZE]; // 判斷是否走過
int each_path_num[MAXSIZE * 10]; // 每條通路的長度
int dist[MAXSIZE][MAXSIZE]; // 點i, j 到終點的最短距離
Maze_Point min_path[MAXSIZE * MAXSIZE];
void menu(); // 一級菜單
void second_menu(); // 二級菜單
Wlx_Maze(); // 構造函數
void init(); // 初始化
void read_maze(); // 獲取迷宮
void get_start_end(); // 獲取起點和終點的行號和列號
void get_min_distance(); // 獲取每個點到終點的最短距離
void get_min_path(); // 獲取起點到終點的路徑
void show_min_path(); // 顯示迷宮最短通路
void draw_min_path(); // 畫最短通路路徑
void get_all_path(int current_row, int current_col); // 獲取所有通路的路徑
void show_all_path(); // 顯示迷宮的所有通路
void draw_all_path(); // 畫出迷宮的所有通路
void draw_maze(bool is_quit, bool is_init); // 畫迷宮
};
若有不足,歡迎指正~~