📈 第3章 最优化
📄 一句话概括

每条约束在图上是一条直线,几条线围出来的那块地就是可行区域。 三条约束两两求交得到十个交点,落在可行区域里的只有四个,最优解就在这四个里。 这一篇把顶点全枚举出来算一遍,让「最优在顶点」从一句要背的结论变成看得见的事实。

📝 本章和前面几章的关系

上一篇有了目标函数。这一篇把约束和目标放到同一张图上,得到最优解。 顶点枚举是第 3.5 篇「Solver 在干什么」的底层,也是这本书里唯一一次让你看到算法内部。

一、三条线怎么围出一块地

每条不等式在图上是一条直线,线的一侧是可行的那半边。 三条线的可行半边叠在一起,加上两个坐标轴,围出来的就是可行区域。 它是一个凸多边形——这条性质很重要,正是因为凸,最优解才只在角上。

二、顶点从哪来

多边形的每个顶点,都是两条边界线的交点。 三条约束两两组合,一共三对,加上约束与坐标轴的交点,凑出十个候选点。 把每个候选点代回全部约束检查一遍,能过的才是顶点。这次十个里过了四个。

顶点 利润 3x+5y
(0, 0) 0
(0, 100) 500
(110, 45) 555
(140, 0) 420

最高的那个就是最优解:做 110 百只鸭、45 百条鱼,利润 555(百元)。

三、哪条约束用满了

在最优解上代回去:注塑 400/400、装配 420/420、原料 155/180。 前两条用满,第三条还有富余——说明瓶颈是产能,不是原料。 要投钱,该投机器。这条结论不是算出来的,是看「哪条约束用满」看出来的。

四、图为什么值得画

因为图能让你一眼看到「哪条线离得远、哪条线贴着角」。 数字表只能告诉你最优是多少,图能告诉你为什么是这个。 而且图画出来,客户才相信你不是在黑箱里按了个按钮。

五、动手 Demo:可行区域:把所有约束画进一张图

先把约束两两求交,筛出可行顶点,再比目标函数。 这段代码就是 Solver 内部干的事,只不过它会把答案包装成一个对话框。

A = [(2, 4), (3, 2), (1, 1), (-1, 0), (0, -1)]
B = [400, 420, 180, 0, 0]
c = (3, 5)

pts = []
for i in range(len(A)):
    for j in range(i + 1, len(A)):
        a1, a2 = A[i], A[j]
        det = a1[0] * a2[1] - a1[1] * a2[0]
        if abs(det) < 1e-12:
            continue
        x = (B[i] * a2[1] - a1[1] * B[j]) / det
        y = (a1[0] * B[j] - B[i] * a2[0]) / det
        if x < -1e-9 or y < -1e-9:
            continue
        if all(A[k][0] * x + A[k][1] * y <= B[k] + 1e-9 for k in range(len(A))):
            pts.append((x, y))

pts.sort()
for x, y in pts:
    print('(%6.1f, %5.1f)  利润 %6.1f' % (x, y, c[0] * x + c[1] * y))
bx, by = max(pts, key=lambda p: c[0] * p[0] + c[1] * p[1])
print('最优 (%.1f, %.1f),利润 %.1f' % (bx, by, c[0] * bx + c[1] * by))

实跑输出:

--------------------------------------------------------------------
可行区域:把所有约束画进一张图
--------------------------------------------------------------------

  ① 三个约束围出来的那块地
  --------------------------------------------------------------------
  三条约束两两求交,一共 10 个交点,其中 4 个落在可行区域里。
  最优解:x = 110.0,y = 45.0,利润 = 555.0(百元)

  ② 顶点清单
  --------------------------------------------------------------------
    (0.0, 0.0)  利润 0.0
    (0.0, 100.0)  利润 500.0
    (110.0, 45.0)  利润 555.0
    (140.0, 0.0)  利润 420.0

  ③ 把图画出来
  --------------------------------------------------------------------
  图存成 PNG:33098 字节。区域是多边形,最优解落在它的一个角上。

  ④ 最优解的意义
  --------------------------------------------------------------------
  x = 110.0,y = 45.0:一天做 110 百只鸭、45 百条鱼。
  这个解把三条约束里的两条用满了,第三条还有富余——
  说明原料不是瓶颈,产能才是。要投钱,该投机器。

十个交点,四个可行,最优在 (110.0, 45.0)。 四个顶点全在坐标轴或约束线的交点上——这就是「最优解在角上」的字面意思。

30 秒实验

  1. 把原料上限从 180 改成 120,顶点集合会变吗?最优解变不变?
  2. 把装配工时从 420 提到 500,重跑,最优解落在哪个顶点?
  3. 想一想:如果约束只有两条(没有原料那条),顶点会变成几个?

常见误区

  1. 以为顶点越多越好:顶点数量取决于约束数量,多到画不出来时,只是枚举变慢,原理不变。
  2. 忘了检查可行性:算出交点就当成顶点,会把区域外的交点也算进去。
  3. 只看最优值不看位置:最优解落在哪个顶点,决定了「哪条约束是瓶颈」,这是投钱的依据。
  4. 用手画的图下结论:图画歪一点,最优解就可能看起来落在别的角上,结论要回到数上核。
自测三问(点开对答案) 1. 为什么可行区域是凸多边形? —— 每条线性不等式切出的都是半平面,半平面是凸的,凸集的交集仍然是凸的。 2. 十个交点为什么只有四个可行? —— 其余六个落在可行区域外面,代回约束就有一条不满足。 3. 怎么判断哪条约束是瓶颈? —— 把最优解代回每条约束,看哪几条正好用满。用满的才是瓶颈。

最优解算出来了,可客户又提了个要求:每天至少给他 130 百只鸭。约束一加,区域就变小,钱就少了。

相关笔记