基于C++和Opencv的简单赛事管理系统(非 GUI 实现)

赛事管理系统

项目解析

赛事管理系统是为了协助中国大学生计算机设计大赛江苏省组委会而设计的,旨在实现赛务相关的数据管理和信息服务。该系统能够管理各参赛队的基本信息,包括参赛队编号、参赛作品名称、参赛学校、赛事类别、参赛者和指导老师等。此外,系统还提供了增加、删除和修改参赛队伍信息的功能。赛事相关数据以文本文件的形式存储,不同的数据项使用"#"进行分隔。

整个系统的代码框架采用了C++语言实现,其中使用了二叉排序树作为数据结构来支持参赛队伍的查找功能。系统通过读取名为"team.txt"的文本文件来加载参赛队伍的基本信息,并将其构建成二叉排序树以支持快速的查找操作。通过输入参赛队编号,系统能够根据二叉排序树进行查找,并输出该赛事类别对应的基本信息。如果查找失败,则输出"查找失败!"的提示。

除了查找功能外,系统还提供了按参赛学校和赛事类别查询参赛队伍的功能。用户可以根据提示输入参赛学校名称或赛事类别,系统会根据输入的信息进行查找,并按照赛事类别进行排序后输出相应的参赛队伍的基本信息。排序算法可以从选择排序、插入排序、希尔排序、归并排序和堆排序中选择,并且需要对选择的算法进行相应的说明。

此外,赛事管理系统还具备决赛叫号系统校园导游程序两个功能。决赛叫号系统用于模拟省赛现场的情况,参赛队根据赛事组织文件中的赛事类别分配到不同的决赛室,决赛室按顺序进行叫号,被叫号的参赛队伍可以进场比赛。校园导游程序为参赛者提供赛地的导航服务,包括校园地图中目标地的查询和任意两个目标地之间最短路径的查询。

综上所述,赛事管理系统通过提供参赛队伍信息的管理和查询功能,以及决赛叫号系统和校园导游程序等实用功能,为中国大学生计算机设计大赛江苏省组委会提供了一种高效、便捷的赛事管理解决方案。

系统分模块设计并解决

该系统分为四大模块进行设计,分别为:

  1. 数据结构模块
  2. 文件读取与写入模块
  3. 队伍管理模块
  4. 其他功能模块

系统的代码框架采用了C++语言实现,使用了二叉排序树作为数据结构,以支持参赛队伍的查找功能。系统结构合理且易于维护,通过交互界面与用户进行信息交互,并通过文件读写操作实现数据的持久化存储。同时,系统还提供了丰富的功能选项,满足了赛事管理的各项需求。

数据结构模块

首先使用结构体struct Team定义了队伍的数据结构,包括队伍编号、项目名称、学校、参赛类别、参赛成员和教练等信息。

//参赛队伍结构体
struct Team {
    string teamId;
    string projectName;
    string school;
    string category;
    string participants;
    string coach;
};

还需要定义一个二叉排序树的结点结构体struct TreeNode,其中包含一个队伍对象和指向左右子节点的指针。

//二叉排序树的结点结构体
struct TreeNode {
    Team team;
    TreeNode* left;
    TreeNode* right;
};

为了方便实现使用平衡二叉树查找队伍信息,可以先定义一个SearchResult结构体:

struct SearchResult {
    TreeNode* node;
    int length;
};

在决赛叫号系统这个模块中,初步考虑使用队列这种数据结构来实现,可以使用C++标准库函数中的队列,也可以自己写,这里为了练手,给出了自定义的队列数据结构Queue,实现代码如下:

//队列结点
struct QueueNode {
    Team team;
    QueueNode* next;
};

//将队列封装成起来方便调用
class Queue {
private:
    QueueNode* front;
    QueueNode* rear;
public:
    Queue() : front(nullptr), rear(nullptr) {}
    ~Queue() {
        while (!isEmpty()) {
            dequeue();
        }
    }
	//入队
    void enqueue(const Team& team) {
        QueueNode* newNode = new QueueNode;
        newNode->team = team;
        newNode->next = nullptr;

        if (isEmpty()) {
            front = rear = newNode;
        }
        else {
            rear->next = newNode;
            rear = newNode;
        }
    }
    // 出队
    void dequeue() {
        if (isEmpty()) {
            return;
        }

        QueueNode* temp = front;
        front = front->next;
        delete temp;

        if (front == nullptr) {
            rear = nullptr;
        }
    }
	// 获取队首元素
    Team frontElement() const {
        if (isEmpty()) {
            throw logic_error("队列为空");
        }

        return front->team;
    }
	// 判断队列是否为空
    bool isEmpty() const {
        return front == nullptr;
    }
};

文件读取与写入模块

1.文件读取与解析。

大致思路:定义一个loadTeamsFromFile()函数,以实现从文件中加载队伍数据,将数据存储到全局的teams 向量和二叉排序树 root 中。

具体实现过程如下:
打开文件:首先通过ifstream创建一个文件对象file,并打开名为team.txt的文件。如果无法打开文件,将输出一条错误信息,并返回函数。

清空数据:在开始读取文件之前,清空之前存储的团队数据和二叉排序树。teams.clear()会清空teams向量,root = nullptr会将根节点指针root设置为空。

逐行读取文件:使用getline函数从文件中逐行读取数据,每次读取的数据存储在字符串变量line中。

解析数据:针对每一行数据,使用stringstream将字符串line转换为数据流ss。然后使用getline和分隔符’#'将数据流ss中的每个项提取出来,存储在字符串变量item中,并将其添加到items向量中。

处理团队数据:如果items向量的大小为6,说明成功提取了6个数据项,代表一条完整的团队数据。将这些数据分别存储在team对象的相应属性中。

更新数据结构:将当前的team对象添加到teams向量中,用于存储所有团队数据。同时,调用insertTreeNode函数将当前的team对象插入到二叉排序树root中。

关闭文件:处理完文件中的所有数据后,关闭文件。

具体函数实现如下:

- trim()函数的定义:

//trim()函数,用于清除数据项之间的空格
string trim(const std::string& str) {
    size_t start = str.find_first_not_of(" \t\n\r");
    size_t end = str.find_last_not_of(" \t\n\r");
    if (start == std::string::npos || end == std::string::npos) {
        return "";  // 字符串中只包含空白字符
    }
    return str.substr(start, end - start + 1);
}

- 二叉树插入函数:

//二叉排序树的插入
void insertTreeNode(TreeNode*& node, const Team& team) {
    if (node == nullptr) {
        node = new TreeNode;
        node->team = team;
        node->left = nullptr;
        node->right = nullptr;
        return;
    }

    if (team.teamId < node->team.teamId) {
        insertTreeNode(node->left, team);
    }
    else {
        insertTreeNode(node->right, team);
    }
}

- 文件加载函数:

//加载txt文件数据并将其存入二叉排序树
void loadTeamsFromFile() {
    ifstream file("team.txt");
    if (!file) {
        cout << "无法打开数据文件 team.txt!" << endl;
        return;
    }

    teams.clear();
    root = nullptr; // 清空二叉排序树

    string line;
    while (getline(file, line)) {
        stringstream ss(line);
        Team team;
        string item;
        vector<string> items;

        while (getline(ss, item, '#')) {
            item = trim(item);
            items.push_back(item);
        }
        if (items.size() == 6) {
            team.teamId = items[0];
            team.projectName = items[1];
            team.school = items[2];
            team.category = items[3];
            team.participants = items[4];
            team.coach = items[5];

            teams.push_back(team);
            insertTreeNode(root, team); // 将当前 Team 对象插入二叉排序树中
        }
    }
    file.close();
}

2. 文件的写入
首先打开一个名为 “team.txt” 的输出文件流对象 file。
检查文件是否成功打开。如果文件无法打开,即文件创建失败,则输出错误消息并返回。
然后对于参赛队伍容器 teams 中的每个队伍,执行以下操作:
将队伍的各项信息按照指定的格式写入文件。信息之间使用 # 分隔,每个队伍的信息写入一行。
最后关闭文件流。看起来是一个挺简单的过程,代码如下:

//保存参赛队伍信息到文件
void saveTeamsToFile() {
    ofstream file("team.txt");
    if (!file) {
        cout << "无法创建数据文件 team.txt!" << endl;
        return;
    }

    for (const auto& team : teams) {
        file << team.teamId << "#" << team.projectName << "#" << team.school << "#"
            << team.category << "#" << team.participants << "#" << team.coach << endl;
    }

    file.close();
}

队伍管理模块

我们可以先实现简单的 增、删、改 这三个基础功能。

1. 添加参赛队伍
这个功能只需要将输入的参赛队伍信息添加到容器里即可。

//参赛队伍的添加
void addTeam() {
    Team team;
    cout << "请输入参赛队编号:";
    cin >> team.teamId;
    cout << "请输入参赛作品名称:";
    cin >> team.projectName;
    cout << "请输入参赛学校:";
    cin >> team.school;
    cout << "请输入赛事类别:";
    cin >> team.category;
    cout << "请输入参赛者:";
    cin >> team.participants;
    cout << "请输入指导老师:";
    cin >> team.coach;

    teams.push_back(team);
    cout << "参赛队伍添加成功!" << endl;
}

2. 删除参赛队伍
这个功能也十分地简单,只需要将要删除的队伍参赛编号与系统中的参赛队伍编号遍历比较,然后将找到的参赛队伍删除即可。

//参赛队伍的删除
void deleteTeam() {
    string teamId;
    cout << "请输入要删除的参赛队编号:";
    cin >> teamId;

    auto it = teams.begin();
    while (it != teams.end()) {
        if (it->teamId == teamId) {
            it = teams.erase(it);
            cout << "参赛队伍删除成功!" << endl;
            return;
        }
        else {
            ++it;
        }
    }

    cout << "未找到要删除的参赛队伍!" << endl;
}

3. 修改参赛队伍
修改参赛队伍信息也是一样的逻辑,只需要将需要修改的队伍的编号与文件中的参赛队伍编号遍历比较,然后将匹配到的参赛队伍信息进行修改即可。

//参赛队伍的修改
void modifyTeam() {
    string teamId;
    cout << "请输入要修改的参赛队编号:";
    cin >> teamId;

    auto it = teams.begin();
    while (it != teams.end()) {
        if (it->teamId == teamId) {
            cout << "请输入新的参赛作品名称:";
            cin >> it->projectName;
            cout << "请输入新的参赛学校:";
            cin >> it->school;
            cout << "请输入新的赛事类别:";
            cin >> it->category;
            cout << "请输入新的参赛者:";
            cin >> it->participants;
            cout << "请输入新的指导老师:";
            cin >> it->coach;

            cout << "参赛队伍修改成功!" << endl;
            return;
        }
        else {
            ++it;
        }
    }

    cout << "未找到要修改的参赛队伍!" << endl;
}

4. 查找参赛队伍
这部分需要实现两种查找方式,第一种是根据输入的参赛队伍编号进行查找;第二种是根据输入的参赛队伍所属学校进行查找,并将查找结果按参赛队编号进行输出。

- 按参赛队伍编号查找

由于在解析参赛队伍文件信息的时候顺便将各参赛队伍放入平衡二叉树中,所以在这里可以直接使用平衡二叉树来进行查找。
在写平衡二叉树查找的函数前,先定义一个结构体用来充当返回值:

struct SearchResult {
    TreeNode* node;// 指向查找到的结点
    int length;// 查找路径的长度
};

接下来平衡二叉树的查找函数searchTreeNodeById采用递归的方式编写。
首先判断当前节点是否为空或者当前节点的参赛队伍编号与目标队伍编号相等,如果满足其中一个条件,则表示找到了目标节点,返回该节点和查找的长度。
如果目标队伍编号小于当前节点的参赛队伍编号,则递归地在当前节点的左子树中进行查找,传入左子节点作为新的当前节点,并将查找长度加1。
如果目标队伍编号大于等于当前节点的参赛队伍编号,则递归地在当前节点的右子树中进行查找,传入右子节点作为新的当前节点,并将查找长度加1。
通过不断递归地在左右子树中进行查找,直到找到目标节点或者遇到空节点为止。
如果未找到目标节点,返回一个空的SearchResult对象表示未找到。
代码实现如下:

//返回利用二叉排序树查找的结果
SearchResult searchTreeNodeById(TreeNode* node, const string& teamId, int length = 1) {
    SearchResult result;

    if (node == nullptr || node->team.teamId == teamId) {
        result.node = node;
        result.length = length;
        return result;
    }

    if (teamId < node->team.teamId) {
        return searchTreeNodeById(node->left, teamId, length + 1);//递归查找左子树
    }
    else {
        return searchTreeNodeById(node->right, teamId, length + 1);//递归查找右子树
    }
}

接下来需要计算平均查找长度ASL。
二叉树的平均查找长度计算方法为:ASL = 二叉树中所有节点的查找长度之和 / 二叉树中的节点总数。
所以我们需要计算两个部分,首先是二叉树中的节点总数:
定义一个函数countNodes(),首先判断当前结点是否为空,如果当前节点为空,则返回 0;否则,通过递归地计算左子树和右子树的节点数,并将当前节点加上左右子树的节点数,得到总的节点数。

int countNodes(TreeNode* node) {
    if (node == nullptr) {
        return 0;  // 如果节点为空,返回 0
    }

    int leftNodes = countNodes(node->left);  // 左子树的节点数(递归计算)
    int rightNodes = countNodes(node->right);  // 右子树的节点数(递归计算)

    return 1 + leftNodes + rightNodes;  // 当前节点 + 左子树节点数 + 右子树节点数
}

接下来是计算二叉树中所有节点的查找长度之和。
定义一个函数calculateASL用于计算二叉树中所有节点的查找长度之和,首先判断当前结点是否为空,如果当前节点为空,则返回 0;否则,通过递归地计算左子树和右子树的 ASL,并将当前节点的查找长度加上左右子树的 ASL,得到所有节点的查找长度之和。

int calculateASL(TreeNode* node, int length = 1) {
    if (node == nullptr) {
        return 0;  // 如果节点为空,返回 0
    }

    int leftASL = calculateASL(node->left, length + 1);  // 左子树的 ASL(递归计算)
    int rightASL = calculateASL(node->right, length + 1);  // 右子树的 ASL(递归计算)

    return length + leftASL + rightASL;  // 当前节点的查找长度 + 左子树的 ASL + 右子树的 ASL
}

最后将二者结合即可计算出平均查找长度ASL:

double calculateAverageSearchLength(TreeNode* rootNode) {
    int totalNodes = countNodes(rootNode);  // 计算总的节点数
    int asl = calculateASL(rootNode);  // 计算所有节点的查找长度之和
    double averageSL = static_cast<double>(asl) / totalNodes;  // 计算平均查找长度
    return averageSL;
}

最后将以上函数在按参赛队伍编号查找函数searchTeamById()中调用即可实现按照参赛队伍编号查找并输出平均查找长度ASL的功能。

void searchTeamById() {
    string teamId;
    cout << "请输入要查询的参赛队编号:";
    cin >> teamId;

    SearchResult searchResult = searchTreeNodeById(root, teamId);
    TreeNode* result = searchResult.node;
    double asl = calculateAverageSearchLength(root);

    if (result != nullptr) {
        Team& team = result->team;
        cout << "参赛队编号:" << team.teamId << endl;
        cout << "参赛作品名称:" << team.projectName << endl;
        cout << "参赛学校:" << team.school << endl;
        cout << "赛事类别:" << team.category << endl;
        cout << "参赛者:" << team.participants << endl;
        cout << "指导老师:" << team.coach << endl;

        cout << "平均查找长度 ASL:" <<"各结点权值/总结点数 = "<< asl << endl;
    }
    else {
        cout << "未找到该参赛队伍!" << endl;
    }
}
  • 按参赛队伍所属学校查找
    查找到所需的参赛队伍信息是没玩啥问题的,只需要遍历匹配即可;然后是将查找到的参赛队伍信息遍历输出,这里选择使用归并排序,首先归并排序的时间复杂度为O(nlogn),其中n是参赛团队的数量,这个时间复杂度非常高效。此外,归并排序是一种稳定的排序算法,即相等的元素在排序后的位置顺序与它们在原始数组中的位置顺序相同,这也符合要求。

在归并排序中,我们将其划分为两个部分:合并两个已排序的子数组 和 递归划分数组并进行排序。

在合并子数组的函数merge()中,首先计算左右子数组的长度(n1和n2),然后创建临时的左右子数组leftArrrightArr。接下来,使用循环将原始数组中的元素分别复制到左右子数组中。
然后,使用三个指针(i、j和k)来遍历左右子数组,并比较元素大小,将较小的元素放入原始数组teams中。循环结束后,可能会有剩余的元素,因此需要将剩余元素依次放入原始数组中。

在递归划分数组并进行排序的函数中,首先判断左右边界是否满足继续划分的条件,如果满足则计算中间索引。然后,使用递归方式分别对左右子数组进行排序,再调用merge函数将排序好的左右子数组合并为一个有序数组。

下面是按参赛队伍所属学校查找的总实现:

//合并两个已排序的子数组
void merge(vector<Team>& teams, int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;

    vector<Team> leftArr(n1);
    vector<Team> rightArr(n2);

    for (int i = 0; i < n1; i++) {
        leftArr[i] = teams[left + i];
    }
    for (int j = 0; j < n2; j++) {
        rightArr[j] = teams[mid + 1 + j];
    }

    int i = 0;
    int j = 0;
    int k = left;

    while (i < n1 && j < n2) {
        if (leftArr[i].teamId <= rightArr[j].teamId) {
            teams[k] = leftArr[i];
            i++;
        }
        else {
            teams[k] = rightArr[j];
            j++;
        }
        k++;
    }

    while (i < n1) {
        teams[k] = leftArr[i];
        i++;
        k++;
    }

    while (j < n2) {
        teams[k] = rightArr[j];
        j++;
        k++;
    }
}

//归并排序的主要部分,递归将给定的数组划分为较小的子数组
void mergeSort(vector<Team>& teams, int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;

        mergeSort(teams, left, mid);
        mergeSort(teams, mid + 1, right);

        merge(teams, left, mid, right);
    }
}

//根据学校查找参赛队伍主函数
void searchTeamsBySchool() {
    string school;
    cout << "请输入要查询的参赛学校名称:";
    cin.ignore();
    getline(cin, school);

    vector<Team> teamsBySchool;
    for (const auto& team : teams) {
        if (team.school == school) {
            teamsBySchool.push_back(team);//将符合条件的参赛队伍加入到teamsBySchool中
        }
    }

    if (!teamsBySchool.empty()) {
        mergeSort(teamsBySchool, 0, teamsBySchool.size() - 1);//调用归并函数对参赛队伍信息进行排序

        cout << "参赛学校为" << school << "的参赛团队信息:" << endl;
        for (const auto& team : teamsBySchool) {
            cout << "参赛队编号:" << team.teamId << endl;
            cout << "参赛作品名称:" << team.projectName << endl;
            cout << "参赛学校:" << team.school << endl;
            cout << "赛事类别:" << team.category << endl;
            cout << "参赛者:" << team.participants << endl;
            cout << "指导老师:" << team.coach << endl;
            cout << endl;
        }
    }
    else {
        cout << "未找到该参赛学校的参赛团队!" << endl;
    }
}

其他功能模块

callFinalRooms(): 决赛叫号系统,实现决赛的叫号逻辑。
这里首先要明确以下具体要实现的功能,我们需要的是一个决赛叫号系统使其能模拟决赛比赛过程中的队伍叫号、进场和比赛时间的安排,以及展示参赛队伍的情况。
简要实现功能如下:

  1. 将参赛队伍按照赛事类别分配到不同的决赛室。
  2. 模拟比赛过程中的叫号情况,按照顺序展示参赛队进场情况。
  3. 为每个队伍随机生成比赛时间。
  4. 每轮叫号后,移除已经没有队伍的决赛室。
  5. 展示指定轮数的叫号情况。

那么具体实现代码如下:

// 决赛叫号系统
void callFinalRooms() {
    map<string, queue<Team>> finalRooms;  // 决赛室,使用队列保存队伍

    // 将参赛队按赛事类别分配到决赛室
    for (const auto& team : teams) {
        finalRooms[team.category].push(team);
    }

    // 随机生成比赛时间
    srand(time(nullptr));

    // 按顺序叫号,展示参赛队进场情况
    int count = 0;
    while (!finalRooms.empty()) {
        cout << "================== 决赛叫号系统 ==================" << endl;
        cout << "当前叫号队伍:" << endl;

        int outputCount = 0;
        for (auto& room : finalRooms) {
            if (!room.second.empty()) {
                const Team& team = room.second.front();
                cout << "参赛队编号:" << team.teamId << ",赛事类别:" << team.category << ",项目名称:" << team.projectName << endl;

                room.second.pop();
                ++outputCount;

                // 随机生成比赛时间,将队伍重新插入队列中
                if (room.second.empty()) {
                    Team nextTeam = team;
                    int randomTime = rand() % 61;  // 生成0-60之间的随机数
                    nextTeam.teamId += "_Next";
                    nextTeam.projectName += "_Next";
                    nextTeam.category += "_Next";
                    nextTeam.school += "_Next";
                    nextTeam.participants += "_Next";
                    nextTeam.coach += "_Next";
                    room.second.push(nextTeam);

                    cout << "比赛时间:" << randomTime << "分钟" << endl;
                }

                if (outputCount == 20) {
                    break;
                }
            }
        }

        cout << "=================================================" << endl;

        // 移除已经没有队伍的决赛室
        auto it = finalRooms.begin();
        while (it != finalRooms.end()) {
            if (it->second.empty()) {
                it = finalRooms.erase(it);
            }
            else {
                ++it;
            }
        }

        ++count;
        if (count == 5) {
            break;  // 只展示5轮叫号情况
        }

        cout << "按任意键继续下一轮叫号..." << endl;
        cin.ignore();
        cin.get();
    }
}

以上代码的分析:

  1. 创建一个映射容器finalRooms来保存队伍的决赛室,按赛事类别进行分类。
  2. 遍历参赛队伍列表,将每个队伍按赛事类别放入对应的决赛室队列中。
  3. 初始化随机数生成器。
  4. 进入主循环,每轮循环代表一次叫号过程。
  5. 在每轮循环开始时,输出标题提示。
  6. 遍历每个决赛室,输出队首队伍的信息,并将其从队列中移除。记录输出的队伍数。
  7. 如果当前决赛室的队列为空,表示该赛事类别的队伍已经全部叫号完毕。
  8. 为模拟比赛时间,从队首队伍生成下一轮的队伍信息,加入队列中。
  9. 生成一个0到60之间的随机数表示比赛时间。
  10. 如果输出的队伍数达到20个,跳出内部循环。
  11. 遍历每个决赛室,检查队列是否为空。若为空,表示该赛事类别的队伍已经全部叫号完毕,将其从finalRooms中移除。
  12. 增加计数器count,表示已进行的轮数。若count等于5,跳出外部循环,只展示5轮叫号情况。
  13. 输出提示信息,等待用户按下任意键继续下一轮叫号。
  14. 继续下一轮循环,直到所有决赛室的队伍都叫号完毕或达到5轮叫号。
  15. 决赛叫号过程结束。

navigateCampus(): 校园导游程序,实现校园导游的逻辑。

这个功能可以简化为无向图中寻找最短路径,寻找最短路径在这里选择使用 Dijkstra算法

首先,需要定义了一个用于存储校园地点和坐标的结构体CampusLocation,以及一个用于比较距离的自定义比较函数CompareDistance,该函数用于优先队列的最小堆。

// 定义校园地点及其坐标的结构体
struct CampusLocation {
    Point coordinates;
    string description;
};
// 自定义比较函数,用于优先队列的最小堆
struct CompareDistance {
    bool operator()(const pair<string, int>& a, const pair<string, int>& b) {
        return a.second > b.second;
    }
};

然后,定义一个campus_map变量,用于存储校园地点和其坐标的映射关系,以及connectivity变量,用于存储地点之间的连接关系及权值。

map<string, CampusLocation> campus_map = {...}

map<pair<string, string>, int> connectivity = {...}

在实现 Dijkstra算法的主体时,我们可以采用一个 lambda函数,这样可以在这个 navigateCampus()函数中定义并调用。
该lambda函数定义如下:

auto navigate = [&](const string& start, const string& end) {...}

navigate lambda 函数中,首先创建了一个距离字典distance,用于存储每个地点到起点的最短距离。初始时,所有地点的距离被设置为无穷大,起点的距离被设置为0。
然后,创建了一个优先队列pq,用于选择下一个距离最小的节点进行处理。优先队列的比较函数为CompareDistance,可以确保队列中的节点按照距离的升序排列。
再创建了一个父节点字典parent,用于记录每个节点在最短路径中的父节点。
在进入主循环时,当优先队列不为空时,取出队列中距离最小的节点。对于与当前节点相连的每个节点,如果经过当前节点到达该节点的距离比之前计算的距离小,则更新距离,并将该节点加入优先队列,同时更新该节点的父节点为当前节点。
最后,从终点开始回溯,获取最短路径,并将其保存在path中。
返回包含最短路径距离、起点坐标、终点坐标和路径的元组。
相关navigate lambda 函数代码如下:

// 导航功能
    auto navigate = [&](const string& start, const string& end) {
        // 创建距离字典,初始距离设为无穷大
        map<string, int> distance;
        for (const auto& place : campus_map) {
            distance[place.first] = INT_MAX;
        }
        distance[start] = 0;

        // 创建优先队列,用于选择最小距离的下一个节点
        priority_queue<pair<string, int>, vector<pair<string, int>>, CompareDistance> pq;
        pq.push({ start, 0 });

        // 创建父节点字典,用于记录最短路径
        map<string, string> parent;
        parent[start] = "";

        while (!pq.empty()) {
            string current = pq.top().first;// 当前距离最小的节点
            int curr_distance = pq.top().second;// 当前距离最小的节点的距离
            pq.pop();// 从优先队列中弹出当前距离最小的节点

            if (current == end) {
                break;  // 到达目标地点,停止搜索
            }

            for (const auto& connection : connectivity) {
                string src = connection.first.first;
                string dest = connection.first.second;
                int weight = connection.second;

                if (src == current && curr_distance + weight < distance[dest]) {
                    distance[dest] = curr_distance + weight;
                    parent[dest] = src;
                    pq.push({ dest, distance[dest] });
                }

                if (dest == current && curr_distance + weight < distance[src]) {
                    distance[src] = curr_distance + weight;
                    parent[src] = dest;
                    pq.push({ src, distance[src] });
                }
            }
        }

        // 从终点回溯获取最短路径
        int shortest_distance = distance[end];
        Point start_coord = campus_map[start].coordinates;
        Point end_coord = campus_map[end].coordinates;
        string current = end;
        vector<string> path;
        while (current != start) {
            path.push_back(current);
            current = parent[current];
        }
        path.push_back(start);
        reverse(path.begin(), path.end());

        return make_tuple(shortest_distance, start_coord, end_coord, path);
    };

要想清晰地输出查找到地路径结果,我们可以尝试使用 Opencv库 将结果绘制在图片上展示。

关于 Opencv库
的配置,可以参考这篇文章http://t.csdn.cn/pI3O7

关于图片上地点坐标的获取方法,需要额外用到Opencv库中的其他函数,这里不进行阐述,相关代码将放在本文最后的附录处,需要的可以直接取用。

要想在图片上绘制路线,首先需要读取图片,用到Opencv库中的这个函数:

Mat image = imread("campus_map.jpg");

然后就是根据使用 Dijkstra算法 得到的最短路径的地点绘制出一条简易的路线,需要用到Opencv库中的 line()函数:

line(image, start, end, Scalar(0, 255, 0), 3);

最后,使用namedWindow()函数创建一个显示窗口;并使用 imshow()函数显示带有导航路线的地图; waitKey()函数来等待用户按下任意按键后关闭窗口;并使用 imwrite()函数将带有导航路线的地图保存为一张新的图片文件。

namedWindow("Campus Map with Route", WINDOW_NORMAL);
imshow("Campus Map with Route", image);
waitKey(0);
imwrite("campus_map_with_route.jpg", image);

有关校园导航程序的相关代码如下:

// 校园导游程序

void navigateCampus() {
    // 校园地图和地点之间的连接关系及权值
    map<string, CampusLocation> campus_map = {
        {"后勤服务楼", {Point(79, 206), "后勤服务楼"}},
        {"西门", {Point(403, 179), "西门"}},
        {"大学生活动中心", {Point(211, 354), "大学生活动中心"}},
        {"明德楼", {Point(403, 263), "明德楼"}},
        {"校训碑", {Point(302, 480), "校训碑"}},
        {"文理大楼", {Point(563, 264), "文理大楼"}},
        {"计算机学院楼", {Point(652, 214), "计算机学院楼"}},
        {"图书馆", {Point(622, 387), "图书馆"}},
        {"东门", {Point(736, 532), "东门"}},
        {"行政大楼", {Point(991, 168), "行政大楼"}}
    };

    map<pair<string, string>, int> connectivity = {
        {{"后勤服务楼", "西门"}, 500},
        {{"后勤服务楼", "大学生活动中心"}, 300},
        {{"大学生活动中心", "明德楼"}, 200},
        {{"西门", "明德楼"}, 100},
        {{"大学生活动中心", "校训碑"}, 250},
        {{"明德楼", "文理大楼"}, 100},
        {{"校训碑", "图书馆"}, 500},
        {{"文理大楼", "图书馆"}, 200},
        {{"文理大楼", "计算机学院楼"}, 100},
        {{"图书馆", "东门"}, 250},
        {{"计算机学院楼", "行政大楼"}, 500}
    };

    // 导航功能
    auto navigate = [&](const string& start, const string& end) {
        // 创建距离字典,初始距离设为无穷大
        map<string, int> distance;
        for (const auto& place : campus_map) {
            distance[place.first] = INT_MAX;
        }
        distance[start] = 0;

        // 创建优先队列,用于选择最小距离的下一个节点
        priority_queue<pair<string, int>, vector<pair<string, int>>, CompareDistance> pq;
        pq.push({ start, 0 });

        // 创建父节点字典,用于记录最短路径
        map<string, string> parent;
        parent[start] = "";

        while (!pq.empty()) {
            string current = pq.top().first;
            int curr_distance = pq.top().second;
            pq.pop();

            if (current == end) {
                break;  // 到达目标地点,停止搜索
            }

            for (const auto& connection : connectivity) {
                string src = connection.first.first;
                string dest = connection.first.second;
                int weight = connection.second;

                if (src == current && curr_distance + weight < distance[dest]) {
                    distance[dest] = curr_distance + weight;//更新距离
                    parent[dest] = src;//记录父节点
                    pq.push({ dest, distance[dest] });//将起点和终点互换,因为是无向图
                }

                if (dest == current && curr_distance + weight < distance[src]) {
                    distance[src] = curr_distance + weight;//更新距离
                    parent[src] = dest;//记录父节点
                    pq.push({ src, distance[src] });//将起点和终点互换,因为是无向图
                }
            }
        }

        // 从终点回溯获取最短路径
        int shortest_distance = distance[end];
        Point start_coord = campus_map[start].coordinates;//起点坐标
        Point end_coord = campus_map[end].coordinates;//终点坐标
        string current = end;
        vector<string> path;
        while (current != start) {
            path.push_back(current);
            current = parent[current];
        }
        path.push_back(start);
        reverse(path.begin(), path.end());

        return make_tuple(shortest_distance, start_coord, end_coord, path);
    };

    // 目标地导航
    cout << "欢迎来到校园导游程序!" << endl;
    cout << "以下是校园地图中的一些目标地:" << endl;
    for (const auto& place : campus_map) {
        cout << place.second.description << endl;
    }

    // 任意两个目标地导航查询
    while (true) {
        string start_place, end_place;
        cout << "请输入起点建筑物名称:";
        cin >> start_place;
        cout << "请输入终点建筑物名称:";
        cin >> end_place;

        if (campus_map.count(start_place) && campus_map.count(end_place)) {
            int distance;
            Point start_coord, end_coord;
            vector<string> path;
            tie(distance, start_coord, end_coord, path) = navigate(start_place, end_place);
            cout << "从 " << start_place << " 到 " << end_place << " 的最短路径距离为: " << distance << endl;

            // 输出最短路径
            cout << "最短路径为:";
            for (const auto& place : path) {
                cout << place << "  ->  ";
            }
            cout << endl;

            // 读取现有校园地图图片
            Mat image = imread("campus_map.jpg");

            // 在图片上绘制导航路线
            for (size_t i = 1; i < path.size(); i++) {
                Point start = campus_map[path[i - 1]].coordinates;
                Point end = campus_map[path[i]].coordinates;
                line(image, start, end, Scalar(0, 255, 0), 3);
            }

            // 显示图片
            namedWindow("Campus Map with Route", WINDOW_NORMAL);
            imshow("Campus Map with Route", image);
            waitKey(0);

            // 保存修改后的图片
            imwrite("campus_map_with_route.jpg", image);

            cout << "导航路线已在图片上显示,并保存为 campus_map_with_route.jpg" << endl;
            break;
        }
        else {
            cout << "输入的建筑物名称有误,请重新输入!" << endl;
        }
    }
}

主函数实现用户交互模块
程序入口,加载队伍数据,通过循环菜单与用户进行交互,根据用户选择调用相应的函数。

int main() {
    loadTeamsFromFile();

    while (true) {
        cout << "================== 赛事管理系统 ==================" << endl;
        cout << "1. 添加参赛队伍" << endl;
        cout << "2. 删除参赛队伍" << endl;
        cout << "3. 修改参赛队伍" << endl;
        cout << "4. 按参赛队编号查询参赛队伍" << endl;
        cout << "5. 按参赛学校查询参赛队伍" << endl;
        cout << "6. 决赛叫号系统" << endl;
        cout << "7. 校园导游系统" << endl;
        cout << "0. 退出系统" << endl;
        cout << "=================================================" << endl;
        cout << "请输入操作选项:";

        int option;
        cin >> option;

        switch (option) {
        case 1:
            addTeam();
            saveTeamsToFile();
            break;
        case 2:
            deleteTeam();
            saveTeamsToFile();
            break;
        case 3:
            modifyTeam();
            saveTeamsToFile();
            break;
        case 4:
            searchTeamById();
            break;
        case 5:
            searchTeamsBySchool();
            break;
        case 6:
            callFinalRooms();
            break;
        case 7:
            navigateCampus();
			break;
        case 0:
            cout << "感谢使用赛事管理系统!再见!" << endl;
            return 0;
        default:
            cout << "无效的操作选项!请重新输入。" << endl;
            break;
        }
        cout << endl;
    }
}

附录

完整代码:[https://github.com/chrishacc/Event_management_system]
(学业繁忙,git出了点问题无法上传,还未解决,请耐心等待,若有需要可私信)

附录1
图片地点坐标获取相关代码

#include <opencv2/opencv.hpp>

cv::Point selectedPoint;

void onMouse(int event, int x, int y, int flags, void* userdata) {
    if (event == cv::EVENT_LBUTTONDOWN) {
        selectedPoint = cv::Point(x, y);
        std::cout << "Selected Point: (" << x << ", " << y << ")" << std::endl;
    }
}

int main() {
    cv::Mat image = cv::imread("campus_map.jpg");  // 替换为您自己的图像路径

    cv::namedWindow("Image");
    cv::setMouseCallback("Image", onMouse);

    while (true) {
        cv::imshow("Image", image);

        int key = cv::waitKey(1);
        if (key == 27)  // 按下Esc键退出
            break;
    }

    cv::destroyAllWindows();
    return 0;
}

附录2
校园导航程序实现示例
校园导航程序示例图片
附录3
相关文件示例
待解析文件示例

感谢阅读

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值