1.1问题定义
本次课程的设计题目是设计一个大型景区的管理系统,此系统是为了便于景区的管理以及提升游客的旅游体验,更好的适应现如今日益发展的旅游业。
1.2问题分析
现有某景区需要开发一个景区信息管理系统,具体需求有:建立一个主程序应用菜单选项,方便用户操作和选择需要使用的功能。为了能够适应不同景区的需求,系统需要能够创建景点分布图和输出景点分布图,对于创建,可以逐条添加景点信息和路径信息;对于输出,需要输出景点分布图的邻接矩阵。而且,系统需要能够制订旅游景点导游线路策略,以便于游客游览所有景点,即给出一个入口景点,生成一个导游线路图;但是导游路线图中可能含有回路,所以为了使导游线路图能够优化,需要判断图中有无回路,若有回路,则打印输出回路中的景点。同时,在导游线路图中,还需要为一些不愿按线路走的游客提供信息服务,比如从一个景点到另一个特定的景点。所以两个景点之间的最短路径和最短距离就变得很重要了,系统需要提供输出两个景点间的最短路径和最短距离的功能。此外,在景区建设中,道路建设是其中一个重要内容,道路建设首先要保证能连通所有景点,但又要花最小的代价,修建道路的代价与它的里程相关。系统同样需要提供景点搜索和景点排序的功能,游客通过输入关键字来了解相关景点以便计划好游览地点和顺序;而景点排序可以通过景点的欢迎度或景点的人流量来排序,让游客对各个景点有个直观的感受。最后,景区中还有固定容量的停车场,系统需要对到达车辆和离开车辆进行管理;当车辆到达时,如果停车场中还有剩余位置,则直接进入并开始计时,否则在候车道等待;当车辆离开时,在它之后进入的车辆必须先退出车场为它让路,待该辆车开出大门外,其它车辆再按原次序进入车场,每辆停放在车场的车在它离开停车场时必须按它停留的时间长短交纳费用。本系统面向管理员和游客,所以可以分为管理员功能和游客功能。
1.3研究意义
随着科学技术的发展,人民生活水平的提高以及我国休假制度的完善,外出旅游成为了越来越多的人民度假的选择。在这般背景前提下,造成了旅游景区在高峰期部分景点人流过大导致拥堵,从而影响景区形象和游客旅游体验。从根本上说,这往往是因为景区服务不够细致,管理不够科学,效率不高所造成的。另一方面,游客人数的急剧增长所带来的安全问题也日益凸显起来。系统化、电子化、网络化、智能化的景区管理系统也成为了日益迫切的需求,本项目就是在这样的背景下提出的。针对此问题,所以研究和开发高质量、高性能的景区信息管理系统变得尤为重要,可以提高景区的管理水平,解放了繁重的体力劳动和脑力劳动,极大的改善了用户服务的质量,提高了景区的信誉。
第二章 系统设计
2.1总体设计
2.1.1设计思想
系统采用前后端结合的方式,后端采用servlet运行核心算法,前端使用网页进行结果动态展示,通信格式为JSON。整个系统的架构图如下图所示:
转存失败重新上传取消
<1> 管理员功能:
(1)登录:管理员使用账户密码登录。
(2)公告发布:设计发布公告时将内容写入一个文本文件,在游客登录或管理员登录时读取这个文本文件并显示在界面上。
(3)管理景点:景点信息的增加,删除和修改的功能。
(4)管理道路:实现道路的插入和删除的功能。
(5)输出停车场信息:显示停车场信息,包括等待车辆,停车场空位。
<2> 游客功能:
(1)景点查询和排序:可以根据自己的需求对景区进行各种信息查询并对景点进行一定顺序上的排序。
(2)景点最短路径:系统还应能够根据游客输入的两个景点的名称来输出两地之间的最短路径和最短距离。
(3)输出导游路线图:系统还能为游客输出导游路线图。
2.1.2数据结构
- 图的存储结构
public class ALGraph {
private int arcNum; //景点数量
private int vetNum; //路的数量
private List<ArcNode> nodes; //存储景点的list
···
}
- 景点结点
public class ArcNode {
private String name; //景点名称
private String des; //景点描述
private int pop; //景点欢迎度
private boolean hasRest; //有无休息区
private boolean hasToilet; //有无公厕
private VNode first; //景点的第一条边
···
}
- 边结点
public class VNode {
private int index; //另一个景点在景点数组中的位置
private int dist; //两个景点的距离
private int time; //所需时间
private VNode next; //与头结点相连的下一条边
···
}
- 汽车
public class Car {
private String number; //汽车车号
private Date ar_time; //汽车到达时间
···
}
- 栈
public class Stack<T> {
private T[] mArray;
private int size;
public void push(T val){
···
}
public void pop(){
···
}
public T peek(){
···
}
}
- 队列
public class Queue {
private CarNode phead; //汽车结点的链表头
private int size; //队列的元素个数
public void add(Car car){
···
}
public void pop(){
···
}
public CarNode front(){
···
}
}
2.1.3用户界面
转存失败重新上传取消
转存失败重新上传取消
转存失败重新上传取消
转存失败重新上传取消
转存失败重新上传取消
转存失败重新上传取消
2.2程序设计
2.2.1类图
转存失败重新上传取消
2.2.2流程图
转存失败重新上传取消
第三章 系统实现与调试
3.1 导游线路图的创建
- 实现过程
建立无向带权图,输入顶点和边的信息,输出邻接链表G。因无向边的原因,输入一条边时构建两条点。对于导游线路图的创建,该系统直接从服务器端存储的Arc.txt中读取所有景点信息和路径信息。如果需要更换旅游图,则直接在txt文件中修改即可。
读入数据的代码段为:
for(int i=1; i<=graph.getArcNum(); i++){
tmp = reader.nextLine();
infos = tmp.split(" ");
name = infos[0];
des = infos[1];
pop = Integer.parseInt(infos[2]);
hasRest = ((infos[3].equals("1"))?true:false);
hasToilet = ((infos[4].equals("1"))?true:false);
graph.getNodes().add(new ArcNode(name, des, pop, hasRest, hasToilet));
}
对于导游路线图的输出,从邻接链表转换成邻接矩阵,并输出邻接矩阵。
输出数据的代码段为:
for(int i=0; i<graph.getArcNum(); i++){
System.out.print(graph.getNodes().get(i).getName()+" ");
for(int j=0; j<graph.getArcNum(); j++){
if(i == j){
System.out.print("0 ");
continue;
}
int dis = isContact(i, j);
System.out.print(dis + " ");
}
System.out.println();
}
- 遇到的难点
将邻接链表中的景点和路径信息显示在前端html页面的中,即在前端显示模拟的导游路线图和景点信息。
- 解决方案
HTML5中有canvas这个网页元素,可以用JavaScript代码在canvas中直接绘制圆、直线等几何图形。所以选用了该元素,并使用JavaScript绘制了景点、路径、路径长度以及各个景点的信息。
context.beginPath();
//画圆
context.arc(xPosition,yPosition,30,0,Math.PI*2,true);
//画线
context.moveTo(x1Position,y1Position);
context.lineTo(x2Position,y2Position);
//添加文字
context.fillText(dist, xPosition, yPosition);
context.closePath();
context.stroke();
3.2输出导游路线图及其图中的回路部分
- 实现过程
TourSystem中可输出旅游景点导游线路,输入一个景点作为入口,建立一个导游线路图,导游线路图用有向图表示。遍历采用深度优先遍历,这也比较符合游客心理。
深度优先遍历的关键代码为:
public List<Integer> DFSTraverse(String start){
Stack<Integer> traverseNodes = new Stack<Integer>(Integer.class, graph.getArcNum());
startIndex = getPos(start);
traverseNodes.push(startIndex);
while(!traverseNodes.isEmpty() && !hasAllVisited()){
int arcNodeIndex = traverseNodes.peek();
visited[arcNodeIndex] = true;
VNode vNode = graph.getNodes().get(arcNodeIndex).getFirst();
while(vNode != null){
if(!visited[vNode.getIndex()]){
traverseNodes.push(vNode.getIndex());
break;
}
vNode = vNode.getNext();
}
if(vNode == null){
traverseNodes.pop();
}
tourIndexList.add(arcNodeIndex);
}
return tourIndexList;
}
为优化导游线路图功能,可通过拓扑排序的方法判断图中有无回路,若有回路,则打印输出回路中的景点。拓扑排序只在有向无环图中有效,这里针对输出的有向路线图。过程如下:(1)计算有向图中每个顶点的入度,存储在数组中,将所有入度为0的顶点入队;(2)遍历队列,在遍历的过程中,将出队的顶点所有相邻的顶点的入度减1,如果有入度减为0的顶点则继续入队,否则继续遍历队列剩余元素;(3)遍历入度数组,若入度不为0,则该顶点在回路中。
拓扑排序的关键代码为:
while(!queue.isEmpty()){
int ind = queue.poll();
VNode node = directGraph.getNodes().get(ind).getFirst();
while(node != null){
indegree[node.getIndex()]--;
if(indegree[node.getIndex()] == 0){
queue.offer(node.getIndex());
}
node = node.getNext();
}
}
- 遇到的难点
拓扑排序过程中,首先会将所有入度为0的顶点入队,然后才可以继续进行队列元素的提取和添加。在本功能实现过程中,通过路线图输出过程中建立起来的有向图里,没有入度为0的顶点,所以拓扑排序无法正常进行。
- 解决方案
将入口顶点的入度置为0,然后将其放入队列中,这样就能正常进行拓扑排序了。比如北门是起始点,则将北门这个顶点的入度置为0,然后放入队列。
3.3输出两个景点之间最短路径和最短距离
- 实现过程
Dijkstra算法是典型的最短路径算法,用于计算一个节点到其他所有节点的最短路径。设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
Dijkstra的关键代码为:
public void dijkstra(String source, String des){
sourceIndex = getPos(source);
desIndex = getPos(des);
//初始化最短距离数组为INF
for(int i=0; i<graph.getArcNum(); i++){
dis[i] = (i==sourceIndex ? 0 : Constants.INF);
}
for(int i=0; i<graph.getArcNum(); i++){
int minPos = -1, m = Constants.INF;
for(int j=0; j<graph.getArcNum(); j++){
if(vis[j]==0 && dis[j]<m){
m = dis[minPos=j];
}
}
vis[minPos] = 1;
for(int j=0; j<graph.getArcNum(); j++){
if(vis[j]==0 && dis[minPos]+getLength(minPos, j)<dis[j]){
dis[j] = dis[minPos]+getLength(minPos, j);
fath[j] = minPos;
}
}
}
}
- 遇到的难点
通过Dijkstra算法直接计算两点间的最短距离比较容易,如何保存最短路径中的顶点信息则相对困难一些。
- 解决方案
通过设定一个fath数组,用来记录最短路径寻找过程中,寻找路线确定的顶点信息。当到达某点的距离需要更新时,fath数组则用来记录边的起点信息。最后通过目标点的坐标依次遍历fath数组,则可得到逆序的路线图。
3.4输出道路修建规划图
- 实现过程
在景区建设中,道路建设是其中一个重要内容。道路建设首先要保证能连通所有景点,但又要花最小的代价,可以通过求最小生成树来解决这个问题。首先将图中所有的边按其权值大小排序,然后按由小到大的次序顺序选取这些边,若选边后不形成回路,则保留作为一条边,若形成回路则除去.依次选够(n-1)条边,即得最小生成树。(n为顶点数)
最小生成树的关键代码为:
//用于保存已有最小生成树中每个顶点在该最小生成树中的终点
int[] ends = new int[graph.getVetNum()];
//用于保存结果最小生成树的边
VData[] results = new VData[graph.getVetNum()];
//获取图中所有的边
VData[] edges = getEdges();
//将边按照权的大小进行排序(从小到大)
sortEdges(edges);
//对所有边进行遍历
for(int i=0; i<graph.getVetNum(); i++){
int m = getEnd(ends, edges[i].getStart()); //获取起点在已有最小生成树中的终点
int n = getEnd(ends, edges[i].getEnd()); //获取该边终点在已有最小生成树中的终点
//如果m!=n,说明在已有最小生成树中添加该边不会形成回路
if(m != n){
ends[m] = n;
results[index++] = edges[i];
}
}
- 遇到的难点
最小生成树算法在选边的过程中要判断添加该边后是否会形成回路,若形成回路则出去,不形成则属于最小生成树里的一条边。那么如何判断是否会形成回路。
- 解决方案
使用并查集的思想。首先将顶点划分到不同集合中,每个集合中的顶点表示一个无回路的连通分量;然后当选取一条边时,若它的两个顶点属于不同的集合,则这条边保留,并把两个顶点所在的集合合并成一个集合。如果选取的一条边的两个顶点属于同一个集合,则该边舍弃。
3.5查找及排序
- 实现过程
对于景点查找功能,本系统采用KMP匹配算法。KMP字符串模式匹配通俗点说就是一种在一个字符串中定位另一个串的高效算法。首先求得模式串中每个字符的next[j]值;然后进行模式匹配。此算法的思想是直截了当的:将主串S中某个位置i起始的子串和模式串T相比较。即从j=0起比较S[i+j]与T[j],若相等,则在主串S中存在以i为起始位置匹配成功的可能性,继续往后比较(j逐步增1),直至与T串中最后一个字符相等为止,否则改从S串的下一个字符起重新开始进行下一轮的"匹配",即将串T向后滑动一位,即i增1,而j退回至0,重新开始新一轮的匹配,此时j=0。
KMP匹配算法的关键代码为:
int[] K = calculateK(keyword);
int i = 1, j = 1;
while(i<=doc.length() && j<=keyword.length()){
if(j==0 || newDoc.charAt(i)==newKeyword.charAt(j)){
i++;
j++;
}else{
j = K[j];
}
}
对于排序功能,由于快速排序算法时间复杂度为O(N*logN),所以本系统采用快速排序算法。算法描述如下:从数列中取出一个数作为基准数,然后开始分区过程,将比基准数大的数全放在它的右边,小于或等于它的数全放在它的左边。再对左右区间分别重复第二步,直到各区间只有一个数,算法结束。
快速排序算法的关键代码为:
public void quickSort(int[] a, int l, int r) {
if (l < r) {
int i,j,x;
i = l;
j = r;
x = a[i];
while (i < j) {
while(i < j && a[j] > x) j--; // 从右向左找第一个小于x的数
if(i < j) a[i++] = a[j];
while(i < j && a[i] < x) i++; // 从左向右找第一个大于x的数
if(i < j) a[j--] = a[i];
}
a[i] = x;
quickSort(a, l, i-1); /* 递归调用 */
quickSort(a, i+1, r); /* 递归调用 */
}
}
- 遇到的难点
最初字符串匹配算法采用的方法是朴素字符串匹配算法,即目标字符串与原字符串进行比较,如果出现不相同的字符,就将目标字符串前移到出现不同字符的位置然后重新进行比较,可是这样的算法效率太低。
- 解决方案
考虑到KMP匹配算法的时间复杂度为O(n+m),所以其效率要比朴素字符串匹配算法高效很多,最终选择使用KMP匹配算法用来搜索功能。
3.6输出车辆的进出信息
- 实现过程
为实现任务要求的车辆停放要求,本项目自定义实现了栈和队列两种数据结构。有车辆进入停车场时,首先判断停车场是否为已满;如果已经满了,就将车加入候车道中,否则就添加车到停车场中。
车辆添加关键代码为:
//判断停车场是否满了,若满了则添加车到候车道,否则直接添加
if(parking.isFull()){
shortcut.add(new Car(number, null));
}else{
parking.push(new Car(number, new Date()));
}
汽车驶出停车场时,先将目标车辆后面的车压入暂时停车场中;然后目标车辆离开,算出停留时间和应缴费用;接着将暂时停车场栈中的车重新压入到停车场栈中;最后,如果候车道中停放有车辆,就将候车道队列最前面的车移出来压到内部栈中并修改驶入时间。
车辆离开关键代码为:
//查找指定车牌号的car,将其后的car停入暂时的停车场
while(parking.peek().getNumber() == number){
tempParking.push(parking.peek());
parking.pop();
}
parking.pop();
//将暂时停车场中的车重新压入停车场栈
while(!tempParking.isEmpty()){
parking.push(tempParking.peek());
tempParking.pop();
}
//若候车道中停放有车辆,就移出最前面的车
if(!shortcut.isEmpty()){
parking.push(shortcut.front().getCar());
shortcut.pop();
}
- 遇到的难点
当汽车离开停车场时,动态显示汽车进入暂时停车场、汽车离开和候车道中的汽车进入停车场的过程。
- 解决方案
在后台算法运行的过程中,栈和队列每更新一次,就将两个栈和一个队列中的车辆信息记录一次。然后生成JSON字符串返回给网页端,网页端通过JavaScript有间隔的更新每次记录的信息,即可生成动态过程。
第四章 系统测试
4.1测试用例
- 对输出线路图的起点输入进行测试
- 输入为空
- 输入景点分布图中不存在的景点名称
- 对景点间最短路径的起始点输入进行测试
- 输入为空
- 输入景点分布图中不存在的名称
- 输入相同的起始点
- 对停车场管理的初始化、车辆到达以及车辆离开进行测试
- 车场初始化前进行车辆到达或车辆离开操作
- 初始化输入为负数或小数
- 车辆到达和车辆离开输入为空
- 车辆到达输入已存在车牌号
- 车辆离开输入不存在车牌号
4.2测试结果
- 对输出线路图起点输入的测试结果
- 弹出提示框,显示起点输入不能为空
- 弹出提示框,显示景点不存在,请重新输入
- 对景点间最短路径的起始点输入的测试结果
- 弹出提示框,显示起始点输入不能为空
- 弹出提示框,提示起点或终点名称输入错误,请重新输入
- 后台报错,抛出异常
- 对停车场管理的初始化、车辆到达以及车辆离开的测试结果
5.3 主要创新点
5.4 遇到的困难
- 弹出提示框,提示请先进行车场初始化
- 弹出提示框,提示请输入正确格式的数
- 弹出提示框,提示信息不能为空
- 弹出提示框,提示车牌号已存在,请输入其它车牌
- 弹出提示框,提示车场中无此车牌号,请重新输入
-
第五章 结论
5.1实现结果
正在上传…重新上传取消
转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消
5.2系统功能
- 创建和输出分布图
- 输出线路图和回路
- 景点间最短路径
- 道路修建图
- 景点查找和排名
- 停车场管理
- 发布公告
- 将算法部署在Tomcat中,算法运行结果在网页端显示
- 网页端显示过程均为动态展示过程
- 搜索算法使用KMP匹配算法对景点名称和景点描述进行搜索
- 输出回路时,若有多条回路,则可以全部输出
- 某些算法的实现过程,比如KMP匹配算法的计算next数组
- 路线图的绘制方法