问题描述

在游戏《部落冲突》中,每个玩家都有一些不同占地面积的建筑,如城墙(1 x 1)、地狱之塔(2 x 2)、加农炮(3 x 3)、天鹰火炮(4 x 4)等。而在游戏中,每8个小时会在整个地图的一个随机位置生成一个 2 x 23 x 3 的障碍物,但是如果这个位置上有建筑或者紧邻着一个建筑,则不会生成1。问:如何布置阵型才能使得障碍物成功生成的概率最大?

为了简化障碍,大家假设玩家拥有a aa1 x 1 的建筑、b bb2 x 2 的建筑、c cc3 x 3 的建筑、d dd4 x 4 的建筑,整张地图一共有 n 2 n^2n2个格子,其中有p pp 的概率生成 2 x 2 的障碍物,1 − p 1-p1p 的概率生成 3 x 3 的障碍物。

注:n nn是指有效放置建筑的边长,但在这个放置建筑区域外3格也可以生成障碍物2,即障碍物可放置的区域面积为( n + 6 ) 2 (n+6)^2(n+6)2

问题分析

首先以左上角为坐标原点,建立坐标系:x xx轴垂直向下为正方向,y yy轴水平向右为正方向。则障碍物区域V O = { 0 , 1 , … , n + 5 } 2 V_O=\{0, 1, \dots, n+5\}^2VO={0,1,,n+5}2,建筑区域 V B = { 3 , 4 , … , n + 2 } 2 V_B=\{3, 4, \dots, n+2\}^2VB={3,4,,n+2}2

容易得到,上述困难实际上为2个子问题:

  1. (最大团问题)取D ⊆ V B D\sube V_BDVB使得其面积为S D = a + 4 b + 9 c + 16 d S_D=a+4b+9c+16dSD=a+4b+9c+16d,且 D ⊕ B 2 D \oplus B_2DB2 的剩余区域 V O ∖ ( D ⊕ B 2 ) V_O\setminus (D\oplus B_2)VO(DB2)的边长为2和边长为3的正方形子集最多3
  2. (正方分割问题)得到D DD的一个划分,使得D DD 分为 a aa个边长为1的正方形、b bb个边长为2的正方形、c cc个边长为3的正方形、d dd个边长为4的正方形。若不存在这样的划分则回到第1个问题。

NP完全的,且在实际游戏中的就是而其中第1个问题n ∈ ( 40 , 50 ) n\in(40,50)n(40,50),无法在有限时间内求解,因此大家采用贪心算法寻找局部最优解。

最初证明,最优的区域D ⊆ V B D\sube V_BDVB必为一个凸区域且所有的建筑尽可能紧密地布置在一个集中的区域内。

引理:设 D ⊆ Z 2 D\sube\Z^2DZ2 为凸集,则 ∣ D ⊕ B ∣ |D \oplus B|DBP ( D ) \mathcal{P}(D)P(D) 正相关4,其中 B = { 0 , 1 , ⋯   , b } 2 B=\{0,1,\cdots,b\}^2B={0,1,,b}2 方形区域。
:由 Steiner 公式∣ D ⊕ B ∣ = ∣ D ∣ + P ( D ) ⋅ w ( B ) + ∣ B ∣ |D \oplus B| = |D| + \mathcal{P}(D) \cdot w(B) + |B|DB=D+P(D)w(B)+B,且 B BB是紧凸集且具有非空内部,故系数w ( B ) = 1 π ∫ 0 2 π h B ( θ )   d θ > 0 \displaystyle w(B)=\frac{1}{\pi} \int_{0}^{2\pi} h_B(\theta)\ \mathrm d\theta > 0w(B)=π102πhB(θ)dθ>0,其中 h B ( θ ) h_B(\theta)hB(θ)B BB 在方向 θ \thetaθ上的支持函数,故∣ D ⊕ B ∣ |D \oplus B|DBP ( D ) \mathcal{P}(D)P(D) 正相关。

2 x 2 障碍物可放置的左上角点集合为 A 2 = { 0 , 1 , … , n + 4 } 2 A_2 = \{0, 1, \dots, n+4\}^2A2={0,1,,n+4}23 x 3 障碍物可放置的左上角点集合为 A 3 = { 0 , 1 , … , n + 3 } 2 A_3 = \{0, 1, \dots, n+3\}^2A3={0,1,,n+3}2B 2 , B 3 B_2,B_3B2,B3 分别表示 2 x 2 结构元素和 3 x 3 结构元素,F 2 = A 2 ∖ ( D ⊕ B 2 ) , F 3 = A 3 ∖ ( D ⊕ B 3 ) F_2=A_2 \setminus (D \oplus B_2),F_3=A_3 \setminus (D \oplus B_3)F2=A2(DB2),F3=A3(DB3)分别表示障碍物可放置的空闲位置集合,则障碍物成功生成的概率为

P ( D ) = p ∣ F 2 ∣ ∣ A 2 ∣ + ( 1 − p ) ∣ F 3 ∣ ∣ A 3 ∣ = p ∣ F 2 ∣ ( n + 4 ) 2 + ( 1 − p ) ∣ F 3 ∣ ( n + 3 ) 2 = p ∣ A 2 ∣ − ∣ D ⊕ B 2 ∣ ( n + 4 ) 2 + ( 1 − p ) ∣ A 3 ∣ − ∣ D ⊕ B 3 ∣ ( n + 3 ) 2 = 1 − ( p ( n + 4 ) 2 ∣ D ⊕ B 2 ∣ + 1 − p ( n + 3 ) 2 ∣ D ⊕ B 3 ∣ ) = : 1 − F ( D ) (1) \begin{aligned} P(D)&=p\frac{|F_2|}{|A_2|}+(1-p)\frac{|F_3|}{|A_3|}\\ &=p\frac{|F_2|}{(n+4)^2}+(1-p)\frac{|F_3|}{(n+3)^2}\\ &=p \frac{|A_2|-|D \oplus B_2|}{(n+4)^2} + (1-p) \frac{|A_3|-|D \oplus B_3|}{(n+3)^2}\\ &=1-\left(\frac p{(n+4)^2}|D \oplus B_2| + \frac{1-p}{(n+3)^2}|D \oplus B_3|\right)\\ &=:1-F(D) \end{aligned}\tag1P(D)=pA2F2+(1p)A3F3=p(n+4)2F2+(1p)(n+3)2F3=p(n+4)2A2DB2+(1p)(n+3)2A3DB3=1((n+4)2pDB2+(n+3)21pDB3)=:1F(D)(1)

其中 F ( D ) F(D)F(D)为失败的概率。

D DD不是凸的,则存在两点p , q ∈ D p, q \in Dp,qD 和一点 r rrp ppq qq的线段上,且r ∉ D r \notin Dr/D。考虑将点 p pp 移动到点 r rr 且保持 ∣ D ∣ |D|D不变。移动后,覆盖正方形的并集变为Δ F = − ∣ S ( p ) ∖ S ( r ) ∣ + ∣ S ( r ) ∖ S ( p ) ∣ \Delta F = - |S(p) \setminus S(r)| + |S(r) \setminus S(p)|ΔF=S(p)S(r)+S(r)S(p),其中 S ( p ) S(p)S(p) 表示覆盖点 p pp的正方形集合。若r rr 更靠近 q qq,则 ∣ S ( r ) ∪ S ( q ) ∣ ≤ ∣ S ( p ) ∪ S ( q ) ∣ |S(r) \cup S(q)| \leq |S(p) \cup S(q)|S(r)S(q)S(p)S(q),从而 ∣ D ⊕ B 2 ∣ |D \oplus B_2|DB2∣ D ⊕ B 3 ∣ |D \oplus B_3|DB3可能减小。利用反复将点向重心移动,可以减小F ( D ) F(D)F(D),最终使 D DD成为一个连通集。

由于在连续极限下且V O V_OVO足够大,损失函数F ( D ) F(D)F(D)D DD的函数且是子模的或具有凸性性质。又V B V_BVB是正方形且问题对称,最优D DD是一个 centered rectangle,从而是凸的。

故最优建筑布置D DD是一个凸区域。

由引理,∣ D ⊕ B ∣ |D \oplus B|DBP ( D ) \mathcal{P}(D)P(D)正相关且凸集具有最小周长,故最优的区域D ⊆ V B D\sube V_BDVB为一个凸区域且具有非空内部,即所有的建筑尽可能紧密地布置在一个集中的区域内。

证毕。

算法实现

由(1)式知,P ( D ) P(D)P(D) 关于 D DD具有平移不变性,因此我们可以直接从V B V_BVB的左上角开始放置建筑且不影响P ( D ) P(D)P(D)

因此,我们可以采取贪心策略:从V B V_BVB左上角开始,优先放置大型建筑,避免分散布置,利用紧凑布局将所有建筑集中放置。具体算法如图所示:

边缘对齐优化
紧凑布局核心算法
按尺寸降序
按坐标升序
未放置
找到最优位置
无合法位置
全部放置
计算包围盒
收缩包围盒到左上角
扩展建筑到主区域边界
遍历所有建筑
初始化建筑放置矩阵
计算候选位置
计算禁区覆盖增量
选择最小增量位置
放置建筑
返回失败
更新禁区矩阵
输入参数
初始化建筑列表
建筑排序
4x4建筑优先
同一尺寸按左上角排序
创建禁区覆盖矩阵
输出布局矩阵
计算障碍物生成概率

代码实现

#include <stdint.h>
  #include <algorithm>
    #include <climits>
      #include <cmath>
        #include <iostream>
          #include <limits>  // 用于numeric_limits
            #include <vector>
              using namespace std;
              // 建筑信息结构体
              struct Building {
              uint8_t size;  // 建筑尺寸(1-4)
              int count;	   // 建筑数量
              Building(int s, int c) : size(s), count(c) {}
              };
              // 比较函数:按尺寸降序排列
              bool compareSize(const Building &a, const Building &b) {
              return a.size > b.size;
              }
              // 计算放置建筑后障碍物生成概率的损失
              double calcObstacleProbLoss(const vector<vector<bool>> &forbidden, int x, int y, int size, int n, double p, int borderSize = 3) {
                // forbidden已经包含主区域外borderSize格范围
                int offset = borderSize;
                int totalSize = n + (borderSize << 1);
                // 模拟放置建筑后的临时禁区
                vector<vector<bool>> tempForbidden = forbidden;
                  // 计算放置建筑后新增的禁区(扩展到周围1格)
                  for (int i = -1; i <= size; ++i) {
                  for (int j = -1; j <= size; ++j) {
                  int nx = x + i + offset;  // 转换到扩展后的坐标系
                  int ny = y + j + offset;
                  if (nx >= 0 && nx < totalSize && ny >= 0 && ny < totalSize) {
                  tempForbidden[nx][ny] = true;
                  }
                  }
                  }
                  int valid2x2Before = 0, valid2x2After = 0;
                  int valid3x3Before = 0, valid3x3After = 0;
                  // 计算放置前2x2障碍物可生成区域
                  for (int gx = 0; gx <= totalSize - 2; ++gx) {
                  for (int gy = 0; gy <= totalSize - 2; ++gy) {
                  bool valid = true;
                  for (int i = 0; i < 2 && valid; ++i) {
                  for (int j = 0; j < 2 && valid; ++j) {
                  int nx = gx + i;
                  int ny = gy + j;
                  if (nx >= 0 && nx < totalSize && ny >= 0 && ny < totalSize && forbidden[nx][ny]) {
                  valid = false;
                  }
                  }
                  }
                  if (valid) valid2x2Before++;
                  }
                  }
                  // 计算放置后2x2障碍物可生成区域
                  for (int gx = 0; gx <= totalSize - 2; ++gx) {
                  for (int gy = 0; gy <= totalSize - 2; ++gy) {
                  bool valid = true;
                  for (int i = 0; i < 2 && valid; ++i) {
                  for (int j = 0; j < 2 && valid; ++j) {
                  int nx = gx + i;
                  int ny = gy + j;
                  if (nx >= 0 && nx < totalSize && ny >= 0 && ny < totalSize && tempForbidden[nx][ny]) {
                  valid = false;
                  }
                  }
                  }
                  if (valid) valid2x2After++;
                  }
                  }
                  // 计算放置前3x3障碍物可生成区域
                  for (int gx = 0; gx <= totalSize - 3; ++gx) {
                  for (int gy = 0; gy <= totalSize - 3; ++gy) {
                  bool valid = true;
                  for (int i = 0; i < 3 && valid; ++i) {
                  for (int j = 0; j < 3 && valid; ++j) {
                  int nx = gx + i;
                  int ny = gy + j;
                  if (nx >= 0 && nx < totalSize && ny >= 0 && ny < totalSize && forbidden[nx][ny]) {
                  valid = false;
                  }
                  }
                  }
                  if (valid) valid3x3Before++;
                  }
                  }
                  // 计算放置后3x3障碍物可生成区域
                  for (int gx = 0; gx <= totalSize - 3; ++gx) {
                  for (int gy = 0; gy <= totalSize - 3; ++gy) {
                  bool valid = true;
                  for (int i = 0; i < 3 && valid; ++i) {
                  for (int j = 0; j < 3 && valid; ++j) {
                  int nx = gx + i;
                  int ny = gy + j;
                  if (nx >= 0 && nx < totalSize && ny >= 0 && ny < totalSize && tempForbidden[nx][ny]) {
                  valid = false;
                  }
                  }
                  }
                  if (valid) valid3x3After++;
                  }
                  }
                  // 计算概率损失(放置前概率 - 放置后概率)
                  double probBefore = p * valid2x2Before + (1 - p) * valid3x3Before;
                  double probAfter = p * valid2x2After + (1 - p) * valid3x3After;
                  return probBefore - probAfter;
                  }
                  // 检查位置是否有效(能容纳建筑)
                  bool isValidPosition(const vector<vector<int>> &grid, int x, int y, int size) {
                    if (x + size > grid.size() || y + size > grid[0].size()) return false;
                    // 检查位置是否已被占用
                    for (int i = 0; i < size; ++i)
                    for (int j = 0; j < size; ++j)
                    if (grid[x + i][y + j] != 0) return false;
                    return true;
                    }
                    // 放置建筑并更新禁区
                    void placeBuilding(vector<vector<int>> &grid, vector<vector<bool>> &forbidden, int x, int y, int size, int borderSize = 3) {
                      int n = grid.size();
                      int offset = borderSize;
                      int totalSize = n + 2 * borderSize;
                      // 添加边界检查作为额外安全保障
                      if (x + size > n || y + size > n)
                      return;	 // 无效位置,直接返回
                      // 标记建筑位置
                      for (int i = 0; i < size; ++i)
                      for (int j = 0; j < size; ++j)
                      grid[x + i][y + j] = size;
                      // 扩展禁区到周围1格(考虑主区域外borderSize格范围)
                      for (int i = -1; i <= size; ++i) {
                      for (int j = -1; j <= size; ++j) {
                      int nx = x + i + offset;
                      int ny = y + j + offset;
                      if (nx >= 0 && nx < totalSize && ny >= 0 && ny < totalSize) {
                      forbidden[nx][ny] = true;
                      }
                      }
                      }
                      }
                      // 主布局算法
                      vector<vector<char>> optimizeLayout(int a, int b, int c, int d, int n, double p = 0.5) {			// 默认p=0.5,可根据需要调整
                        const int borderSize = 3;																		// 主区域外扩展的边界大小
                        vector<Building> buildings = {Building(4, d), Building(3, c), Building(2, b), Building(1, a)};	// 初始化建筑列表
                          sort(buildings.begin(), buildings.end(), compareSize);											// 按尺寸降序排序
                          vector<vector<int>> grid(n, vector<int>(n, 0));													// 初始化地图
                            // 初始化禁区,包含主区域外borderSize格范围
                            int totalSize = n + 2 * borderSize;
                            vector<vector<bool>> forbidden(totalSize, vector<bool>(totalSize, false));
                              // 标记主区域外的边界部分为非禁区(初始状态)
                              for (auto &b : buildings) {	 // 放置所有建筑
                              for (int i = 0; i < b.count; ++i) {
                              int bestX = -1, bestY = -1;
                              double minLoss = numeric_limits<double>::max();
                                // 遍历所有可能位置
                                for (int x = 0; x < n; ++x) {
                                for (int y = 0; y < n; ++y) {
                                if (isValidPosition(grid, x, y, b.size)) {
                                double loss = calcObstacleProbLoss(forbidden, x, y, b.size, n, p, borderSize);
                                if (loss < minLoss) {  // 选择概率损失最小的位置
                                minLoss = loss;
                                bestX = x;
                                bestY = y;
                                }
                                }
                                }
                                }
                                if (bestX != -1 && bestY != -1) placeBuilding(grid, forbidden, bestX, bestY, b.size, borderSize);  // 放置最佳位置
                                }
                                }
                                // 边缘对齐优化
                                int minX = n, minY = n;
                                for (int x = 0; x < n; ++x)
                                for (int y = 0; y < n; ++y)
                                if (grid[x][y] != 0) {
                                minX = min(minX, x);
                                minY = min(minY, y);
                                }
                                // 创建对齐后的地图
                                vector<vector<char>> res(n, vector<char>(n, '.'));
                                  for (int x = 0; x < n; ++x)
                                  for (int y = 0; y < n; ++y)
                                  if (grid[x][y] != 0) {
                                  int newX = x - minX;
                                  int newY = y - minY;
                                  // 确保新坐标在有效范围内
                                  if (newX >= 0 && newX < n && newY >= 0 && newY < n) res[newX][newY] = '0' + grid[x][y];	 // 1-4表示建筑
                                  }
                                  return res;
                                  }
                                  // 测试代码
                                  int main() {
                                  int a = 10, b = 5, c = 3, d = 2, n = 10;
                                  double p = 0.5;	 // 障碍物生成概率参数:p概率生成2x2障碍物,1-p概率生成3x3障碍物,以 p == 0.5 为例
                                  cout << "当前障碍物生成概率:2 x 2 障碍物概率 = " << p << ", 3 x 3 障碍物概率 = " << (1 - p) << endl;
                                  auto layout = optimizeLayout(a, b, c, d, n, p);
                                  // 输出布局
                                  for (const auto &row : layout) {
                                  for (char c : row) cout << c << "\t";
                                  cout << "\n";
                                  }
                                  return 0;
                                  }

算法时间复杂度为O ( n 2 ( a + b + c + d ) ) O(n^2(a+b+c+d))O(n2(a+b+c+d))

算法改进

尽管在理论上可以证明最优解是紧密放置的,但这只是一个必要条件,因此上述贪心算法仍可能陷入局部最优,对此允许引入一定随机性,避免陷入局部最优。此外,也许可借助模拟退火或遗传算法等启发式算法以一定概率选择更差解来避免局部最优。


  1. 即生成在离建筑至少1格距离的地方。↩︎

  2. 建筑主区域外延伸3格。↩︎

  3. D ⊕ B = { d + b ∣ d ∈ D , b ∈ B } D \oplus B=\{ d + b \mid d \in D, b \in B \}DB={d+bdD,bB}表示闵可夫斯基和,B i , i ∈ Z + B_i,i\in\Z_+Bi,iZ+ 表示边长为 i ii的正方形,即B i = { 0 , 1 , ⋯   , i − 1 } 2 B_i=\{0,1,\cdots,i-1\}^2Bi={0,1,,i1}2↩︎

  4. P ( D ) \mathcal{P}(D)P(D) 表示 D DD 的周长,即 D DDD c D^cDc之间的边界边(两个端点分别属于D DDD c D^cDc)的数量。 ↩︎