C语言学习笔记20260626-道路树木统计(暴力标记与区间合并)
一、学习目标
通过经典的"道路树木统计"问题,掌握处理"区间覆盖"类问题的两种核心算法思想。深入理解暴力标记法的直观逻辑,并学习如何通过区间合并法进行算法优化,体会从"逐点操作"到"整体区间处理"的思维跃迁。
二、问题拆解与核心逻辑
本题要求计算一条长度为 L 的道路(共有 L+1 棵树),在经过 M 次施工清理(每次清理区间 l, r 内的所有树木)后,剩余树木的数量。核心约束条件为:
- 区间覆盖:施工区域可能会重叠,但同一位置的树木只能被清理一次。
- 最终统计:需要求出未被任何施工区间覆盖的整数点个数。
三、方法一:标记数组法(暴力模拟)
3.1 核心思路
利用一个数组 arr 来物理模拟整条道路。初始时将所有位置标记为"有树(1)"。每当有一个施工区间 l, r 时,直接通过循环将该区间内的所有位置标记为"无树(0)"。最后遍历整个数组,统计剩余"有树"的位置。
3.2 完整代码实现
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
// 方法1:标记数组法
int main()
{
int L, M;
scanf("%d %d", &L, &M);
// 初始化道路,0~L 每个位置都有树(标记为1)
int arr[10000] = { 0 };
for (int i = 0; i <= L; i++)
{
arr[i] = 1;
}
// 处理 M 次施工,将区间内的树标记为0
for (int i = 0; i < M; i++)
{
int l, r;
scanf("%d %d", &l, &r);
for (int j = l; j <= r; j++) // 注意:内层循环变量应避开外层的 i
{
arr[j] = 0;
}
}
// 统计剩余树木
int count = 0;
for (int i = 0; i <= L; i++)
{
if (arr[i] == 1)
count++;
}
printf("%d", count);
return 0;
}
3.3 方法优缺点分析
- 优点:逻辑极其直观,完全符合人类"画一条路,把挖掉的地方涂黑,最后数白点"的直觉。代码编写简单,不易出错。
- 缺点:时间复杂度为 O(L + M × 区间平均长度)。如果道路长度 L 达到 10^9,或者区间长度极长,暴力法不仅会严重超时,还会因为数组过大导致内存溢出。
四、方法二:区间合并法(数学优化)
4.1 核心思想:化零为整
不逐个去处理道路上的每一个点,而是将 M 个施工区间看作整体。如果两个区间有重叠(例如 100, 200 和 150, 300),它们实际上等效于合并成了一个更大的区间 100, 300。通过合并所有重叠区间,我们可以快速计算出"被挖掉的总长度",最后用总树数减去被挖掉的树数即可。
4.2 完整代码实现
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
// 方法二:区间合并法,效率高
// 区间结构体
struct Range
{
int l, r;
} arr[105];
// 冒泡排序,按左端点升序,arr[i] 代表完整一段区间 [l, r],l 和 r 是一对绑定的数据
void sort(int m)
{
for(int i = 0; i < m - 1; i++)
{
for(int j = 0; j < m - 1 - i; j++)
{
if(arr[j].l > arr[j+1].l)
{
struct Range t = arr[j];
arr[j] = arr[j+1];
arr[j+1] = t;
}
}
}
}
int main()
{
int L, M;
scanf("%d%d", &L, &M);
// 读取 M 个施工区间
for(int i = 0; i < M; i++)
scanf("%d%d", &arr[i].l, &arr[i].r);
// 1. 排序:按区间左端点升序排列
sort(M);
// 2. 贪心合并区间
int cnt = 0; // 记录被挖掉的树的总数
int nowL = arr[0].l, nowR = arr[0].r; // 维护当前正在合并的区间 [nowL, nowR]
for(int i = 1; i < M; i++)
{
if(arr[i].l <= nowR)
{
// 情况一:区间重叠(新左端点 <= 当前右端点)
// 更新右端点,取两者的最大值以扩展合并区间
if(arr[i].r > nowR)
nowR = arr[i].r;
}
else
{
// 情况二:区间不重叠(新左端点 > 当前右端点)
// 当前合并区间 [nowL, nowR] 结束,累加其长度
cnt += nowR - nowL + 1;
// 开启新的合并区间
nowL = arr[i].l;
nowR = arr[i].r;
}
}
// 3. 加上最后一段未累加的合并区间
cnt += nowR - nowL + 1;
// 4. 计算剩余树木:总树数 (L+1) - 被挖掉的树 (cnt)
int ans = (L + 1) - cnt;
printf("%d", ans);
return 0;
}
4.3 核心细节解析
- 排序的必要性:只有按左端点排序后,所有可能重叠的区间才会连续排列,我们才能通过一次线性遍历完成合并。
- 重叠的判断条件 :
arr[i].l <= nowR。只要新区间的起点在当前合并区间的终点左边(或刚好相接),就说明它们连成了一片。 - 易错点 :循环结束后,最后一段正在维护的区间
[nowL, nowR]还没有被累加到cnt中,必须在循环外手动加上。
五、总结与工程实践建议
标记数组法是理解"区间覆盖"问题的基石,其"数组映射"的思想在数据规模较小时非常有效。但在面对大规模数据时,区间合并法展示了极高的算法智慧。它不仅将时间复杂度降低到了 O(M log M)(主要消耗在排序上),更展示了"将离散的点操作转化为连续的区间操作"这一解决复杂问题的通用范式。在实际开发中,当 L 较小(如 10^5 以内)时,可采用标记法快速解题;当 L 极大但 M 较小(如 10^5)时,必须采用区间合并法。