植树
题目描述
小明和朋友们一起去郊外植树,他们带了一些在自己实验室精心研究出的小树苗。
小明和朋友们一共有 nn 个人,他们经过精心挑选,在一块空地上每个人挑选了一个适合植树的位置,总共 nn 个。他们准备把自己带的树苗都植下去。
然而,他们遇到了一个困难:有的树苗比较大,而有的位置挨太近,导致两棵树植下去后会撞在一起。
他们将树看成一个圆,圆心在他们找的位置上。如果两棵树对应的圆相交,这两棵树就不适合同时植下(相切不受影响),称为两棵树冲突。
小明和朋友们决定先合计合计,只将其中的一部分树植下去,保证没有互相冲突的树。他们同时希望这些树所能覆盖的面积和(圆面积和)最大。
输入描述
输入的第一行包含一个整数 nn ,表示人数,即准备植树的位置数。
接下来 nn 行,每行三个整数 x, y, rx, y, r,表示一棵树在空地上的横、纵坐标和半径。
其中,1≤n≤30,0≤x,y≤1000,1≤r≤10001≤n≤30,0≤x,y≤1000,1≤r≤1000。
输出描述
输出一行包含一个整数,表示在不冲突下可以植树的面积和。由于每棵树的面积都是圆周率的整数倍,请输出答案除以圆周率后的值(应当是一个整数)。
输入输出样例
示例
输入
6
1 1 2
1 4 2
1 7 2
4 1 2
4 4 2
4 7 2
输出
12
运行限制
- 最大运行时间:3s
- 最大运行内存: 512M
总通过次数: 524 | 总提交次数: 785 | 通过率: 66.8%
难度: 困难 标签: 2020, 省模拟赛, 搜索
算法思路
本问题需要选择一组互不冲突的树(圆不相交),最大化总覆盖面积(半径平方和)。由于树的数量 n ≤ 30,可以采用深度优先搜索(DFS)配合优化策略:
- 冲突检测:两棵树冲突的条件是圆心距² < (r₁ + r₂)²(严格小于,相切不算冲突)
- 连通分量分解:将树按冲突关系划分为多个独立子集(分量内树可能冲突,分量间树绝对不冲突)
- DFS搜索:对每个连通分量独立搜索最优解
- 剪枝优化:
- 按半径降序排序(优先选择大半径树)
- 后缀和剪枝(当前和 + 剩余树最大可能和 ≤ 当前最大值时剪枝)
- 状态压缩:对小型分量(≤15 节点)使用位运算 DP 加速
算法步骤
- 输入处理:读取树的数量和每棵树的坐标半径
- 按半径降序排序:确保优先处理大半径树
- 构建冲突图:
- 计算任意两棵树圆心距²
- 若圆心距² < (rᵢ + rⱼ)² 则标记冲突
- 分解连通分量:
- 用 BFS/DFS 将树划分为冲突连通的子集
- 不同分量独立处理
- 分量求解:
- 小型分量(≤15):状态压缩 DP
- 大型分量:DFS + 后缀和剪枝
- 结果合并:累加各分量最优解
算法演示
代码实现
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <cmath>
#include <unordered_map>
using namespace std;
struct Tree {
int x, y, r, id;
};
// 计算圆心距平方(避免浮点误差)
long long distSq(const Tree& a, const Tree& b) {
long long dx = a.x - b.x;
long long dy = a.y - b.y;
return dx*dx + dy*dy;
}
// 冲突检测:圆心距² < (r1+r2)²
bool isConflict(const Tree& a, const Tree& b) {
long long rSum = (long long)a.r + b.r;
return distSq(a, b) < rSum * rSum;
}
// 连通分量分解
vector<vector<int>> findComponents(vector<vector<bool>>& conflict) {
int n = conflict.size();
vector<bool> visited(n, false);
vector<vector<int>> components;
for (int i = 0; i < n; ++i) {
if (visited[i]) continue;
queue<int> q;
vector<int> comp;
q.push(i);
visited[i] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
comp.push_back(u);
for (int v = 0; v < n; ++v) {
if (conflict[u][v] && !visited[v]) {
visited[v] = true;
q.push(v);
}
}
}
components.push_back(comp);
}
return components;
}
// 状态压缩DP(处理小规模分量)
int dpComp(const vector<Tree>& trees, const vector<vector<bool>>& compConflict) {
int n = trees.size();
if (n == 0) return 0;
vector<int> dp(1 << n, 0);
int best = 0;
for (int mask = 1; mask < (1 << n); ++mask) {
bool valid = true;
int sumSq = 0;
for (int i = 0; i < n && valid; ++i) {
if (!(mask & (1 << i))) continue;
sumSq += trees[i].r * trees[i].r;
for (int j = i + 1; j < n; ++j) {
if ((mask & (1 << j)) && compConflict[i][j]) {
valid = false;
break;
}
}
}
if (valid) {
dp[mask] = sumSq;
best = max(best, sumSq);
} else {
// 状态转移:枚举子集优化
dp[mask] = 0;
for (int submask = mask; submask; submask = (submask - 1) & mask) {
dp[mask] = max(dp[mask], dp[submask]);
}
}
}
return best;
}
// DFS + 剪枝(处理大规模分量)
void dfsComp(const vector<Tree>& trees, const vector<vector<bool>>& compConflict,
vector<bool>& selected, int index, int currentSum,
int& bestSum, const vector<int>& suffix) {
if (index == trees.size()) {
bestSum = max(bestSum, currentSum);
return;
}
// 剪枝:当前和 + 剩余最大可能和 ≤ 最优解
if (currentSum + suffix[index] <= bestSum) return;
// 不选当前树
dfsComp(trees, compConflict, selected, index + 1, currentSum, bestSum, suffix);
// 检查与已选树是否冲突
bool noConflict = true;
for (int i = 0; i < index; ++i) {
if (selected[i] && compConflict[index][i]) {
noConflict = false;
break;
}
}
// 选当前树(无冲突时)
if (noConflict) {
selected[index] = true;
int newSum = currentSum + trees[index].r * trees[index].r;
dfsComp(trees, compConflict, selected, index + 1, newSum, bestSum, suffix);
selected[index] = false;
}
}
int main() {
int n;
cin >> n;
vector<Tree> trees(n);
for (int i = 0; i < n; ++i) {
cin >> trees[i].x >> trees[i].y >> trees[i].r;
trees[i].id = i;
}
// 按半径降序排序(优化剪枝)
sort(trees.begin(), trees.end(), [](const Tree& a, const Tree& b) {
return a.r > b.r;
});
// 构建冲突矩阵
vector<vector<bool>> conflict(n, vector<bool>(n, false));
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
conflict[i][j] = conflict[j][i] = isConflict(trees[i], trees[j]);
}
}
// 分解连通分量
auto components = findComponents(conflict);
int totalSum = 0;
for (const auto& comp : components) {
if (comp.empty()) continue;
// 提取当前分量的树
vector<Tree> compTrees;
for (int idx : comp) {
compTrees.push_back(trees[idx]);
}
int compSize = compTrees.size();
// 构建分量冲突矩阵
vector<vector<bool>> compConflict(compSize, vector<bool>(compSize, false));
for (int i = 0; i < compSize; ++i) {
for (int j = i + 1; j < compSize; ++j) {
compConflict[i][j] = compConflict[j][i] =
isConflict(compTrees[i], compTrees[j]);
}
}
int compBest = 0;
// 小分量用状态压缩DP
if (compSize <= 15) {
compBest = dpComp(compTrees, compConflict);
}
// 大分量用DFS+剪枝
else {
// 计算后缀和(从i开始的最大可能和)
vector<int> suffix(compSize, 0);
suffix[compSize-1] = compTrees.back().r * compTrees.back().r;
for (int i = compSize-2; i >= 0; --i) {
suffix[i] = suffix[i+1] + compTrees[i].r * compTrees[i].r;
}
vector<bool> selected(compSize, false);
dfsComp(compTrees, compConflict, selected, 0, 0, compBest, suffix);
}
totalSum += compBest;
}
cout << totalSum << endl;
return 0;
}
代码解析
-
冲突检测优化:
- 使用
distSq
计算圆心距平方(整数运算避免浮点误差) isConflict
通过圆心距² < (r₁ + r₂)²判断冲突
- 使用
-
连通分量分解:
findComponents
用BFS将树分组- 不同分量完全独立,减少搜索空间
-
混合求解策略:
- ≤15节点:状态压缩DP(
dpComp
),用位掩码表示选择状态 - >15节点:DFS(
dfsComp
)配合后缀和剪枝
- ≤15节点:状态压缩DP(
-
关键优化点:
- 按半径降序排序(使剪枝更早生效)
- 后缀和数组(快速计算剩余最大可能值)
- 状态压缩DP(高效处理小分量)
-
时间复杂度:
- 最坏情况:O(2^(n/2))(优于朴素O(2^n))
- 平均情况:O(n² + k·2^(m))(m为最大分量大小)
实例验证
输入样例:
6
1 1 2
1 4 2
1 7 2
4 1 2
4 4 2
4 7 2
执行过程:
- 连通分量分解:6棵树全连通(因(1,1)与(4,1)冲突)
- 分量求解:
- 分量大小6 ≤ 15 → 状态压缩DP
- 有效组合:{(1,1,2), (1,7,2), (4,4,2)}
- 半径平方和 = 2² + 2² + 2² = 12
- 输出结果:12 ✓
其他测试:
测试场景 | 输入特征 | 输出 | 验证结果 |
---|---|---|---|
单棵树 | [(0,0,5)] | 25 | ✓ |
全冲突 | [(0,0,1),(0,0,2),(0,0,3)] | 9 | ✓ |
无冲突 | [(0,0,1),(3,0,1),(6,0,1)] | 3 | ✓ |
链式冲突 | [(0,0,2),(0,3,2),(0,6,2)] | 8 | ✓ |
注意事项
-
精度处理:
- 全程使用整数运算(圆心距²和半径和²)
- 避免浮点数比较误差
-
边界条件:
- 单棵树直接返回r²
- 空输入返回0
- 全冲突时选最大单树
-
特殊场景:
- 圆心重合时:任意两棵树都冲突
- 超大半径树:可能"吞并"周围所有树
- 分散小树群:独立分量加速处理
测试点设计
测试类型 | 测试数据 | 验证重点 |
---|---|---|
功能测试 | 示例输入 | 基础功能 |
边界测试 | n=1, n=30 | 极端输入 |
冲突测试 | 全冲突/无冲突 | 选择逻辑 |
精度测试 | 临界距离(r₁+r₂)²±1 | 冲突判断 |
性能测试 | n=30随机数据 | 运行时间≤1s |
优化建议
-
迭代加深搜索:
// 限制深度逐步增加 for (int depth = 1; depth <= maxDepth; depth++) { if (dfsWithDepthLimit(...)) break; }
-
启发式排序:
// 按冲突度排序(高冲突树优先) sort(trees.begin(), trees.end(), [&](Tree a, Tree b) { return conflictCount[a.id] > conflictCount[b.id]; });
-
并行计算:
#pragma omp parallel for for (auto& comp : components) { solveComponent(comp); }
-
记忆化搜索:
unordered_map<string, int> memo; // 状态缓存