P3073 [USACO13FEB] Tractor S
https://www.luogu.com.cn/problem/P3073
题目描述
FJ 有块农田太崎岖了,他要买一辆新拖拉机才能在这里巡视。这块农田由 N × N N \times N N×N 个格子的非负整数表示高度( 1 ≤ N ≤ 500 1 \le N \le 500 1≤N≤500)。拖拉机从当前格子走到相邻格子(东、南、西、北四个方向)的代价为高度差 D D D,则 FJ 驶过这两个格子的拖拉机最少也要值 D D D 块钱。
FJ 愿意花足够的钱买一辆新的拖拉机使得他能以最小的高度差走遍所有格子的一半(如果格子总数是奇数,那么一半的值为四舍五入的值)。因为 FJ 很懒,所以他找到你帮他编程计算他最小需要花多少钱买到符合这些要求的拖拉机。
输入格式
第一行为一个整数 N N N。
第 2 2 2 到 N + 1 N+1 N+1 行每行包含 N N N 个非负整数(不超过 10 6 10^6 106),表示当前格子的高度。
输出格式
共一行,表示 FJ 买拖拉机要花的最小价钱。
输入输出样例 #1
输入 #1
5
0 0 0 3 3
0 0 0 0 3
0 9 9 3 3
9 9 9 3 3
9 9 9 9 3
输出 #1
3
题解
一、题目分析
核心问题
- 目标:最小化拖拉机价值 ( D )。
- 约束:连通区域内相邻格子高度差 ≤ ( D ),且区域大小 ≥ ( K )(( K ) 是总格子数的一半,向上取整)。
二、算法选择
1. 二分答案
- 原理:( D ) 的取值具有单调性(( D ) 越大,越容易满足条件)。
- 步骤:二分查找 ( D ) 的可能值,对每个 ( D ) 检查是否存在符合条件的连通区域。
2. 并查集
- 原理:快速处理动态连通性问题,支持合并集合和查找根节点操作。
- 步骤:对于每个候选 ( D ),用并查集合并高度差 ≤ ( D ) 的相邻格子,统计最大连通块大小。
三、代码实现
1. 数据结构
cpp
struct UnionFind {
int parent; // 父节点
int size; // 集合大小(仅根节点有效)
} uf[MAX_NODE]; // 结构体数组,存储每个格子的并查集状态
2. 关键函数
find 函数(路径压缩)
cpp
int find(int x) {
while (uf[x].parent != x) {
uf[x].parent = uf[uf[x].parent].parent; // 路径压缩
x = uf[x].parent;
}
return x;
}
- 作用:找到 ( x ) 所在集合的根节点,并进行路径压缩,加速后续查找。
unite 函数(按秩合并)
cpp
void unite(int x, int y) {
x = find(x); // 找到x的根节点
y = find(y); // 找到y的根节点
if (x == y) return; // 同一集合,无需合并
if (uf[x].size < uf[y].size) { // 按大小合并
uf[x].parent = y; // 小集合合并到大集合
uf[y].size += uf[x].size; // 更新大集合大小
} else {
uf[y].parent = x;
uf[x].size += uf[y].size;
}
}
- 调用时机 :在
check函数中,当相邻格子的高度差 ≤ ( mid ) 时调用。 - 功能:合并两个相邻格子所在的集合,保持树的高度较低,提高后续操作效率。
check 函数
cpp
bool check(int mid) {
// 1. 初始化并查集
for (int i = 0; i < N * N; i++) {
uf[i].parent = i; // 每个格子初始为独立集合
uf[i].size = 1; // 集合大小为1
}
// 2. 合并相邻且高度差符合条件的格子
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
// 检查右边格子
if (j + 1 < N) {
int d = abs(h[i][j] - h[i][j+1]);
if (d <= mid) {
int u = i * N + j; // 当前格子编号
int v = i * N + (j + 1); // 右边格子编号
unite(u, v); // 合并集合
}
}
// 检查下边格子(避免重复)
if (i + 1 < N) {
// 同理...
}
}
}
// 3. 统计最大连通块大小
int max_size = 0;
for (int i = 0; i < N * N; i++) {
if (uf[i].parent == i) { // 根节点
if (uf[i].size > max_size) {
max_size = uf[i].size;
}
}
}
return max_size >= K; // 判断是否满足条件
}
的拖拉机价值 ( D ),满足题目要求。
完整代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 505;
const int MAX_NODE = MAXN * MAXN;
struct UnionFind {
int parent;
int size;
} uf[MAX_NODE];
int N, K;
int h[MAXN][MAXN];
int find(int x) {
while (uf[x].parent != x) {
uf[x].parent = uf[uf[x].parent].parent;
x = uf[x].parent;
}
return x;
}
void unite(int x, int y) {
x = find(x);
y = find(y);
if (x == y) return;
if (uf[x].size < uf[y].size) {
uf[x].parent = y;
uf[y].size += uf[x].size;
} else {
uf[y].parent = x;
uf[x].size += uf[y].size;
}
}
bool check(int mid) {
// 初始化并查集
for (int i = 0; i < N * N; i++) {
uf[i].parent = i;
uf[i].size = 1;
}
// 合并相邻格子
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
// 右边格子
if (j + 1 < N) {
int d = abs(h[i][j] - h[i][j+1]);
if (d <= mid) {
int u = i * N + j;
int v = i * N + (j + 1);
unite(u, v);
}
}
// 下边格子
if (i + 1 < N) {
int d = abs(h[i][j] - h[i+1][j]);
if (d <= mid) {
int u = i * N + j;
int v = (i + 1) * N + j;
unite(u, v);
}
}
}
}
// 统计最大连通块大小
int max_size = 0;
for (int i = 0; i < N * N; i++) {
if (uf[i].parent == i) {
if (uf[i].size > max_size) {
max_size = uf[i].size;
}
}
}
return max_size >= K;
}
int main() {
scanf("%d", &N);
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
scanf("%d", &h[i][j]);
}
}
// 计算K:总格子数的一半(向上取整)
int total = N * N;
K = total / 2;
if (total % 2 == 1) {
K += 1;
}
// 确定二分右边界:所有相邻格子的最大高度差
int right = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (j + 1 < N) {
int d = abs(h[i][j] - h[i][j+1]);
if (d > right) {
right = d;
}
}
if (i + 1 < N) {
int d = abs(h[i][j] - h[i+1][j]);
if (d > right) {
right = d;
}
}
}
}
// 二分查找
int left = 0;
int ans = right;
while (left <= right) {
int mid = (left + right) / 2;
if (check(mid)) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
cout << ans << endl;
return 0;
}