1 棋盘最大涂色区域
1.1 需求规格说明
有一个 n*m 的棋盘,每个格子涂有不同颜色。需要找到其中同一颜色面积最大的连续区域(按照四连通标准),并求出其面积。
【输入格式】
第一行 2 个正整数 n,m,描述棋盘尺寸。
接下来 n 行描述这个棋盘,每行 m 个字符,每个字符为 W(白)/G(绿)/R(红)/B(蓝),表示对应格子的颜色。
【基本要求】
1.Chess_data.txt为用于测试的样例数据,要求输出被涂色面积最大的连续区域,并求该最大面积。
2.可基于图形界面编程实现,读入棋盘文件,表达棋盘和颜色,并显示计算结果。要求算法代码和界面代码分离。
1.2 总体分析与设计
(1)设计思想
在棋盘最大涂色区域的数据结构设置中,我首先定义了一个棋盘颜色映射(colorMap)并使用 QMap<QString, QBrush> 类型存储,它将字符串(代表颜色代码)映射到 QBrush 对象(代表画刷),用于在图形场景中绘制不同颜色的格子;由于输入数据是字符型,故这里我将棋盘数据(boardData)使用 QStringList 类型存储,其中每个 QString 代表棋盘的一行,包含该行所有格子的颜色代码。
棋盘行列数(rows 和 cols)分别使用 int 类型来存储棋盘的行数和列数,这两个变量定义了棋盘的尺寸;棋盘图形项(items)使用 std::vector<std::vector<QGraphicsRectItem*>> 类型存储,这是一个二维数组,每个元素指向一个 QGraphicsRectItem 对象,代表棋盘上的一个格子。
使用标准模板库(STL)中的vector可以让我们更方便地使用各种算法和操作,例如排序、查找、遍历等。通过使用引用,我可以直接在原始的棋盘上进行操作,而不需要复制整个棋盘,这既提高了效率(特别是对于大棋盘),也允许多个函数或对象共享和操作同一个棋盘。
在此基础上,程序采用了面向对象的设计思想,在棋盘计算类ChessBoardScene用于计算棋盘上同一颜色面积最大的连续区域,其中设计容器用于存储所有存放同一颜色的连续网格区域的队列,二维动态数组vector<vector>&chessboard存储初始棋盘网格颜色,maxAreaColor存储求得最大连续区域后棋盘颜色。vectormaxAreaPoints存储最大连续区域的位置坐标数组,用于可视化结果显示。
②主要算法思想
本题可以使用两种方法解决,即深度优先搜索(DFS)和广度优先搜索(BFS)。它们是图和树遍历中的两种重要算法,虽然它们有相似的目的,但在策略、应用场景和内存消耗等方面却有很大的不同。
深度优先搜索(DFS)是一种通过递归或显式栈的方式实现的算法。DFS采用的是后入先出(LIFO)的策略,通过递归或栈的方式进行搜索。从起始节点开始,DFS会沿着某一分支深入探索,直到达到一个叶子节点,然后回溯到上一个节点,继续探索下一个分支,直到所有节点都被访问过。这种搜索策略的优势在于它会尽可能深入到图或树的最底层,然后回溯到上一层,依次遍历所有可能路径。它可以更快速地找到所有可能的解,或者在树的结构中寻找特定的元素。由于DFS的递归特性,它在内存使用上相对较低。
广度优先搜索(BFS)采用的是先入先出(FIFO)的策略,通过队列的方式进行搜索。从起始节点开始,BFS会首先访问所有的邻居节点,然后再访问这些节点的邻居,以此类推,直到所有的节点都被访问过。由于BFS总是先访问距离起始节点最近的节点,因此它在寻找最短路径或最近节点时非常有效。但是,BFS由于需要存储所有的邻居节点,它在内存使用上可能比DFS更高。
综合考虑到内存开销和搜索时间,在本题中,我采用了DFS来解决问题。可以将棋盘看作是一个无向图,每个棋子是一个节点,如果两个棋子相邻(通常是上下左右,四连通区域),且颜色相同,那么这两个棋子就是联通的。我们的目标是找到棋盘上最大的连通区域,也就是最大的相同颜色的棋子集合。DFS从一个节点(也就是一个棋子)开始,首先访问相同节点并一直访问下去(也就是所有同色的棋子),然后再访问这些节点的邻居,以此类推,直到所有的节点都被访问过。在这个过程中,我们需要使用一个队列来存储待访问的节点,每次从队列中取出一个节点,并将其所有未访问过的同色节点添加到队列中。在这个问题中,我们需要对棋盘上的每个节点(棋子)都进行一次DFS。每次DFS都会找到一个连通区域,我们需要记录下这个区域的大小(也就是包含的棋子的数量)。在所有的DFS结束后,最大的连通区域就是我们要找的答案。
1.求最大连通区域在ChessBoardScene类中,通过遍历整个棋盘,对于每个非白色的点,进行DFS搜索,计算以该点为起点的颜色区域的面积。在搜索的同时,记录所有区域的角点,以便后续的操作。然后,更新最大面积的信息。最后,将最大区域的颜色信息还原到原始棋盘。
2.四连通求最大面积遍历所有网格,上下左右为该网格的连通方向,递归遍历四个方向的网格,直到遇到不同颜色的网格,停止递归,全部递归结束,即求得一个相同颜色的连续区域。
(2)设计表示
在本程序的实现中,我主要涉及了两个类,其中ChessBoardScene类主要继承自QGraphicsScene,其主要实现了将导入的数据在界面场景中的可视化,并且完好实现了后续搜索出结果的完整高亮可视化效果;而Question_1类主要实现的则是包括数据的读取和存储以及实现DFS算法,具体程序UML类图展示如图1.2-1所示。
图1.2-1 程序UML类图
(3)详细设计表示
程序的运行流程图如图1.2-2所示。
图1.2-2 棋盘最大涂色流程图
其主要步骤如下:
1.初始化:构造函数负责初始化棋盘数据、行数、列数等成员变量,并设置初始的最大面积信息,包括面积值、颜色。同时,创建drawBoard以备棋盘场景创建之需。
2.读取数据:通过on_selectButton_clicked响应函数打开文件对话框,选择棋盘文件,并调用on_showButton_clicked函数读取其中的棋盘数据,自动绘制读取到的棋盘数据绘制在界面上。
3.计算连通块:通过两重循环遍历棋盘的每个格子,访问后标记为白色。若某个格子未被访问过,则调用DFS算法计算以该点为起点的颜色区域的面积。每次计算得到一个连通分量的面积后,更新最大面积信息。
4.绘制结果棋盘:利用drawBoard函数,保持棋盘原本的颜色,而突出显示最大涂色面积区域边界,以突出最大涂色面积。并将最大面积值、最大涂色区域颜色显示在界面上。
1.3 编码
【问题1】:调试过程中发现函数存在越界问题
【解决方法】:n为棋盘行数,m为棋盘列数。在每个方向的连通的判断条件中加上行列号的限制。右方向行号大于等于0且小于n,列号大于0且列号+1小于m;左方向行号大于等于0且小于n,列号-1大于0且列号小于m;下方向行号大于等于0且i+1小于n,列号大于0且列号小于m;上方向行号-1大于等于0且行号小于n,列号大于0且列号小于m。
【问题2】:如何更好地可视化最大连通区域计算结果?
【解决方法】:在绘制棋盘drawBoard函数中,引入状态标志(boolcalculatorFinish),用于判断是否进行了连通区域计算。绘制时根据状态标志选择使用原始棋盘数据还是计算结果。更新绘制逻辑函数,如果状态标志为true,绘制计算结果,为最大区域绘制边界;如果为false,则绘制原有颜色块。在计算完成后刷新绘图,确保结果能即时反映在界面上,使用户体验到更直观的结果展示。
1.4 程序及算法分析
①使用说明
本题的使用方法比较简单,运行程序后即可看到相应的初始化界面,如图1.4-1所示。
图1.4-1 程序启动初始化界面
点击“加载数据”即可加载相应的棋盘txt数据,如图1.4-2所示。
图1.4-2 加载棋盘数据
将相应的棋盘数据打开后,程序会调用函数将棋盘可视化到界面的GraphicsView中,同时会在右侧的LineEdit中生成相应的文件路径,这里的单个棋盘实例都是借助QGraphicsRectItem生成的,如图1.4-3所示。
图1.4-3 同步可视化棋盘
点击“着色判断”即可启用DFS算法选取出相应的最大着色区域,同时会显示最大着色区域的颜色和面积,如图1.4-4所示。
图1.4-4 着色区域判断
此外,为了可视化效果,笔者这里还设计了一个判断结果的可视化展示,当点击“展示结果”时,还会将场景中的最大涂色区域给可视化显示出来,如图1.4-5所示。
图1.4-5 可视化结果展示
为了展示程序的执行效果,这里也可以对测试数据略加修改,再次尝试一次,提供源数据(右图)和修改后数据(左图)如图1.4-6所示。
图1.4-6 两次输入数据集对比
这里,继续按照测试的步骤进行,依次点击“加载数据”-“着色判断”-“展示结果”,即可以得到相应的结果样式,显然,可以发现,最大着色区域和其相应面积也被正确的判断出来。如图1.4-7所示。
图1.4-7 修改输入数据集后结果
1.5 小结
本题选择使用DFS算法完成要求,同时为更好地实现结果的呈现,本题实现了以下优化:①在DFS过程中,可能存在重复计算相同的区域,可以通过记录已访问的点,避免重复计算。使用一个布尔数组来标记已访问的位置,避免对同一位置进行多次DFS。②使用固定大小的队列和动态数组来避免频繁的内存分配和释放,完成对内存分配的优化。③图形绘制优化,原来的想法是查找完面积最大区域后,直接修改初始化的棋盘,在原棋盘上显示面积最大区域,但是这样没有前后对比,无法通过可视化人工检验结果正确性。于是我绘制了两个棋盘以及区域边界,通过对比显示的方式更清晰地展现结果。
本算法的时间复杂度为O(N×M×K),其中N和M分别为棋盘的行数和列数,K为颜色数目。考虑到可能存在大规模数据和多种颜色的情况,需要进一步优化算法。而且空间复杂度受DFS过程中使用的队列的影响较大,为O(N×M),对于大规模数据,需要考虑进一步的空间优化。可以尝试使用并行计算加以改进,例如使用std::thread来实现,考虑将DFS算法的计算过程进行多线程化。
1.6 附录
在附录里主要包括核心算法:
//实现最大连通区域求解算法
void Question_1::on_loadButton_clicked()
{
QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), "", tr("Text Files (*.txt)")); // 弹出文件选择窗口
if (!fileName.isEmpty()) {
loadBoardFromFile(fileName); // 加载文件
ui.FileLine->setText(fileName); // 显示文件路径
}
}
void Question_1::loadBoardFromFile(const QString& fileName)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return;
QTextStream in(&file);
QStringList boardData;
// 读取棋盘尺寸
QString line = in.readLine();
QStringList sizeInfo = line.split(' ', QString::SkipEmptyParts);
if (sizeInfo.size() != 2)
return; // 尺寸信息不完整
int rows = sizeInfo[0].toInt();
int cols = sizeInfo[1].toInt();
// 读取棋盘数据
for (int i = 0; i < rows; ++i) {
line = in.readLine();
if (line.length() != cols) // 确保每行长度与列数匹配
return;
boardData.append(line);
}
file.close();
scene->setBoardData(boardData, rows, cols);
}
void Question_1::on_selectButton_clicked()
{
findMaxArea(); // 找到最大连续区域并更新TextFile
}
void Question_1::on_showButton_clicked()
{
highlightMaxArea(); // 高亮显示最大连续区域
}
void Question_1::findMaxArea()
{
QStringList boardData = scene->getBoardData(); // 从scene中获取棋盘数据
int rows = scene->getRows();
int cols = scene->getCols();
if (boardData.isEmpty() || rows <= 0 || cols <= 0) {
QMessageBox::warning(this, tr("Warning"), tr("Please load the board data first."));
return;
}
int maxArea = 0;
QChar maxColor; // 记录最大区域的颜色
QPoint maxAreaPosition(-1, -1);
std::vector<std::vector<bool>> visited(rows, std::vector<bool>(cols, false)); // 创建二维visited数组
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (!visited[i][j]) {
QChar color = boardData[i].at(j); // 获取颜色字符
int area = dfs(boardData, visited, i, j, color);
if (area > maxArea) {
maxArea = area;
maxColor = color; // 更新最大区域的颜色
maxAreaPosition = QPoint(i, j);
}
}
}
}
// 显示最大连续区域的面积和颜色
ui.TextFile->setText(tr("Max Area: %1\nColor: %2").arg(maxArea).arg(maxColor));
}
void Question_1::highlightMaxArea()
{
QStringList boardData = scene->getBoardData();
if (boardData.isEmpty()) {
QMessageBox::warning(this, tr("Warning"), tr("Please load the board data first."));
return;
}
int rows = scene->getRows();
int cols = scene->getCols();
int maxArea = 0;
QPoint maxAreaPosition(-1, -1);
// 全局 visited 数组,用于存储最大区域
std::vector<std::vector<bool>> globalVisited(rows, std::vector<bool>(cols, false));
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (!globalVisited[i][j]) {
// 局部 visited 数组,仅用于当前 DFS
std::vector<std::vector<bool>> localVisited(rows, std::vector<bool>(cols, false));
QChar color = boardData[i].at(j); // 获取当前格子的颜色
int area = dfs(boardData, localVisited, i, j, color);
if (area > maxArea) {
maxArea = area;
maxAreaPosition = QPoint(i, j);
// 更新全局 visited 数组为当前最大区域
globalVisited = localVisited;
}
}
}
}
// 将最大区域高亮显示
scene->highlightArea(maxAreaPosition, globalVisited, maxArea);
}
int Question_1::dfs(const QStringList& boardData, std::vector<std::vector<bool>>& visited, int x, int y, const QString& color)
{
if (x < 0 || x >= boardData.size() || y < 0 || y >= boardData[x].size() || visited[x][y] || boardData[x].at(y) != color) {
return 0;
}
visited[x][y] = true; // 标记当前格子为已访问
int area = 1; // 当前格子作为连续区域的一部分
// 递归地检查四个方向的相邻格子
area += dfs(boardData, visited, x + 1, y, color); // 下
area += dfs(boardData, visited, x - 1, y, color); // 上
area += dfs(boardData, visited, x, y + 1, color); // 右
area += dfs(boardData, visited, x, y - 1, color); // 左
return area;
}