news 2026/9/26 11:21:50

用Qt打造BP神经网络可视化工具:从C++实现到实时训练曲线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Qt打造BP神经网络可视化工具:从C++实现到实时训练曲线

简介:这是一个基于Qt框架实现的BP(反向传播)神经网络项目,面向希望在图形界面环境下动手实践神经网络的C++开发者,解决算法实现与界面交互脱节的常见问题。压缩包共6个文件,其中包含2个C++源文件与1个头文件,另附Markdown说明文档及Git属性配置,整体大小仅6KB,结构小巧却完整覆盖了从源码到说明的各个环节。已有289人学习该资源。项目代码展示了BP神经网络的关键流程,包括网络结构定义、权重初始化、前向传播、反向传播以及训练与测试过程;借助Qt的可视化组件,开发者还能设计数据加载、参数设置和误差曲线展示等交互功能。对于希望把算法封装成桌面应用、或刚接触Qt与神经网络结合的读者,这套示例提供了可直接阅读和改写的参考,便于理解算法原理并快速落地。

1. 用Qt搭BP神经网络工具:为什么值得自己写而不是用Python跑

很多人在GitHub上搜“BP-Neural-Network-master”这类包,下载下来编译完,发现核心就是一个黑框框训练循环,改了学习率、隐含层节点数,只能靠看打印出来的数字猜网络有没有在学。这个标题拆开就是两件事:QT负责把训练过程搬进窗口,BP神经网络负责真正干活。把两者合在一起,交付的东西是一个能实时看到损失曲线、能画二维决策边界、能调参后马上看效果的桌面小工具。它解决了BP调参像开盲盒的痛点:改一个参数,立刻能看到曲线从震荡变平滑,或者从下降变停滞。适合三类人:做课程设计或毕业设计的学生、想在C++里把机器学习和界面缝起来练手的开发者、以及在上位机和离线场景里需要本地训练和推理的工程师。网上BP入门代码大多是Python版,但真要落到Qt桌面工具,核心其实不超过两百行C++,值得自己写一遍。

2. BP神经网络在Qt里的落地骨架:三层网络、训练循环与界面线程怎么分

2.1 从结构图到代码:BP里最值得保留的三件事

经典BP神经网络结构图就三层:输入层、隐含层、输出层,层与层之间全连接。初学阶段不要急着往上堆Dropout、BatchNorm那些东西,先把最基础的三层吃透。这个工具要保留的核心只有三件事:前向计算、反向传播、梯度更新。

前向计算就是一层层算y = f(Wx + b),中间插入非线性激活函数。没有激活函数,两层线性变换叠起来还是线性变换,网络再深也白搭。输出去做二分类时,隐含层和输出层都用sigmoid,输出值落在0到1之间,能直接当概率看。

反向传播用的是链式法则。误差从输出层往输入层传,每一层算“局部梯度×上游梯度”,得到误差对每个权重的偏导。这些偏导就是权重应该调整的方向和步长。最后沿负梯度方向更新权重,这就是一次完整的学习。

我习惯在动手写代码之前先记住两个公式。第一个是sigmoid及其导数:

static double sigmoid(double x) { return 1.0 / (1.0 + std::exp(-x)); } static double sigmoidDerivative(double y) { return y * (1.0 - y); }

第二个是输出层和隐含层的误差传递:输出层delta等于预测减真值再乘sigmoid导数;隐含层delta等于上一层delta加权求和后再乘本层sigmoid导数。这两个公式对应到代码里就是两个循环,不复杂,但方向别搞反。

这里有一个经常翻车的点:样本输入必须先归一化到0到1附近。sigmoid在输入绝对值大于4的时候导数几乎为零,梯度消失,训练半天loss纹丝不动。很多人一上来就把原始数据直接喂进去,然后怀疑代码写错了,其实先做一次min-max归一化,问题立刻消失。

2.2 训练放工作线程、界面放主线程:一个不卡死的QThread模板

训练是长循环,动辄几千个epoch。如果直接塞进按钮的槽函数里跑,界面的损失曲线不会实时刷新,窗口拖都拖不动,表现就是“程序卡死”。Qt的界面更新必须在主线程的消息循环里执行,长时间占用主线程就等于把整个窗口冻住。

常见做法是把训练逻辑放进一个继承QObject的工作类,用moveToThread挪到子线程,通过信号槽把进度传回主线程。这样训练循环不占主线程,界面始终能响应。下面是我常用的模板:

class TrainWorker : public QObject { Q_OBJECT public: TrainWorker(const QVector<Sample> &samples, int epochs, double lr) : m_samples(samples), m_epochs(epochs), m_lr(lr) {} public slots: void run() { BPNetwork net(2, 4, 1); // 输入2维,隐含层4个节点,输出1维 std::vector<int> order(m_samples.size()); std::iota(order.begin(), order.end(), 0); for (int epoch = 0; epoch < m_epochs; ++epoch) { std::shuffle(order.begin(), order.end(), std::mt19937(epoch)); double totalLoss = 0.0; for (int idx : order) { auto out = net.forward(m_samples[idx].input); net.backward(m_samples[idx].input, m_samples[idx].target); net.update(m_lr, 0.9); totalLoss += std::pow(out[0] - m_samples[idx].target[0], 2) * 0.5; } if (epoch % 10 == 0) { emit epochFinished(epoch, totalLoss / m_samples.size()); } } emit finished(QString("训练完成")); } signals: void epochFinished(int epoch, double loss); void finished(const QString &message); };

在主窗口里这样接线:

TrainWorker *worker = new TrainWorker(samples, 2000, 0.05); QThread *thread = new QThread(this); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &TrainWorker::run); connect(worker, &TrainWorker::epochFinished, this, &MainWindow::onEpochFinished); connect(worker, &TrainWorker::finished, thread, &QThread::quit); connect(worker, &TrainWorker::finished, worker, &QObject::deleteLater); thread->start();

线程启动后,run会在子线程里执行,epochFinished信号通过队列连接自动排队到主线程,主线程的槽函数里可以放心更新图表。这里有两个关键点:第一,worker里绝对不能直接访问任何QWidget控件,否则大概率触发内存访问违例,也就是Windows上报0000005的闪退;第二,跨线程传递自定义类型时,要用qRegisterMetaType注册,否则信号里的参数传不过去,槽函数收不到数据。我最省事的做法是信号里只带int、double、QString这几种Qt内置类型,完全避开注册问题。

3. 把网络跑起来:纯C++的BP核心与前向反向传播最小实现

3.1 数据结构和权重初始化:为什么用vector不用数组

BP网络的数据结构不复杂,就是两套权重矩阵和两组偏置向量。我推荐用vector<vector >存矩阵,而不是裸的二维数组。理由不是性能,而是代码可读性以及后续序列化:训练完要把权重存成JSON,vector可以直接遍历,裸数组还得额外记录行列数。

class BPNetwork { public: int inputDim, hiddenDim, outputDim; std::vector<std::vector<double>> w1, w2; std::vector<double> b1, b2; std::vector<double> hIn, hOut, oIn, oOut; BPNetwork(int in, int hid, int out) : inputDim(in), hiddenDim(hid), outputDim(out) { std::mt19937 rng(7); std::uniform_real_distribution<double> dist(-0.5, 0.5); w1.assign(hid, std::vector<double>(in)); w2.assign(out, std::vector<double>(hid)); for (auto &row : w1) for (auto &v : row) v = dist(rng); for (auto &row : w2) for (auto &v : row) v = dist(rng); b1.assign(hid, 0.0); b2.assign(out, 0.0); } };

权重初始化不能全零,全零会导致同一层所有神经元学到完全相同的梯度,网络退化成一个神经元。均匀分布在-0.5到0.5之间足够用,正式一点可以按Xavier初始化,根据输入维度缩放。偏置初始化为0没毛病,不需要玩花样。

3.2 前向和反向:从数学公式到C++循环的对应关系

前向传播就是一个矩阵乘加激活的循环。隐含层每个节点把输入加权求和,过sigmoid;输出层再把隐含层的输出加权求和,再过一次sigmoid。中间结果全部缓存下来,反向传播要用:

std::vector<double> forward(const std::vector<double> &x) { hIn.assign(hiddenDim, 0.0); hOut.assign(hiddenDim, 0.0); for (int j = 0; j < hiddenDim; ++j) { double sum = b1[j]; for (int i = 0; i < inputDim; ++i) { sum += w1[j][i] * x[i]; } hIn[j] = sum; hOut[j] = sigmoid(sum); } oIn.assign(outputDim, 0.0); oOut.assign(outputDim, 0.0); for (int j = 0; j < outputDim; ++j) { double sum = b2[j]; for (int i = 0; i < hiddenDim; ++i) { sum += w2[j][i] * hOut[i]; } oIn[j] = sum; oOut[j] = sigmoid(sum); } return oOut; }

反向传播的代码要对着公式写,每一行都能找到对应关系。输出层delta等于预测减真值乘sigmoid导数;隐含层delta等于输出层delta通过转置权重传回来再乘本层sigmoid导数:

void backward(const std::vector<double> &x, const std::vector<double> &target, std::vector<std::vector<double>> &gradW1, std::vector<std::vector<double>> &gradW2, std::vector<double> &gradB1, std::vector<double> &gradB2) { // 输出层 delta: (out - target) * sigmoid'(oIn) std::vector<double> deltaOut(outputDim); for (int j = 0; j < outputDim; ++j) { deltaOut[j] = (oOut[j] - target[j]) * sigmoidDerivative(oOut[j]); } // 隐含层 delta: W2^T * deltaOut * sigmoid'(hIn) std::vector<double> deltaHid(hiddenDim); for (int i = 0; i < hiddenDim; ++i) { double sum = 0.0; for (int j = 0; j < outputDim; ++j) { sum += w2[j][i] * deltaOut[j]; } deltaHid[i] = sum * sigmoidDerivative(hOut[i]); } // 梯度 = 上一层输出 * 本层delta for (int j = 0; j < outputDim; ++j) { gradB2[j] += deltaOut[j]; for (int i = 0; i < hiddenDim; ++i) { gradW2[j][i] += deltaOut[j] * hOut[i]; } } for (int i = 0; i < hiddenDim; ++i) { gradB1[i] += deltaHid[i]; for (int k = 0; k < inputDim; ++k) { gradW1[i][k] += deltaHid[i] * x[k]; } } }

注意梯度累加的顺序:w2的梯度用到的是hOut,不是hIn;w1的梯度用到的是输入x,不是hOut。很多实现把这两行写反,损失下降一小段就停住,表现成“训练到一半不动了”。遇到这种情况先检查梯度矩阵里是不是有异常的大值,输出一下gradW2的均值就能定位。

3.3 训练循环、学习率、动量:参数怎么调

训练循环的逻辑很简单:前向算输出,反向算梯度,更新权重。单样本更新也叫随机梯度下降,每来一个样本就更新一次权重;也可以攒一个batch再求平均梯度。Qt小工具里样本量不大,逐样本更新最省事,收敛也快。样本要先打乱顺序,避免数据集自带顺序把网络带偏。更新时加一个动量项,让梯度方向不会来回震荡:

void update(double lr, double momentum, const std::vector<std::vector<double>> &gradW1, const std::vector<std::vector<double>> &gradW2, const std::vector<double> &gradB1, const std::vector<double> &gradB2, std::vector<std::vector<double>> &prevW1, std::vector<std::vector<double>> &prevW2, std::vector<double> &prevB1, std::vector<double> &prevB2) { for (int j = 0; j < outputDim; ++j) { double du = lr * gradB2[j] + momentum * prevB2[j]; b2[j] -= du; prevB2[j] = du; for (int i = 0; i < hiddenDim; ++i) { du = lr * gradW2[j][i] + momentum * prevW2[j][i]; w2[j][i] -= du; prevW2[j][i] = du; } } for (int i = 0; i < hiddenDim; ++i) { double du = lr * gradB1[i] + momentum * prevB1[i]; b1[i] -= du; prevB1[i] = du; for (int k = 0; k < inputDim; ++k) { du = lr * gradW1[i][k] + momentum * prevW1[i][k]; w1[i][k] -= du; prevW1[i][k] = du; } } }

动量系数一般取0.8到0.95。它相当于是给梯度方向做了个惯性滤波,能明显减少loss曲线的锯齿。下面这张参数表是我调这类小网络的经验值,直接抄问题不大:

参数经验范围效果
学习率 lr0.01 ~ 0.3太大震荡,太小收敛慢
隐含层节点数输入维度的2~3倍太少欠拟合,太多过拟合
动量 momentum0.8 ~ 0.95抑制震荡,加速收敛
epoch1000起,看曲线尾段loss不再下降就停

判断过拟合有个直观标准:训练集loss一直降,验证集loss降一段后反弹,就是学过头了。Qt工具里最好把训练集和验证集分开两个曲线画在同一张图上,比单看训练loss有用得多。

4. 在Qt里画训练过程:QChart损失曲线与二维决策边界可视化

4.1 QChart画损失曲线:三行代码挂进窗口

Qt画曲线最省事的方式是用QChart模块。先在工程文件里打开模块:qmake的.pro里加一行,CMake的CMakeLists.txt里加对应依赖:

# qmake 工程文件 QT += core gui charts greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

用CMake的话:

find_package(Qt5 REQUIRED COMPONENTS Widgets Charts) target_link_libraries(myapp Qt5::Widgets Qt5::Charts)

工程配置好后,在窗口里创建一个QSplineSeries挂到QChart上。QChartView放在布局里,setRubberBand开启鼠标框选缩放,这个就是Qt里图表缩放最常用的交互方式:

#include <QtCharts/QChartView> #include <QtCharts/QSplineSeries> QSplineSeries *lossSeries = new QSplineSeries(); lossSeries->setName("训练损失"); QChart *chart = new QChart(); chart->addSeries(lossSeries); chart->createDefaultAxes(); chart->axisX()->setRange(0, 2000); chart->axisY()->setRange(0, 1.0); QChartView *chartView = new QChartView(chart); chartView->setRubberBand(QChartView::RectangleRubberBand); chartView->setRenderHint(QPainter::Antialiasing); ui->chartLayout->addWidget(chartView);

训练线程每N个epoch发一次信号,主线程槽函数里往序列里追加点:

void MainWindow::onEpochFinished(int epoch, double loss) { lossSeries->append(epoch, loss); chart->axisY()->setRange(0, qMin(1.0, loss >= 0.9 ? 1.0 : loss * 2.5)); }

这里有一个新手必踩的坑:QChart在.pro里直接写QT += charts,很多教程用的Qt版本默认没有编译完全,运行时报“unknown module(s) in qt: charts”。我习惯装Qt时把Qt Charts勾上,别只装基础组件。如果项目不想依赖Charts,自己用QPainter画折线也就几十行代码,但缩放、坐标轴这些要自己处理,得不偿失。

4.2 决策边界:把网络的“想法”画成一张图

损失曲线只能告诉你模型在收敛,不能告诉你模型到底学到了什么。对于二维输入的二分类问题,最直观的验证方式是把整个特征空间跑一遍网络,按输出概率着色,画成一张决策边界图。样本点叠加在上面,真实分类用红蓝两色标出来,偏差一目了然。

void MainWindow::redrawBoundary() { const int w = ui->boundaryLabel->width(); const int h = ui->boundaryLabel->height(); QImage img(w, h, QImage::Format_RGB32); double xMin = m_featureMin.x(), xMax = m_featureMax.x(); double yMin = m_featureMin.y(), yMax = m_featureMax.y(); for (int px = 0; px < w; ++px) { for (int py = 0; py < h; ++py) { double fx = xMin + (xMax - xMin) * px / w; double fy = yMin + (yMax - yMin) * py / h; auto pred = m_net.forward({fx, fy}); img.setPixelColor(px, py, pred[0] > 0.5 ? QColor(255, 200, 200) : QColor(190, 210, 255)); } } for (const auto &s : m_samples) { int px = (s.input[0] - xMin) / (xMax - xMin) * w; int py = (s.input[1] - yMin) / (yMax - yMin) * h; img.setPixelColor(px, py, s.target[0] > 0.5 ? Qt::red : Qt::blue); } ui->boundaryLabel->setPixmap(QPixmap::fromImage(img)); }

图像分辨率取决于label控件的像素尺寸,一般300乘300就有9万个点。每个点做一次forward,也就是两层矩阵乘法,重绘一次大概几十毫秒。别把这个函数放进鼠标移动事件里持续触发,那样会很卡。我一般只让它响应“训练完成”和“参数已修改”这两个动作,或者给按钮加个点击事件手动刷新。

这套可视化对分类问题帮助最大。样本不是线性可分时,能清楚看见网络是用一条弯折的曲线硬分,还是真的学到了分布。如果边界图上的分界线很乱、像噪声一样,多半是隐含层节点太多了,减少节点数通常能立刻改善。

5. QT+BP工程跑不起来和跑飞了:环境、插件、崩溃的5个避坑记录

5.1 qpa plugin找不到:移植到板子和无显示器环境必现

现象:程序在开发机上正常,拷到树莓派或ARM板子上运行,启动时报“qt.qpa.plugin: could not find the qt platform plugin linuxfb”,然后直接退出。

原因:Qt的插件机制按平台加载,运行时需要找到platforms目录下的libqlinuxfb.so或libqxcb.so。交叉编译出来的程序,默认插件路径还在宿主机上,或者打包时只拷了可执行文件没拷plugins目录。

解决:程序启动早期用qputenv强制指定插件路径,最直接:

qputenv("QT_QPA_PLATFORM_PLUGIN_PATH", "/opt/qt/plugins/platforms");

更省心的办法是发布时把plugins/platforms整个目录拷到可执行文件旁边,运行时用相对路径。如果只是本地开发调试,也可以直接指定平台插件为linuxfb或offscreen,但会牺牲窗口显示能力。

5.2 cannot mix incompatible qt library:同一台机器混了两套Qt

现象:编译报“fatal: cannot mix incompatible qt library (version ex50601) with this library”,或者运行时同样报错退出。

原因:编译链接用的Qt版本和运行时加载的Qt版本不一致。最常见的是PATH环境变量里同时存在多个Qt版本,或者系统里同时装了Qt 5.6和Qt 5.15,qmake搜索头文件用的是A版本,链接器找到的是B版本。

解决:先用qmake -query QT_VERSION确认当前命令对应的版本,再检查LD_LIBRARY_PATH有没有混旧路径。Windows下优先在Qt Creator的Kit设置里统一套件,发布用windeployqt把配套dll全部拷过去。我通常只保留一个Qt版本在PATH里,其余全部去掉,省得这种玄学问题反复出现。

5.3 unknown module in qt: charts:安装Qt时只装了基础组件

现象:qmake执行时报“:-1: error: unknown module(s) in qt: charts”,CMake构建时find_package找不到Qt5Charts。

原因:安装Qt时没有勾选Charts模块,或者用了精简安装包。另一个常见变体是把webenginewidgets写进.pro但没装WebEngine,报错逻辑一模一样。

解决:重新打开Qt安装器,勾选“Qt Charts”组件补装。如果项目只是画折线,不依赖交互缩放,也可以把图表改成QPainter自绘,去掉charts依赖,这样在没装Charts的机器上也能直接编译。

5.4 线程里碰UI导致0000005闪退:跨线程访问控件是最大元凶

现象:训练过程中或训练刚结束,程序在Windows上报“0000005访问违例”闪退,看起来毫无规律,时好时坏。

原因:工作线程里直接调了UI控件的方法,或者信号槽跨线程传了未注册的自定义类型,导致槽函数收到的参数是空数据,解引用时崩溃。还有一个隐蔽触发点是用delete直接释放跨线程对象,而不是用deleteLater。

解决:工作线程里只有emit一条路能通知主线程,UI更新全部交给主线程槽函数。跨线程参数如果用了QVector、QMap这些,先调用qRegisterMetaType注册,或者干脆用QString传JSON。对象释放统一走deleteLater,不要手动delete跨线程对象。

5.5 训练越跑越卡:每轮全量刷新图表是性能黑洞

现象:训练刚开始流畅,跑到几百个epoch后界面明显卡顿,拖动窗口都费劲,风扇狂转。

原因:每完成一个epoch就把点append进QSplineSeries,几千个点之后,QChart每次重绘都要重新计算整个序列的贝塞尔曲线路径,复杂度随点数上升。

解决:不要每个epoch都刷新,每10个或者每50个epoch追加一个点;对已有的点用replace而不是反复append;实在需要高频率更新,就定期把序列清空重建。训练曲线是给人看的趋势图,不是示波器,精度没有意义。

6. 权重存盘与界面联动:把训练好的BP变成能反复用的工具

训练完成后把权重存成JSON,是让这个工具从“能看”变成“能用”的关键一步。Qt自带QJsonDocument,序列化权重矩阵很方便:

void saveWeights(const BPNetwork &net, const QString &path) { QJsonObject root; root["input_dim"] = net.inputDim; root["hidden_dim"] = net.hiddenDim; root["output_dim"] = net.outputDim; QJsonArray w1Arr; for (const auto &row : net.w1) { QJsonArray rowArr; for (double v : row) rowArr.append(v); w1Arr.append(rowArr); } root["w1"] = w1Arr; QJsonArray w2Arr; for (const auto &row : net.w2) { QJsonArray rowArr; for (double v : row) rowArr.append(v); w2Arr.append(rowArr); } root["w2"] = w2Arr; QFile f(path); if (f.open(QIODevice::WriteOnly)) { f.write(QJsonDocument(root).toJson()); } }

加载就是反向操作,逐层解析JSON填回权重矩阵。存盘之后,界面上的“预测”按钮才有意义:用户选择一个新样本,程序调用forward,把输出概率显示出来。我做这个功能时习惯把最近10次预测结果也画在决策边界图上,这样能看到模型在场景边界上的稳定性。

验证模型是否真的能用,我一般做三个动作:第一,训练集和验证集loss曲线并排看,尾段还在抖就说明学习率偏大;第二,换随机种子重新跑一遍同一组参数,对比两次决策边界差异,差异大说明隐含层节点太多,模型不稳定;第三,把保存好的权重重新加载,对训练时没见过的样本做一次预测回归测试,确认序列化没有把权重格式写错。这三个动作都通过,这个BP小工具才算真正闭环。

我现在的习惯是训练完不急着关窗口,先看损失曲线尾段是不是还在抖,再切到验证集跑一遍,最后把权重存成JSON重新加载做预测回归。这一套流程走下来花的不是时间,是以后少踩坑的后悔药。希望这个思路对你有用,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 11:21:46

学生成绩管理系统源码包落地指南:从环境配置到答辩加分

简介&#xff1a;面向高校计算机相关专业学生与开发者的完整毕业设计资源包&#xff0c;围绕学生成绩管理系统的设计与实现&#xff0c;提供论文与可运行源码&#xff0c;可满足课程设计、毕设参考及二次开发需求。系统基于ASP.NET Web Forms开发&#xff0c;后端采用C#&#x…

作者头像 李华
网站建设 2026/9/26 11:16:29

Ubuntu TigerVNC是否支持全屏模式

功能与版本支持TigerVNC 在 Ubuntu 上支持全屏模式&#xff0c;客户端&#xff08;vncviewer&#xff09;提供“进入全屏”的菜单项&#xff0c;默认按 F8 呼出菜单即可切换&#xff1b;同时支持多显示器全屏&#xff08;跨屏铺满&#xff09;等选项&#xff0c;适用于常见的桌…

作者头像 李华
网站建设 2026/9/26 11:16:20

整点定时闹钟skill(考试复习适用)

摘要&#xff1a;本文分享作者如何用一个小工具解决"提醒自己休息"的难题。作者先复盘了手机闹钟、冥想 App、便利贴等传统提醒方式的失败原因&#xff0c;指出真正好用的提醒需要满足零门槛、就近、会变三个条件。随后介绍了自己开发的"整点灵感闹钟"——…

作者头像 李华
网站建设 2026/9/26 11:15:47

DeepSeek LeetCode 109. 有序链表转换二叉搜索树 Scala实现

LeetCode 109. 有序链表转换二叉搜索树 — Scala 实现 思路&#xff1a;转数组 递归构建 链表有序&#xff0c;BST 中序遍历也有序。最直接的做法&#xff1a; 把链表值收集到 Array[Int]。递归取中点作为根&#xff0c;左右子区间分别构建左右子树&#xff0c;自然得到平衡 B…

作者头像 李华
网站建设 2026/9/26 11:14:55

算法(62):Bellford algorithm的证明,negative weights-18.5

Q&#xff1a;对于每个v&#xff0c;都由相应的处理。对于每个v&#xff0c;都计算过邻边。这个绿色循环&#xff0c;每次都计算了整个图像。但是&#xff0c;这还不够&#xff0c;因为要包含负权重&#xff0c;所以可能出现被计算过的distTo变小的情况&#xff0c;因此还有一个…

作者头像 李华