题目链接
题目描述
某校大门外长度为 L
的马路上有一排树,每两棵相邻的树之间的间隔都是1米。我们可以把马路看成一个数轴,马路的一端在数轴 0
的位置,另一端在 L
的位置;数轴上的每个整数点,即 0, 1, ..., L
,都种有一棵树。
现在马路上有一些区域要用来建地铁,这些区域用它们在数轴上的起始点和终止点表示。已知任一区域的起始点和终止点的坐标都是整数,区域之间可能有重合的部分。现在要把这些区域中的树(包括区域端点处的两棵树)移走。请计算将这些树都移走后,马路上还有多少棵树。
输入描述:
- 第一行输入两个整数
L
和M
,用空格隔开,表示马路的长度和地铁施工区域数。 - 接下来
M
行,每行输入两个整数u
和v
,用空格隔开,表示一个施工区域的起始点和终止点坐标。
输出描述:
- 输出一个整数,表示移除所有施工区域内(包括端点)的树后,剩余的树的总棵树。
示例说明:
- 输入:
500 3 150 300 100 200 470 471
- 输出:
298
- 说明:
- 马路总长
500
,所以初始有500 + 1 = 501
棵树。 - 三个区域
[150, 300]
,[100, 200]
,[470, 471]
内的树要被移除。 - 由于区域
[150, 300]
和[100, 200]
有重合部分[150, 200]
,直接计算各区段树木数量然后相加再减去会很复杂。 - 更简单的方法是标记所有被移除的树,然后统计未被标记的树的数量。
- 马路总长
解题思路
本题的核心是处理区间重叠问题。由于马路总长度 L
的最大值(10000)不是特别大,我们可以使用一个数组来模拟马路上的每一棵树的状态,这是一种非常直观且易于实现的方法。
思路:标记法 (数组模拟)
-
创建马路模型: 我们可以创建一个大小为
L+1
的数组(比如布尔类型或整型),road[i]
用来表示在位置i
的树的状态。例如,road[i] = 1
(或true
) 表示树存在,road[i] = 0
(或false
) 表示树被移除。 -
初始化:
- 马路的总长度是
L
,但树是种在整数点上的,从0
到L
,所以总共有L+1
个位置。 - 我们创建一个大小为
L+1
的数组,并将所有元素初始化为1
,表示所有位置最初都有树。 - 初始的树木总数
total_trees
就是L+1
。
- 马路的总长度是
-
处理施工区域:
- 我们循环
M
次,每次读入一个施工区域的起点u
和终点v
。 - 对于每个区域
[u, v]
,我们再用一个循环,遍历从u
到v
的所有位置。 - 在遍历过程中,我们将对应位置的树标记为"已移除"。即,如果
road[i]
的值还是1
,我们就把它设置为0
,并从总树数total_trees
中减一。如果road[i]
已经为0
,说明这棵树因为与其他区域重叠已经被移除过了,我们就不需要重复计数。
- 我们循环
-
输出结果:
- 当所有
M
个区域都处理完毕后,变量total_trees
中存储的就是最终剩余的树木数量。直接输出即可。
- 当所有
这个方法巧妙地避开了复杂的区间合并计算,通过空间换时间,将问题简化为简单的标记和计数。
思路二:差分数组 (优化)
"标记法"的思路虽然直观,但其时间复杂度和区间长度有关。当 M
和 L
都很大时,内层循环会执行很多次,效率不高。我们可以使用 差分数组 的思想来优化区间的标记过程。
-
差分思想: 差分是前缀和的逆运算。我们可以创建一个差分数组
diff
,用它来记录每个点相对于前一个点的变化量。diff
数组的作用是:对原数组一个区间[u, v]
里的所有元素都加上一个数x
,只需要在差分数组上做两次修改:diff[u] += x
和diff[v+1] -= x
。- 最后,通过计算差分数组的前缀和,就可以还原出被修改后的原数组。
-
应用于本题:
- 我们可以用一个数组
coverage
来记录每个位置被多少个施工区域所覆盖。初始时,所有位置的覆盖次数都是0。 - 我们想对每个施工区域
[u, v]
,将coverage
数组在[u, v]
区间内的所有元素都+1
。 - 我们可以创建
coverage
数组对应的差分数组diff
,其大小为L+2
(因为v
最大可以是L
,v+1
就需要L+1
的索引,数组大小需要L+2
)。 - 对于每个区域
[u, v]
,我们执行diff[u]++
和diff[v+1]--
。 - 处理完所有
M
个区域后,我们对diff
数组求前缀和,来得到最终的coverage
数组。coverage[i] = coverage[i-1] + diff[i]
(其中coverage[-1]=0
)。
coverage[i]
的值就代表了位置i
的树被多少个施工区域覆盖。
- 我们可以用一个数组
-
统计结果:
- 在得到
coverage
数组后,我们遍历从0
到L
的每个位置。 - 如果
coverage[i] == 0
,说明这个位置没有被任何施工区域覆盖,这里的树还存在。 - 我们统计
coverage[i] == 0
的位置个数,就是最终剩余的树木数量。
- 在得到
算法步骤概览:
- 读入
L
和M
。 - 创建一个大小为
L+1
的数组road
,全部初始化为1
。 for m from 1 to M:
- 读入区域
u, v
。 for i from u to v:
- 将
road[i]
标记为0
(表示移除)。
- 将
- 读入区域
count = 0
for i from 0 to L:
- 如果
road[i]
是1
,则count++
。
- 如果
- 输出
count
。
代码
#include <iostream>
#include <vector>
#include <numeric>
using namespace std;
int main() {
int l, m;
cin >> l >> m;
// 创建一个大小为 l+1 的数组,true 表示有树
vector<bool> road(l + 1, true);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
// 将区域 [u, v] 内的树标记为 false (移除)
for (int j = u; j <= v; ++j) {
road[j] = false;
}
}
int remaining_trees = 0;
// 统计还剩多少棵树
for (int i = 0; i <= l; ++i) {
if (road[i]) {
remaining_trees++;
}
}
cout << remaining_trees << endl;
return 0;
}
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int l = sc.nextInt();
int m = sc.nextInt();
// 创建布尔数组,true 表示有树
boolean[] road = new boolean[l + 1];
for (int i = 0; i <= l; i++) {
road[i] = true;
}
for (int i = 0; i < m; i++) {
int u = sc.nextInt();
int v = sc.nextInt();
// 标记要移除的树
for (int j = u; j <= v; j++) {
road[j] = false;
}
}
int remainingTrees = 0;
// 统计剩余的树
for (int i = 0; i <= l; i++) {
if (road[i]) {
remainingTrees++;
}
}
System.out.println(remainingTrees);
}
}
l, m = map(int, input().split())
# 创建列表,1 表示有树
road = [1] * (l + 1)
for _ in range(m):
u, v = map(int, input().split())
# 标记要移除的树
for i in range(u, v + 1):
road[i] = 0
# 统计剩余的树
# sum()可以直接计算列表中所有元素的和
remaining_trees = sum(road)
print(remaining_trees)
方法二:差分数组
#include <iostream>
#include <vector>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int l, m;
cin >> l >> m;
// 差分数组,大小为 l+2 以处理 v=l 的情况 (v+1)
vector<int> diff(l + 2, 0);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
diff[u]++;
diff[v + 1]--;
}
int remaining_trees = 0;
int coverage = 0; // 当前点的覆盖数
// 通过前缀和计算每个点的覆盖数并统计
for (int i = 0; i <= l; ++i) {
coverage += diff[i];
if (coverage == 0) {
remaining_trees++;
}
}
cout << remaining_trees << endl;
return 0;
}
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int l = sc.nextInt();
int m = sc.nextInt();
// 差分数组
int[] diff = new int[l + 2];
for (int i = 0; i < m; i++) {
int u = sc.nextInt();
int v = sc.nextInt();
diff[u]++;
diff[v + 1]--;
}
int remainingTrees = 0;
int coverage = 0; // 当前点的覆盖数
// 通过前缀和还原覆盖数组并统计
for (int i = 0; i <= l; i++) {
coverage += diff[i];
if (coverage == 0) {
remainingTrees++;
}
}
System.out.println(remainingTrees);
}
}
import sys
def solve():
l, m = map(int, sys.stdin.readline().split())
# 差分数组
diff = [0] * (l + 2)
for _ in range(m):
u, v = map(int, sys.stdin.readline().split())
diff[u] += 1
diff[v + 1] -= 1
remaining_trees = 0
coverage = 0
# 计算前缀和并统计
for i in range(l + 1):
coverage += diff[i]
if coverage == 0:
remaining_trees += 1
print(remaining_trees)
solve()
算法及复杂度
- 方法一:标记法
- 算法: 数组模拟/标记法。
- 时间复杂度:
。最坏情况下,每个区域都覆盖整个马路,复杂度接近
。
L
来自于最后的统计(或初始化),M*L
来自于M
个区域的标记过程。鉴于本题L
和M
的范围,这个复杂度是完全可以接受的。 - 空间复杂度:
,用于创建模拟马路的数组。
- 方法二:差分数组
- 算法: 差分数组。
- 时间复杂度:
。
O(M)
用于遍历M
个区间并更新差分数组,O(L)
用于计算前缀和并统计结果。 - 空间复杂度:
,用于创建差分数组。