数学建模中的常用模型和算法

"数学建模国赛训练"

Posted by Summer on September 1, 2025

遗传算法

介绍

现在有一个函数, 这个函数存在着很多的极大值和极小值。而最大值则是指定区间的极大值中的最大的那一个。从图像上具体表现为,极大值像是一座座山峰,极小值则是像一座座山谷。因此,我们也可以把遗传算法的过程看作是一个在多元函数里面求最优解的过程。

这些山峰对应着局部最优解,其中有一个山峰是海拔最高的,这个山峰则对应的是全局最优解。那么,遗传算法要做的就是尽量爬到最高峰,而不是困在较低的小山峰上。

既然我们把函数曲线理解成一个一个山峰和山谷组成的山脉。那么我们可以设想所得到的每一个解就是一只袋鼠,我们希望它们不断的向着更高处跳去,直到跳到最高的山峰。所以求最大值的过程就转化成一个“袋鼠跳”的过程。

袋鼠可以这样跳

  • 爬山算法:一只袋鼠朝着比现在高的地方跳去。它找到了不远处的最高的山峰。但是这座山不一定是最高峰。这就是爬山算法,它不能保证局部最优值就是全局最优值。
  • 模拟退火:袋鼠喝醉了。它随机地跳了很长时间。这期间,它可能走向高处,也可能踏入平地。但是,它渐渐清醒了并朝最高峰跳去。这就是模拟退火算法。
  • 遗传算法:有很多袋鼠,它们降落到喜玛拉雅山脉的任意地方。这些袋鼠并不知道它们的任务是寻找珠穆朗玛峰。但每过几年,就在一些海拔高度较低的地方射杀一些袋鼠。于是,不断有袋鼠死于海拔较低的地方,而越是在海拔高的袋鼠越是能活得更久,也越有机会生儿育女。就这样经过许多年,这些袋鼠们竟然都不自觉地聚拢到了一个个的山峰上,可是在所有的袋鼠中,只有聚拢到珠穆朗玛峰的袋鼠被带回了美丽的澳洲。

算法步骤

遗传算法中每一条染色体,对应着遗传算法的一个解决方案,一般我们用适应性函数(fitness function)来衡量这个解决方案的优劣。所以从一个基因组到其解的适应度形成一个映射。遗传算法的实现过程实际上就像自然界的进化过程那样。

1.首先寻找一种对问题潜在解进行“数字化”编码的方案。(建立表现型和基因型的映射关系) 2.随机初始化一个种群(那么第一批袋鼠就被随意地分散在山脉上),种群里面的个体就是这些数字化的编码。 3.接下来,通过适当的解码过程之后(得到袋鼠的位置坐标)。 4.用适应性函数对每一个基因个体作一次适应度评估(袋鼠爬得越高当然就越好,所以适应度相应越高)。 5.用选择函数按照某种规定择优选择(每隔一段时间,射杀一些所在海拔较低的袋鼠,以保证袋鼠总体数目持平。)。 6.让个体基因变异(让袋鼠随机地跳一跳)。 7.然后产生子代(希望存活下来的袋鼠是多产的,并在那里生儿育女)。

遗传算法并不保证你能获得问题的最优解,但是使用遗传算法的最大优点在于你不必去了解和操心如何去“找”最优解。(你不必去指导袋鼠向那边跳,跳多远。)而只要简单的 “否定” 一些表现不好的个体就行了。(把那些总是爱走下坡路的袋鼠射杀,这就是遗传算法的精粹!)

编码

二进制编码法, 浮点编码法, 符号编码法

评价个体的适应度–适应度函数

输入袋鼠的位置坐标,在通过相应查找运算,返回袋鼠当前位置的海拔高度。

射杀袋鼠-选择函数

几种常用的选择算子:

  • 轮盘赌选择: 是一种回放式随机采样方法。每个个体进入下一代的概率等于它的适应度值与整个种群中个体适应度值和的比例。选择误差较大。
  • 随机竞争选择(Stochastic Tournament):每次按轮盘赌选择一对个体,然后让这两个个体进行竞争,适应度高的被选中,如此反复,直到选满为止。
  • 最佳保留选择:首先按轮盘赌选择方法执行遗传算法的选择操作,然后将当前群体中适应度最高的个体结构完整地复制到下一代群体中。
  • 无回放随机选择(也叫期望值选择Excepted Value Selection):根据每个个体在下一代群体中的生存期望来进行随机选择运算。方法如下:
    • 计算群体中每个个体在下一代群体中的生存期望数目N。
    • 若某一个体被选中参与交叉运算,则它在下一代中的生存期望数目减去0.5,若某一个体未 被选中参与交叉运算,则它在下一代中的生存期望数目减去1.0。
    • 随着选择过程的进行,若某一个体的生存期望数目小于0时,则该个体就不再有机会被选中。
  • 确定式选择:按照一种确定的方式来进行选择操作。具体操作过程如下:
    • 计算群体中各个个体在下一代群体中的期望生存数目N。
    • 用N的整数部分确定各个对应个体在下一代群体中的生存数目。
    • 用N的小数部分对个体进行降序排列,顺序取前M个个体加入到下一代群体中。至此可完全确定出下一代群体中M个个体。
  • 无回放余数随机选择:可确保适应度比平均适应度大的一些个体能够被遗传到下一代群体中,因而选择误差比较小。

  • 均匀排序:对群体中的所有个体按期适应度大小进行排序,基于这个排序来分配各个个体被选中的概率。

  • 最佳保存策略:当前群体中适应度最高的个体不参与交叉运算和变异运算,而是用它来代替掉本代群体中经过交叉、变异等操作后所产生的适应度最低的个体。

  • 随机联赛选择:每次选取几个个体中适应度最高的一个个体遗传到下一代群体中。

  • 排挤选择:新生成的子代将代替或排挤相似的旧父代个体,提高群体的多样性。

详细代码可见文章末尾

层次分析法(AHP)

建立递阶层次结构模型(目标层,准则层,方案层)

构造判断矩阵, 对指标的重要性两两比较, 构造判断矩阵. 矩阵中元素$a_{ij}$的含义是, 第i个元素相对于第j个元素的重要程度, 取值范围从1到9之间离散, 取1是两个因素同样重要, 取9是i比j极端重要. (同样,稍微,明显,强烈,极端);第j个元素相对于第i个元素的重要性,取倒数

因为两两比较的时候忽略了其他因素, 因此需要一致性检验

一致矩阵:

  • 若矩阵中每个元素 $a_i>0$ 且满足$a_{ij} \times a_{ji}=1$,则我们称该矩阵为正互反矩阵。在层次分析法中,我们构造的判断矩阵均是正互反矩阵
  • 若正互反矩阵满足$a_{ik}xa_{kj} = a_{ij}$,则我们称其为一致矩阵。
  • 在使用判断矩阵求权重之前, 必须先经过一致性检验, 以免产生矛盾
  • 一致性检验原理: 检验我们构造的判断矩阵和一致性矩阵是否有太大差别

引理: n阶正互反矩阵A为一致矩阵时当且仅当最大特征值 $\lambda_{max}=n$ . 且当正互反矩阵A非一致时, 一定满足$\lambda_{max}>n$. 矩阵越不一致, 最大特征值相差与n相差就越大.

一致性检验的步骤:

  1. 计算一致性指标CI, 矩阵越一致, CI越小 \(CI=\frac{\lambda_{max}-n}{n-1}\)
  2. 查找平均随机性指标RI
  3. 计算一致性比例CR \(CR = \frac{CI}{RI}\) 若CR为0, 则判断矩阵为一致矩阵; 若$CR<0.1$, 判断矩阵一致; 若$CR>=0.1$, 判断矩阵不一致

算数平均法求权重: 对n阶矩阵求算数平均权重的步骤

  1. 按列进行归一化
  2. 归一化后矩阵按行相加得到一个向量
  3. 向量的每个元素除以n, 得到权重向量

几何平均法求权重: 对n阶矩阵求几何平均权重的步骤

  1. 将判断矩阵的元素每一行相乘, 再开n次方, 得到一个列向量
  2. 对这个列向量进行归一化, 得到权重向量

特征值法求权重: 略

算法实现

代码

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
import numpy as np

# 默认使用9级评分
RI_dict = {1: 0, 2: 0, 3: 0.58, 4: 0.90, 5: 1.12, 6: 1.24, 7: 1.32, 8: 1.41, 9: 1.45}

# 矩阵, 注意使用小数, 否则计算结果为整数
A_arr = [[1, 2, 1.0/3, 3],
        [1.0/2, 1, 1.0/3, 2],
        [3, 3, 1, 4],
        [1.0/3, 1.0/2, 1.0/4, 1]]

def main():
    # 矩阵
    A = np.array(A_arr)

    a_sum0 = A.sum(axis=0)
    B = A / a_sum0  
    print('新矩阵:')
    print(B)
    b_sum = B.sum(axis=1)
    print('新矩阵行和: %s' % b_sum)

    
    W = b_sum.sum()
    w_arr = []
    for w in b_sum:
        w_arr.append(w/W)

    print('W: %s' % w_arr)

    AW = []
    for a in A :
        aa = a * w_arr
        AW.append(aa.sum())

    print('AW: %s' % AW)

    result = np.array(AW) / np.array(w_arr)
    print('AW/W: %s' % result)

    row = result.shape[0]
    Max = result.sum()/row
    print('λMax: %s' % Max)

    CI = (Max - row) / (row - 1)
    print('CI: %s' % CI)
    
    CR = CI / RI_dict[row]
    print('CR: %s' % CR)
    
 
if __name__ == '__main__':
    main()

终端结果

1
2
3
4
5
6
7
8
9
10
11
12
新矩阵:
[[ 0.20689655  0.30769231  0.17391304  0.3       ]
 [ 0.10344828  0.15384615  0.17391304  0.2       ]
 [ 0.62068966  0.46153846  0.52173913  0.4       ]
 [ 0.06896552  0.07692308  0.13043478  0.1       ]]
新矩阵行和: [ 0.9885019   0.63120747  2.00396725  0.37632338]
W: [0.2471254757236766, 0.15780186829662091, 0.5009918117864145, 0.094080844193287966]
AW: [1.0119690154922538, 0.63652356514050656, 2.0920972206204591, 0.38060488986276086]
AW/W: [ 4.09496031  4.0336884   4.17591101  4.04550887]
λMax: 4.08751714694
CI: 0.0291723823126
CR: 0.0324137581251

线性规划法(LP)

线性规划模型的三要素

  • 决策变量:问题中要确定的未知量,用于表明规划问题中的用数量表示的方案、措施等,可由决策者决定和控制;
  • 目标函数:决策变量的函数,优化目标通常是求该函数的最大值或最小值;
  • 约束条件:决策变量的取值所受到的约束和限制条件,通常用含有决策变量的等式或不等式表示。

从实际问题中建立数学模型一般有以下三个步骤:

  1. 根据影响所要达到目的的因素找到决策变量
  2. 由决策变量和所在达到目的之间的函数关系确定目标函数
  3. 由决策变量所受的限制条件确定决策变量所要满足的约束条件

简写形式

\[\begin{align*} &\max(\text{或} \min) \ z = \sum_{j=1}^{n} c_j x_j, \\ &\text{s.t.} \begin{cases} \sum_{j=1}^{n} a_{ij} x_j \leq (\text{或} =, \geq) b_i, \ i = 1,2,\cdots,m, \\ x_j \geq 0, \ j = 1,2,\cdots,n. \end{cases} \end{align*}\]

矩阵表现形式

\[\begin{align*} &\max(\text{或} \min) \ z = c^T x, \\ &\text{s.t.} \begin{cases} Ax \leq (\text{或} =, \geq) b, \\ x \geq 0. \end{cases} \end{align*}\]

其中:

  • $\boldsymbol{c = [c_1, c_2, \cdots, c_n]^T}$ —— 目标函数的系数向量,即价值向量;
  • $\boldsymbol{x = [x_1, x_2, \cdots, x_n]^T}$ —— 决策向量;
  • $\boldsymbol{A = (a_{ij})_{m \times n}}$ —— 约束方程组的系数矩阵;
  • $\boldsymbol{b = [b_1, b_2, \cdots, b_m]^T}$ —— 约束方程组的常数向量。

多目标规划: 可以通过给其中一个目标加一个界限, 把目标转变为约束; 变为单目标规划

算法实现

使用Python中的linprog函数, 标准格式如下

\(\begin{align*} result &= linprog(c,A\_ub,b\_ub,A\_eq,b\_eq,bounds,method) \\ &\min_{x} \ c x, \\ &\text{s.t.} \ \begin{cases} A\_ub \cdot x \le b\_ub, \\ A\_eq \cdot x = b\_eq, \\ x \in \text{bounds}. \end{cases} \end{align*}\)

  • $c$ : 目标函数的决策变量对应的系数向量(行列向量都可以,下同 )
  • $A_{ub}, b_{ub}$ : 不等式约束条件的变量系数矩阵和常数项矩阵(必须是$\leq$形式 )
  • $A_{eq}, b_{eq}$ : 等式约束条件的系数矩阵和常数项矩阵
  • $\text{bounds}$ : 表示决策变量定义域的$n \times 2$矩阵,$\text{None}$表示无穷; 例如, bounds = [(0,None), (0,None)]代表$x_1$的取值范围是(0,无穷), $x_2$的取值范围是(0,无穷)
  • $\text{method}$ : 调用的求解方法,默认为 $\text{highs}$
  • $\text{result}$有多个参数,常用$x$为最优解,$\text{fun}$为函数最小值,$\text{nit}$迭代次数等,调用时用$\text{result.x}$

注意, 如果要调用linprog函数, 变量一定要是标准形式, 即目标函数是求最小值, 约束条件是小于等于或者等于.

代码示例

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
import numpy as np
from scipy.optimize import linprog

# 目标函数系数,这里取负值因为 linprog 默认进行最小化优化
c = [-20, -30, -45]  

# 不等式约束的系数矩阵 A
A_ub = [
    [4, 8, 15],
    [1, 1, 1]
]  

# 不等式约束的右侧值向量 b
b_ub = [100, 20]  

# 定义域,变量取值范围(下限为 0,上限无限制)
bounds = [[0, None], [0, None], [0, None]]  

# 求解线性规划问题
# 由于 linprog 默认求解最小化问题,通过对目标函数系数取负值转换为最大化问题
result = linprog(c, A_ub, b_ub, bounds=bounds)  

print(result)

# 输出 A、B、C 三图分别通关的次数(解向量)
print('A、B、C 三图分别通关的次数为:')
print(result.x)  

# 目标函数的最大值是最小化问题结果的相反数
y = -result.fun  
print('最终获得的经验为:')
print(y)

蒙特卡洛法

是一种思想, 没有比较固定的代码

非线性规划

常见收益率, 传播率, 经济增长率等规划问题, 涉及到$\frac{1}{x}$形式; 另外, 空间运动问题如空间约束, 避免碰撞, 角度调整等; 运输问题, 已知坐标运送物品, 会使用距离公式. 只要有一个是非线性的, 就属于非线性规划

非线性规划模型的标准型

\(\begin{align*} \min &\quad f(x) \\ \text{s.t.} &\quad \begin{cases} h_i(x) = 0, &i = 1, \dots, n \\ g_j(x) \geq 0, &j = 1, \dots, n \\ lb \leq x \leq ub \end{cases} \end{align*}\)

非线性规划求解的minimize 函数

1
res = minimize(fun, x0, args, method, jac, hess, hessp, bounds, constraints, tol, callback, options)  
  • res:返回值,包含以下关键属性:
    • res.fun:最优值
    • res.success:求解状态(是否成功)
    • res.x:最优解
    • res.message:优化终止的信息
  • fun:需要最小化的目标函数。需接收参数并返回目标函数值,示例:
    1
    2
    
    def fun(x):  
        return x[0]**2 + x[1]**2  # 示例:f(x) = x₁² + x₂²  
    
  • x0:决策变量的初始值, 从这一点开始优化。一维 numpy 数组,示例:
    1
    
    x0 = [1, 2]  # 初始值设为 [1, 2], 即x_1取1, x_2取2时开始优化
    
  • args:向目标函数/约束函数传递的额外固定参数,示例:
    1
    2
    3
    
    def objective_with_args(x, a, b):  
        return a * x[0]**2 + b * x[1]**2  # 带参数的目标函数:a·x₁² + b·x₂²  
    args = (2, 3)  # 参数 a=2,b=3  
    
  • method: 指定求解优化问题所使用的算法方法, 常见的方法有Nelder-Mead(单纯形法, 适用于无约束优化问题), SLSQP(序列二次规划法, 可以处理一般的有约束非线性优化问题), 例如:
    1
    
      method = `SLSQP`
    
  • bounds: 决策变量的取值范围约束, 例如:
    1
    
      bounds = [(0, None), (None,10)]
    
  • constraints: 用于定义优化问题的约束条件,通过 'type' 键指定约束类型('eq' 表示等式约束'ineq' 表示不等式约束),'fun' 键对应实现约束逻辑的函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      # 不等式约束函数:要求返回值 ≥ 0,即满足 x[0] + x[1] ≤ 1  
      def inequality_constraint(x):  
          return 1 - (x[0] + x[1])  
    
      # 等式约束函数:要求返回值 ≈ 0,即满足 x[0]² + x[1]² = 1  
      def equality_constraint(x):  
          return x[0]**2 + x[1]**2 - 1  
    
      # 约束条件列表  
      constraints = [  
          {'type': 'ineq', 'fun': inequality_constraint},  # 不等式约束  
          {'type': 'eq', 'fun': equality_constraint}       # 等式约束  
      ]  
    
  • jac, hess, hessp:用于加速梯度优化的高阶导数参数(如雅可比矩阵、海森矩阵)。多数中小规模问题简单函数无需手动设置,可依赖默认行为。
  • tol: 收敛容差,控制优化算法的收敛判断。当迭代改进程度小于该容差时,算法停止。一般无需手动设置,依赖默认值即可满足多数需求。
  • callback: 每次迭代时的回调函数, 通常用于调试和跟踪优化进度, 一般不需要
  • option: 用于设置各种与算法相关的额外选项, 例如maxiter(最大迭代次数), disp(显示设置),gtol(梯度收敛容差)
    1
    2
    3
    4
    5
    6
    7
    
      options = {'maxiter': 500}  # 最大迭代次数设为 500  
      result = minimize(  
          objective_function,  # 目标函数  
          x0,                   # 初始值  
          method='SLSQP',       # 优化方法  
          options=options       # 传入配置  
      )  
    

    非线性规划算法对于初始值x0的选取十分重要, 因为非线性规划算法求出来的是局部最优解, 如果要求全局最优解, 有两个思路:

    1. 给定不同的初始值, 在里面找到一个最优解
    2. 先用蒙特卡洛模拟, 得到一个蒙特卡洛解, 然后将这个解作为初始解来求解

代码

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
import numpy as np
from scipy.optimize import minimize

# 目标函数,接受长度为 2 的一维数组 x(代表变量 x1 和 x2),计算并返回目标函数值
def fun1(x):
    return x[0] ** 2 + x[1] ** 2 - x[0] * x[1] - 2 * x[0] - 5 * x[1]

# 第一个非线性约束条件函数,接受变量 x,返回对应约束条件的值
def nonlcon1(x):
    return -(x[0] - 1) ** 2 + x[1]

# 第二个非线性约束条件函数,接受变量 x,返回对应约束条件的值
def nonlcon2(x):
    return 2 * x[0] - 3 * x[1] + 6

# 初始值设定,设置为一个二维的 numpy 数组,初始值为 [0, 0]
x0 = np.array([0, 0])

# 使用默认算法(类似内点法,scipy 会根据情况选择合适的)求解
res = minimize(
    fun1, 
    x0, 
    constraints=(
        {'type': 'ineq', 'fun': nonlcon1},
        {'type': 'ineq', 'fun': nonlcon2}
    ),
    bounds=None, 
    tol=None, 
    options=None, 
    args=()
)

# 输出求解结果
print("默认算法(类似内点法等情况)求解结果:")
print("最优解:", res.x)
print("最优值:", res.fun)

import numpy as np

# 假设目标函数和约束函数已定义(需与之前的 fun1、nonlcon1、nonlcon2 对应)
def fun1(x):
    return x[0]**2 + x[1]**2 - x[0]*x[1] - 2*x[0] - 5*x[1]

def nonlcon1(x):
    return -(x[0]-1)**2 + x[1]

def nonlcon2(x):
    return 2*x[0] - 3*x[1] + 6

# 使用蒙特卡罗方法找初始值(推荐)
n = 10000000  # 随机采样数量,越大越可能找到优值但耗时更长
# 在 [-100, 100] 区间生成均匀分布的随机数
x1 = np.random.uniform(-100, 100, size=n)  
x2 = np.random.uniform(-100, 100, size=n)  

fmin = 100000000  # 初始化最小目标函数值
x0 = None         # 初始化蒙特卡罗找到的初始值

for i in range(n):
    x = np.array([x1[i], x2[i]])  # 构造变量数组
    # 检查约束条件:两个不等式约束均需满足(返回值 ≥ 0 表示满足)
    if nonlcon1(x) >= 0 and nonlcon2(x) >= 0:  
        result = fun1(x)  # 计算满足约束的目标函数值
        if result < fmin:  # 找到更优的目标函数值时更新
            fmin = result
            x0 = x  # 记录对应自变量

print("蒙特卡罗选取的初始值为:", x0)

# 使用找到的初始值再次进行优化求解
res_final = minimize(
    fun1, 
    x0, 
    method='SLSQP',
    constraints=(
        {'type': 'ineq', 'fun': nonlcon1},
        {'type': 'ineq', 'fun': nonlcon2}
    ),
    bounds=None, 
    tol=None, 
    options=None, 
    args=()
)

print("基于蒙特卡罗初始值的最终求解结果:")
print("最优解:", res_final.x)
print("最优值:", res_final.fun)

整数规划和0-1规划

在规划问题中, 有时答案的部分or全部变量必须是整数. 初看起来似乎是四舍五入即可, 但是事实上取整后的解不一定是最优解, 所以应该有特殊的方法做整数规划. 如果所有变量都取整数, 则是纯整数规划; 如果一部分变量是整数, 就是混合整数规划. 整数规划的一种特殊情况是数值只能取0或者1, 此时称为0-1规划问题.

python可以求解线性的整数规划, 在线性规划的基础上, 加入决策变量取整数的条件; 但是对于非线性规划, 没有特定的算法, 只能用近似算法如蒙特卡罗模拟, 只能算法等.

pulp库的用法

1. 定义问题类型

使用 LpProblem 类定义优化问题,语法:

1
LpProblem(name, sense)  
  • name:问题的名称(字符串)。
  • sense:优化目标类型,可选 LpMaximize(最大化)或 LpMinimize(最小化)。

示例

1
2
3
4
5
6
7
from pulp import LpProblem, LpMaximize, LpMinimize  

# 定义一个最大化问题  
problem = LpProblem(name="Problem_Name", sense=LpMaximize)  

# 或定义最小化问题  
problem = LpProblem(name="Problem_Name", sense=LpMinimize)  

2. 定义决策变量

使用 LpVariable 类定义变量,语法:

1
LpVariable(name, lowBound, upBound, cat)  
  • name:变量名称(字符串,需唯一)。
  • lowBound:变量下界(默认 None,表示无下界)。
  • upBound:变量上界(默认 None,表示无上界)。
  • cat:变量类型(字符串):
    • "Continuous":连续变量(默认)。
    • "Integer":整数变量。
    • "Binary":0-1 变量(仅取 0 或 1 )。

示例

1
2
3
4
5
6
7
8
9
10
from pulp import LpVariable  

# 连续变量 x,下界 0,无上界  
x = LpVariable(name="x", lowBound=0, upBound=None, cat="Continuous")  

# 整数变量 y,下界 0,上界 10  
y = LpVariable(name="y", lowBound=0, upBound=10, cat="Integer")  

# 0-1 变量 z,默认下界 -infinity、上界 +infinity(实际受限于 0-1)  
z = LpVariable(name="z", cat="Binary")  

3.添加目标函数和约束条件

使用+=操作符将目标函数和约束条件添加到问题中, 约束可以是等式(==)或者不等式(<=>=) 示例:

1
2
3
4
5
6
7
8
# 添加目标函数:3x + 2y
problem += 3 * x + 2 * y, "Objective_Function"
# 添加约束条件 1:x + y <= 10
problem += x + y <= 10, "Constraint_1"
# 添加约束条件 2:x - y >= 2
problem += x - y >= 2, "Constraint_2"
# 添加等式约束 3:x + z == 1
problem += x + z == 1, "Constraint_3"

4. 求解问题

使用默认求解器(CBC)求解问题

1
problem.solve()

5. 获取结果

变量值: value(variable) 目标函数值: value(problem.objective) 求解状态: LpStatus[problem.status]

例题: 背包问题

题目: 有十件物品要从甲地运送到乙地, 十件物品的重量, 利润均不一样; 现在只有一辆大货车来运送货物, 货车有载重上限, 且只运送一次; 怎么样选择货物才能使利润最高?

0-1规划往往对应的是发生或未发生, 有还是没有等状态


\(x_i = \begin{cases} 1, & \text{运送了第 } i \text{ 件货物} \\ 0, & \text{没有运送第 } i \text{ 件货物} \end{cases} \quad (i = 1, 2, \dots, 10)\)

  • $w_i$:第 $i$ 件物品的重量
  • $p_i$:第 $i$ 件物品的利润

目标:最大化总利润
约束:总重量不超过 30(限重),且 $x_i$ 是 0-1 变量

\[\begin{align*} \max &\quad \sum_{i=1}^{10} p_i x_i \\ \text{s.t.} &\quad \begin{cases} \sum_{i=1}^{10} w_i x_i \leq 30 \\ x_i \in \{0, 1\},\ i = 1,2,\dots,10 \end{cases} \end{align*}\]

由于求解器(如 scipy)默认支持最小化,可通过取负将最大化问题转换为最小化问题:

\[\begin{align*} \min &\quad -\sum_{i=1}^{10} p_i x_i \\ \text{s.t.} &\quad \begin{cases} \sum_{i=1}^{10} w_i x_i \leq 30 \\ x_i \in \{0, 1\},\ i = 1,2,\dots,10 \end{cases} \end{align*}\]

例题: 指派问题

下面, 我们来看一个二维的0-1规划. 已知5名游泳候选人的百米成绩, 怎么选拔队员组成$4\times100$混合泳接力队伍

候选人: $i = 1,2,3,4,5$ 泳姿: $j=1,2,3,4$

\[x_{ij} = \begin{cases} 1, & \text{队员 } i \text{ 参加第 } j \text{ 种泳姿} \\ 0, & \text{队员 } i \text{ 不参加第 } j \text{ 种泳姿} \end{cases}\]

$t_{ij}$:队员 $i$ 参加第 $j$ 种泳姿的耗时, 是一个二维矩阵。

转换为数学表达, 即 最小化总耗时:
\(\min \sum_{j=1}^{4} \sum_{i=1}^{5} t_{ij} x_{ij}\)

\[\text{s.t.} \begin{cases} \sum_{j=1}^{4} x_{ij} \leq 1, & i = 1,2,3,4,5 \quad \text{(每人最多选1种泳姿)} \\ \sum_{i=1}^{5} x_{ij} = 1, & j = 1,2,3,4 \quad \text{(每种泳姿恰好1人参加)} \\ x_{ij} \in \{0, 1\}, & i = 1,2,3,4,5;\ j = 1,2,3,4 \end{cases}\]

代码实现

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
from pulp import LpMaximize, LpProblem, LpVariable, lpSum, value

# 创建一个最大化问题,问题名称为 "Maximize_Experience"
problem = LpProblem(name="Maximize_Experience", sense=LpMaximize)

# 定义决策变量,均为非负整数,分别代表 A、B、C 图通关次数
x1 = LpVariable(name="x1", lowBound=0, cat="Integer")  # A 图通关次数
x2 = LpVariable(name="x2", lowBound=0, cat="Integer")  # B 图通关次数
x3 = LpVariable(name="x3", lowBound=0, cat="Integer")  # C 图通关次数

# 定义目标函数:总经验,表达式为 20*x1 + 30*x2 + 40*x3,名称为 "Total_Experience"
problem += 20 * x1 + 30 * x2 + 40 * x3, "Total_Experience"

# 添加约束条件
# 资源约束:4*x1 + 8*x2 + 10*x3 <= 100,名称为 "Resource_Constraint"
problem += 4 * x1 + 8 * x2 + 10 * x3 <= 100, "Resource_Constraint"
# 时间约束:x1 + x2 + x3 <= 20,名称为 "Time_Constraint"
problem += x1 + x2 + x3 <= 20, "Time_Constraint"

# 求解问题
problem.solve()

# 输出结果
print("A、B、C 三图分别通关的次数为:")
print(int(value(x1)), int(value(x2)), int(value(x3)))

print("最终获得的经验为:")
print(int(value(problem.objective)))

列表推导式 [表达式 for 变量 in 可迭代对象] 的用法

1
2
3
squares = [i**2 for i in range(10)]
print(squares)
# 在上述代码中, range(10)就是可迭代对象, i是变量, i**2是表达式, 代码会依次将range(10)中的每个值赋给i

运行结果是[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] 也可以加上条件, 例如[表达式 for 变量 in 可迭代对象 if 条件]

1
2
even_squares = [i**2 for i in range(10) if i%2 ==0]
print(even_squares)

会比普通的循环更简洁

灰色预测模型GM(1,1)

黑色的说明没有信息, 黑洞洞的; 白色的说明信息明了, 信息清楚. (1,1)的意思是只含有一个变量的一阶微分方程模型

灰色预测模型适用情况

  • (1)数据是以年份度量的非负数据(如果是月份或者季度数据一般要用时间序列模型),比如定时求量的题目,即已知一些年份数据,预测下一年的数据,常见有GDP、人口数量、耕地面积、粮食产量等;或者定量求时,已知一些年份数据和某灾变的阈值,预测下次灾变时间。
  • (2)数据能经过准指数规律的检验(除了前两期外,后面至少90%的期数的光滑比要低于0.5)。
  • (3)数据的期数较短且和其他数据之间的关联性不强(小于等于10,也不能太短了,比如只有3期数据),要是数据期数较长,一般用传统的时间序列模型比较合适。

长江排污预测

年份 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004
排污总量 174 179 183 189 207 234 220.5 256 270 285
  • 观察发现,没有明显规律,数据也比较少,而且是以年份度量的,可以考虑用灰色预测。
  • 那看不出规律怎么办,可以制造规律: 设$ x^{(0)}=(x^{(0)}(1),x^{(0)}(2),\dots,x^{(0)}(n)) $是最初的非负数据列,我们可以对其累加,得到新的数据列$ x^{(1)} $ $ x^{(1)}=(x^{(1)}(1),x^{(1)}(2),\dots,x^{(1)}(n)) $ 其中:$ x^{(1)}(m)=\sum_{i = 1}^{m}x^{(0)}(i),m = 1,2,\dots,n $

一.级比检验(可行性验证)

GM(1,1)模型要求原始序列满足“级比落入可容覆盖区”,以确保数据适合累加生成。

  • 级比定义:对 $ x^{(0)} $,计算相邻两项的比值 $ \lambda(k) = \frac{x^{(0)}(k-1)}{x^{(0)}(k)} $($ k=2,…,10 $);
  • 可容覆盖区:当样本量 $ n=10 $ 时,可容区为 $ (e^{-2/(n+1)}, e^{2/(n+1)}) = (e^{-2/11}, e^{2/11}) \approx (0.83, 1.20) $。

级比计算结果

| 时间序号 $ k $ | 2(1996) | 3(1997) | 4(1998) | 5(1999) | 6(2000) | 7(2001) | 8(2002) | 9(2003) | 10(2004) | |——————|———–|———–|———–|———–|———–|———–|———–|———–|————| | 级比 $ \lambda(k) $ | 0.972 | 0.978 | 0.968 | 0.913 | 0.884 | 1.061 | 0.861 | 0.948 | 0.947 |

所有级比均落入可容区(0.83, 1.20),数据适合构建GM(1,1)模型

一次累加生成(AGO):削弱随机性

对原始序列 $ x^{(0)} $ 进行一次累加,得到累加序列 $ x^{(1)} $,公式为:
$ x^{(1)}(k) = \sum_{i=1}^k x^{(0)}(i) $($ k=1,2,…,10 $)

时间序号 k 1(1995) 2(1996) 3(1997) 4(1998) 5(1999) 6(2000) 7(2001) 8(2002) 9(2003) 10(2004)
$ x^{(1)}(k) $ 174 353 536 725 932 1166 1386.5 1642.5 1912.5 2197.5

累加后的数据 $ x^{(1)} $ 呈现明显的单调递增趋势,随机性显著削弱。

构建紧邻均值生成序列 $ z^{(1)} $

为建立微分方程,需生成累加序列的“紧邻均值序列”,公式为:
$ z^{(1)}(k) = 0.5x^{(1)}(k) + 0.5x^{(1)}(k-1) $($ k=2,…,10 $)

计算结果

时间序号 k 2 3 4 5 6 7 8 9 10
$ z^{(1)}(k) $ 263.5 444.5 630.5 828.5 1049 1276.25 1514.5 1777.5 2055

3. 最小二乘法估计模型参数

GM(1,1)的核心是求解微分方程 $ \frac{dx^{(1)}}{dt} + ax^{(1)} = b $,其中 $ a $(发展系数)、$ b $(灰作用量)为待估参数,通过最小二乘法求解:
参数向量 $ \hat{a} = \begin{bmatrix} a \ b \end{bmatrix} = (B^T B)^{-1} B^T Y $,其中:

  • 数据矩阵 $ B = \begin{bmatrix} -z^{(1)}(2) & 1 \ -z^{(1)}(3) & 1 \ … \ -z^{(1)}(10) & 1 \end{bmatrix} $(9行2列);
  • 常数向量 $ Y = \begin{bmatrix} x^{(0)}(2) \ x^{(0)}(3) \ … \ x^{(0)}(10) \end{bmatrix} $(9行1列)。

参数计算结果

通过矩阵运算(可借助Excel、MATLAB或Python实现),得到:

  • 发展系数 $ a \approx -0.0652 $(负号表示累加序列 $ x^{(1)} $ 呈指数增长趋势);
  • 灰作用量 $ b \approx 165.78 $(反映系统的驱动强度,值越大,序列增长的“动力”越强)。

4. 建立预测模型

1)时间响应方程

微分方程 $ \frac{dx^{(1)}}{dt} - 0.0652x^{(1)} = 165.78 $ 的解(时间响应函数)为:

  • 累加序列预测值:$ \hat{x}^{(1)}(k+1) = \left( x^{(0)}(1) - \frac{b}{a} \right) e^{-ak} + \frac{b}{a} $
    代入 $ a=-0.0652 $、$ b=165.78 $、$ x^{(0)}(1)=174 $,得:
    $ \hat{x}^{(1)}(k+1) = 2723.13 e^{0.0652k} - 2549.13 $

  • 原始序列还原值(累减生成):$ \hat{x}^{(0)}(k+1) = \hat{x}^{(1)}(k+1) - \hat{x}^{(1)}(k) $

2)模型预测结果与误差分析

将 $ k=1,…,9 $ 代入时间响应方程,计算1996-2004年排污总量的预测值 $ \hat{x}^{(0)} $,并与原始值对比,分析模型精度(关键指标:平均相对误差,越小精度越高)。

  • 平均相对误差:$ \frac{1}{9}(1.84\%+6.01\%+…+6.42\%) \approx 6.07\% $
    根据灰色模型精度等级(一级:<10%,二级:10%-20%),该模型为一级精度,拟合效果良好,可用于规律挖掘。

时间序列模型ARIMA

同一对象在不同时间连续观察得到的数据, 第一要素是时间要素, 第二要素是数值要素

例如: 从出生到现在, 每年称一次体重, 你体重的数据; 中国历年来GDP的数据; 在某地方每隔一小时测得的温度数据.

  • 时期序列反映数值在一定时期内发展的结果;
  • 时点序列反映现象在一定时点上的瞬间水平

自回归模型AR(p)

  • 描述当前值和历史值之间的关系,用变量自身的历史数据对自身进行预测,其必须要满足平稳性要求,只适用于预测与自身前期相关的现象(时间序列的自相关性)。
  • $ p $ 阶自回归过程的公式定义:$ y_t = \mu + \sum_{i=1}^{p} \gamma_i y_{t-i} + \epsilon_t $,其中 $ p $ 表示用几期的历史值来预测。
  • $ y_t $ 是当前值,$ \mu $ 是常数项,$ p $ 是阶数,$ \gamma_i $ 是自相关系数。

移动平均模型 MA(q)

  • 移动平均模型关注的是自回归模型中误差项的累计。
  • $ q $ 阶移动平均过程的公式定义:$ y_t = \mu + \epsilon_t + \sum_{i = 1}^{q} \theta_i \epsilon_{t - i} $。
  • 即时间序列当前值与历史值没有关系,而只依赖于历史白噪声的线性组合。
  • 移动平均法能有效地消除预测中的随机波动。

自回归移动平均模型 ARMA(p, q)

  • 自回归与移动平均的结合。
  • 公式定义:$ y_t = \mu + \sum_{i = 1}^{p} \gamma_i y_{t - i} + \epsilon_t + \sum_{i = 1}^{q} \theta_i \epsilon_{t - i} $。
  • 该式表明:
    • 一个随机时间序列可以通过一个自回归移动平均模型来表示,即该序列可以由其自身的过去或滞后值以及随机扰动项来解释。
    • 如果该序列是平稳的,即它的行为并不会随着时间的推移而变化,那么我们就可以通过该序列过去的行为来预测未来。

差分自回归移动平均模型 ARIMA(p,d,q)

  • 自回归模型($ AR $)移动平均模型($ MA $)差分法结合,我们就得到了差分自回归移动平均模型 $ ARIMA(p、d、q) $。
  • $ p $ 是自回归项,$ q $ 为移动平均项数,$ d $ 为时间序列成为平稳时所做的差分次数。
  • 原理:将非平稳时间序列转化为平稳时间序列,然后将因变量仅对它的滞后值以及随机误差项的现值和滞后值进行回归所建立的模型。

建模步骤

  • 对序列绘图,进行平稳性检验,观察序列是否平稳;对于非平稳时间序列要先进行 $ d $ 阶差分,转化为平稳时间序列;
  • 经过第一步处理,已经得到平稳时间序列。要对平稳时间序列分别求得其自相关系数($ ACF $)偏自相关系数($ PACF $),通过对自相关图和偏自相关图的分析,得到最佳的阶数 $ p、q $;
  • 由以上得到的 $ d、q、p $,得到 $ ARIMA $ 模型。然后开始对得到的模型进行模型检验。

咱们可以把差分自回归移动平均模型(ARIMA)拆成几个部分,用更通俗的方式来讲,就像搭积木一样,一步步理解~

先从“自回归(AR)”说起. 想象你要预测明天的气温,自回归的思路就是:觉得今天的气温和昨天、前天等之前的气温有关系。就好比说,如果昨天热,今天可能也热,那明天或许也会受前面这些天的影响。用公式的话,简单理解就是现在的数值(比如明天的气温 $ y_t $),和之前几天(比如前 $ p $ 天)的数值($ y_{t - 1}, y_{t - 2}, \dots, y_{t - p} $)有关联,再加上一些随机的波动(误差项 $ \epsilon_t $)。不过有个前提,这些数据得是“平稳”的,也就是整体趋势不会忽上忽下、大起大落,像气温要是常年夏天和冬天温差稳定,才好根据过去推测未来。

再看“移动平均(MA)”. 还是拿气温举例,有时候气温变化不只是和之前的气温有关,还和之前的“误差”有关。比如某一天实际气温和预测气温差了2度(这就是误差 $ \epsilon $),那第二天的气温可能也会受这个误差影响。移动平均模型就是说,现在的数值($ y_t $)和过去若干期的误差($ \epsilon_{t - 1}, \epsilon_{t - 2}, \dots, \epsilon_{t - q} $)有关,再加上常数项这些,它能把预测里那些随机的、不规则的波动给“抹平”一些。

然后是“自回归移动平均(ARMA)”. 这就是把自回归和移动平均结合起来啦。相当于既考虑之前的数值,又考虑之前的误差,一起用来预测现在的数值。公式里就既有自回归部分(和过去数值相关的项),又有移动平均部分(和过去误差相关的项)。

最后到“差分自回归移动平均(ARIMA)”.前面的自回归和移动平均,都要求数据是平稳的。但实际生活里,很多数据是不平稳的,比如公司销售额,可能逐年增长,有明显的上升趋势。这时候“差分”就派上用场了。差分就是看相邻数据的变化,比如今天销售额减昨天的,得到的是当天的增长量。通过一次或者多次差分,让原本不平稳的数据变得平稳,然后再用ARMA模型去处理这些差分后平稳的数据,这样就成了ARIMA模型。简单说,ARIMA就是先差分让数据平稳,再用AR和MA的组合来预测。

多目标规划

衡量一个方案的目标往往不止一个, 这些目标有的时候是不协调甚至矛盾的. 多目标规划就是研究多于一个的目标函数在给定区域的最优化.

例: 投资组合问题

投资收益问题:给上述公司设计投资组合方案,用给定资金$ M $,有选择地购买若干种资产或存银行生息,使净收益尽可能大,总体风险尽可能小。

目标函数为 \(\begin{cases} \max \sum_{i=0}^{n}(r_i - p_i)x_i, \\ \min \left\{ \max_{1 \leq i \leq n} \{ q_i x_i \} \right\} \end{cases}\)

约束条件为 \(\begin{cases} \sum_{i=0}^{n}(1 + p_i)x_i = M, \\ x_i \geq 0,\ \ i = 0,1,\cdots,n. \end{cases}\)

在线性规划中我们有一种解决思路, 就是把一个目标函数转换为约束条件, 把多目标规划转化为单目标规划.

多目标规划的一般形式

多目标规划是多目标决策的重要内容之一,在进行多目标决策时,当希望每个目标都尽可能的大(或尽可能的小)时,就形成了一个多目标规划问题,其一般形式为:

\[\begin{cases} \min \boldsymbol{f}(\boldsymbol{x}) = [f_1(\boldsymbol{x}), f_2(\boldsymbol{x}), \cdots, f_m(\boldsymbol{x})]^{\text{T}}, \\ \text{s.t.} \begin{cases} g_i(\boldsymbol{x}) \leq 0, & i = 1,2,\cdots,p, \\ h_j(\boldsymbol{x}) = 0, & j = 1,2,\cdots,q, \end{cases} \end{cases}\]
  • 其中$\boldsymbol{x}$为决策向量,$f_1(\boldsymbol{x}), f_2(\boldsymbol{x}), \cdots, f_m(\boldsymbol{x})$为目标函数,$\text{s.t.}$式为约束条件。
  • 记$\Omega = {\boldsymbol{x} g_i(\boldsymbol{x}) \leq 0, i = 1,2,\cdots,p; h_j(\boldsymbol{x}) = 0, j = 1,2,\cdots,q}$。
  • 称$\Omega$为多目标规划的可行域(决策空间),$f(\Omega) = {f(\boldsymbol{x}) \boldsymbol{x} \in \Omega}$为多目标规划问题的像集(目标空间),多目标规划问题以下简称问题(MP)。

多目标规划一般有三种解: 最优解, 有效解, 满意解

  • 最优解定义:设$\bar{x} \in \Omega$,若对于任意$i = 1,2,\cdots,m$及任意$x \in \Omega$,均有 \(f_i(\bar{x}) \leq f_i(x)\) 则称$\bar{x}$为问题(MP)的绝对最优解,记问题(MP)的绝对最优解集为$\Omega_{\text{ab}}^*$。

    一般来说,多目标规划问题的绝对最优解是不常见的,当绝对最优解不存在时,需要引入新的“解”的概念。多目标规划中最常用的解为非劣解或有效解,也称为Pareto最优解。

  • 有效解定义:考虑多目标规划问题(MP),设$\bar{x} \in \Omega$,若不存在$x \in \Omega$,使得 \(f_i(x) \leq f_i(\bar{x}),\ \ i = 1,2,\cdots,m\) 且至少有一个 \(f_j(x) < f_j(\bar{x})\) 则称$\bar{x}$为问题(MP)的有效解(或Pareto有效解),$f(\bar{x})$为有效点。

  • 满意解定义:主要是从决策过程角度,根据决策者的偏好与要求而提出的。

    设可行域为$\Omega$,要求$m$个目标函数$f_i(i = 1,2,\cdots,m)$越小越好。有时决策者的期望较低,给出了$m$个阈值$\alpha_i$,当$\bar{x} \in \Omega$满足$f_i(\bar{x}) \leq \alpha_i(i = 1,2,\cdots,m)$时,就认为$\bar{x}$是可以接受的、是满意的。这样的$\bar{x}$就称为一个满意解。

有效解是可以使得至少一个目标取得最大或最小, 如果使得所有目标都满足最大或最小, 那么就是最优解

由于对绝大多数多目标决策实际问题,决策者最偏好的方案都是有效解,下面介绍几种常用的求解问题(MP)的有效解的常用方法。

值得注意的是,在多目标规划中,除去目标函数一般是彼此冲突外,还有另一个特点:目标函数的不可公度性。所以通常在求解前,先对目标函数进行预处理。预处理的内容包括:无量纲化处理,归一化处理等。

MP问题求解有效解的常用方法

  • 线性加权法:该方法的基本思想是根据目标的重要性确定一个权重,以目标函数的加权平均值为评价函数,使其达到最优。权重的确定一般由决策者给出,因而具有较大的主观性,不同的决策者给的权重可能不同,从而会使计算的结果不同。
  • $\varepsilon$约束法:根据决策者的偏好,选择一个主要关注的参考目标,而将其他目标函数放到约束条件中。约束法也称主要目标法或参考目标法,参数$\varepsilon$是决策者对变为约束条件的目标函数的容许接受阈值。
  • 理想点法:该方法的基本思想是:以每个单目标最优值为该目标的理想值,使每个目标函数值与理想值的差的加权平方和最小。
  • 优先级法:该方法的基本思想是根据目标重要性分成不同优先级,先求优先级高的目标函数的最优值,在确保优先级高的目标获得不低于最优值的条件下,再求优先级低的目标函数。

化工厂生产问题

某化工厂今年拟生产两种新产品A和B,其生产费用分别为2万元/吨和5万元/吨。这两种产品均将造成环境污染,每生产一吨A产品会产生0.4吨的污染,每生产一吨B产品会产生0.3吨的污染。由于条件限制,工厂生产产品A和B的最大生产能力各为每月5吨和6吨,而市场需要这两种产品的总量每月不少于7吨。该工厂决策认为,这两个目标中环境污染应该优先考虑,且根据经验生产费用的参考值为30万元,污染量参考值为2吨。试问工厂如何安排生产计划,在满足市场需要的前提下,使设备的花费和产生的污染均达到最小。

解:设工厂每月产品A生产$ x_1 $吨,B生产$ x_2 $吨,那么产生的污染分别为$ 0.4x_1 $吨和$ 0.3x_2 $吨。

之后要进行敏感度分析, 逐一改变$f_1$和$f_2$的权重, 观察对结果的影响

多目标规划模型

\[\begin{cases} \min f_1 = 2x_1 + 5x_2 \\ \min f_2 = 0.4x_1 + 0.3x_2 \\ \text{st.} \begin{cases} x_1 + x_2 \geq 7 \\ 0 \leq x_1 \leq 5 \\ 0 \leq x_2 \leq 6 \end{cases} \end{cases}\]

转换为单目标规划问题

下面我们将其转换为一个单目标规划问题,即对上面的两个目标函数进行加权,由于该工厂决策认为环境污染应优先考虑,因此我们可以选取$ f_1 $和$ f_2 $的权重分别为0.4和0.6。注意到两个目标函数的单位不同,一个为“万元”,一个为“吨”,因此我们需要首先对目标函数进行标准化来消除量纲的影响,然后再进行加权,由于题目中已经给了产品费用和污染量的参考值,因此我们将这两个目标函数分别除以其参考值来消除量纲。

加权组合后的目标函数

\[f = 0.4 \times \frac{f_1}{30} + 0.6 \times \frac{f_2}{2} = \frac{0.4}{30} \times (2x_1 + 5x_2) + \frac{0.6}{2} \times (0.4x_1 + 0.3x_2)\]

得到一个单目标规划问题,利用前面学习过的linprog函数进行求解可以得到: $ x_1 = 5 $,$ x_2 = 2 $,$ f_1 = 20 $,$ f_2 = 2.6 $

杉树求解器

可以解决的问题

  • 线性规划(LP)
  • 二阶锥规划(SOCP)
  • 指数锥规划(ExpCone Programming)
  • 半定规划(SDP)
  • 二次规划(QP)
  • 二次约束规划(QCP)
  • 非线性规划(General Nonlinear Programming)
  • 混合整数规划(MIP)
  • 特殊约束

导入Python接口

使用杉数求解器的Python接口,需要首先导入Python接口库。

1
2
import coptpy as cp
from coptpy import COPT

创建环境

对于任意求解任务,杉数求解器要求首先创建求解环境。

1
2
# Create COPT environment
env = cp.Envr()

创建问题

创建求解环境成功后,用户需要创建模型,模型中将包括待求解的变量、约束等信息。

1
2
# Create COPT model
model = env.createModel("lp_ex1")

添加变量

创建变量时允许同时指定变量在目标函数中的系数、变量上下界等信息。本示例中创建变量时仅指定上下界和名称信息,其它为默认值。

1
2
3
4
# Add variables: x, y, z
x = model.addVar(lb=0.1, ub=0.6, name="x")
y = model.addVar(lb=0.2, ub=1.5, name="y")
z = model.addVar(lb=0.3, ub=2.8, name="z")

添加约束

添加变量成功后,进一步添加作用于变量的约束条件。

1
2
3
# Add constraints
model.addConstr(1.5*x + 1.2*y + 1.8*z <= 2.6)
model.addConstr(0.8*x + 0.6*y + 0.9*z >= 1.2)

设置目标函数

添加完变量和约束后,进一步指定模型的目标函数。

1
2
# Set objective function
model.setObjective(1.2*x + 1.8*y + 2.1*z, sense=COPT.MAXIMIZE)

设置求解参数

用户可以在求解模型前设置求解参数,如设置求解时间限制为10秒。

1
2
# Set parameter
model.setParam(COPT.Param.TimeLimit, 10.0)

求解模型

调用下述方法求解模型。

1
2
# Solve the model
model.solve()

启发式算法代码实现

复制: 按照适应度大小映射为概率, 进行轮盘赌复制

交叉: 1和2, 3和4, 以一定概率决定是否交叉. 若交叉, 则二者随机选择一个段进行交叉

变异: 按照一定概率决定该个体是否变异, 若变异, 则随机选择一个位点取反

例如, 随机生成一个初始解, 每次循环再生成一个解, 如果生成解比初始解好就更新(择优进化), 否则不更新. 即蒙特卡洛.

下面是对于30个城市旅行商问题的启发式算法代码

1
三十个城市的坐标 41 94 37 84 54 67 25 62 7 64 2 99 68 58 71 44 54 62 83 69 64 60 18 54 22 60 83 46 91 38 25 38 24 42 58 69 71 71 74 78 87 76 18 40 13 40 82 7 62 32 58 35 45 21 41 26 44 35 4 50

遗传算法(GA)

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
import random
import numpy as np
import math
num_city=30#城市总数0-29
num_total=100#随机生成的初始解的总数
copy_num=70#保留的解的个数
cross_num=20#交叉解的个数
var_num=10#变异解的个数
location=np.loadtxt('city_location.txt')
#print(location)
#随机生成初始解[[],[],[]...]
def generate_initial():
    initial=[]
    city=list(range(num_city))
    for i in range(num_total):
        random.shuffle(city)
        p=city.copy()
        while (p in initial):
            #print('2333')#随机了一个重复的解
            random.shuffle(city)
            p=city.copy()
        initial.append(p)
    return initial

#对称矩阵,两个城市之间的距离
def distance_p2p_mat():
    dis_mat=[]
    for i in range(30):
        dis_mat_each=[]
        for j in range(30):
            dis=math.sqrt(pow(location[i][0]-location[j][0],2)+pow(location[i][1]-location[j][1],2))
            dis_mat_each.append(dis)
        dis_mat.append(dis_mat_each)
   # print(dis_mat)
    return dis_mat

#目标函数计算,适应度计算,中间计算。适应度为1/总距离*10000
def dis_adp_total(dis_mat,initial):
    dis_adp=[]
#    dis_test=[]   
    for i in range(num_total):
        dis=0
        for j in range(num_city-1):
            dis=dis_mat[initial[i][j]][initial[i][j+1]]+dis
        dis=dis_mat[initial[i][29]][initial[i][0]]+dis#回家
#        dis_test.append(dis)        
        dis_adp_each= 10000.0/dis
        dis_adp.append(dis_adp_each)
#    print(dis_test)
    return dis_adp

def choose_fromlast(dis_adp,answer_source):
    mid_adp=[]
    mid_adp_each=0
    for i in range(num_total):
        mid_adp_each=dis_adp[i]+mid_adp_each
        mid_adp.append(mid_adp_each)
   # print(mid_adp)
    #产生0-mid_adp[num_total-1]之间的随机数
    #选择n-1<随机数<n的那个n的解,保留
    copy_ans=[]
    for p in range(copy_num):
        rand=random.uniform(0,mid_adp[num_total-1])#产生随机数
       # print(rand)
       # print(p)
        for j in range(num_total):
            if (rand<mid_adp[j]):#查找位置
                copy_ans.append(answer_source[j])
                break
            else:
                continue
    return copy_ans

#随机选择保留下来的70中的25个进行交叉
def cross_pronew(copy_ans):
    for i in range(cross_num):
        which=random.randint(0,copy_num-1)#选择对那个解交叉
        cross_list=copy_ans[which].copy()
        while (cross_list in copy_ans):
            p=random.randint(0,num_city-1)
            q=random.randint(0,num_city-1)
            cross_list[p],cross_list[q]=cross_list[q],cross_list[p]#第一次交换位置
            m=random.randint(0,num_city-1)
            n=random.randint(0,num_city-1)
            cross_list[m],cross_list[n]=cross_list[n],cross_list[m]#第二次交换位置            
        copy_ans.append(cross_list)
    cross_ans=copy_ans.copy()
    return cross_ans

#随机选择那95中的5个进行变异
def var_pronew(cross_ans):
    for i in range(var_num):
        which=random.randint(0,copy_num+cross_num-1)#选择对那个解交叉
        var_list=cross_ans[which].copy()
        while (var_list in cross_ans):
            p=random.randint(0,num_city-1)
            q=random.randint(0,num_city-1)
            var_list[p],var_list[q]=var_list[q],var_list[p]#交换位置
        cross_ans.append(var_list)
    var_ans=cross_ans.copy()
    return var_ans
    
answer_source=generate_initial()
dis_mat=distance_p2p_mat()
#print(dis_mat)
dis_adp=dis_adp_total(dis_mat,answer_source)
adp_max_new=max(dis_adp)
if (max(dis_adp)>10000/700):
    print('找到的最近距离是:',max(dis_adp))
else:
    print('哎呀没找到,我再找找~')    
    answer_new=answer_source
    dis_adp_new=dis_adp
    while(adp_max_new<=10000/700):
        copy_answer=choose_fromlast(dis_adp_new,answer_new)
        cross_answer=cross_pronew(copy_answer)
        var_answer=var_pronew(cross_answer)
        answer_new=var_answer.copy()
        dis_adp_new=dis_adp_total(dis_mat,answer_new)
        adp_max_new=max(dis_adp_new)
#        dis_min=10000/adp_max_new
#        print('这次是:',dis_min)
    dis_min=10000/adp_max_new
    print('终于找到你啦:',dis_min)

模拟退火算法(SA)

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
import random
import numpy as np
import math

num_city=30#城市总数
initial_t=120#初始温度
lowest_t=0.001#最低温度
M=150#当连续多次都不接受新的状态,开始改变温度
iteration=500#设置迭代次数

location=np.loadtxt('city_location.txt')

#==========================================
#对称矩阵,两个城市之间的距离
def distance_p2p_mat():
    dis_mat=[]
    for i in range(30):
        dis_mat_each=[]
        for j in range(30):
            dis=math.sqrt(pow(location[i][0]-location[j][0],2)+pow(location[i][1]-location[j][1],2))
            dis_mat_each.append(dis)
        dis_mat.append(dis_mat_each)
   # print(dis_mat)
    return dis_mat

#计算所有路径对应的距离
def cal_newpath(dis_mat,path):
    dis=0
    for j in range(num_city-1):
        dis=dis_mat[path[j]][path[j+1]]+dis
    dis=dis_mat[path[29]][path[0]]+dis#回家
    return dis

#==========================================
#点对点距离矩阵
dis_mat=distance_p2p_mat()
#初始路径
path=list(range(30))
#初始距离
dis=cal_newpath(dis_mat,path)
#初始温度
t_current=initial_t

while (t_current>lowest_t):#外循环,改变温度
    count_m=0#M的计数
    count_iter=0#迭代次数计数
    while (count_m<M and count_iter<iteration):#内循环,连续多次不接受新的状态或者是迭代多次,跳出内循环        
        i=0
        j=0
        while(i==j):#防止随机了同一城市
            i=random.randint(1,29)
            j=random.randint(1,29)
        path_new=path.copy()
        path_new[i],path_new[j]=path_new[j],path_new[i]#任意交换两个城市的位置,产生新解
        #计算新解的距离
        dis_new=cal_newpath(dis_mat,path_new)
        #求差
        dis_delta=dis_new-dis
        #取0-1浮点随机数
        rand=random.random()
        #计算指数函数的值
        exp_d=math.exp(-dis_delta/t_current)
        #选择
        if dis_delta<0:
            path=path_new
            dis=dis_new
        elif exp_d>rand:
            path=path_new
            dis=dis_new    
        else:
            count_m=count_m+1
        count_iter=count_iter+1
    t_current=0.99*t_current#改变温度
#外循环结束
dis_min=dis
path_min=path
print('最短距离:',dis_min)
print('最短路径:',path_min)

蚁群算法(SI)

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
import random
import numpy as np
import math

location=np.loadtxt('city_location.txt')
num_ant=200 #蚂蚁个数
num_city=30 #城市个数
alpha=1 #信息素影响因子
beta=1  #期望影响因子
info=0.1 #信息素的挥发率
Q=1 #常数

count_iter = 0
iter_max = 500
#dis_new=1000
#==========================================
#对称矩阵,两个城市之间的距离
def distance_p2p_mat():
    dis_mat=[]
    for i in range(num_city):
        dis_mat_each=[]
        for j in range(num_city):
            dis=math.sqrt(pow(location[i][0]-location[j][0],2)+pow(location[i][1]-location[j][1],2))
            dis_mat_each.append(dis)
        dis_mat.append(dis_mat_each)
   # print(dis_mat)
    return dis_mat

#计算所有路径对应的距离
def cal_newpath(dis_mat,path_new):
    dis_list=[]
    for each in path_new:
        dis=0
        for j in range(num_city-1):
            dis=dis_mat[each[j]][each[j+1]]+dis
        dis=dis_mat[each[num_city-1]][each[0]]+dis#回家
        dis_list.append(dis)
    return dis_list

#==========================================
#点对点距离矩阵
dis_list=distance_p2p_mat()
dis_mat=np.array(dis_list)#转为矩阵
#期望矩阵
e_mat_init=1.0/(dis_mat+np.diag([10000]*num_city))#加对角阵是因为除数不能是0
diag=np.diag([1.0/10000]*num_city)
e_mat=e_mat_init-diag#还是把对角元素变成0
#初始化每条边的信息素浓度,全1矩阵
pheromone_mat=np.ones((num_city,num_city))
#初始化每只蚂蚁路径,都从0城市出发
path_mat=np.zeros((num_ant,num_city)).astype(int)


#while dis_new>400:
while count_iter < iter_max:
    for ant in range(num_ant):
        visit=0#都从0城市出发
        unvisit_list=list(range(1,30))#未访问的城市
        for j in range(1,num_city):
            #轮盘法选择下一个城市
            trans_list=[]
            tran_sum=0
            trans=0
            for k in range(len(unvisit_list)):
                trans +=np.power(pheromone_mat[visit][unvisit_list[k]],alpha)*np.power(e_mat[visit][unvisit_list[k]],beta)
                trans_list.append(trans)
                tran_sum =trans
            
            rand=random.uniform(0,tran_sum)#产生随机数

            for t in range(len(trans_list)):
                if(rand <= trans_list[t]):
                    visit_next=unvisit_list[t]


                    break
                else:
                    continue
            path_mat[ant,j]=visit_next#填路径矩阵

            unvisit_list.remove(visit_next)#更新
            visit=visit_next#更新

    #所有蚂蚁的路径表填满之后,算每只蚂蚁的总距离
    dis_allant_list=cal_newpath(dis_mat,path_mat)

    #每次迭代更新最短距离和最短路径        
    if count_iter == 0:
        dis_new=min(dis_allant_list)
        path_new=path_mat[dis_allant_list.index(dis_new)].copy()      
    else:
        if min(dis_allant_list) < dis_new:
            dis_new=min(dis_allant_list)
            path_new=path_mat[dis_allant_list.index(dis_new)].copy() 

    # 更新信息素矩阵
    pheromone_change=np.zeros((num_city,num_city))
    for i in range(num_ant):
        for j in range(num_city-1):
            pheromone_change[path_mat[i,j]][path_mat[i,j+1]] += Q/dis_mat[path_mat[i,j]][path_mat[i,j+1]]
        pheromone_change[path_mat[i,num_city-1]][path_mat[i,0]] += Q/dis_mat[path_mat[i,num_city-1]][path_mat[i,0]]
    pheromone_mat=(1-info)*pheromone_mat+pheromone_change
    count_iter += 1 #迭代计数+1,进入下一次
        
print('最短距离:',dis_new)
print('最短路径:',path_new)

禁忌搜索(TS)

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
import random
import numpy as np
import math

num_city=30#城市总数
table_len=20#禁忌表长度
location=np.loadtxt('city_location.txt')
taboo_table=[]


#==========================================
#对称矩阵,两个城市之间的距离
def distance_p2p_mat():
    dis_mat=[]
    for i in range(30):
        dis_mat_each=[]
        for j in range(30):
            dis=math.sqrt(pow(location[i][0]-location[j][0],2)+pow(location[i][1]-location[j][1],2))
            dis_mat_each.append(dis)
        dis_mat.append(dis_mat_each)
   # print(dis_mat)
    return dis_mat

#计算所有路径对应的距离
def cal_newpath(dis_mat,path_new):
    dis_list=[]
    for each in path_new:
        dis=0
        for j in range(num_city-1):
            dis=dis_mat[each[j]][each[j+1]]+dis
        dis=dis_mat[each[29]][each[0]]+dis#回家
        dis_list.append(dis)
    return dis_list

#寻找上一个最优路径对应的所有领域解
def find_newpath(path_best):
    path_new=[]
    for i in range(1,num_city-1):
        for j in range(i+1,num_city):
            path=path_best.copy()
            path[i],path[j]=path[j],path[i]
            path_new.append(path)
    return path_new


#==========================================
#点对点距离矩阵
dis_mat=distance_p2p_mat()

#设置初始解
path_initial=[]
initial=list(range(30))
path_initial.append(initial)
#print(path_initial)

#加入禁忌表
taboo_table.append(initial)

#求初始解的路径长度
dis_list=cal_newpath(dis_mat,path_initial)
dis_best=min(dis_list)#最短距离
path_best=path_initial[dis_list.index(dis_best)]#对应的最短路径方案
#print(path_best)

#初始期望
expect_dis=dis_best
expect_best=path_best
for iter in range(5000):#迭代
    #寻找全领域新解
    path_new=find_newpath(path_best)
    #print(path_new)

    #求出所有新解的路径长度
    dis_new=cal_newpath(dis_mat,path_new)
#    print(dis_new)

    #选择路径
    dis_best=min(dis_new)#最短距离
    path_best=path_new[dis_new.index(dis_best)]#对应的最短路径方案
    if dis_best < expect_dis:#最短的<期望
        expect_dis=dis_best
        expect_best=path_best#更新两个期望
        if path_best in taboo_table:
            taboo_table.remove(path_best)
            taboo_table.append(path_best)
        else:
            taboo_table.append(path_best)
    else:#最短的还是不能改善期望
        if path_best in taboo_table:#在禁忌表里
            dis_new.remove(dis_best)
            path_new.remove(path_best)
            dis_best=min(dis_new)#求不在禁忌表中的最短距离
            path_best=path_new[dis_new.index(dis_best)]#对应的最短路径方案
            taboo_table.append(path_best)
        else:#不在禁忌表
            taboo_table.append(path_best)
    if len(taboo_table)>=table_len:
        del taboo_table[0]
        
print('最短距离',expect_dis)
print('最短路径:',expect_best)

参考链接:超详细的遗传算法