线段或区间重叠问题

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


反思

新手小白学算法时的题记。本文都是个人的见解,如有疑惑的地方欢迎评论,小白会改进的。
问题:做一道有线段重叠的问题时,一直报TLE。
反映的情况:
(1)对时间复杂度不够理解,也不怎么关注时间复杂度,导致常常报TLE的错误。
(2)对于前缀和、差分算法掌握不牢,没能深刻明白差分的应用。
(3)缺少合并区间这种更新左右端点的思想。


一、线段重叠问题

两段线段有2种分布情况:

(1)两条线段有重叠部分(完全重叠、部分重叠)

(2)两条线段相离
线段分布情况


二、例题

1.又下雨了

题目描述
假设在无限平面上的直角坐标系中,X 轴代表地面,天空中有 n 块雨布(我们不考虑它们的厚度)。 每块雨幕都可以描述为一个线段,它从(x1,y1)开始,到 (x2,y2) 结束。现在,如果从无限高的天空开始下雨,我想让你告诉我 x 轴上有多少个单位不会被雨淋到。
为了简化问题,雨水落下时只能垂直移动。 请注意,两块雨幕可以相互重叠和交叉,而且没有垂直放置的雨幕。

输入
第一行包含一个正整数 n(1≤n≤100000)。 接下来的 n 行中,每一行都包含四个正整数
x1,y1,x2,y2(1≤x1<x2≤100000,1≤y1,y2≤100000),代表一个从 (x1,y1)开始到
(x2,y2)结束的雨幕。

输出
唯一的整数 - 不会被雨淋到的 x 轴单位数。

Example
input
5
1 2 2 1
1 1 2 2
3 3 4 3
5 1 6 3
6 3 7 2
output
4

题意
输入线段的表示:(x1, y1),(x2, y2),其对应x轴方向上的投影线段:(x1, x2)。
题目要求输入的各线段在x轴投影的线段长度总和,注意这些线段在x轴上的重叠部分。因为只涉及到x轴方向上投影的线段长度,不用考虑y轴的情况。

1.排序

思路

合并各个投影线段的重复区间(合并区间),累加处理后的区间长度就是要求的答案。

实现步骤

第一步:存储输入的各线段的投影线段,也就是(x1, x2)。
存储方式用二维数组,每个一维数组两个元素,分别存储x1, x2。
在这里插入图片描述

vector<vector<int>> a(n, vector<int>(2, 0));
	for(int i = 0; i < n; i++) {// 第一步:存储数据
		cin >> x1 >> y1 >> x2 >> y2;
		a[i][0] = x1;
		a[i][1] = x2;
	}

第二步:对数组a进行排序(左端点升序排序)
方便我们按从左到右的顺序进行合并区间操作
在这里插入图片描述

sort(a.begin(), a.end());// 第二步:左端点进行排序

第三步:合并区间,这样可以消去重叠区间的影响(在排序的基础上)
就着线段重叠情况来分析:
在这里插入图片描述

第一种情况:两线段有重叠部分
判断条件:第二条线段的左端点小于等于第一条线段的右端点,则两线段有重叠部分。
更新操作:将第二条线段的左端点更新为第一条线段的左端点,第二条线段的右端点更新为这两条线段右端点更大的一个。
第二种情况:两线段没有重叠部分,不用进行更新操作。

for(int i = 1; i < n; i++) {
		if(a[i][0] <= a[i - 1][1]) {// 第三步:线段重叠处理
			a[i][0] = a[i - 1][0];
			a[i][1] = max(a[i][1], a[i -1][1]);
			a[i - 1][0] = 0;// 注意:把原本的线段归0,因为当前的a[i]已经包含了当前线段和原本的线段
			a[i - 1][1] = 0;
		}
	}

第四步:累加区间长度

for(int i = 0; i < n; i++) {// 第四步:累加区间长度
		ans += a[i][1] - a[i][0];
	}

完整代码实现:

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;

using ll = long long;
const int mod = 998244353;
int x, y, x2, y2, ans = 0;

void solve() {
	int n;
	cin >> n;
    vector<vector<int>> a(n, vector<int>(2, 0));
	for(int i = 0; i < n; i++) {// 第一步:存储数据
		cin >> x >> y >> x2 >> y2;
		a[i][0] = x;
		a[i][1] = x2;
	}
	sort(a.begin(), a.end());// 第二步:左端点进行排序
	for(int i = 1; i < n; i++) {
		if(a[i][0] <= a[i - 1][1]) {// 第三步:线段重叠处理
			a[i][0] = a[i - 1][0];
			a[i][1] = max(a[i][1], a[i -1][1]);
			a[i - 1][0] = 0;// 注意:把原本的线段归0,因为当前的a[i]已经包含了当前线段和原本的线段
			a[i - 1][1] = 0;
		}
	}
	for(int i = 0; i < n; i++) {// 第四步:累加区间长度
		ans += a[i][1] - a[i][0];
	}
	cout << ans << endl;
}

int main() {
	ios::sync_with_stdio(0); cin.tie(nullptr);
	int t = 1;
	//cin >> t;
	while(t--) {
	    solve();
	}
	return 0;
}

2.前缀和、差分

1.知识点回顾
1.前缀和

示例:
在这里插入图片描述
前缀和,字面意思就是前面元素的累加。
sums[i]:表示索引i以及索引i以前的元素之和。
问题:
给你n个整数,再给你t个询问,每次询问给出区间[l, r],快速求出由这n个数组成的数组区间为[l,r]的元素之和。
(1)前缀和解
时间复杂度为O(t + n)

#include <bits/stdc++.h>
using namespace std;

int main() {
	ios::sync_with_stdio(0); cin.tie(nullptr);
	int n, t;
	cin >> t >> n;
	vector<int> nums(n, 0);
	vector<int> sums(n, 0);
	for(int i = 0; i < n; i++) {
		cin >> nums[i];
		if(i) {// 当i不为0时,0 - 1为负
			sums[i] = sums[i - 1] + nums[i];
		} else {// 相当于 sums[0] = nums[0];
			sums[i] = nums[i];
		}
	}
	while(t--) {// t次询问
		int l, r;
		cin >> l >> r;
		if(l) {// 注意:l为0时,直接输出sums[r]
			cout << sums[r] - sums[l - 1] << endl;
		} else {
			cout << sums[r] << endl;
		}
	}
	return 0;
}

1.区间[l, r]之间和元素和怎么用前缀和来表示?

在这里插入图片描述

2.为什么求区间[l, r]元素的和,是sums[r] - sums[l - 1]而不是sums[r] - sums[l]?

首先我们要清楚sums[i]表示的含义:索引i以及i之前的元素之和。区间[l, r]之间的元素之和,包含两个端点元素,如果是sums[r] - sums[l],那么nums[l]就会被减去,从而错误。
(2) 暴力解法
时间复杂度为O(t * n)

#include <bits/stdc++.h>
using namespace std;

vector<int> nums = {1, 2, 3, 4, 5};

void solve(int l, int r) {
	int ans = 0;
	for(int i = l; i <= r; i++) {
		ans += nums[i];
	}
	cout << ans << endl;
}

int main() {
	ios::sync_with_stdio(0); cin.tie(nullptr);
	int t;
	cin >> t;
	while(t--) {// t次询问
		int l, r;
		cin >> l >> r;
	    solve(l, r);
	}
	return 0;
}

比较这两种方法,暴力算法时间复杂度比较高,容易TLE,而前缀和求区间长度相比之下就是更优解了。


2.差分

定义:
序列a的差分序列b定义为:
b[1] = a[1];// 初始化
b[i] = a[i] - a[i - 1];
b[i]表示:元素a[i]与它前一个元素a[i - 1]的差值。
在这里插入图片描述
b序列是a序列的差分,a序列是b序列的前缀和。
tip:
给序列a区间为[l, r]的元素都加上d,等价于b[l] + d, b[r + 1] + d;即区间操作可以转换为两点操作。

在这里插入图片描述

分析
b[i]表示:nums[i]与前面一个元素nums[i - 1]的差值
nums[1, r] + d后
b[1] = nums[1] + d - nums[l - 1] = nums[i] - nums[l - 1];// 也就是原本的b[l] + d
b[r] = nums[r] + d -( nums[r - 1] +d) = nums[r] - nums[r - 1];// 也就是原本的b[r]
b[r + 1] = nums[r + 1] - (nums[r] + d) = b[r + 1] - d;// 原本的b[r + 1] - d;
总结:
序列a的区间,[l, r]加d后,相当于b[l] + d, b[r + 1] - d。

2.前缀和、差分解题

用差分表示线段的长度:
想到用差分是因为:可以用差分还原来标记一段线段覆盖的点,例如:
在这里插入图片描述
如果想要标记线段(1,5),可以通过差分来表示:
b[1]++;
b[5 + 1]--;
这样还原序列a后,变为:

for(int i = 0; i < n; i++) {
		int x1, y1, x2, y2;
		cin >> x1 >> y1 >> x2 >> y2;
		a[x1]++;
		a[x2]--;
		mx = max(mx, x2);
	}

累加区段长:
在这里插入图片描述
可以发现,覆盖的点在相应的a[i]是相加了,但是我们只需要计算其中非0元素的个数就能得到答案了。因为覆盖的点比如图中的2,表示有两条线段覆盖了该点,但是我们只需要判断该点是否被覆盖,也就是判断a[i]是否为1,为1,就长度加1,否则就不加。
代码实现:

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;

using ll = long long;
const int mod = 998244353;
const int N = 100005;
int a[N];

void solve() {
    int n;
    cin >> n;
	int mx = 0;
	for(int i = 0; i < n; i++) {
		int x1, y1, x2, y2;
		cin >> x1 >> y1 >> x2 >> y2;
		a[x1]++;
		a[x2]--;
		mx = max(mx, x2);
	}
	int ans = 0;
	for(int i = 0; i <= mx; i++) {
		if(i) {
			a[i] += a[i - 1];
		}
		if(a[i]) {
			ans++;
		}
	}
	cout << ans << endl;
}

int main() {
	ios::sync_with_stdio(0); cin.tie(nullptr);
	int t = 1;
	//cin >> t;
	while(t--) {
	    solve();
	}
	return 0;
}


2.用最少数量的箭引爆气球

题目连接:452. 用最少数量的箭引爆气球

题目描述
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart,xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend,且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。给你一个数组 points ,返回引爆所有气球所必须射出的 最小弓箭数 。

示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
在x = 6处射出箭,击破气球[2,8]和[1,6]。
在x = 11处发射箭,击破气球[10,16]和[7,12]。 示例 2:

输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。
示例 3:

输入:points = [[1,2],[2,3],[3,4],[4,5]] 输出:2 解释:气球可以用2支箭来爆破:

  • 在x = 2处发射箭,击破气球[1,2]和[2,3]。
  • 在x = 4处射出箭,击破气球[3,4]和[4,5]。

提示:

1 <= points.length <= 10^5
points[i].length == 2
-2^31 <= xstart < xend <= 2^31 - 1

题意:用最少的弓箭射击气球,贪心,每次用一根弓箭击穿最多能击穿的气球,这就涉及到线段重复问题,因为气球会有重叠的。为了方便我们对重复区间进行操作,要先排序。
贪心 + 排序
在这里插入图片描述
射击的情况:
(1)当前气球下侧没有气球时,进行射击。
(2)当前气球下侧有气球时,继续往下面找寻是否还有气球在它的下侧,直到气球下侧没有气球,进行射击。
代码实现:

int findMinArrowShots(vector<vector<int>>& points) {
       int len = points.size();
       if(len == 0) {
            return 0;
       }
       int ans = 1;
       sort(points.begin(), points.end());
       for(int i = 1; i < len; i++) {
            if(points[i][0] > points[i - 1][1]) {
			    ans++;
		    } else {// 关键点
			    points[i][1] = min(points[i][1], points[i - 1][1]);
            }
        }
    return ans;
    }

3.合并区间

题目连接:Leetcode 56. 合并区间

题目描述
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

示例 1

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2

输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示:

1 <= intervals.length <= 104 intervals[i].length == 2 0 <= starti <=
endi <= 104

分析:其实也是线段重叠了,然后对重叠的区间进行一个更新操作。

vector<vector<int>> merge(vector<vector<int>>& intervals) {
        int len = intervals.size();
        if(len == 1) {
            return intervals;
        }
        vector<vector<int>> merged;
        sort(intervals.begin(), intervals.end());
        for(int i = 0; i < len; i++) {
            if(!merged.size() || merged.back()[1] < intervals[i][0]) {
                merged.push_back(intervals[i]);
            } else {// 关键点
                merged.back()[1] = max(merged.back()[1], intervals[i][1]);
            }
        }
        return merged;
    }

总结

本文主要介绍了关于线段或者区间重叠的问题,涉及到求重叠线段的总长度,合并区间等操作。

  • 13
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
快速3D线段检测是一种从无组织点云中提取线段信息的方法。无组织点云是由离散的3D点构成的集合,没有特定的排列顺序。线段检测能够从这些点中提取出与线段相关的信息,如线段的位置、方向和长度。 在快速3D线段检测算法中,首先需要对点云数据进行预处理,例如去除噪声点、滤波和下采样等操作,以提高后续线段检测的效率和准确性。然后,采用适当的方法对预处理后的点云进行分割,将点云划分为具有相似特征的子集。这个过程可以使用聚类方法或其他分割算法来实现。 接下来,通过计算每个分割子集的特征值,如点云的法线方向、曲率或密度信息,来判断是否存在线段。利用这些特征值,可以通过阈值或其他准则来筛选出可能表示线段的子集。对于筛选出的子集,可以采用拟合线模型的方法,如最小二乘法或RANSAC算法,来拟合出具体的线段参数,如起点、终点和方向等。 最后,通过对拟合线模型进行验证和评估,可以排除拟合误差较大的线段,提高线段拟合的准确性。例如,可以计算残差或拟合误差,来评估线段的质量,并对不满足一定阈值的线段进行排除。 总之,快速3D线段检测从无组织点云中提取线段信息的过程包括预处理、分割、特征提取、拟合和验证等多个步骤。通过这些步骤的组合,可以实现高效准确地检测出无组织点云中的线段信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值