C语言学习笔记20260626-道路树木统计(暴力标记与区间合并)

C语言学习笔记20260626-道路树木统计(暴力标记与区间合并)

一、学习目标

通过经典的"道路树木统计"问题,掌握处理"区间覆盖"类问题的两种核心算法思想。深入理解暴力标记法的直观逻辑,并学习如何通过区间合并法进行算法优化,体会从"逐点操作"到"整体区间处理"的思维跃迁。

二、问题拆解与核心逻辑

本题要求计算一条长度为 L 的道路(共有 L+1 棵树),在经过 M 次施工清理(每次清理区间 l, r 内的所有树木)后,剩余树木的数量。核心约束条件为:

  1. 区间覆盖:施工区域可能会重叠,但同一位置的树木只能被清理一次。
  2. 最终统计:需要求出未被任何施工区间覆盖的整数点个数。

三、方法一:标记数组法(暴力模拟)

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, 200150, 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)时,必须采用区间合并法。