描述
读研期间主要方向为模型的求解和启发式算法,每次使用C++调用CPLEX后的结果都需要用python再画图可视化,非常麻烦。但是Qt的界面和操作都比较简单,容易上手,就使用QT完成旅行商问题可视化的部分。对于需要使用C++调用CPLEX和想要学习Qt的同学们都可以学习一下。
环境
1.CPLEX12.9
2.Qt 5.14.2 (MSVC2017)
3.windows10
文件列表
1.mainwindow.h,mainwindow.cpp
2.tsp_cplex.h,tsp_cplex.cpp
3.main.cpp
配置环境
cplex和Qt的安装不在这里展开,可以在其它地方博客中搜索。
新建工程文件后,在.pro文件中需要加入以下路径
INCLUDEPATH +="D:/Program Files/CPLEX/concert/include"
INCLUDEPATH += "D:/Program Files/CPLEX/cplex/include"
DEFINES += IL_STD
DEFINES += _CRT_SECURE_NO_WARNINGS
QMAKE_LFLAGS_RELEASE += /OPT:REF /OPT:ICF
QMAKE_LFLAGS_DEBUG += /OPT:REF /OPT:ICF
LIBS += "D:/Program Files/CPLEX/concert/lib/x64_windows_vs2017/stat_mdd/concert.lib"
LIBS += "D:/Program Files/CPLEX/cplex/lib/x64_windows_vs2017/stat_mdd/ilocplex.lib"
LIBS += "D:/Program Files/CPLEX/cplex/lib/x64_windows_vs2017/stat_mdd/cplex1290.lib"
其中INCLUDEPATH和LIBS的路径按照电脑实际cplex安装的目录填写。
问题描述和模型
旅行商问题又叫TSP问题,是一类非常经典的运筹学问题,问题描述为是旅行家要旅行n个城市,要求各个城市经历且仅经历一次然后回到出发城市,并要求所走的路程最短。这个很多博客都讲过。具体的目标函数和约束条件都对应于代码,这里的子环约束使用的是Miller-Tucker-Zemlin模型,对于编程来说较容易,但是效率是比不过(DFJ)模型的。
- 模型如下:
最小总路程时间S,其中 i i i和 j j j指的是两个不同的城市, c i j c_{ij} cij表示两个城市间的距离或成本:
m
i
n
S
=
∑
i
=
0
n
c
i
j
∗
x
i
min \quad S=\sum_{i=0}^n c_{ij}*x_i
minS=i=0∑ncij∗xi
s
u
b
j
e
c
t
t
o
subject \quad to
subjectto
∑
i
=
0
n
x
i
=
1
,
∀
j
(
1
)
\sum_{i=0}^n x_i=1, \forall j\quad\quad\quad(1)
i=0∑nxi=1,∀j(1)
∑
j
=
0
n
x
j
=
1
,
∀
i
(
2
)
\sum_{j=0}^n x_j=1,\forall i\quad\quad\quad(2)
j=0∑nxj=1,∀i(2)
u
i
−
u
j
+
(
n
–
1
)
∗
x
i
j
<
=
n
−
2
,
∀
i
,
j
(
3
)
u_i-u_j+(n–1)*x_{ij}<=n-2,\forall i,j\quad(3)
ui−uj+(n–1)∗xij<=n−2,∀i,j(3)
其中约束(1)和(2)分别表示任何一个点只能有一条边进或出,约束3是MTZ约束,消除子环的。
代码部分
main函数
这里是Qt creator自动生成的,我也放上来吧
main.cpp
的代码如下:
#include "mainwindow.h"
#include "ilcplex/ilocplex.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
Qt界面的代码
mainwindow类的代码主要完成界面的取点画点,以及将结果绘制在图上的操作
-
mainwindow.ui
的文件是拖出来的,这里直接放图片
-
mainwindow.h
的代码如下:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QtWidgets/QMainWindow>
#include<qpainter.h>
#include<QMouseEvent>
#include<qdebug.h>
#include<qfile.h>
#include<qfiledialog.h>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
void mousePressEvent(QMouseEvent* event) override;
void paintEvent(QPaintEvent* event) override;
private:
Ui::MainWindow *ui;
bool solve_d = false;//定义没有解决
bool action = false;
QImage m_Image;
QVector<int> path;
QVector<QPoint> points;
private slots:
void on_click_solve(); //
void on_click_clear();
void on_actionopen_triggered();
};
#endif // MAINWINDOW_H
mainwindow.cpp
的代码如下:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "tsp_cplex.h"
#define M_PI 3.14159265358979323846 // pi
#include <qdebug.h>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow),solve_d(false)
{
ui->setupUi(this);
setPalette(QPalette(Qt::white));
setAutoFillBackground(true);
// resize(300, 300);
connect(ui->pushButton_2, SIGNAL(clicked()), this, SLOT(on_click_solve()));
connect(ui->pushButton, SIGNAL(clicked()), this, SLOT(on_click_clear()));
ui->label2->installEventFilter(this); //这行不能省
ui->label2->setScaledContents(true);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
points.append(event->pos());
//update();
}
update();
}
void DrawLineWithArrow(QPainter& painter, QPen pen, QPoint start, QPoint end)
{
painter.setRenderHint(QPainter::Antialiasing, true);
qreal arrowSize = 20;
painter.setPen(pen);
painter.setBrush(pen.color());
QLineF line(end, start);
double angle = std::atan2(-line.dy(), line.dx());
QPointF arrowP1 = line.p1() + QPointF(sin(angle + M_PI / 3) * arrowSize,
cos(angle + M_PI / 3) * arrowSize);
QPointF arrowP2 = line.p1() + QPointF(sin(angle + M_PI - M_PI / 3) * arrowSize,
cos(angle + M_PI - M_PI / 3) * arrowSize);
QPolygonF arrowHead;
arrowHead.clear();
arrowHead << line.p1() << arrowP1 << arrowP2;
painter.drawLine(line);
painter.drawPolygon(arrowHead);
}
void MainWindow::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.setPen(QPen(Qt::red, 10)); // 设置点的大小
for (QPoint& point : points) {
//painter.drawPoint(point);
painter.drawPoint(point);
}
if (solve_d) {
QPen Pen = QPen(QColor(18, 157, 221), 2, Qt::SolidLine);
for (auto i = path.begin(); i < path.end() - 1; i++)
DrawLineWithArrow(painter,Pen,points[*i], points[*(i+1)]);
DrawLineWithArrow(painter, Pen, points[*(path.end() - 1)], points[*path.begin()]);
}
}
void MainWindow::on_click_solve()
{
tsp_cplex solver(points);
solver.solve();
double value = solver.cost;
path = solver.minPath;
ui->lineEdit->setText(QString::number(value));
solve_d = true;
update();
}
void MainWindow::on_click_clear()
{
solve_d = false;
points.clear();
update();
}
void MainWindow::on_actionopen_triggered()
{
QString filename = QFileDialog::getOpenFileName(this, tr("Open Image"), QDir::homePath(), tr("(*.jpg)\n(*.bmp)\n(*.png)")); //打开图片文件,选择图片
QImage *myimage=new QImage();
myimage->load(filename);
myimage->scaled(ui->label2->size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);//放缩图片,以固定大小显示
m_Image=*myimage;
ui->label2->setPixmap(QPixmap::fromImage(*myimage));
}
主要模型文件的代码
test_cplex的代码主要完成对界面点的处理,以及调用cplex生成结果的过程
test_cplex.h
的代码如下:
#ifndef TSP_CPLEX_H
#define TSP_CPLEX_H
#include <ilcplex/ilocplex.h>
#include<qdebug.h>
typedef IloArray<IloBoolVarArray> BoolVarMatrix;//二维bool变量
typedef IloArray<IloIntVarArray> IntVarMatrix;//二维整型变量
typedef IloArray<IloNumVarArray> NumVarMatrix;//二维连续性变量
typedef IloArray<IloIntArray> IntMatrix;//二维整型常量
typedef IloArray<IloNumArray> NumMatrix;//二维连续常量
class tsp_cplex
{
public:
tsp_cplex(QVector<QPoint>);
void solve();
void show();
QVector<int> minPath;
double cost = 0;
private:
QVector<QPoint> m_points;
};
#endif // TSP_CPLEX_H
test_cplex.cpp
的代码如下:
#include "tsp_cplex.h"
#include<iostream>
using namespace std;
ILOSTLBEGIN
tsp_cplex::tsp_cplex(QVector<QPoint> t)
{
m_points = t;
}
void tsp_cplex::solve()
{
for (QPoint& point : m_points) {
cout << point.x() << " " << point.y() << endl;
}
IloEnv env;
IloModel model(env);
const int N_CITIES = m_points.size();
//定义二维决策变量
BoolVarMatrix x(env,N_CITIES );
IloIntVarArray u(env,N_CITIES);
for (int i = 0; i < N_CITIES; i++)
{
x[i] = IloBoolVarArray(env, N_CITIES);
for (int j = 0; j <N_CITIES; j++)
{
x[i][j] = IloBoolVar(env);
}
}
for (auto i = 1u; i < N_CITIES; ++i)
{
u[i] = IloIntVar(env,2,N_CITIES);
}
float** d = new float* [N_CITIES];
for (int k = 0; k < N_CITIES; ++k)
{
d[k] = new float[N_CITIES];
memset(d[k], 0, sizeof(float) * N_CITIES);
}
//计算客户之间的距离
for (int i = 0; i < N_CITIES; i++) {
for (int j = 0; j < N_CITIES; j++) {
if (i == j) {
d[i][j] = 0;
}
else {
d[i][j] = floor(sqrt((m_points[i].x() - m_points[j].x()) * (m_points[i].x() - m_points[j].x())
+ (m_points[i].y() - m_points[j].y()) * (m_points[i].y() - m_points[j].y())) * 10) / 10;
}
}
}
for (int i = 0; i < N_CITIES; i++) {
cout << endl;
for (int j = 0; j < N_CITIES; j++) {
cout << d[i][j]<< '\t';
}
}
IloExpr sum_xij(env);
for (int j = 0; j < N_CITIES; j++) {
sum_xij.clear();
for (int i = 0; i < N_CITIES; i++) {
sum_xij += (x[i][j]);
}
model.add(sum_xij == 1);
}
//约束2 仅有一条边出
for (int i = 0; i < N_CITIES; i++) {
sum_xij.clear();
for (int j = 0; j < N_CITIES; j++) {
sum_xij += (x[i][j]);
}
model.add(sum_xij == 1);
}
//不能返回它自己
for (int i = 0; i < N_CITIES; i++) {
model.add(x[i][i] == 0);
}
//子回路约束
for (int i = 1; i < N_CITIES; i++) {
for (int j = 1; j < N_CITIES; j++) {
if (i == j)continue;
model.add(u[i] - u[j] + (N_CITIES-1) *x[i][j] <= N_CITIES - 2);
}
}
//目标函数
IloExpr obj(env);
for (int i = 0; i < N_CITIES; i++) {
for (int j = 0; j < N_CITIES; j++) {
obj += d[i][j] * x[i][j];
}
}
model.add(IloMinimize(env, obj));
IloCplex cplex(model);
bool solved = false;
try {
// Try to solve with CPLEX (and hope it does not raise an exception!)
solved = cplex.solve();
}
catch (const IloException& e) {
std::cerr << "\n\nCPLEX Raised an exception:\n";
std::cerr << e << "\n";
env.end();
throw;
}
if (solved) {
// If CPLEX successfully solved the model, print the results
std::cout << "\n\nCplex success!\n";
std::cout << "\tStatus: " << cplex.getStatus() << "\n";
std::cout << "\tObjective value: " << cplex.getObjValue() << "\n";
}
else {
std::cerr << "\n\nCplex error!\n";
std::cerr << "\tStatus: " << cplex.getStatus() << "\n";
std::cerr << "\tSolver status: " << cplex.getCplexStatus() << "\n";
}
cost = cplex.getObjValue();
auto current_vertex = 0;
auto starting_vertex = 0;
do {
std::cout << current_vertex << "---->";
minPath.append(current_vertex);
for (auto i = 0u; i < N_CITIES; ++i) {
if (cplex.getValue(x[current_vertex][i]) > .5) {
current_vertex = i;
break;
}
}
} while (current_vertex != starting_vertex);
std::cout <<starting_vertex;
env.end();
return;
}
效果展示
可以通过直接在屏幕中取点,然后点击 计算
。得到最优路径,结果在输入框中显示。放大
,缩小
我也忘了是干嘛的了- 。-,懒得细看,清空
可以直接清空当前点
结语
代码均为刚刚毕业时候写的,写的很丑后面也没精力优化。如今已经毕业,希望自己能给有需要的同学们一些帮助。