题解:洛谷 P1496 火烧赤壁
【题目来源】
【题目描述】
曹操平定北方以后,公元 \(208\) 年,率领大军南下,进攻刘表。他的人马还没有到荆州,刘表已经病死。他的儿子刘琮听到曹军声势浩大,吓破了胆,先派人求降了。
孙权任命周瑜为都督,拨给他三万水军,叫他同刘备协力抵抗曹操。
隆冬的十一月,天气突然回暖,刮起了东南风。
没想到东吴船队离开北岸大约二里距离,前面十条大船突然同时起火。火借风势,风助火威。十条火船,好比十条火龙一样,闯进曹军水寨。那里的船舰,都挤在一起,又躲不开,很快地都烧起来。一眨眼工夫,已经烧成一片火海。
曹操气急败坏的把你找来,要你钻入火海把连环线上着火的船只的长度统计出来!
给定每个起火部分的起点和终点,请你求出燃烧位置的长度之和。
【输入】
第一行一个整数,表示起火的信息条数 \(n\)。
接下来 \(n\) 行,每行两个整数 \(a, b\),表示一个着火位置的起点和终点(注意:左闭右开)。
【输出】
输出一行一个整数表示答案。
【输入样例】
3
-1 1
5 11
2 9
【输出样例】
11
【核心思想】
-
问题分析:给定 \(n\) 个区间 \([a_i, b_i)\)(左闭右开),求这些区间的并集的总长度。直接累加会有重叠部分重复计算,因此需要先对区间按起点排序,然后合并重叠或相邻的区间,最后累加合并后区间的长度。
-
算法选择:
- 贪心 + 区间合并:将所有区间按左端点升序排序。维护当前合并区间的左右端点
st, ed。遍历排序后的区间,若当前区间的左端点 ≤ed(有重叠或连续),则更新ed = max(ed, r);否则,将当前合并区间长度累加,并开始新的合并区间。最终累加所有合并区间的长度。 - 复杂度:排序 \(O(n \log n)\),合并 \(O(n)\),总体 \(O(n \log n)\)。
- 贪心 + 区间合并:将所有区间按左端点升序排序。维护当前合并区间的左右端点
-
关键步骤:
- 读入 \(n\) 以及 \(n\) 个区间的起点和终点。
- 按左端点从小到大排序。
- 初始化
st = 第一个区间的左端点,ed = 第一个区间的右端点,sum = 0。 - 对于每个后续区间 \([l, r]\):
- 若 \(l \le ed\),则与当前合并区间重叠,扩展右端点
ed = max(ed, r)。 - 否则,将
ed - st累加到sum,并将st = l, ed = r。
- 若 \(l \le ed\),则与当前合并区间重叠,扩展右端点
- 最后将最后一个合并区间的长度累加。
- 输出
sum。
-
时间/空间复杂度:
- 时间复杂度:\(O(n \log n)\),排序主导。
- 空间复杂度:\(O(n)\),存储区间。
-
贪心 + 区间合并的核心思想:
- 排序保证有序扫描:按左端点排序后,可以依次处理,确保当前区间是后续所有可能重叠区间中左端点最小的。
- 重叠判定:只要新区间的起点 ≤ 当前合并区间的终点,则必然重叠(左闭右开,等于也算重叠)。
- 贪心扩展:合并时只需更新右端点,因为左端点已经固定。
- 适用场景:适用于计算多个区间并集长度、覆盖总长度等问题,是经典的区间合并模板。
【算法标签】
普及- #贪心
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 定义int为long long类型
int n; // 线段数量
int st, ed; // 当前合并区间的起点和终点
int sum; // 合并后的总长度
// 线段结构体
struct Line
{
int l, r; // 线段的起点和终点
// 重载小于运算符,用于排序
bool operator < (Line &t)
{
return l < t.l;
}
} a[20005]; // 存储所有线段
signed main()
{
// 输入线段数量
cin >> n;
// 输入每条线段的起点和终点
for (int i = 1; i <= n; i++)
{
cin >> a[i].l >> a[i].r;
}
// 按线段起点排序
sort(a + 1, a + n + 1);
// 初始化第一个合并区间
st = a[1].l;
ed = a[1].r;
sum += a[1].r - a[1].l;
// 遍历处理每条线段
for (int i = 2; i <= n; i++)
{
// 如果当前线段与合并区间有重叠
if (a[i].l <= ed)
{
// 完全被包含则跳过
if (a[i].r < ed)
{
continue;
}
// 否则扩展合并区间
else
{
st = ed; // 更新起点为原终点
ed = a[i].r; // 更新终点为新线段的终点
sum += ed - st; // 增加扩展部分的长度
}
}
// 如果没有重叠,创建新的合并区间
else
{
st = a[i].l;
ed = a[i].r;
sum += ed - st;
}
}
// 输出合并后的总长度
cout << sum << endl;
return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;
#define int long long // 定义int为long long类型
const int N = 2005; // 定义最大线段数量
typedef pair<int, int> PII; // 定义线段类型为pair
int n; // 线段数量
int sum; // 合并后的总长度
vector<PII> segs; // 存储所有线段的容器
/**
* 合并重叠或相邻的线段
* @param segs 待合并的线段集合(引用传递)
*/
void merge(vector<PII> &segs)
{
vector<PII> res; // 存储合并后的结果
sort(segs.begin(), segs.end()); // 按线段起点排序
int st = -2e9, ed = -2e9; // 初始化当前合并区间为极小值
// 遍历所有线段
for (auto seg : segs)
{
// 如果当前线段与合并区间无重叠
if (ed < seg.first)
{
// 如果不是初始状态,保存当前合并区间
if (st != -2e9)
res.push_back({st, ed});
// 开始新的合并区间
st = seg.first;
ed = seg.second;
}
// 如果有重叠,扩展当前合并区间
else
ed = max(ed, seg.second);
}
// 保存最后一个合并区间
if (st != -2e9)
res.push_back({st, ed});
segs = res; // 更新线段集合为合并后的结果
}
signed main()
{
// 输入线段数量
cin >> n;
// 输入每条线段的起点和终点
for (int i = 1; i <= n; i++)
{
int l, r;
cin >> l >> r;
segs.push_back({l, r});
}
// 合并线段
merge(segs);
// 计算合并后的总长度
for (int i = 0; i < segs.size(); i++)
sum += segs[i].second - segs[i].first;
// 输出结果
cout << sum << endl;
return 0;
}
【运行结果】
3
-1 1
5 11
2 9
11
浙公网安备 33010602011771号