基于贪心最小化包围盒策略的布阵算法
文章目录
- 问题描述
- 问题分析
- 算法实现
- 代码实现
- 算法改进
问题描述
在游戏《部落冲突》中,每个玩家都有一些不同占地面积的建筑,如城墙(1 x 1
)、地狱之塔(2 x 2
)、加农炮(3 x 3
)、天鹰火炮(4 x 4
)等。而在游戏中,每8个小时会在整个地图的一个随机位置生成一个 2 x 2
或 3 x 3
的障碍物,但是如果这个位置上有建筑或者紧邻着一个建筑,则不会生成1。问:如何布置阵型才能使得障碍物成功生成的概率最大?
为了简化问题,我们假设玩家拥有 aaa 个 1 x 1
的建筑、bbb 个 2 x 2
的建筑、ccc 个 3 x 3
的建筑、ddd 个 4 x 4
的建筑,整张地图一共有 n2n^2n2 个格子,其中有 ppp 的概率生成 2 x 2
的障碍物,1−p1-p1−p 的概率生成 3 x 3
的障碍物。
注:nnn 是指有效放置建筑的边长,但在这个放置建筑区域外3格也可以生成障碍物,即障碍物可放置的区域面积为 (n+6)2(n+6)^2(n+6)22。
问题分析
首先以左上角为坐标原点,建立坐标系:xxx 轴垂直向下为正方向,yyy 轴水平向右为正方向。则障碍物区域 VO={0,1,…,n+5}2V_O=\{0, 1, \dots, n+5\}^2VO={0,1,…,n+5}2,建筑区域 VB={3,4,…,n+2}2V_B=\{3, 4, \dots, n+2\}^2VB={3,4,…,n+2}2。
容易得到,上述问题实际上为2个子问题:
- (最大团问题)取 D⊆VBD\sube V_BD⊆VB 使得其面积为 SD=a+4b+9c+16dS_D=a+4b+9c+16dSD=a+4b+9c+16d,且剩余区域 VO∖DV_O\setminus DVO∖D 的边长为2和边长为3的正方形子集最多。
- (正方分割问题)得到 DDD 的一个划分,使得 DDD 分为 aaa 个边长为1的正方形、bbb 个边长为2的正方形、ccc 个边长为3的正方形、ddd 个边长为4的正方形。若不存在这样的划分则回到第1步。
而其中第1步是NP完全的,且在实际游戏中的 n∈(40,50)n\in(40,50)n∈(40,50),无法在有限时间内求解,因此我们采用贪心算法寻找局部最优解。
首先证明,最优的区域 D⊆VBD\sube V_BD⊆VB 必为一个凸区域且所有的建筑尽可能紧密地布置在一个集中的区域内。
证:
引理:设 D⊆Z2D\sube\Z^2D⊆Z2 为凸集,则 ∣D⊕B∣|D \oplus B|∣D⊕B∣ 与 P(D)\mathcal{P}(D)P(D) 正相关3,其中 B={0,1,⋯ ,b}2B=\{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|∣D⊕B∣=∣D∣+P(D)⋅w(B)+∣B∣,且 BBB 是紧凸集且具有非空内部,故系数 w(B)=1π∫02πhB(θ) dθ>0\displaystyle w(B)=\frac{1}{\pi} \int_{0}^{2\pi} h_B(\theta)\ \mathrm d\theta > 0w(B)=π1∫02πhB(θ) dθ>0,其中 hB(θ)h_B(\theta)hB(θ) 为 BBB 在方向 θ\thetaθ 上的支持函数,故 ∣D⊕B∣|D \oplus B|∣D⊕B∣ 与 P(D)\mathcal{P}(D)P(D) 正相关。
设 2 x 2
障碍物可放置的左上角点集合为 A2={0,1,…,n+4}2A_2 = \{0, 1, \dots, n+4\}^2A2={0,1,…,n+4}2,3 x 3
障碍物可放置的左上角点集合为 A3={0,1,…,n+3}2A_3 = \{0, 1, \dots, n+3\}^2A3={0,1,…,n+3}2,B2,B3B_2,B_3B2,B3 分别表示 2 x 2
结构元素和 3 x 3
结构元素,F2=A2∖(D⊕B2),F3=A3∖(D⊕B3)F_2=A_2 \setminus (D \oplus B_2),F_3=A_3 \setminus (D \oplus B_3)F2=A2∖(D⊕B2),F3=A3∖(D⊕B3) 分别表示障碍物可放置的空闲位置集合,则障碍物成功生成的概率为
P(D)=p∣F2∣∣A2∣+(1−p)∣F3∣∣A3∣=p∣F2∣(n+4)2+(1−p)∣F3∣(n+3)2=p∣A2∣−∣D⊕B2∣(n+4)2+(1−p)∣A3∣−∣D⊕B3∣(n+3)2=1−(p(n+4)2∣D⊕B2∣+1−p(n+3)2∣D⊕B3∣)=: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}\tag1 P(D)=p∣A2∣∣F2∣+(1−p)∣A3∣∣F3∣=p(n+4)2∣F2∣+(1−p)(n+3)2∣F3∣=p(n+4)2∣A2∣−∣D⊕B2∣+(1−p)(n+3)2∣A3∣−∣D⊕B3∣=1−((n+4)2p∣D⊕B2∣+(n+3)21−p∣D⊕B3∣)=:1−F(D)(1)
其中 F(D)F(D)F(D) 为失败的概率。
若 DDD 不是凸的,则存在两点 p,q∈Dp, q \in Dp,q∈D 和一点 rrr 在 ppp 和 qqq 的线段上,且 r∉Dr \notin Dr∈/D。考虑将点 ppp 移动到点 rrr 且保持 ∣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) 表示覆盖点 ppp 的正方形集合。若 rrr 更靠近 qqq,则 ∣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⊕B2∣|D \oplus B_2|∣D⊕B2∣ 和 ∣D⊕B3∣|D \oplus B_3|∣D⊕B3∣ 可能减小。通过反复将点向重心移动,可以减小 F(D)F(D)F(D),最终使 DDD 成为一个连通集。
由于在连续极限下且 VOV_OVO 足够大,损失函数 F(D)F(D)F(D) 是 DDD 的函数且是子模的或具有凸性性质。又 VBV_BVB 是正方形且问题对称,最优 DDD 是一个 centered rectangle,从而是凸的。
故最优建筑布置 DDD 是一个凸区域。
由引理,∣D⊕B∣|D \oplus B|∣D⊕B∣ 与 P(D)\mathcal{P}(D)P(D) 正相关且凸集具有最小周长,故最优的区域 D⊆VBD\sube V_BD⊆VB 为一个凸区域且具有非空内部,即所有的建筑尽可能紧密地布置在一个集中的区域内。
证毕。
算法实现
由(1)式知,P(D)P(D)P(D) 关于 DDD 具有平移不变性,因此我们可以直接从 VBV_BVB 的左上角开始放置建筑且不影响 P(D)P(D)P(D)。
因此,我们可以采取贪心策略:从 VBV_BVB 左上角开始,优先放置大型建筑,避免分散布置,利用紧凑布局将所有建筑集中放置。具体算法如图所示:
代码实现
#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; // 主区域外扩展的边界大小(现在是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(n2(a+b+c+d))O(n^2(a+b+c+d))O(n2(a+b+c+d))。
算法改进
尽管在理论上可以证明最优解是紧密放置的,但这只是一个必要条件,因此上述贪心算法仍可能陷入局部最优,对此可以引入一定随机性,避免陷入局部最优。此外,也可以通过模拟退火或遗传算法等启发式算法以一定概率选择更差解来避免局部最优。
即生成在离建筑至少1格距离的地方。 ↩︎
建筑主区域外延伸3格。 ↩︎
∣D⊕B∣={d+b∣d∈D,b∈B}|D \oplus B|=\{ d + b \mid d \in D, b \in B \}∣D⊕B∣={d+b∣d∈D,b∈B} 表示闵可夫斯基和,P(D)\mathcal{P}(D)P(D) 表示 DDD 的周长,即 DDD 与 DcD^cDc 之间的边界边(两个端点分别属于 DDD 和 DcD^cDc)的数量。 ↩︎