文章目录
问题描述
在游戏《部落冲突》中,每个玩家都有一些不同占地面积的建筑,如城墙(1 x 1
)、地狱之塔(2 x 2
)、加农炮(3 x 3
)、天鹰火炮(4 x 4
)等。而在游戏中,每8个小时会在整个地图的一个随机位置生成一个 2 x 2
或 3 x 3
的障碍物,但是如果这个位置上有建筑或者紧邻着一个建筑,则不会生成[1](#1)。问:如何布置阵型才能使得障碍物成功生成的概率最大?
为了简化问题,我们假设玩家拥有 a a a 个 1 x 1
的建筑、 b b b 个 2 x 2
的建筑、 c c c 个 3 x 3
的建筑、 d d d 个 4 x 4
的建筑,整张地图一共有 n 2 n^2 n2 个格子,其中有 p p p 的概率生成 2 x 2
的障碍物, 1 − p 1-p 1−p 的概率生成 3 x 3
的障碍物。
注: n n n 是指有效放置建筑的边长,但在这个放置建筑区域外3格也可以生成障碍物[2](#2),即障碍物可放置的区域面积为 ( n + 6 ) 2 (n+6)^2 (n+6)2。
问题分析
首先以左上角为坐标原点,建立坐标系: x x x 轴垂直向下为正方向, y y y 轴水平向右为正方向。则障碍物区域 V O = { 0 , 1 , ... , n + 5 } 2 V_O=\{0, 1, \dots, n+5\}^2 VO={0,1,...,n+5}2,建筑区域 V B = { 3 , 4 , ... , n + 2 } 2 V_B=\{3, 4, \dots, n+2\}^2 VB={3,4,...,n+2}2。
容易得到,上述问题实际上为2个子问题:
- (最大团问题)取 D ⊆ V B D\sube V_B D⊆VB 使得其面积为 S D = a + 4 b + 9 c + 16 d S_D=a+4b+9c+16d SD=a+4b+9c+16d,且 D ⊕ B 2 D \oplus B_2 D⊕B2 的剩余区域 V O ∖ ( D ⊕ B 2 ) V_O\setminus (D\oplus B_2) VO∖(D⊕B2) 的边长为2和边长为3的正方形子集最多[3](#3)。
- (正方分割问题)得到 D D D 的一个划分,使得 D D D 分为 a a a 个边长为1的正方形、 b b b 个边长为2的正方形、 c c c 个边长为3的正方形、 d d d 个边长为4的正方形。若不存在这样的划分则回到第1个问题。
而其中第1个问题是NP完全的,且在实际游戏中的 n ∈ ( 40 , 50 ) n\in(40,50) n∈(40,50),无法在有限时间内求解,因此我们采用贪心算法寻找局部最优解。
首先证明,最优的区域 D ⊆ V B D\sube V_B D⊆VB 必为一个凸区域且所有的建筑尽可能紧密地布置在一个集中的区域内。
证:
引理 :设 D ⊆ Z 2 D\sube\Z^2 D⊆Z2 为凸集,则 ∣ D ⊕ B ∣ |D \oplus B| ∣D⊕B∣ 与 P ( D ) \mathcal{P}(D) P(D) 正相关[4](#4),其中 B = { 0 , 1 , ⋯ , b } 2 B=\{0,1,\cdots,b\}^2 B={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∣,且 B B B 是紧凸集且具有非空内部,故系数 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 > 0 w(B)=π1∫02πhB(θ) dθ>0,其中 h B ( θ ) h_B(\theta) hB(θ) 为 B B B 在方向 θ \theta θ 上的支持函数,故 ∣ D ⊕ B ∣ |D \oplus B| ∣D⊕B∣ 与 P ( D ) \mathcal{P}(D) P(D) 正相关。
设 2 x 2
障碍物可放置的左上角点集合为 A 2 = { 0 , 1 , ... , n + 4 } 2 A_2 = \{0, 1, \dots, n+4\}^2 A2={0,1,...,n+4}2,3 x 3
障碍物可放置的左上角点集合为 A 3 = { 0 , 1 , ... , n + 3 } 2 A_3 = \{0, 1, \dots, n+3\}^2 A3={0,1,...,n+3}2, B 2 , B 3 B_2,B_3 B2,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∖(D⊕B2),F3=A3∖(D⊕B3) 分别表示障碍物可放置的空闲位置集合,则障碍物成功生成的概率为
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}\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) 为失败的概率。
若 D D D 不是凸的,则存在两点 p , q ∈ D p, q \in D p,q∈D 和一点 r r r 在 p p p 和 q q q 的线段上,且 r ∉ D r \notin D r∈/D。考虑将点 p p p 移动到点 r r r 且保持 ∣ 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 p p 的正方形集合。若 r r r 更靠近 q q q,则 ∣ 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| ∣D⊕B2∣ 和 ∣ D ⊕ B 3 ∣ |D \oplus B_3| ∣D⊕B3∣ 可能减小。通过反复将点向重心移动,可以减小 F ( D ) F(D) F(D),最终使 D D D 成为一个连通集。
由于在连续极限下且 V O V_O VO 足够大,损失函数 F ( D ) F(D) F(D) 是 D D D 的函数且是子模的或具有凸性性质。又 V B V_B VB 是正方形且问题对称,最优 D D D 是一个 centered rectangle,从而是凸的。
故最优建筑布置 D D D 是一个凸区域。
由引理, ∣ D ⊕ B ∣ |D \oplus B| ∣D⊕B∣ 与 P ( D ) \mathcal{P}(D) P(D) 正相关且凸集具有最小周长,故最优的区域 D ⊆ V B D\sube V_B D⊆VB 为一个凸区域且具有非空内部,即所有的建筑尽可能紧密地布置在一个集中的区域内。
证毕。
算法实现
由(1)式知, P ( D ) P(D) P(D) 关于 D D D 具有平移不变性,因此我们可以直接从 V B V_B VB 的左上角开始放置建筑且不影响 P ( D ) P(D) P(D)。
因此,我们可以采取贪心策略:从 V B V_B VB 左上角开始,优先放置大型建筑,避免分散布置,利用紧凑布局将所有建筑集中放置。具体算法如图所示:
边缘对齐优化 紧凑布局核心算法 按尺寸降序 按坐标升序 未放置 找到最优位置 无合法位置 全部放置 计算包围盒 收缩包围盒到左上角 扩展建筑到主区域边界 遍历所有建筑 初始化建筑放置矩阵 计算候选位置 计算禁区覆盖增量 选择最小增量位置 放置建筑 返回失败 更新禁区矩阵 输入参数 初始化建筑列表 建筑排序 4x4建筑优先 同一尺寸按左上角排序 创建禁区覆盖矩阵 输出布局矩阵 计算障碍物生成概率
代码实现
cpp
#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格距离的地方。 ↩︎
-
建筑主区域外延伸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} 表示闵可夫斯基和, B i , i ∈ Z + B_i,i\in\Z_+ Bi,i∈Z+ 表示边长为 i i i 的正方形,即 B i = { 0 , 1 , ⋯ , i − 1 } 2 B_i=\{0,1,\cdots,i-1\}^2 Bi={0,1,⋯,i−1}2。 ↩︎
-
P ( D ) \mathcal{P}(D) P(D) 表示 D D D 的周长,即 D D D 与 D c D^c Dc 之间的边界边(两个端点分别属于 D D D 和 D c D^c Dc)的数量。 ↩︎