问题描述

《终末地》 1.4 版本新增浮空回收玩法。一级问题都简单,但是二、三级有些还挺难

场景是:一个货物需要绑上热气球浮空回收。升空既有总浮力要求,也有平衡要求

货物被分成 5x5 的格子,给定浮力为 1~6 的不同数量的气球,要求在格子上放置完所有气球的情况下,使整块面板达到平衡

平衡分为上下平衡和左右平衡,不同格子浮力倍率不同,越远离中心的格子,倍率越高。最外圈 16 格每个格子浮力倍率是 2,内圈 8 格每个格子倍率是 1,中心 1 格倍率是 0

以下图为例,这是不平衡的状态

  • 上下平衡: 第 1 行总数是 9,得到 9 x 2 的上倾斜量,第 4 行总数是 6,有 6 x 1 的下倾斜,因此累计 18 - 6 = 12,有 12 的上倾斜量
  • 左右平衡: 第 3 列总数是 6,因为位于中线,没有倾斜量,第 5 列总数是 9,有 9 x 2 = 18,因此有 18 的右倾斜量

再看一个例子,这是平衡的状态

  • 上下平衡: 第 2 行上倾斜量 11,第 4 行下倾斜 9,第 5 行写倾斜 1 x 2 = 2,忽略第 3 行,11 - 9 + 2,因此上下平衡
  • 左右平衡: 第 1 列左倾 2,第 2 列左倾 9,第 4 列右倾 11,忽略第 3 列,11 = 9 + 2,因此左右平衡


分析解决

一开始我以为用方程组求解就行了,后面发现方程组解决不了每个谜题不同气球型号的数量限制问题。所以转向运筹学方法

设 5x5 的重量矩阵 A,记录每个格子上的气球重量

$$ w = [-2, -1, 0, 1, 2] $$

$$ I = [1, 1, 1, 1, 1]^T $$

那么,
上下平衡可表示成:

$$ wAI = 0 $$

左右平衡可表示成:

$$ wA^TI = 0 $$

一共有 6 种重量的气球,每种气球数量有限制。直接用矩阵 A 做决策变量是有困难的,考虑做独热编码,把决策要素拼凑成 A

对于每个单元格 (i,j) 和每个可能的取值 k∈{0,1,2,3,4,5,6},定义 5x5x7 的逻辑矩阵 P:

$$ P_{ijk} = \begin{cases} 0 \qquad 不放置重量为\:k\:的气球 \\ 1 \qquad 放置重量为\:k\:的气球 \end{cases} $$

互斥性约束: 考虑每个位置上只能放一种重量的气球,因此:

$$ \sum_{k=0}^6 P_{ijk} = 1\quad(i=1..5,j=1..5) $$

地图约束: 考虑每个谜题,不是每个位置都能放气球,因此:

$$ P_{ij0} = 1 \quad (i, j 是所有不能放气球的位置) $$

至此,重量矩阵 A 拼图已成,可表示成:

$$ A_{ij} = \sum_{k=1}^6 kP_{ijk} \quad (i=1..5,j=1..5) $$

数量约束: 假设重量为 k 的气球数量限制为 c_k
此限制可表示为:

$$ \sum_{i=1}^5\sum_{j=1}^5 P_{ijk} = c_k \quad (k=1..6) $$

至此,问题所有要素分析完成。把它输入 lingo,求可行解即可


编写代码

上面分析中,发现重量矩阵 A 虽然直观,但对整个流程并不必须,可以把平衡条件优化为:
上下平衡:

$$ \sum_{i=1}^5 w_i \sum_{j=1}^5\sum_{k=1}^6 kP_{ijk} = 0 $$

左右平衡:

$$ \sum_{j=1}^5 w_j \sum_{i=1}^5\sum_{k=1}^6 kP_{ijk} = 0 $$

按上面的 0-1 公式,编写 LINGO 代码如下(只需修改 DATA 段里的 C(各重量气球数量)和 B(禁止格)即可求解任意谜题):

MODEL:

SETS:
    IDX  /1..5/ : W;          ! 权重向量(行/列通用)
    K    /0..6/;              ! 每个格子的取值:0=空,1..6=气球重量
    KW   /1..6/ : C;          ! 每种重量的气球数量限制
    CELL (IDX,IDX) : B;       ! B(i,j)=1 表示该格禁止放气球
    CELLK(CELL,K) : P;        ! P(i,j,k)=1 表示 (i,j) 放重量 k(k=0 表示空)
ENDSETS

DATA:
    ! 权重向量
    W = -2 -1 0 1 2;
    
    ! 各重量气球数量 c_1..c_6,按实际修改
    C = 2 3 4 0 0 0;
    
    ! 地图 B(i,j):1=禁止格,不能放气球,0=可放气球,按实际修改
    B = 1 0 0 0 1
        0 1 0 0 0
        0 0 0 0 0
        0 1 0 1 1
        0 1 0 0 1;
ENDDATA

! 常数目标:占位用
MIN = 0;

! 互斥性:每个格子只能放一种重量的气球,或者不放气球
@FOR(CELL(I,J):
    @SUM(K(K0): P(I,J,K0)) = 1
);

! 地图约束:禁止格不能放气球
@FOR(CELL(I,J):
    P(I,J,0) >= B(I,J)
);

! 数量约束:每种重量的气球数量恰好用完
@FOR(KW(K):
    @SUM(CELL(I,J): P(I,J,K)) = C(K)
);

! 上下平衡
@SUM(IDX(I):
    W(I) * @SUM(IDX(J):
        @SUM(KW(K): K * P(I,J,K))
    )
) = 0;

! 左右平衡
@SUM(IDX(J):
    W(J) * @SUM(IDX(I):
        @SUM(KW(K): K * P(I,J,K))
    )
) = 0;

! 限制 P(i,j,k) 为 0-1 整数
@FOR(CELLK(I,J,K): @BIN(P(I,J,K)));

END

P(i,j,k) 就是 P_{ijk}

三条 @FOR 分别对应互斥、地图、数量约束,两条 @SUM 对应平衡约束


基于 LINGO 代码的 JS 实现

根据上面的 LINGO 模型,可用 js 做求解器网站

前端界面的交互部分由 AI 完成,求解算法使用 javascript-lp-solver 库实现

lingo 代码转译为 js,效率虽然低了,但好处是可以免除后端,一个 <script> 就能引入,纯前端运行

求解器网站https://balloon.simenchan.xyz


实际验证

第 1 问

地图: 中心九宫格不可用
气球: 6 号气球 2 个,3 号 2 个,2 号 3 个,1 号 1 个

抽象为下图

点击求解

第 2 问

地图: 右上箭头形状外格子都不可用
气球: 6 号气球 2 个,3 号气球 4 个,1 号气球 3 个

抽象为下图

点击求解


补充:4×4 与 6×6 的情况

实测发现游戏里实际还有 4×46x6 的浮空问题,所以给模型补上了 4×4 / 6×6 两种尺寸

改造方式非常简单:只需要换权重向量和维度上限 即可

一、替换 5x5 的 w = [-2, -1, 0, 1, 2](原模型)为:

  • 4×4:w = [-2, -1, 1, 2]
  • 6×6:w = [-3, -2, -1, 1, 2, 3]

二、把集合 IDX 的维数从 5 改成 4 或 6,IDX /1..4/(或 /1..6/

其余逻辑全部复用,界面加一个棋盘尺寸下拉即可

补充测试

第 3 问

地图: 除中心四格,外围一圈都可放气球
气球: 1 个 6 号,2 个 3 号,2 个 2 号,6 个 1 号

抽象为下图

求解得到