Runfeng的万事屋

随便写写啦,反正没人看

工具与工具版本

使用power point 2019+或office 365,不要使用WPS以避免与office混用的格式错误。

使用2019+版本的原因是部分功能(例如,平滑切换)在此版本加入,另外使用旧版本的Office(可能)有潜在的兼容性问题。

制作原则

  • 最小化外部依赖: 能在 ppt 内部实现的 不要用外部软件(mathtype公式,简单画图,简单调色等)
  • 尽可能复用:复用对象,避免新建对象导致字体、对齐不一致等格式问题 ,一次实现,多次复制使用
  • 简单实现:能通过简单方法实现的效果,不要使用复杂方法

命名与文件版本

PPT命名规则为 项目名_版本号_月份(英大写)_日期_修改人_修改内容概述

例如:新朔_0.0.1_APR_18_YRF_新建文档

其中,版本号使用语义化版本(Semantic Versioning)

  1. 主版本号:当你做了不兼容的修改(例如,换模板等颠覆性操作,与别人修改的版本再也无法兼容)
  2. 次版本号:当你做了向下兼容的功能性新增(例如,新增某个章节)
  3. 修订号:当你做了向下兼容的问题修正(例如,修改了错别字)

不要在任何时刻定义final版本,避免出现final-final-final版。

应当有唯一一人负责版本合并,作为主分支

日志

每页ppt应该有从新建、修改、删除的详细日志

谁在什么时候做了什么修改,放在ppt图框外面的一个文本框内

2025.01.01 YRF 新建此页

2025.01.02 YRF 修改公式的动画

2025.01.05 YRF 新增切换样式

2025.01.05 SJY 因演讲超时弃用此页

删除

任何制作出来的页,在弃用时不应删除(不知何时会再次启用),应放置到所有页的末尾,并在日志中标明弃用及原因

字体

尽量不要将无衬线字体和有衬线字体混合排版,常用字体:

  • 无衬线字体
    • 微软雅黑
    • Arial
    • 黑体
    • 等线
  • 有衬线字体
    • 宋体
    • Times new Roman

插图

ppt插图尽量高清,低分辨率图片可用AI方法放大修复,例如 upscayl或bigjpg 等。

配色

ppt配色应围绕主题色进行修改,建议建立取色卡。

尽量不要使用亮度、饱和度过高的红色、绿色、黄色等。

不建议同一页面使用过多颜色搭配,建议使用同色系颜色进行搭配。

动画

动画应简洁、美观、丝滑、快速(不宜过长等待)。动画应当辅助核心内容,而不是为了制作动画而制作。

文件管理

保留可编辑的素材、源图、公式等,避免二次制作。

风格

扁平风格不要与写实或立体风格混用

背景

背景可以为白色,也可添加背景色,但不要使用带纹理的背景。

注意:若背景不为白色,则不要使用白色块(原因:丑,像补丁)

公式

公式尽量使用ppt自带公式编辑器,不要插入图片,不用使用mathtype(可能无法编辑)

表格

表格要简洁,小数的小数点对齐且保留相同位数,整数统一右对齐

地图

使用地图一定要小心敏感地区:台湾、藏南、香港是我国领土的一部分。

在线协作

微软office 365与onedrive目前有在线协作的功能,可作为主分支进行合并以及最后的共同校验。

杂项

善用锁定和组合,善用内部文字左右居中对齐、对象排列

考虑去除句尾的句号

以往都是用Python调用Gurobi进行数学规划建模,最近开始涉及C的编程。由于不想使用C和python联合编程(也许是C调用一个Python解释器?总之,没这样去做),所以直接用C调用Gurobi了。
由于我用的是mac,因此VScode + Cmake是最佳的组合,本文主要记录的是Cmake文件应该如何编写。(Windows系统请不要参考这个教程,不能保证正确性)

首先,这是Gurobi官方的链接:
https://support.gurobi.com/hc/en-us/articles/360039499751-How-do-I-use-CMake-to-build-Gurobi-C-C-projects

按照官方的链接,在Gurobi正确安装的情况下, mip1_c++.cpp亲测可以运行。如果发现,上述的教程不能通过Cmake编译,且原因是找不到Gurobi,那大概率是Gurobi的环境变量设置的有问题了,需要修改环境变量到指定的路径,
这里放上macos的.zshrc设置。

1
2
export GUROBI_LICENSE_FILE=/Users/yurunfeng/gurobi.lic
export GUROBI_HOME=/Library/gurobi1103/macos_universal2

配置好环境变量,编译应该就没问题了。

在自己的项目中,把FindGUROBI.cmake文件直接放在工程目录中, CmakeLists.txt 需要自己更改FindGUROBI.cmake也可根据需要进行更改,
比如以下这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# CmakeLists.txt 
# 这只是一个例子,如果不知道是什么意思的话,不要直接抄,肯定是运行不了的
cmake_minimum_required(VERSION 3.29)
cmake_policy(SET CMP0167 NEW)
enable_language(CXX)

# set(CMAKE_OSX_ARCHITECTURES "arm64")
# set(GUROBI_INCLUDE_DIRS "/Library/gurobi1103/macos_universal2/include")
# set(GUROBI_CXX_LIBRARY "/Library/gurobi1103/macos_universal2/lib/libgurobi_c++.a")
set(CMAKE_PREFIX_PATH "/Users/yurunfeng/vcpkg/installed/arm64-osx/share")
set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_EXPORT_COMPILE_COMMANDS 1)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CXXFLAGS -D_GNU_SOURCE True)

project(FWPSP)

list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})

find_package(nlohmann_json CONFIG REQUIRED) # json
find_package(Boost REQUIRED COMPONENTS program_options stacktrace_basic stacktrace_noop system)
find_package(Boost REQUIRED COMPONENTS log log_setup thread filesystem system date_time regex)
find_package(GUROBI REQUIRED)

include_directories(${GUROBI_INCLUDE_DIRS})
include_directories(${Boost_INCLUDE_DIRS})

set(sources
src/main.cpp
src/alg_config.cpp
src/log_config.cpp
src/ip_model.cpp
src/data_loader.cpp
src/cbs_node.cpp
src/cbs.cpp
src/node_manager.cpp
)

add_executable(${CMAKE_PROJECT_NAME} ${sources})

target_include_directories(FWPSP PRIVATE ${CMAKE_SOURCE_DIR}/inc)
target_link_libraries(${CMAKE_PROJECT_NAME}
nlohmann_json::nlohmann_json
${Boost_LIBRARIES}
${GUROBI_LIBRARY}
optimized ${GUROBI_CXX_LIBRARY}
debug ${GUROBI_CXX_LIBRARY}
)

if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR})
include(FeatureSummary)
feature_summary(WHAT ALL)
endif()

总之,重点在于如何告诉Cmake链接到Gurobi,如果某个环节错了的话,那就检查这个环节的问题,Cmake不熟悉也只能多学习Cmake了(因为我也不熟悉啊🥹)。
此外,如果Gurobi的环境变量没设置也没关系,上述的CmakeLists.txt被注释的部分就是设置Gurobi的路径(macos):

1
2
set(GUROBI_INCLUDE_DIRS "/Library/gurobi1103/macos_universal2/include")  
set(GUROBI_CXX_LIBRARY "/Library/gurobi1103/macos_universal2/lib/libgurobi_c++.a")

在使用Latex写作时,经常遇到数学优化模型的排版问题。在一般情况下,我希望写一个具有最小化\最大化的目标公式,
约束、以及“subject to: ”或者“s.t.: ”这样的约束前缀。
完成这样的任务涉及到一些Latex公式对齐的问题, 经过测试,以下两个实现方式效果最佳。

公式独立标号

先放源码:

1
2
3
4
5
\begin{align}
& \min Cx \label{eq:obj} \\
\mbox{subject to: } \quad & Ax = b \label{eq:constr} \\
& x \ge 0
\end{align}

编译结果如下:

Latex

公式共用一个编号(适合多个模型的情况)

1
2
3
4
5
6
7
8
9
10
\begin{subequations}
\label{eq:optim}
\begin{align}
\underset{X}{\text{minimize}}
& \quad \mathrm{trace}(X) \label{eq:cost}\\
\text{subject to}
& \quad X_{ij} = M_{ij}\,, \; (i,j) \in \Omega\,, \label{eq:const1}\\
& \quad X \succeq 0 \,. \label{eq:const2}
\end{align}
\end{subequations}

编译结果如下:

Latex

前几天刷到一个公众号上的推文,关于CVRP的两索引的建模,其中有一个约束是条件约束。
之前我遇到条件约束都是手工转成线性约束,然后输入gurobi中,一方面是锻炼自己线性化的能力,另一方面是懒的翻手册查找方法。
然后发现Gurobi是真好用,内置了条件约束的添加方法。

条件约束的广义形式见文章末尾的参考来源,这里只是举一个例子, 如下:

xij=1ui+qi=uj,i,jA,j0,i0x_{ij} = 1 \rightarrow u_i + q_i = u_j, \quad i,j \in A, j \ne 0, i \ne 0

上面的约束表明只有在xij=1x_{ij} = 1的情况下,ui+qi=uju_i + q_i = u_j才成立,其中x,ux,u是决策变量。
Gurobi添加的约束如下(python):

1
m.addConstrs((X[i,j] == 1) >> (Y[i] + demand[j] == Y[j]) for i,j in A if i!=1 and j!= 1)

方法不止这一种,上述的是重载的形式,原来的形式应该使用addGenConstrIndicator这个方法,详见参考链接。

参考文章:
https://gurobi.com/documentation/current/refman/py_model_agc_indicator.html

最近在写一个论文,审稿人让我增加一部分的实验内容,验证我启发式的性能。我选择和Gurobi进行对比。
然后在我和Gurobi进行对比的过程中,发现某几个算例的启发式结果竟然比Gurobi的下界(gap=0)还低。
按道理来说,对于一个最小化的问题,上界是不可能比下界还低的,因此我怀疑是我的启发式算法有bug,最开始我是不相信Gurobi会有问题的。

复杂的验证过程以及漫长的Debug中,我发现自己的算法好像并没有什么问题,而且只有一两个算例出现了上述的情况。
接着我发现如果我减少FeasibilityTolOptimalityTol这两个参数(至最小值10e-9)以提升数值精度,那么情况有会有所改善,但是启发式的结果依旧比下界还低。

然后,我使用了一个常见的方法,强制决策变量等于启发式的解,把启发式的结果制作成约束添加进模型中,再求解发现Gurobi的结果竟然和我的启发式一致了,且gap等于0。这就出现了悖论:

  • 在那几个特殊的算例中(增加额外的约束gap=0,不增加约束也是gap=0),最小化问题增加额外的约束竟然使最优目标值更小了。

这让我觉得太离谱了,增加额外约束最好的情况下也只能是和原问题的最优目标值一致。
然后和Gurobi社区的工作人员拉扯了一个月,终于搞清楚了,Gurobi承认有bug,并且只能在下个版本修复了。

在此附上原帖子链接,纪念一下第一次为一个求解器厂商提出实质性bug。

https://support.gurobi.com/hc/en-us/community/posts/27254595262993-Add-additional-constraints-but-the-objective-decreased-in-minimize-problem

声明:

  • 原文之前发表在本人的CSDN上,现搬运到博客里。
  • 本文的目的是记录和分享学到的知识。作者已尽一切努力确保本文内容的准确性。作者在此声明不承担因本文中的错误或遗漏造成的任何损失所带来的责任,无论这些错误或遗漏是意外、疏忽或其他原因导致的。
  • 转载请备注来源,欢迎点赞收藏或指出本文不足。
  • 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
  • 本文链接:https://blog.csdn.net/weixin_45746917/article/details/125524606

本文由作者用心整理,希望对您有所帮助。如果您入门或困扰于该问题,请您静下心来阅读,准备纸笔推导,一定会有所收获。所有例子和引证均带有来源,可点超链接深入了解。

概述

  拉格朗日松弛是一种求解整数规划的、基于优化的启发式算法。该算法能提供较为优质的解,可证明解的质量(可得到优化上下界),但不保证最优性。拉格朗日松弛的应用十分广泛,例如无容量限制的设施选址问题(该问题为NP-hard问题)。
  拉格朗日松弛算法可追溯到Everett 1963年,Fisher 1973使用了该算法求解了调度问题。注意,该算法并不是拉格朗日提出的,是借鉴了拉格朗日乘子法的一些启示,由(Geoffrion 1974)命名。

  • 松弛方法对于一个优化问题十分重要,因为这种方法为最优值提供了“上下界限”。

  • 松弛就是把一个问题的约束变弱,这样可行域就大了,也能方便求解了。

  • 最小化问题中,松弛问题的最优值是下界。最大化问题是上界。

  • 松弛技术的精髓就是设计一个容易求解的松弛问题并得到一个优质的“界”。

链接: Everett 1963

链接: Fisher 1973

从拉格朗日乘子法开始

  微积分中,一类问题是求解一个函数的极大值或者极小值。当这个函数受限于另一个函数的时候,问题求解将变得困难。详见同济版《高等数学 下》(高等教育出版社)多元函数的极值及其求法——条件极值 拉格朗日乘数法。

维基百科的一个例子

  参见维基百科
  求函数f(x,y)f(x,y)的极值,且要求满足条件g(x,y)=cg(x,y)=c
  这里简单描述一下原理和证明过程,详细证明参见引用来源。假设f(x,y)f(x,y)的取值为dnd_n,注意dnd_n是随xxyy的值变化的。只有f(x,y)=dnf(x,y)=d_ng(x,y)=cg(x,y)=c相切时,同时沿着有f(x,y)=dnf(x,y)=d_ng(x,y)=cg(x,y)=c的方向前进,在这种情况下,会出现极值。
  由于二者是相切关系,那么二者的梯度满足如下关系:(切线、法线、梯度的关系)

f(x,y)=λ(g(x,y)c)\nabla f(x,y) = -\lambda \nabla (g(x,y)-c)

于是,有

[f(x,y)+λ(g(x,y)c)]=0\nabla[f(x,y) + \lambda (g(x,y)-c)]=0\\

  这样,如果求出了λ\lambda的值,带到F(x,y,λ)=f(x,y)+λ(g(x,y)c)F(x,y,\lambda)=f(x,y) + \lambda(g(x,y)-c)中,就能求出无约束条件下的极值和对应的极值点。F(x,y,λ)F(x,y,\lambda)f(x,y)f(x,y)在极值点处相等。极值点处,F(x,y,λ)=0\nabla F(x,y,\lambda)=0,且Fλ=g(x,y)c\frac{\partial F}{\partial \lambda}=g(x,y)-cg(x,y)c=0g(x,y)-c=0​,所以极值点处有F(x,y,λ)=f(x,y)F(x,y,\lambda)=f(x,y)

同济版第7版《高等数学 下》的例子

  第九章p117给出了更详细的证明,这里直接给出结论:要找z=f(x,y)z=f(x,y)在附加条件ϕ(x,y)=0\phi (x,y)=0下的可能极值点,可做拉格朗日函数:

L(x,y)=f(x,y)+λϕ(x,y)L(x,y) = f(x,y) + \lambda \phi(x,y)

其中λ\lambda为参数,然后求LLx,y,λx,y,\lambda的一阶偏导,并令其为0,将这个三个等式联立。方程组求解得到的解(x,y)(x,y)就是f(x,y)f(x,y)可能的极值点。

整数规划拉格朗日松弛

问题构建

  给定一个待求解的整数规划问题(PP):

Z=mincx s.t. Ax=bDxex0 and integral (P)\begin{aligned} Z=& \min c x \\ \text { s.t. } & A x=b \\ & D x \leq e \\ & x \geq 0 \text { and integral } \end{aligned} \quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\left(\mathrm{P}\right)

  我们松弛其整数约束,得到LPLP问题:

Z=mincx s.t. Ax=bDxex0(LP)\begin{aligned} Z=& \min c x \\ \text { s.t. } & A x=b \\ & D x \leq e \\ & x \geq 0 \end{aligned} \quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\left(\mathrm{LP}\right)

  我们松弛第一个等于约束,得到一个松弛后的拉格朗日问题(LRuLR_u),其中u={u1,u2,...um}u=\{u_1,u_2,...u_m\}是拉格朗日乘子。

ZD(u)=mincx+u(Axb) s.t. Dxex0 and integral (LRu)\begin{aligned} Z_D(u)=& \min c x + u(Ax-b) \\ \text { s.t. } & D x \leq e \\ & x \geq 0 \text { and integral } \end{aligned} \quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\left(\mathrm{LR_u}\right)

  设xx^*PP问题的最优解,此时,有:

ZD(u)=mincx+u(Axb)cx+u(Axb)=cx=ZZ_D(u)= {\rm{min}}\,cx + u(Ax-b)\le cx^*+u(Ax^*-b) = cx^* = Z

  这说明,ZD(u)Z_D(u)的值一定小于等于ZZ的值。不等号的来源是松弛,因为LRuLR_uPP的约束更少,所以LRuLR_u的最优解一定小于等于把xx^*直接带入LRuLR_u得到的目标函数值。由于在PP中,Ax=bAx^* =b,所以cx+u(Axb)=cx=Zcx^*+u(Ax^*-b) = cx^* = Z
  如果Ax=bAx=b的约束变成了AxbAx\le b,则需要求u0u\ge 0,如果Ax=bAx=b的约束变成了AxbAx\ge b,则需要求u0u\le 0
  总之,不管怎么样,ZD(u)ZZ_D(u)\le Z在一定条件下成立,即ZD(u)Z_D(u)小于等于原问题的最优目标函数值。

乘子uu取值

拉格朗日对偶问题D{D}

   首先,应明确在uu的特定取值下,ZD(u)Z_D(u)可以等于ZZ。此时,这个问题就变成了跟uu相关的问题。如何给uu取值,使得ZD(u)Z_D(u)尽可能取最大值以接近ZZ的问题,称作拉格朗日对偶问题DD,即:

ZD=maxuZD(u)(D)Z_D = \max _u Z_D(u) \tag{D}

该问题的决策变量是uu,目标函数是最大化ZD(u)Z_D(u)的值。也就是说,求解原问题PP的最小化目标函数值ZZ已经转换成求解拉格朗日对偶问题DD的目标函数值ZD(u)Z_D(u)了。

   那么问题来了,拉格朗日对偶问题的形式很抽象,我们如何具象描述它呢?

拉格朗日对偶重构问题Dˉ\bar{D}

   假设拉格朗日松弛LRuLR_u的可行解集X={xDxe,x0 and integer}X=\{x|Dx\le e ,x\ge 0 \text{ and integer}\}是有限集,则我们可以令X={xt,t=1,...,T}X=\{x^t,t=1,...,T\}

这样,问题DD可重构为Dˉ\bar{D}

ZD=maxws.t.wcxt+u(Axtb),t=1,...,T(Dˉ)\begin{aligned} Z_D&=\quad {\rm{max}}\,w\\ \text{s.t.}\quad w &\le cx^t + u(Ax^t-b),t=1,...,T\\ \end{aligned} \quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\left(\mathrm{\bar{D}}\right)

问:为什么可以这样重构?

*答:令w=ZD(u)w=Z_D(u),问题DD想求ZD(u)Z_D(u)关于uu的最大值,所以Dˉ\bar{D}的目标函数是求最大值。ZD(u)Z_D(u)是最小化cx+u(Axb)c x + u(Ax-b),那么ZD(u)Z_D(u)一定小于等于LRuLR_u的任意一个可行解xtx^t带入该表达式的值,即

mincx+u(Axb)=ZD(u)=wcxt+u(Axtb)\min c x + u(Ax-b) = Z_D(u) = w \le cx^t + u(Ax^t-b)

xtx^tLRuLR_u问题的最优解时等号成立。*

拉格朗日对偶重构问题的线性规划对偶Pˉ\bar{P}

  这时,再求Dˉ\bar{D}的线性规划对偶Pˉ\bar{P}

ZD=mint=1Tλtcxt s.t. t=1TλtAxt=bt=1Tλt=1λt0,t=1,,T(Pˉ)\begin{aligned} Z_{D}=& \min \sum_{t=1}^{T} \lambda_{t} c x^{t} \\ \text { s.t. } & \sum_{t=1}^{T} \lambda_{t} A x^{t}=b \\ & \sum_{t=1}^{T} \lambda_{t}=1 \\ & \lambda_{t} \geq 0, t=1, \ldots, T \end{aligned} \quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\left(\mathrm{\bar{P}}\right)

  对偶推导:从Pˉ\bar{P}Dˉ\bar{D}推导,把bb分解成凸组合;从Dˉ\bar{D}Pˉ\bar{P}推导,把ww看成1w+0u1w+0u,其中wwuu是无约束的决策变量。

  虽然Pˉ\bar{P}LPLP不是等价问题,即Pˉ\bar{P}并不是原问题PP的线性松弛版本,但是当λt\lambda_t是整数(0或1)时,Pˉ\bar{P}等价于PP。理由如下:xtx^tLRuLR_u的可行解,一定满足约束DxeDx\le e。既然满足了这个约束,在所有的可行解中,也肯定存在一个xtx^t满足Axt=bAx^t=b,那么令对应的λt\lambda _t等于1即可。(满足条件a的所有解中,一定有一个解同时满足a和b,子集关系)

拉格朗日对偶重构问题Dˉ\bar{D}的意义

  问题Dˉ\bar{D}表明,ZD(u)Z_D(u)是一系列线性函数的下限值。为了方便理解,假设xx的变量数只有一个,T=4T=4表示对于问题LRuLR_u仅有4个可行解。那么,ZD(u)Z_D(u)uu变化的图像如图所示:
来源:Fisher: The Lagrangian Relaxation Method for Solving Integer Programming Problems  Management Science 50(12S), pp. 1861–1871, ©2004 INFORMS

  在这个图中,xtx^t是已知的,是常数,那么得到了四个关于uu的线性函数。取这些函数在uu不同取值的最小值,即为函数ZD(u)Z_D(u),如图中加粗线条所示。

  我们发现,ZD(u)Z_D(u)是连续的,是凹函数(参照国际定义)。这样找最大值就很简单,比如,我们使用爬山算法,但是我们不能求导,因为最优点处不可导。

  既然不可导,那就用次梯度法

次梯度法

次梯度概念

   满足如下公式的 yy 值为 ZD(u)Z_D(u)uˉ\bar{u} 处的次梯度。

y(uuˉ)ZD(u)ZD(u),uy(u-\bar{u}) \ge Z_D(u) - Z_D(u), \forall u

次梯度性质

  • 向量AxtbAx^t-bxtx^t求解 LRuLR_u 对应位置的次梯度。其余的次梯度都是这些“基础”次梯度的凸组合。
  • 当且仅当uu^*λ\lambda^*是可行的且满足互补松弛定理时,uu^*Dˉ\bar{D}的最优解且λ\lambda^*Pˉ\bar{P}的最优解,这等价于当且仅当ZD(u)Z_D(u)uu^* 处的次梯度为0时,uu^*DD的最优解。(说白了就是ZD(u)Z_D(u)取到了关于uu的最大值)

次梯度法更新乘子

   给定一个初始的乘子值 u0u^0,后续乘子第kk次迭代的值为uku^k,迭代公式如下:

uk+1=uk+tk(Axkb)u^{k+1} = u^{k} + t_k(Ax^k-b)

其中xkx^k(LRuk)(LR_{u^k})的最优解,tkt_k是一个正数,称为迭代步长。

Goffin (1977)表明:

tk0t_k→0i=0kti=0\sum_{i=0}^kt_i=0时,ZD(uk)ZDZ_D(u^k)→Z_D

   常用的迭代步长公式为:

tk=λk(ZZD(uk))Axkb2t_k = \frac{\lambda_k(Z^*-Z_D(u^k))} {||Ax^k-b||^2}

其中,λk\lambda_k是一个(0,2](0,2]之间的标量,ZZ^*ZDZ_D的上界,且使用启发式修改为PP的可行解。通常,λk\lambda_k的初始值设置为2,当一定迭代次数后,ZD(u)Z_D(u)的值仍未增加,λk\lambda_k的值减半。

   注意,这是一个经验性的方法,并不一定能满足最优收敛。

总结

上述的内容只是一个方法论,我们想求解一个问题,还是面临很多的困难的。具体的问题需要具体的分析,包含但不限于松弛哪个约束、获取上界的启发式方法、算法参数的调整等。具体的例子,可参考本人的硕士毕设(在github仓库中)使用拉格朗日松弛求解了一个选址问题。

在读研一的时候第一次接触到子回路消除这个概念,今天特意写一下相关的内容,也算是一个心得与总结,以及一些我对于子回路消除的认知与理解。本文将从一个简单的TSP问题开始讲起,提出模型与消除方法,并使用求解器Gurobi实现各种子回路消除方法。

什么是子回路?

子回路是一个在TSP中和VRP中常见的现象,体现为一辆车走了两个或多个闭环路径。如果不知道VRP和TSP相关的问题与模型,建议补习相关内容。比如下面这张图的TSP问题。正常情况下,一个TSP问题的可行解应该如右图所示,即一辆车遍历所有节点。而左图中,很明显每个点被遍历了,但是竟然出现了两辆车。这显然不符合对于TSP问题的假设。然而在这种情况下(左图),两个回路竟然在数学公式上使用了同一辆车。这就是一个典型的子回路现象。

subtours

为什么出现子回路

简单来说,子回路的出现就是我们缺少一部分约束,导致没有完全地刻画问题中车辆的行为。我们知道车辆路径问题(vehicle routing problem, VRP)是旅行商问题(traveling salesman problem, TSP)问题的一个更加困难的版本,与其用一个VRP的例子作为教程,不如使用更为简单的TSP作为上手的开始。因为TSP过于经典,就不再过多赘述。简单来说就是如何以最短的距离不重复地走完地图上所有的城市并且回到出发点,其(不完整的)数学模型如下:

miniNjNcijxij(1)\min \sum_{i\in N} \sum_{j\in N} c_{ij}x_{ij} \tag{1}

iNxij=iNxji=1,jN(2)\sum_{i\in N} x_{ij} = \sum_{i\in N} x_{ji} = 1, \forall j \in N \tag{2}

xij{0,1},iN,jN(3)x_{ij} \in \{0,1\}, \forall i \in N, j\in N \tag{3}

上述公式使用了常见的符号集合,NN是节点集合,cijc_{ij}是弧长。公式(2)表示每个点的入度等于出度,即流平衡。
公式(3)是0-1决策变量。

我们观察上面的图的左侧子图,每个点的入度也确实等于出度。也就是说上述的模型是不完整的,我们没有消除子回路的约束。

消除子回路

如上所述,我们需要增加一些约束,这些约束帮助我们消除子回路。这些约束都相当精彩,在领悟其中的原理之后拍手叫绝。

DFJ方法

Dantzig-Fulkerson-Johnson 简称 DFJ 方法是消除子回路的经典方法,公式如下:

iSjSxij1,SN,S\sum_{i\in S} \sum_{j \notin S} x_{ij} \ge 1, \forall S \subset N, S \ne \emptyset

DFJ约束规定一个节点集合NN的非空子集SS,要求连接SS外面的点和在SS内部的点的弧的数量大于等于1。换句话说,对于任意一个NN的子集SS,一定有一条弧离开SS。反之,如果存在一个子集SS,没有一条弧能够离开SS,那就说明SS存在子回路。

DFJ约束很简单也很直观,但是问题是约束的数量不是多项式级的,我们不太可能把一个大规模问题的所有的约束枚举出来。

MTZ方法

Miller, Tucker, and Zemlin 简称 MTZ 方法也是消除子回路的经典方法,公式如下:

titj+1M(1xij),i,jN,ijt_i-t_j+1\leq M(1-x_{ij}), \forall i,j\in N,i\neq j

0tiN1,iN0 \le t_i \le |N|-1, \forall i\in N

MTZ方法引入了一个新的变量,这个变量没有任何物理意义。MTZ公式可以这样理解:
首先,如果两个点i,ji,j之间存在一个弧(i,j)(i,j),那么两个点之间将会产生一个前后关系ii先被访问,jj后被访问。这个前后关系我们先假设为时间,也就是说访问ii的时间一定比访问jj的时间小。或者我们假设一个计数器,每经过一个点计数器增加一个单位。这个量(计数器、时间)并不存在于定义的TSP问题的物理含义中,可以解释成任意一种能够被人理解的东西(时间、计数器、容量变化…)。现在,我们用额外的决策变量代表每个点被访问的前后顺序,然后让这些值相互关联起来,就能产生前后顺序进而消除子回路。

这个额外的决策变量就是ti,iNt_i, \forall i \in N,那么按照我们上述的计数器假设,如果点jjii之后被访问(xij=1x_{ij}=1),那么一定有tjti+1t_j \ge t_i + 1。如果这种关系(点jjii之后被访问)不存在,那么tjti+1t_j \ge t_i + 1的关系也不存在了,即tjt_jtit_i随意取值。我们就得到titj+1Mt_i-t_j+1\leq M,其中MM是一个足够大的数。这样就得到了上述的titj+1M(1xij),i,jN,ijt_i-t_j+1\leq M(1-x_{ij}), \forall i,j\in N,i\neq j的基础原理,MM最小取值为N|N|

如果存在一个子回路,那tit_i的关系是不可能成立的,因为会无限绕圈,使得tit_itjt_j的关系总是不能被满足。

其他方法

在介绍了上述的两个经典的方法之后,以下是一个综述文章,介绍了各种TSP子回路消除方法。感兴趣自己看,这里就不复现了。

A comparative analysis of several asymmetric traveling salesman problem formulations,
Computers & Operations Research, 2009

Gurobi 实现MTZ和DFJ子回路消除

上述介绍中已经提到DFJ方法的约束个数是有限的,但是指数级的。我们不太可能用求解器把所有子回路的情况都考虑进去。首先声明这是一个简单的代码,用了20分钟写的简单例子,没有面向对象。

那么 talk is cheap, show me your code。

1
2
3
4
5
import gurobipy as gp
import math
import random
import re
from itertools import combinations

extract_numbers()[a,b]这样的方括号中提取ab这两个数字,并返回一个list,用于从变量名中提取索引。

1
2
3
4
5
6
7
def extract_numbers(s):  # 使用正则表达式匹配方括号中的两个数字
match = re.search(r"\[(\d+),(\d+)\]", s)
if match:
# 提取匹配到的数字并转换为整数
return [int(match.group(1)), int(match.group(2))]
else:
return None

get_tour()函数从一个字典edges中获得一个回路。字典edges的一个键值对是一条弧,键是起点,值是终点。
例如edges={1:2, 3:4, 2:3, 4:1},则得到tour=[1,2,3,4]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def get_tour(edges: dict):
tour = [] # 一个回路
while True:
if len(tour) == 0: # 空的回路
if 0 in edges: # 包含起点,则以起点开始
tour.append(0)
tour.append(edges[0])
del edges[0]
else: # 不包含起点,则以第一个点开始
tmp = list(edges.keys())
tour.append(tmp[0])
tour.append(edges[tmp[0]])
del edges[tmp[0]]
else:
tmp = edges.get(tour[-1])
if tmp == None:
break # 产生了一个回路
else:
tour.append(tmp)
del edges[tour[-2]]
return edges, tour[:-1]

mycallback函数为DFJ的约束添加过程,由于这样的约束数量很多无法一次添加,那么使用callback函数进行添加。
MIPSOL处(mip solution)如果发现了一个带子回路的解,则添加针对这个回路的DFJ约束。注意,我们假设TSP的起点一定不在子回路里面,也就是只有不带起点的回路才是子回路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def mycallback(model, where):
if where == gp.GRB.Callback.MIPSOL:
# 解析获得的解
var_val = model.cbGetSolution(model._var)

edges = {} # TSP 决定走的边
for i in range(len(var_val)):
if var_val[i] > 0.5:
tmp = extract_numbers(model._var[i].VarName)
edges.update({tmp[0]: tmp[1]})

# 在edges搜索一个子回路,要求起点一定不在子回路里面
flag_subtour = False
edges, tour = get_tour(edges)
if len(tour) < model._node_num: # 一定存在子回路
flag_subtour = True

# 针对这个子回路添加 DFJ约束
if flag_subtour:
edges, tour = get_tour(edges) # 可能有多个subtour,获得一个子回路即可
assert len(tour) < model._node_num
# 加约束
pairs = [(i, j) for i in tour for j in tour if i != j]
lexp = 0
for p in pairs:
lexp += model.getVarByName("x[" + str(p[0]) + "," + str(p[1]) + "]")
model.cbLazy(lexp <= len(tour) - 1)

使用DFJ方法的模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def solve_tsp_dfj(nodes, distances):
# TSP 求解
model = gp.Model()
x = model.addVars(
distances.keys(), obj=distances, vtype=gp.GRB.BINARY, name="x"
) # 变量

model.addConstrs(gp.quicksum(x[i, j] for j in nodes if i != j) == 1 for i in nodes)
model.addConstrs(gp.quicksum(x[j, i] for j in nodes if i != j) == 1 for i in nodes)

model.update()
model._var = model.getVars()
model._node_num = len(nodes)

# 使用LazyConstraints声明
model.Params.LazyConstraints = 1
model.optimize(mycallback)

if model.Status == gp.GRB.OPTIMAL:
edges = {} # 被选中的边
for i in range(len(model._var)):
if model._var[i].X > 0.5:
tmp = extract_numbers(model._var[i].VarName)
edges.update({tmp[0]: tmp[1]})
edges, tour = get_tour(edges)
print(tour)
print(f"路径中点的个数:{len(tour)}")
print(f"路径长度:{model.ObjVal}")

使用MTZ方法的模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def solve_tsp_mtz(nodes, distances):
# TSP 求解
model = gp.Model()
x = model.addVars(
distances.keys(), obj=distances, vtype=gp.GRB.BINARY, name="x"
) # 变量
t = model.addVars((i for i in nodes), vtype=gp.GRB.CONTINUOUS, name='t')

model.addConstrs(gp.quicksum(x[i, j] for j in nodes if i != j) == 1 for i in nodes)
model.addConstrs(gp.quicksum(x[j, i] for j in nodes if i != j) == 1 for i in nodes)
# model.addConstr(t[0] == 0)
node_without_starts = copy.deepcopy(nodes)
node_without_starts.remove(0)
model.addConstrs(
t[i] - t[j] + 1 <= len(nodes) * (1 - x[(i, j)])
for i in node_without_starts
for j in node_without_starts
if i != j
)

model.update()
model._node_num = len(nodes)

model.optimize()

if model.Status == gp.GRB.OPTIMAL:
edges = {} # 被选中的边
for i in nodes:
for j in nodes:
if i != j:
if model.getVarByName("x[" + str(i) + "," + str(j) + "]").X > 0.5:
edges.update({i:j})
edges, tour = get_tour(edges)
print(tour)
print(f"路径中点的个数:{len(tour)}")
print(f"路径长度:{model.ObjVal}")

主程序生成一个50个点的TSP问题,同一个问题使用两种方法求解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if __name__ == "__main__":
npoints = int(50)
seed = int(41372890)

# 创建一个随机的TSP算例
random.seed(seed)
nodes = list(range(npoints))
points = [(random.randint(0, 100), random.randint(0, 100)) for i in nodes]

# 欧式距离
distances = {
(i, j): math.sqrt(sum((points[i][k] - points[j][k]) ** 2 for k in range(2)))
for i, j in combinations(nodes, 2)
}

for i, j in combinations(nodes, 2):
distances.update({(j, i): distances[(i, j)]})

# 算法求解
solve_tsp_dfj(nodes, distances)
solve_tsp_mtz(nodes, distances)

LaTex是科研写作中常见的排版工具。爱思唯尔期刊提供了Latex模板,使用该模板可简化在投稿过程中关于稿件的排版过程,在编译过后就能得到期刊认可的排版格式。

不少期刊都已经采用了“Your paper your way”的投稿方式,即根据作者的喜好的排版方式提交稿件,在接收后排版成期刊的要求的格式即可,但也提供了Latex模板。
关于爱思唯尔期刊的Latex模板,详见https://www.elsevier.com/researcher/author/policies-and-guidelines/latex-instructions

尽管内容大于形式,红楼梦手抄一万遍也是经典,我的博客打印出来也只是我的博客
但我们使用Latex模板将给审稿人表达一种专业且认真的态度,也可以简化写作、排版、公式输入及编号、交叉引用的流程。

请注意:阅读本文需要一定Latex使用经验,且本文只针对爱思唯尔期刊Latex模板elsarticle有效。

1. 使用的宏包介绍

1
2
3
% \usepackage[numbers]{natbib}
% \usepackage[authoryear,longnamesfirst]{natbib}
\usepackage[authoryear]{natbib}

该宏包是elsarticle自带的引用格式的宏包,一般我选择使用第三个[authoryear]参数。

1
\usepackage{lineno}

lineno是行号宏包,投稿时如需加行号则使用此宏包。
然而,该宏包对于模板的双列布局可用,但是支持性一般。
此外不推荐使用双列模板,除非没有长公式或者长图、长表等等,
elsarticle的双列模板虽然好看,但是给排版(尤其是跨列图、表、公式)带来诸多困难。

1
\usepackage{threeparttable} 

threeparttable可以实现表格以及表格下方的注释,十分好用。

1
\usepackage[ruled,linesnumbered]{algorithm2e} 

algorithm2e是算法伪代码的常用宏包。

1
\usepackage{subcaption}

subcaption 可以实现子图

1
\usepackage[nameinlink,capitalize]{cleveref} 

cleveref 实现交叉引用带label名字,比如Section 1, 而不是使用\ref仅引用一个数字。
该宏包最好放在所有宏包命令的最后

1
\usepackage{pdflscape} % 单页横置

pdflscape是某页横置的包,支持pdf旋转,比landscape更好一些。
landscape编译出来后,pdf内容是横置的,但是页面仍是竖置,而pdflscape的页面也是横置的。

1
\usepackage{midpage} % 页面垂直居中内容

midpage配合pdflscape使用,可以使用横置的内容位于页面的垂直方向的中心(垂直居中)。
再配合横向居中\centering使用,就是页面的正中心。

1
\usepackage{afterpage} % 减少横向排版某页前后的空白页

afterpage配合pdflscape使用,可避免pdflscape使用前后文章出现大面积空白(因为pdflscape在哪里使用就在哪里分页)。

2. 实践例子

Minimal Working Example (MWE) 是Latex是一个精简到最小长度的.tex文件。是一个可以运行的,最简短的tex文件。
以下例子均基于MWE或者elsarticle的cas-sc-sample.tex文件进行展示。
文本填充使用了lipsum宏包生成随机文本

行号 lineno

展示行号使用了lineno宏包,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
\documentclass[a4paper,fleqn]{cas-sc}

\usepackage{lipsum}
\usepackage{lineno}

\begin{document}
\let\WriteBookmarks\relax
\def\floatpagepagefraction{1}
\def\textpagefraction{.001}

\title [mode = title]{This is a title}
\author[1]{Name}[]

\begin{abstract}
Null.
\end{abstract}

\begin{keywords}
Null
\end{keywords}

\maketitle

\linenumbers
\section{Introduction}
\lipsum[1]

\end{document}

\linenumbers命令放在节标题前面就可以生成行号了,结果如下:

行号生成

表格注释threeparttable

在表格展示结果的过程中总会有需要在表格下方注释的情况,使用threeparttable宏包可满足该需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
\documentclass[a4paper,fleqn]{cas-sc}

\usepackage{lipsum}
\usepackage{threeparttable}

\begin{document}
\let\WriteBookmarks\relax
\def\floatpagepagefraction{1}
\def\textpagefraction{.001}

\title [mode = title]{This is a title}
\author[1]{Name}[]

\begin{abstract}
Null.
\end{abstract}

\begin{keywords}
Null
\end{keywords}

\maketitle

\section{Introduction}
\lipsum[1]

\begin{table}[width=0.4\textwidth, pos=thb]
\scriptsize
\tabcolsep=2.5pt
\renewcommand\arraystretch{1.5}
\centering
\caption{Table notes}
\label{tb:review}%
\begin{threeparttable}
\begin{tabular*}{\tblwidth}{lcclccl}
\toprule
Ref. & \multicolumn{2}{c}{Omnichannel concept} & \multicolumn{2}{l}{Customer} & \multicolumn{1}{l}{Store} \\
\midrule
O\tnote{a} & F & S & M & P & S & L,C \\
C & R & S & M\tnote{*} & C & H & L,R \\
O & R & S & M & C & H & L,R\tnote{$\dagger$} \\
\bottomrule
\end{tabular*}%

\begin{tablenotes}
\scriptsize
\item[a] table note a
\item[*] asterisk
\item[$\dagger$] dagger
\end{tablenotes}
\end{threeparttable}
\end{table}

\end{document}

这会生成一个带有注释的表格。

表格注释

伪代码

请参考Overleaf的伪代码包,写的非常详细。
https://www.overleaf.com/learn/latex/Algorithms

子图 subcaption

使用子图,只需要引用subcaption宏包即可,子图换行使用\\即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
\documentclass[a4paper,fleqn]{cas-sc}

\usepackage{lipsum}
\usepackage{subcaption}

\begin{document}
\let\WriteBookmarks\relax
\def\floatpagepagefraction{1}
\def\textpagefraction{.001}

\title [mode = title]{This is a title}
\author[1]{Name}[]

\begin{abstract}
Null.
\end{abstract}

\begin{keywords}
Null
\end{keywords}

\maketitle

\section{Introduction}
\lipsum[1]

\begin{figure}[pos = htb]
\centering
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 1}
\label{subfig:routes}
\end{subfigure}
\hfill
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 2}
\label{subfig:cycle}
\end{subfigure}
\hfill
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 3}
\label{subfig:aggregate}
\end{subfigure}
\\
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 4}
\label{subfig:assign}
\end{subfigure}
\hspace{0.15\textwidth}
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 5}
\label{subfig:newroute}
\end{subfigure}
\caption{\label{fig:aggr} Subfigures}
\end{figure}

\end{document}

这会生成一个两行子图,第一行有3张,第二行有2张。
子图生成

交叉引用 cleveref

交叉引用是Latex一个重要的功能,自带的\ref功能只能引用数字,生成出来的效果并不好看,比如“Section 1.1 ”, 而不是期刊常见的“Section 1.1 ”。使用cleveref宏包解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
\documentclass[a4paper,fleqn]{cas-sc}

\usepackage{lipsum}
\usepackage{subcaption}
\usepackage[nameinlink,capitalize]{cleveref}


\begin{document}
\let\WriteBookmarks\relax
\def\floatpagepagefraction{1}
\def\textpagefraction{.001}

\title [mode = title]{This is a title}
\author[1]{Name}[]

\begin{abstract}
Null.
\end{abstract}

\begin{keywords}
Null
\end{keywords}

\maketitle

\section{Introduction}
\Cref{subfig:assign} is in \Cref{fig:aggr}.

\begin{figure}[pos = htb]
\centering
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 1}
\label{subfig:routes}
\end{subfigure}
\hfill
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 2}
\label{subfig:cycle}
\end{subfigure}
\hfill
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 3}
\label{subfig:aggregate}
\end{subfigure}
\\
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 4}
\label{subfig:assign}
\end{subfigure}
\hspace{0.15\textwidth}
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 5}
\label{subfig:newroute}
\end{subfigure}
\caption{\label{fig:aggr} Subfigures}
\end{figure}

\end{document}

这会生成一个带引用类型的交叉引用格式。
交叉引用

横置某页 pdflscape midpage afterpage

横置一个pdf页码可以解决某个表格或者图太长的问题,使用pdflscape可以将某一页横置,以放下这些图表。
基础使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
\documentclass[a4paper,fleqn]{cas-sc}

\usepackage{lipsum}
\usepackage{subcaption}
\usepackage{pdflscape}
\usepackage[nameinlink,capitalize]{cleveref}

\begin{document}
\let\WriteBookmarks\relax
\def\floatpagepagefraction{1}
\def\textpagefraction{.001}

\title [mode = title]{This is a title}
\author[1]{Name}[]

\begin{abstract}
Null.
\end{abstract}

\begin{keywords}
Null
\end{keywords}

\maketitle

\section{Introduction}
\Cref{subfig:assign} is in \Cref{fig:aggr}.

\begin{landscape}
\begin{figure}[pos = htb]
\centering
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 1}
\label{subfig:routes}
\end{subfigure}
\hfill
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 2}
\label{subfig:cycle}
\end{subfigure}
\hfill
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 3}
\label{subfig:aggregate}
\end{subfigure}
\\
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 4}
\label{subfig:assign}
\end{subfigure}
\hspace{0.15\textwidth}
\begin{subfigure}{0.3\textwidth}
\includegraphics[width=\textwidth]{figs/cas-grabs.pdf}
\caption{subfigure 5}
\label{subfig:newroute}
\end{subfigure}
\caption{\label{fig:aggr} Subfigures}
\end{figure}
\end{landscape}
\end{document}

这会得到一个横置的页面,注意到这个页面是横置的,所有的内容在页面的上方,下一步是把这些图片垂直居中。
横置页面
只需要在landscapefigure环境之间套一层midpage环境即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
\documentclass[a4paper,fleqn]{cas-sc}

\usepackage{lipsum}
\usepackage{subcaption}
\usepackage{pdflscape}
\usepackage{midpage}

\begin{document}

\begin{landscape}
\begin{midpage}
\begin{figure}[pos = htb]
... 图表代码
\end{figure}
\end{midpage}
\end{landscape}

\end{document}

得到居中的插图:
横置+垂直居中

但是,横置页前后的文字会因为横置页突然打断,出现大面积空白。这时还需要afterpage宏包,使得横置页平滑插入到文字中,用\afterpage{}命令把landscape环境包裹起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
\documentclass[a4paper,fleqn]{cas-sc}

\usepackage{lipsum}
\usepackage{subcaption}
\usepackage{pdflscape}
\usepackage{midpage}

\begin{document}
\afterpage{
\begin{landscape}
\begin{midpage}
\begin{figure}[pos = htb]
... 图表代码
\end{figure}
\end{midpage}
\end{landscape}
}
\end{document}

生成的效果如图,可见横置页丝滑插入段落中,没有产生大面积空白

横置前后段落中间可断页

禁止分页

使用\nopagebreak表示禁止在此处断页。

此外,还可以使用

1
2
3
4
5
6
7
8
9
10
11
\usepackage{parskip}
\AddToHook{env/samepage/begin}{%
\AddToHook{para/before}{\nopagebreak}%
\AddToHook{para/after}{\nopagebreak}%
}

\begin{samepage}
在一页的第一段

在一页的第二段
\end{samepage}

另两个段落出现在同一页。

公式间距微调

参考这篇文章https://tex.stackexchange.com/questions/74353/what-commands-are-there-for-horizontal-spacing
下面的源代码将在字母ab之间产生不同大小的横向间距。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
\documentclass{article}

\usepackage[margin=1in]{geometry}% Just for this example
\setlength{\parindent}{0pt}% Just for this example

\begin{document}

There are a number of horizontal spacing macros for LaTeX:

\begin{tabular}{lp{5cm}}
\verb|a\,b| & a\,b \quad $a\, b$ \\
\verb|a\thinspace b| & a\thinspace b \quad $a\thinspace b$ \\
\verb|a\!b| & a\!b \quad $a\!b$ \\
\verb|a\negthinspace b| & a\negthinspace b \quad $a\negthinspace b$ \\
\verb|a\:b| & a\:b \quad $a\:b$ \\
\verb|a\>b| & a\>b \quad $a\>b$ \\
\verb|a\medspace b| & a\medspace b \quad $a\medspace b$ \\
\verb|a\negmedspace b| & a\negmedspace b \quad $a\negmedspace b$ \\
\verb|a\;b| & a\;b \quad $a\;b$ \\
\verb|a\thickspace b| & a\thickspace b \quad $a\thickspace b$ \\
\verb|a\negthickspace b| & a\negthickspace b \quad $a\negthickspace b$ \\
\verb|$a\mkern\thinmuskip b$| & $a\mkern\thinmuskip b$ (similar to \verb|\,|) \\
\verb|$a\mkern-\thinmuskip b$| & $a\mkern-\thinmuskip b$ (similar to \verb|\!|) \\
\verb|$a\mkern\medmuskip b$| & $a\mkern\medmuskip b$ (similar to \verb|\:| or \verb|\>|) \\
\verb|$a\mkern-\medmuskip b$| & $a\mkern-\medmuskip b$ (similar to \verb|\negmedspace|) \\
\verb|$a\mkern\thickmuskip b$| & $a\mkern\thickmuskip b$ (similar to \verb|\;|) \\
\verb|$a\mkern-\thickmuskip b$| & $a\mkern-\thickmuskip b$ (similar to \verb|\negthickspace|) \\
\verb|a\enspace b| & a\enspace b \\
\verb|$a\enspace b$| & $a\enspace b$ \\
\verb|a\quad b| & a\quad b \\
\verb|$a\quad b$| & $a\quad b$ \\
\verb|a\qquad b| & a\qquad b \\
\verb|$a\qquad b$| & $a\qquad b$ \\
\verb|a\hskip 1em b| & a\hskip 1em b \\
\verb|$a\hskip 1em b$| & $a\hskip 1em b$ \\
\verb|a\kern 1pc b| & a\kern 1pc b \\
\verb|$a\kern 1pc b$| & $a\kern 1pc b$ \\
\verb|$a\mkern 17mu b$| & $a\mkern 17mu b$ \\
\verb|a\hspace{35pt}b| & a\hspace{35pt}b \\
\verb|$a\hspace{35pt}b$| & $a\hspace{35pt}b$ \\
\verb|axyzb| & axyzb \\
\verb|a\hphantom{xyz}b| & a\hphantom{xyz}b (or just \verb|\phantom|) \\
\verb|$axyzb$| & $axyzb$ \\
\verb|$a\hphantom{xyz}b$| & $a\hphantom{xyz}b$ (or just \verb|\phantom|) \\
\verb|a b| & a b \\
\verb|$a b$| & $a b$ \\
\verb|a\space b| & a\space b \\
\verb|$a\space b$| & $a\space b$ \\
\verb|a\ b| & a\ b \\
\verb|$a\ b$| & $a\ b$ \\
\verb|a{ }b| & a{ }b \\
\verb|$a{ }b$| & $a{ }b$ \\
\verb|a~b| & a~b \\
\verb|$a~b$| & $a~b$ \\
\verb|a\hfill b| & a\hfill b \\
\verb|$a\hfill b$| & $a\hfill b$
\end{tabular}

\end{document}

其中,可以在公式环境内部调整间距。

1
2
3
4
5
6
\begin{equation}
\thinmuskip=3mu
\medmuskip=4mu plus 2mu minus 4mu
\thickmuskip=5mu plus 5mu
a + b = c
\end{equation}

最后,align环境调整行距,只需在\\之后加[行距]即可。

1
2
3
4
\begin{align*}
f(x)=x^{2}-2x+1\\[10pt]
f(x)=(x-1)^{2}
\end{align*}

前言:在运筹论文中,总会出现一些Proposition, Theorem, 等类似的模型相关术语,
理解这些术语的内涵有助于揣测作者写作的着重点,更有助于理解文章的内容。

术语、含义

术语 含义 用途
Definition 定义 定义一个词的含义
Theorem 定理 已经被证明的是正确的内容
Proposition 命题 不那么重要,但是又有点意思的正确的陈述
Lemma 引理 用于证明其他内容的正确陈述
Corollary 推论 从定理或命题中推导的正确陈述
Proof 证明 解释写的东西为什么是正确的
Conjecture 猜想 认为是正确的,但是没证出来的陈述
Axiom 公理 关于数学的基本假设
Note 注释 澄清没有解释清楚的事情
Remark 评述 强调\讨论\评论其他的概念或见解

个人理解

上述的术语在应用类型的运筹文章中,比较常见的有Definition、Theorem、Proposition、Lemma、Proof。
通常在模型或问题提出后,为了增加文章的理论深度,提出一些关于模型的有意思的、略带深度的性质,并提供证明Proof。
个人觉得Theorem是一个严肃的内容,其定义是“已经被证明的正确的内容“,但是在文章中证明出来了,
就算的上是”已经被证明“了。

Lemma、Proposition、Theorem的边界并不是清晰的,所以怎么用都行。
但是重要性的排序个人认为是:Theorem > Proposition > Lemma。

Remark一般在证明最后,强调一下和其他人的不同,或者强调有意义的事情。

记录在学校的一次matlab课上的技术分享。

前言:本次分享的主要内容是介绍一些我个人Matlab开发常用的经验总结,
目的是让大家了解到matlab的优缺点、独特性、技巧,以及用什么方法解决什么问题。
Matlab是一个复杂的工具,并不是我凭借一己之力可以完全说清楚的。
大部分各种理工科离不开这一工具,而各个学科之间隔行如隔山,
我只是把自己经常使用的经验进行总结,和各位共同分享。

假设:今天不会涉及编程思想(函数式、面向对象等),主要是向大家分享如何用好这一工具。
假设各位已经掌握matlab的基本语法,并且自己也会写一些代码了。

代码优化

同样是跑代码,为什么我要等很久?

我们不得不承认,Matlab这种解释型语言比编译型语言要慢(代码在运行时会被一行一行地解释执行),
这并不代表Matlab代码不能变得高效。

预分配内存

在进行循环语句时,应当先声明变量所需空间的大小,再进行循环,避免每次循环向数组中增加数字。

不推荐写法

1
2
3
4
5
6
tic
for i = 1:1000
mat(i,i) = i;
end
toc
% 历时 0.115061 秒。

推荐写法

1
2
3
4
5
6
7
tic
mat_2 = zeros(1000); % 先把数组的大小声明,在内存中开辟一块空间
for i = 1:1000
mat_2(i,i) = i;
end
toc
% 历时 0.005585 秒。

为什么出现这样的差异?

反复调整数组的大小将导致花费额外的时间找到更大的、连续的内存空间。

所有类型的变量都需要这样做吗?

不一定,cell类型则不需要,但是放在cell中的数据类型可能需要。
cell数组内存不连续,但是每个cell要求连续内存。
(部分情况下cell在编译成mex文件时,同样需要初始化内存)

参考链接:

https://ww2.mathworks.cn/help/matlab/matlab_prog/preallocating-arrays.html

https://ww2.mathworks.cn/help/matlab/matlab_prog/techniques-for-improving-performance.html

https://ww2.mathworks.cn/help/matlab/matlab_prog/avoid-unnecessary-copies-of-data.html

https://ww2.mathworks.cn/help/matlab/matlab_prog/strategies-for-efficient-use-of-memory.html

向量化

官方定义:

修正基于循环且面向标量的代码以使用 MATLAB 矩阵和向量运算的过程称为向量化

简单来说,就是替代for循环做一些简单操作,以提高运行速度、可读性、正确性。即,算法流程控制for循环,简单操作向量化。
向量化是matlab的一个重要特点,这在其他语言很少出现
向量化的语句通常有底层的支持,利用到了CPU指令集进行优化,因此速度较快,
而for循环遍历慢的原因是matlab为解释性语言需要一行一行执行。

求1至10000的每个数的正弦的和

循环写法:

1
2
3
4
5
6
7
8
9
tic
all_sum = 0;
for i = 1:1e4
all_sum = all_sum + sin(i);
end
disp(['all_sum: ', num2str(all_sum)])
toc
% all_sum: 1.6339
% 历时 0.038938 秒。

向量化写法:

1
2
3
4
5
6
tic
all_sum = sum(sin(1:1e4));
disp(['all_sum: ', num2str(all_sum)])
toc
% all_sum: 1.6339
% 历时 0.008211 秒。

为什么all_sum这么小?

隐式扩展,求每个人每科成绩与平均值的差距

1
2
3
4
5
6
7
8
9
% 每个人有三门课,
% 数 语 外
A = [97 89 84;
95 82 92;
64 80 99;
76 77 67;
88 59 74;
78 66 87;
55 93 85]; % 每一列是一个人

循环写法

1
2
3
4
5
mA = mean(A);			% 求每列的平均值
B = zeros(size(A)); % 记录每个人每门课的差距
for n = 1:size(A,2) % 对每一列进行操作
B(:,n) = A(:,n) - mA(n);
end

向量化写法

1
devA = A - mean(A)

注意,在向量化写法中,mean(A)的结果是1x3大小的。即使 A 是一个 7×3 矩阵,mean(A) 是一个 1×3 向量,MATLAB 也会隐式扩展该向量,就好像其大小与矩阵相同一样,并且该运算将作为正常的按元素减法运算来执行。(广播操作)

逻辑索引,线性索引,下标索引

介绍三种索引方式的区别

一个矩阵A ,寻找这个矩阵中大于500的数字的行列数

循环方法(单个循环,获得线性索引,双层循环,获得行列数)

1
2
3
4
5
6
7
8
9
10
11
12
13
A = magic(100);
tic
sub = zeros(sum(A>500, 'all'), 2);
count = 1;
for i = 1:100
for j = 1:100
if A(i,j) > 500
sub(count,:) = [i,j];
count = count + 1;
end
end
end
toc

find方法

1
2
3
4
5
A = magic(100);
tic
[row, col] = find(A>500);
sub = [row, col];
toc

处处用find真的合适吗?

求矩阵A中,所有大于500的数的和

sum(A(find(A>500)))线性索引

image-20230930182045754

sum(A>500))这里直接使用了逻辑索引,速度比find()更快。

重复数组

根据向量、矩阵创造大的重复矩阵。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
A = repmat(1:3,5,2)
B = repmat([1 2; 3 4],2,2)

% A =

% 1 2 3 1 2 3
% 1 2 3 1 2 3
% 1 2 3 1 2 3
% 1 2 3 1 2 3
% 1 2 3 1 2 3


% B =

% 1 2 1 2
% 3 4 3 4
% 1 2 1 2
% 3 4 3 4

思考题:多个等间隔指针的轮盘赌如何向量化?

随机遍历抽样(SUS)
从轮盘赌选择N个结果,则需转动N次转盘。
然而这样的抽选方式存在一定的不合理性,比如某个区域在轮盘中占据较大的份额,N次转动很有可能多次选中这个个体。

image-20230930192441679

现在想一次性选中多个区域,而且尽量不重复。
使用SUS只需转动一个等间隔指针的圆盘一次,转一次就可以选出所需区域。

SUS步骤
假设轮盘总值为F(可以是1),指针选择区域数目为N,则SUS具体步骤如下:

STEP1:计算指针的间距P=F/N;

STEP2:随机生成起点指针位置Start=[0~P之间的随机数];

STEP3:计算各指针的位置Pointers=[Start+i*P(其中i=[0,1,…N-1])];

STEP4:根据各指针位置,选择出N个个体。

for循环代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function sel_ind = sus_loop(fitness, sel_num)
% sus for循环实现,输入适应度和要选择的个数
% 输出被选择的个体的索引

% 验证输入的正确性
% 要求fitness是一个列向量, sel_num是一个数
validateattributes(fitness, {'numeric'}, {'size',[nan,1]})
validateattributes(sel_num, {'numeric'}, {'size',[1,1], '<', length(fitness)})

% 初始化
wheel = [0; cumsum(fitness ./ sum(fitness))]; % 构建一个轮盘,所有的适应度之和等于1
interval = 1 /sel_num; % 适应度是1,间距等于选择数量的倒数
start = rand * interval; % 随机起点位置
pointer = (0:interval:interval*(sel_num-1)) + start; % 指针

% 循环获取索引
try
sel_ind = zeros(sel_num, 1);
for i = 1:sel_num
p = pointer(i); % 指针
for j = 1:length(wheel)-1
% 如果指针p落在当前区间内,则这个个体被选中
if p >= wheel(j) && p < wheel(j+1)
sel_ind(i) = j;
continue
end
end
end
catch
save('bug_file_sus_loop.mat')
error('SUS循环出错')
end
end

向量化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function sel_ind = sus_vector(fitness, sel_num)
% sus for循环实现,输入适应度和要选择的个数
% 输出被选择的个体的索引

% 验证输入的正确性
% 要求fitness是一个列向量, sel_num是一个数
validateattributes(fitness, {'numeric'}, {'size',[nan,1]})
validateattributes(sel_num, {'numeric'}, {'size',[1,1], '<', length(fitness)})

% 初始化
wheel = [0; cumsum(fitness ./ sum(fitness))]; % 构建一个轮盘,所有的适应度之和等于1
interval = 1 /sel_num; % 适应度是1,间距等于选择数量的倒数
start = rand * interval; % 随机起点的偏移量
pointer = (0:interval:interval*(sel_num-1)) + start; % 指针

% 向量化获得索引
temp = bsxfun(@gt, pointer, wheel); % greater than

% 此时,索引就等于temp每一列最后一个1的索引,也可以是每一列第一个0的索引
% 对每一列求和就行
sel_ind = sum(temp)';

end

参考链接:https://ww2.mathworks.cn/help/matlab/matlab_prog/vectorization.html

冗余运算

消除for循环中的冗余运算有助于提升算法的性能。能放到外层的,就放到外层。

给定一个矩阵A,要求A的每个数乘以当前行数,在加上当前列数,再加一个固定的数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
for i = 1:size(A, 1) % 行
for j = 1:size(A, 2)
fix = 1;
tmp = A(i,j);
tmp = tmp * i + j;
A(i,j) = tmp + fix;
end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% fix 提出
fix = 1;
for i = 1:size(A, 1) % 行
for j = 1:size(A, 2)
tmp = A(i,j);
tmp = tmp * i + j;
A(i,j) = tmp + fix;
end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

fix = 1;
for i = 1:size(A, 1) % 行
A(i,:) = A(i,:) * i;
for j = 1:size(A, 2)
A(i,j) = A(i,j) + j + fix;
end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% 向量化
A = (1:size(A, 1))' .* A + (1:size(A, 2)) + fix

性能分析

我的代码怎么这么慢?想改,改什么呢?

这个时候,需要使用探查器进行代码性能分析

比如对debug.m脚本(模拟禁忌搜索的过程)进行性能分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
% debug.m
% 模拟禁忌搜索的过程
tic
tabu = zeros(30, 2);
for i = 1:1e5
rand_int = [randi(15), randi(15)];
if ismember(rand_int, tabu, "rows")
continue
else
tabu = [rand_int; tabu(1:end-1, :)];
end
end
toc

Step 1. 打开探查器

image-20230930192441679

Step 2. 在探查器中运行代码

image-20230930192625728

Step 3. 点击函数名称 获得具体情况

image-20230930192855816

发现原来是ismember函数运行太慢了,所以根据问题特性,我们自己写一个my_ismember函数,替换原有的ismember函数。

ismember函数耗时4.931秒,自己写的my_ismember函数耗时0.937秒。

image-20230930194758499

上述例子说明:

  • 不一定内置函数就足够好,一些函数内置函数为了支持各种数据类型、大小,还是很慢的

  • 根据问题、数据结果特征进行重构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function flag = my_ismember(a, b)
    % 判断a是否在b中

    validateattributes(a, {'numeric'}, {'size',[1,2]})
    validateattributes(b, {'numeric'}, {'size',[nan,2]})

    col_1_flag = b(:, 1) == a(1);
    col_2_flag = b(:, 2) == a(2);

    if any(col_1_flag & col_2_flag)
    flag = true;
    else
    flag = false;
    end
    end

    注意: 探查器中运行的代码速度包括了统计时间,因此比一般方式下运行更慢。

    参考:https://www.mathworks.com/help/releases/R2023b/matlab/matlab_prog/profiling-for-improving-performance.html

使用更具效率的算法

有些时候,真正慢的是使用的算法的复杂度很高(O(n2)O(n^2)甚至O(n3)O(n^3), 或者说是算法过于朴素),跟向量化等上述技术无关。这种慢根源于算法的复杂度,解决方法是换个思路重写。

双数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
% 输入:nums = [2,7,11,15], target = 9
% 输出:[0,1]
% 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

% 随机构造
rng(pi);
nums = [2,7,11,15];
rand_num = randperm(5e4) + 20;
nums = [nums, randperm(5e4) + 20];

nums = nums(randperm(length(nums))); % nums长度为50004
target = 9;

tic
two_sum(nums, target)
toc
% ans =
% 36335 36975
% 历时 4.324590 秒。

tic
two_sum_hash(nums, target)
toc
% ans =
% 36335 36975
% 历时 0.020854 秒。

% 足足200倍速度差距

暴力枚举(朴素)算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function output = two_sum(nums, target)
% O(N^2)
len_num = length(nums);
for i = 1:len_num
for j = 1:len_num
if i == j
continue
end

if nums(i) + nums(j) == target
output = [i,j];
return
end
end
end
end

字典 - 空间换时间

1
2
3
4
5
6
7
8
9
10
11
12
function output = two_sum_hash(nums, target)
% 该代码使用了dictionary(), 仅支持2022b以上版本
len_num = length(nums);
d = dictionary(nums, 1:len_num); % O(N)
for i = 1:len_num % O(N)
val_1 = nums(i);
val_2 = target - val_1;
if isKey(d, val_2) && d(val_2) ~= i
output = [d(val_2), i];
end
end
end

工具箱

Matalb Coder

Matlab和python一样是一门解释性语言,因此运行速度不如编译型语言(C++等),因此一些复杂的函数运行起来相对较慢。

不过,Matlab Coder提供了一种方法,使得我们可以将一个函数编译成C或C++或二进制文件,供Matlab使用。步骤如下:

你需要一个Matlab Coder工具箱。如果没有,则在附加功能中获取。在顶部APP中打开Coder。

image-20231001134101744

image-20231001134208296

  • 进入Select环节:键入你想编译的函数,注意只能是函数,不能是脚本。这里以lagrangian_relax函数为例,然后点击next。image-20231001134303941
  • 进入 review环节:在这个环节,Coder会检查你的代码有无问题,这个步骤会检查lagrangian_relax函数调用的所有函数。image-20231001134944337

在底部出现了一条警告,此时应注意,警告和错误是不同的,程序可以有警告(前提是已知警告产生的原因和后果),错误则不同。错误将直接导致程序运行不通。

解决完所有问题后,点Next。

  • 进入Define环节: 该环节定义了所有变量的类型&大小。

    image-20231001135319241

    在上方的框中,输入调用lagrangian_relax函数的脚本,然后点Autodefine Input Types按钮,等程序运行,所有的变量的大小自动确定。但是,自动确定的变量大小类型是固定的,也就是说只能输入这种大小的数据,所以,对一些不确定大小的数据,需要手动将大小调整成inf。点Next,继续。

  • 进入 check环节:Coder会运行之前输入的脚本,检查是否有运行问题、编译问题等等。这一步可能会出现各种报错,这些报错的原因并不是lagrangian_relax函数的结果有问题,而是写脚本的习惯超出编译器可理解的范围,或者编译器认为写的不严谨,数据类型/大小匹配等一系列问题。需要逐个修改,直至通过编译。

image-20231001140304900

  • 进入 generate环节:如果仅仅想在matlab中调用编译好的二进制文件提升运行速度,那么选择mex即可,点Geneate等待生成。

    image-20231001140519650

  • 完成
    image-20231001140532936

接下来,我们就可以像使用一个普通函数一样使用这个mex文件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tic
lagrangian_relax_mex(data, super_cus, param); % 生成的mex文件
toc
% Iter: 25; LB: 32233.19; UB*: 42478.36; Cons: 2.000000; Gap: 24.12 %
% LR finish: max iteration
% 历时 3.555206 秒。


tic
lagrangian_relax(data, super_cus, param); % 原来的函数
toc
% Iter: 25; LB: 32233.19; UB*: 42478.36; Cons: 2.000000; Gap: 24.12 %
% LR finish: max iteration
% 历时 93.820573 秒。

可以观察到,两种方法输出的结果是一样的,但是速度差了30倍。

Coder的缺点:

  • 并不是所有内置函数都支持Coder的,例如dictionary就不支持。不支持的函数会在编译时报错。

  • Coder编译的文件来源于选择的C++编译器,很可能出现mex文件在另一台电脑上无法运行的情况。

  • Coder编译的文件的另一个问题是不能跨平台,mac/win/linux编译得到的文件是不同的。

参考链接:通过生成 MEX 函数加快 MATLAB 算法的执行速度

从 MATLAB 代码生成 C 和 C++ 代码

MATLAB Coder入门到放弃

https://blog.csdn.net/qq_36584460/article/details/110791330

并行计算工具箱 parallel computing toolbox

前面提到过利用多核心进行并行计算,需要并行计算工具箱。

该工具箱通过在本地运行的 worker(MATLAB 计算引擎)上执行应用程序,允许你充分利用多核台式机的处理能力。无需更改代码,即可在集群或云上运行同一个应用程序。还可以将该工具箱与 MATLAB Parallel Server 结合使用,以执行由于太大而无法装入单台机器内存的矩阵计算

并行for循环

1
2
3
4
5
6
7
8
9
tic
n = 200;
A = 500;
a = zeros(1,n);
for i = 1:n
a(i) = max(abs(eig(rand(A))));
end
toc
% 历时 37.256415 秒。
1
2
3
4
5
6
7
8
9
tic
n = 200;
A = 500;
a = zeros(1,n);
parfor i = 1:n
a(i) = max(abs(eig(rand(A)))); % 最大 / 绝对 / 特征值 / 随机
end
toc
% 历时 8.028954 秒。

使用场景:许多简单计算的循环,parfor将循环划分为组,以便每个线程可以执行一组。

**但是,**循环中一组循环的结果依赖前面某次循环的结果时,不能使用并行计算(归约Reduction Variables除外)。并行计算有一系列局限性,而且在一些情况下(例如,广播变量)并不能加速代码运行。

使用parfor的情况:

  • 每次的for循环很慢(单个时间长)
  • 简单计算的多次循环(单个时间短但是数量多)
  • 多层for嵌套的外层使用parfor更具收益(思考:为什么?

不推荐用parfor的情况:

  • 已经将for向量化的情况(向量化比parfor收益更高)
  • 循环已经很快了(并行化的时间开销更高,包括初始化,分配等)

强烈反对的情况:

  • 向量化 -> for循环 -> parfor循环 (反向优化)

forparfor的一个不可行举例

image-20231001201801595

for转parfor一定程度上很复杂,有各式各样的规则限制、写法限制,这里只做一个简单介绍。关于什么时候用parfor,如何将for转为parfor,使用parfor的要求,常见问题等,参见parfor文档

参考链接:在多核计算机、GPU 和集群上执行并行计算

补充:parfor可以和coder结合使用,生成的mex文件也是支持多核计算的

符号计算工具箱 Symbolic Math Toolbox

符号计算工具箱,就是能计算数学公式的一个工具箱,变量不再是数值而是符号。因此可以求计算定积分不定积分解析解,计算符号表达式或函数的导数,并使用级数展开式逼近函数,等等。

例如,求absinxdx\int_a^b \sin x dx[π2,3π2][\frac{\pi}{2}, \frac{3\pi}{2}]上的积分

1
2
3
syms x
int(sin(x),pi/2,3*pi/2)
% ans = 0

这只是一个简单的例子,表示我们可以使用matlab做这件事,具体应用:

某博弈论问题求纳什均衡点

解方程组:

image-20231001203458641

解得四个解中的一个解为纳什均衡点。

image-20231001151417317

纳什均衡点E4E_4的雅克比矩阵

image-20231001151523651

注:符号计算工具箱不支持coder

如何查看是否支持 coder?详见函数参考文档的底部支持列表

技巧经验

使用结构体进行传参

假设你有一辆车,车长4米,平时跑72km/h,车重1000kg,载重2000kg,现在你写了一个函数f用于估计油耗。

1
2
3
function consume = f(len, velocity, weight, capacity)
...
end

使用结构体封装一下

1
2
3
4
5
6
7
8
9
vehicle = struct()
vehicle.len = 4;
vehicle.velocity = 72;
vehicle.weight = 1000;
vehicle.capacity = 2000;

function consume = f(vehicle)
...
end

包括传出,也可以使用结构体,解决了参数记错的问题。

consume = [1, 2, 3, 4]

consume. air = 1; % 空气阻力产生的燃料消耗

consume.weight = 2; % 重量产生的消耗

...

给名字就不容易错了,比consume(1) consume(2) 这种要方便

使用句柄函数@

C = bsxfun(fun,A,B)

Binary Singleton Expansion Function

对数组 AB 应用函数句柄 fun 指定的按元素二元运算。

1
2
3
4
5
6
7
8
9
10
11
A = [8; 17; 20; 24]
B = [0 10 21]
C = bsxfun(@gt,A,B)

% C = 4x3 logical array

% 1 0 0
% 1 1 0
% 1 1 0
% 1 1 1

其中@gt的意思是大于"greater than", 因此生成了隐式扩展结果。

接下来,给定一个向量a和一个向量b,求aeba-e^b的所有组合的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a = [1 2 3 4];
b = [5; 6; 7];

% 第一种方法
c = zeros(length(b), length(a));
for i = 1:length(a)
for j = 1:length(b)
c(j,i) = a(i) - exp(b(j));
end
end
disp(c)

% 第二种方法
f = @(a, b) a - exp(b);
c = bsxfun(f, a, b);
disp(c)

A = cellfun(func, C)

对元胞数组C的每个元胞进行操作。

获取元胞数组中,每个元胞的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C = cell(3, 1);
C{1,1} = 'Sunday';
C{2,1} = magic(3);
C{3,1} = 1:10;

% 循环操作
A = zeros(size(C, 1), 1);
for i = 1:size(C, 1)
A(i) = length(C{i,1}(:));
end
disp(A)
% [6; 9; 10]

% cellfun
A = cellfun(@(x) length(x(:)), C)
% [6; 9; 10]

A = structfun(func, S)

对标量结构体的每个字段应用函数

1
2
3
4
5
6
7
8
9
10
S.f1 = 1:10;
S.f2 = [2; 4; 6];
S.f3 = []

A = structfun(@mean,S)
% A = 3×1

% 5.5000
% 4.0000
% NaN

运行日志

将命令行窗口文本记录到日志文件中

1
2
3
4
5
6
7
diary 'myDiaryFile.txt'

for i = 1:10
disp(exp(i))
end

diary off

你就会得到一个myDiaryFile.txt文件,用于记录所有打印的结果。

用处:追踪程序运行状况,运行到哪次迭代?运行到哪一步?配合其他函数,还可以记录何时运行等信息。

格式化输出

格式化字符串

1
2
3
4
5
6
7
8
9
formatSpec = 'The array is %dx%d.';
A1 = 2;
A2 = 3;
str = sprintf(formatSpec,A1,A2)
% str =
% 'The array is 2x3.'

fprint(formatSpec,A1,A2)
% 'The array is 2x3.'

参考: https://www.mathworks.com/help/releases/R2023b/matlab/matlab_prog/profiling-for-improving-performance.html

有什么用?多次运行一个函数,但是输入参数不同时,配合diary函数可生成结果日志

image-20231001164150891

解放双手,从自动运行做起,拒绝手动保存每次的运行结果。

运行时间

想要获取运行了多久?使用tictoc函数统计代码运行时间。

高级用法:使用多个tic测量时间

1
2
3
4
5
6
7
8
9
10
11
12
tStart = tic;           % pair 2: tic
n = 10;
T = zeros(1,n);
for i = 1:n
A = rand(12000,4400);
B = rand(12000,4400);
tic % pair 1: tic
C = A.*B;
T(i)= toc; % pair 1: toc
end
tMul = sum(T) % tMul = 0.6037
tEnd = toc(tStart) % tEnd = 11.6301

其他统计代码运行时长的方法:

t = cputime

t = datetime

多层嵌套

看这样一个代码,每月每天你孤单会想起谁:

1
2
3
4
5
6
7
8
9
10
for month = 1:12  % 月
for day = 1:30 % 天
if 孤单?
你想起一个人
if 想找个人来陪
找!
end
end
end
end

上述代码的问题:嵌套太多了,一层一层的嵌套。(写代码的习惯,不影响执行效率,但是影响可读性)

1
2
3
4
5
6
7
8
9
10
11
12
for month = 1:12  % 月
for day = 1:30 % 天
if ~ 孤单? % 非孤单 "~"取反
continue
end

你想起一个人
if 想找个人来陪
找!
end
end
end

使用continue关键字跳转,避免嵌套,提升可读性

Latex公式

latex是特别常用且提升效率的公式编辑方法,推荐大家使用!

好处:输入公式时,手不再使用鼠标,全键盘操作

可以使用的场景:

  • word
  • markdown
  • latex
  • mathtype
  • matlab

实时脚本做展示

一般不用这个功能做开发,但是实时脚本是良好的展示工具

类似于 python 的 jupyter notebook

image-20231006133646069

数值精度问题

数值精度问题普遍存在于计算机语言中,我们不讨论产生数值精度的具体原因,只需要知道存在这样的问题。

例如:

1
2
3
4
5
6
7
8
0.1 + 0.2 == 0.3

% ans =

% logical

% 0

在for循环中,需要避免以下的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
for i = 0.0:0.1:1.0
disp('-------------------')
for j = 0.0:0.1:1.0-i
k = 1.0 - i - j;
disp([i,j,k])
end
end

% -------------------
% 0.7000 0 0.3000
% 0.7000 0.1000 0.2000
% 0.7000 0.2000 0.1000
% 0.7000 0.3000 -0.0000

出现一个非常小的负数,极有可能导致bug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for i = 0:10
disp('-------------------')
for j = 0:(10-i)
k = 10 - i - j;
disp([i,j,k] / 10)
end
end

-------------------
0.7000 0 0.3000

0.7000 0.1000 0.2000

0.7000 0.2000 0.1000

0.7000 0.3000 0

在线Matlab

电脑没有matlab?只要有许可证,就能使用matlab在线编程(https://ww2.mathworks.cn/products/matlab-online.html)。

Markdown语言

本文档就是使用的markdown编写的,这是一种文本标记语言,实现文本的不同格式、样式。

如何DEBUG

断点

调试程序是每个编程语言必备的操作,没有人能完美写完一个复杂的代码并且没有任何小错误。当这些错误直观发现不了时,使用断点追踪程序每步的运算有助于发现问题。

image-20231001165505931

条件断点

例如在这个循环中,每次j=3时就报错,我们增加一个条件断点,当满足这个条件时,程序暂停。

image-20231001165725593

image-20231001165858415

错误暂停

image-20231001165922621

建议把出现错误暂停勾选,这样程序会停止在出错的哪一行,有助于分析/复现问题。

image-20231001165952424

搜索问题

当一个问题,自己不能解决的时候,我们通常会使用搜索引擎进行检索,如何获得问题的答案?

例如:

我想删除一个cell数组中空的元胞,保留下非空的元胞

一般我会用 “语言 + 关键词 ”搜索

  • 搜索方法:MATLAB cell删除空元胞

  • 结果质量:

    • Matlab中文论坛 / 知乎 / 博客园
    • CSDN(垃圾堆里找吃的,但不代表都是垃圾) / 简书
    • 百度知道 (几乎驴唇不对马嘴) / 其他小网站

image-20231001170909421

向他人提问

向他人提问,包括同学、老师、网友等。

为什么向他人提问很重要?正确的提问方式有助于提升双方沟通效率,快速发现问题。

错误的提问方式

  1. 不会截图 用手机拍照的

    image-snap

  2. 截图截一半 只截图报错信息traceback, 不截图代码的

    image-20231001171745717

  3. 不截图前后文的

    为什么我这句代码报错了?

    image-20231001172136136

  4. 自己不思考、不检索,直接问别人的。

    我想删除一个cell数组中空的元胞,保留下非空的元胞,matlab怎么写?

正确的提问方式(不仅适用于代码提问)

  • 尽可能详细说出你的问题

  • 提供代码(如果是截图,截图要全面)

  • 问题如何复现

  • 你已经做了什么操作,但是还是不行

  • 发送之前读一读你的提问

    举例 1:

    image-20231001172945916

    举例 2:image-20231001173153802

文档

如果你知道matlab的某个函数的名字,但是不会写,那么可以在matlab右上角搜索文档,也可以在命令行中使用help 'fun_name'获得文档。

image-20231001173452560

此外,在命令行中输入doc 'fun_name'可以打开在线文档。

1
doc zeros

image-20231001173620154

外部功能

Python

很多时候,我们发现某个功能只在python中有,matlab中没有或者不如python好用。

这个时候,我们可以写一个python函数,然后让matlab调用这个py函数,再将结果处理成matlab可接收的类型即可。

举例来说,Gurobi对Python支持远远比matlab好,建模也更加方便,我们可以使用matlab调用python,python调用gurobi来实现:

首先,我们写一个python调用gurobi的函数grbpy_v.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# grbpy_v.py
import gurobipy as gp
import numpy as np
import math

def model(f, alpha, beta, gamma, mu, kappa):
# 集合
I = range(len(f))
P = range(len(alpha[0]))
N = range(len(gamma[0]))
M = range(len(beta[0]))

# 建模
model = gp.Model()

# 变量
v = model.addVars((i for i in I), vtype = gp.GRB.BINARY, name ='v')

# 目标
obj = gp.quicksum((f[i] - gp.quicksum(alpha[i,p] for p in P) \
- gp.quicksum(beta[i,m] - mu[i,m] for m in M)\
- gp.quicksum(gamma[i,n] - kappa[i,n] for n in N)) * v[i] for i in I) \
- gp.quicksum(mu[i,m] for i in I for m in M) \
- gp.quicksum(kappa[i,n] for i in I for n in N)
model.setObjective(obj, sense = gp.GRB.MINIMIZE)

# 约束
# 无

# 求解
model.Params.OutputFlag = 0
model.Params.MIPGap = 0.01
model.optimize()

# 整理
loc = np.zeros([1,len(f)])
for i in I:
if math.isclose(v[i].X, 1):
loc[0,i] = 1

obj_val = model.getAttr('ObjVal')

return (loc, obj_val)

然后,我们写一个matlab脚本(函数)调用这个py函数

1
2
3
4
5
6
7
8
9
10
py.importlib.reload(py.importlib.import_module('grbpy_v'));

fix_ws_py = py.numpy.array(fixed_ws);
alpha_py = py.numpy.array(alpha);
beta_py = py.numpy.array(beta);
mu_py = py.numpy.array(mu);
gamma_py = py.numpy.array(gamma);
kappa_py = py.numpy.array(kappa);

res = py.grbpy_v.model(fix_ws_py, alpha_py, beta_py, gamma_py, mu_py, kappa_py);

注意上述存在类型转换,需要把matlab的矩阵转成python的numpy矩阵才可以使用。

参考:从 MATLAB 中调用 Python

Gurobi (运筹优化专业)

Gurobi是一款求解器,做运筹优化方面常常会用到,这里将介绍直接使用matlab调用gurobi,不借助其他软件或包。

来源于Guorbi官方文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
function facility()

% Copyright 2023, Gurobi Optimization, LLC
%
% Facility location: a company currently ships its product from 5 plants
% to 4 warehouses. It is considering closing some plants to reduce
% costs. What plant(s) should the company close, in order to minimize
% transportation and fixed costs?
%
% Note that this example uses lists instead of dictionaries. Since
% it does not work with sparse data, lists are a reasonable option.
%
% Based on an example from Frontline Systems:
% http://www.solver.com/disfacility.htm
% Used with permission.

% define primitive data
nPlants = 5;
nWarehouses = 4;
% Warehouse demand in thousands of units
Demand = [15; 18; 14; 20];
% Plant capacity in thousands of units
Capacity = [20; 22; 17; 19; 18];
% Fixed costs for each plant
FixedCosts = [12000; 15000; 17000; 13000; 16000];
% Transportation costs per thousand units
TransCosts = [
4000; 2000; 3000; 2500; 4500;
2500; 2600; 3400; 3000; 4000;
1200; 1800; 2600; 4100; 3000;
2200; 2600; 3100; 3700; 3200];

% Index helper function
flowidx = @(w, p) nPlants * w + p;

% Build model
model.modelname = 'facility';
model.modelsense = 'min';

% Set data for variables
ncol = nPlants + nPlants * nWarehouses;
model.lb = zeros(ncol, 1);
model.ub = [ones(nPlants, 1); inf(nPlants * nWarehouses, 1)];
model.obj = [FixedCosts; TransCosts];
model.vtype = [repmat('B', nPlants, 1); repmat('C', nPlants * nWarehouses, 1)];

for p = 1:nPlants
model.varnames{p} = sprintf('Open%d', p);
end

for w = 1:nWarehouses
for p = 1:nPlants
v = flowidx(w, p);
model.varnames{v} = sprintf('Trans%d,%d', w, p);
end
end

% Set data for constraints and matrix
nrow = nPlants + nWarehouses;
model.A = sparse(nrow, ncol);
model.rhs = [zeros(nPlants, 1); Demand];
model.sense = [repmat('<', nPlants, 1); repmat('=', nWarehouses, 1)];

% Production constraints
for p = 1:nPlants
for w = 1:nWarehouses
model.A(p, p) = -Capacity(p);
model.A(p, flowidx(w, p)) = 1.0;
end
model.constrnames{p} = sprintf('Capacity%d', p);
end

% Demand constraints
for w = 1:nWarehouses
for p = 1:nPlants
model.A(nPlants+w, flowidx(w, p)) = 1.0;
end
model.constrnames{nPlants+w} = sprintf('Demand%d', w);
end

% Save model
gurobi_write(model,'facility_m.lp');

% Guess at the starting point: close the plant with the highest fixed
% costs; open all others first open all plants
model.start = [ones(nPlants, 1); inf(nPlants * nWarehouses, 1)];
[~, idx] = max(FixedCosts);
model.start(idx) = 0;

% Set parameters
params.method = 2;

% Optimize
res = gurobi(model, params);

% Print solution
if strcmp(res.status, 'OPTIMAL')
fprintf('\nTotal Costs: %g\n', res.objval);
fprintf('solution:\n');
for p = 1:nPlants
if res.x(p) > 0.99
fprintf('Plant %d open:\n', p);
end
for w = 1:nWarehouses
if res.x(flowidx(w, p)) > 0.0001
fprintf(' Transport %g units to warehouse %d\n', res.x(flowidx(w, p)), w);
end
end
end
else
fprintf('\n No solution\n');
end

end

想表达的是,gurobi对matlab很不友好:

  • 系数矩阵必须是自己算明白索引后,转换为一个稀疏矩阵。
  • 很难按照约束一条一条添加
  • 有些gurobi功能matlab不能使用

同样模型,python的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#!/usr/bin/env python3.7

# Copyright 2023, Gurobi Optimization, LLC

# Facility location: a company currently ships its product from 5 plants
# to 4 warehouses. It is considering closing some plants to reduce
# costs. What plant(s) should the company close, in order to minimize
# transportation and fixed costs?
#
# Note that this example uses lists instead of dictionaries. Since
# it does not work with sparse data, lists are a reasonable option.
#
# Based on an example from Frontline Systems:
# http://www.solver.com/disfacility.htm
# Used with permission.

import gurobipy as gp
from gurobipy import GRB


# Warehouse demand in thousands of units
demand = [15, 18, 14, 20]

# Plant capacity in thousands of units
capacity = [20, 22, 17, 19, 18]

# Fixed costs for each plant
fixedCosts = [12000, 15000, 17000, 13000, 16000]

# Transportation costs per thousand units
transCosts = [[4000, 2000, 3000, 2500, 4500],
[2500, 2600, 3400, 3000, 4000],
[1200, 1800, 2600, 4100, 3000],
[2200, 2600, 3100, 3700, 3200]]

# Range of plants and warehouses
plants = range(len(capacity))
warehouses = range(len(demand))

# Model
m = gp.Model("facility")

# Plant open decision variables: open[p] == 1 if plant p is open.
open = m.addVars(plants,
vtype=GRB.BINARY,
obj=fixedCosts,
name="open")

# Transportation decision variables: transport[w,p] captures the
# optimal quantity to transport to warehouse w from plant p
transport = m.addVars(warehouses, plants, obj=transCosts, name="trans")

# You could use Python looping constructs and m.addVar() to create
# these decision variables instead. The following would be equivalent
# to the preceding two statements...
#
# open = []
# for p in plants:
# open.append(m.addVar(vtype=GRB.BINARY,
# obj=fixedCosts[p],
# name="open[%d]" % p))
#
# transport = []
# for w in warehouses:
# transport.append([])
# for p in plants:
# transport[w].append(m.addVar(obj=transCosts[w][p],
# name="trans[%d,%d]" % (w, p)))

# The objective is to minimize the total fixed and variable costs
m.ModelSense = GRB.MINIMIZE

# Production constraints
# Note that the right-hand limit sets the production to zero if the plant
# is closed
m.addConstrs(
(transport.sum('*', p) <= capacity[p]*open[p] for p in plants), "Capacity")

# Using Python looping constructs, the preceding would be...
#
# for p in plants:
# m.addConstr(sum(transport[w][p] for w in warehouses)
# <= capacity[p] * open[p], "Capacity[%d]" % p)

# Demand constraints
m.addConstrs(
(transport.sum(w) == demand[w] for w in warehouses),
"Demand")

# ... and the preceding would be ...
# for w in warehouses:
# m.addConstr(sum(transport[w][p] for p in plants) == demand[w],
# "Demand[%d]" % w)

# Save model
m.write('facilityPY.lp')

# Guess at the starting point: close the plant with the highest fixed costs;
# open all others

# First open all plants
for p in plants:
open[p].Start = 1.0

# Now close the plant with the highest fixed cost
print('Initial guess:')
maxFixed = max(fixedCosts)
for p in plants:
if fixedCosts[p] == maxFixed:
open[p].Start = 0.0
print('Closing plant %s' % p)
break
print('')

# Use barrier to solve root relaxation
m.Params.Method = 2

# Solve
m.optimize()

# Print solution
print('\nTOTAL COSTS: %g' % m.ObjVal)
print('SOLUTION:')
for p in plants:
if open[p].X > 0.99:
print('Plant %s open' % p)
for w in warehouses:
if transport[w, p].X > 0:
print(' Transport %g units to warehouse %s' %
(transport[w, p].X, w))
else:
print('Plant %s closed!' % p)

Yalmip (运筹优化方向)

yalmip是一款基于matlab进行数学建模的包,或者说是一种基于matlab的建模语言,使用yalmip的好处是解决了matlab建模的痛点,并且可以调用多个求解器对模型进行求解。

然而,求解器方面,如果用gurobi / COPT,则一般使用python建模。如果用cplex,则使用java。

如果不会别的语言,必须使用matlab进行建模和求解的话,yalmip无疑是最好的选择。

gramm(画图)

gramm是基于matlab开发的一个绘图工具,可以画很多好看的图。

matlab的图一般都很丑,需要自己不断调试。而gramm默认的配色,基本可以直接使用。

image-20231001184526448

参考:

https://www.mathworks.com/matlabcentral/fileexchange/54465-gramm-complete-data-visualization-toolbox-ggplot2-r-like

https://github.com/piermorel/gramm

One more thing

科研常用工具列表,成为一个合格的板砖人(科研民工)

名字 价格 用处
Office三件套 学校提供
Mathtype 收费 数学公式编辑(推荐搭配latex数学命令使用)
Typora 收费 markdown编辑器,本次分享使用markdown完成
VScode 免费 地表最强编辑器,我用vscode开发python, latex, markdown
Notion 免费 个人知识库(上手很慢,但是可以很好规划你的生活)
Endnote 学校提供 个人文献库(直接插入国标引用格式到word)
Calibre 免费 个人图书馆
百度云 功能收费 备份+同步(一年不到200块,同步文件+备份文件)
Office 365 收费 与Office年度版本不同,这是订阅制的office,享受更多功能+1T 云盘
Acrobat DC 收费 PDF编辑器
Latex 免费 写期刊论文使用,直接输出pdf
Overleaf / Slagger 免费 在线latex
Powertoys 免费 Windows拓展工具(非常强大,保持唤醒、取色、分屏等等等)

结语

Matlab优缺点

优点:

  • Matlab (matrix laboratory) 是一流的矩阵(Matrix)计算工具,提供了强大的矩阵计算方法
  • 预先定义好的功能,上手容易、语法简单(个人觉得比python还简单)
  • 图形化的界面,独立的平台
  • 来自官方的工具箱,长期支持和维护

缺点:

  • 一些数据结构在matlab中是没有原生支持的(字典结构是最近加入的,以前使用字典,优先队列等需要调用java)
  • 商业软件很少有公司使用,生存在科研机构,底层是闭源的
  • 运行速度较慢

如何使用Matlab

  • 使用正版,获得更多的技术支持,工具箱,社区等。
  • 使用新版,获得更多的功能,更快的运行,更美的界面。

怎么学Matlab

  • 只进行数值计算则matlab不是唯一选择(python, java等都可以),matlab的精髓是工具箱和仿真。
  • 任何一门语言的技能成长过程是多写多看多练,练习中成长。(编程语言不是自然语言,朗读代码不会获得进步。)

希望各位能从今天的分享中收获自己想要的。感谢各位!

0%