最大线段重合问题
2022-10-16 22:40:34 # 算法 # 数据结构 #

1. 问题描述

给定一些整数对(a,b)代表数轴上的两个点,且a<=b,这两个点表示一个线段。请返回最多几个线段重叠在一起。

例如:

  1. [[1,3],[2,5],[7,9]],因为[1,3]和[2,5]重叠在一起,返回2;
  2. [[1,2],[2,3]],返回2;
  3. [[1,2],[2,3],[1,3]],返回3
  4. [[1,2]],返回1

数组长度至少是1,不超过100_0000;

2. 暴力解法

2.1. 算法分析

我们可以用笔画几条线段发现,任何重叠的部分的左端点,都是某一个线段的左端点。比如[1,3],和[2,4]重叠的部分是[2,3],其中2就是第二个线段的左端点;又比如:[1,5],[3,7],[4,6],重叠的部分是[4,5],其中4就是[4,6]的左端点。既然任何一个重叠的部分都满足这个性质,那么重叠线段最多的那一部分,也满足。

因此我们只需要看一下,哪个左端点下经过的线段数目最多即可。

因此我们可以枚举所有线段的左端点,对于每一个左端点,再次枚举有几条线段经过当前点,返回最大值。

线段[a,b]经过当前点x的充要条件是:a<=x<=b

2.2. 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 public class MaxLineCoincide {

public static int maxLineCoincide(int[][] lines) {
int len = lines.length;
if(len == 1) return 1;
int ans = 1, t, leftPoint;
for(int i = 0; i < len; ++i) {
t=0;
leftPoint = lines[i][0];
for(int j=0; j<len; ++j) {
if(lines[j][0] <= leftPoint && leftPoint <= lines[j][1]) ++t;
}
ans = Math.max(t,ans);
}
return ans;
}

public static void main(String[] args) {
int[][] lines = {
{1,3},
{3,4}
};
System.out.println(maxLineCoincide(lines));
}

}

时间复杂度$O(N^2)$

3. 最优解

3.1. 算法分析

上述的解答中,最耗时的部分在于对于每一个左端点,找有几条线段通过这个点?因为给定的线段列表是杂乱的,因此我们只能遍历去找,浪费了时间。

如果我们把所有的线段,按照左端点从小到大排序,那么我们在确定一个左端点有几条线段通过它的时候,就可以排除一些不必要的遍历。

即对于一个左端点leftx,能通过它的线段,只可能是左端点不超过leftx的线段,如果一个线段的左端点都超过了leftx,那么这个线段是不会通过leftx的。

左端点不超过leftx的线段中,不都是满足条件的,只有那些右端点不小于leftx的线段,才是通过leftx的。因此我们还要知道这些线段的所有右端点信息,我们可以把这些线段的右端点都放在小跟堆中,不断从堆顶弹出一个右端点rightx,如果发现一个线段的右端点rightx,小于leftx,那么这个线段是不会计入leftx的答案的,当然在计算leftx的下一个(如果有的话)左端点nextLeftx的时候,更不会计入nextLeftx的答案,因此这个右端点rightx就丢弃了,直到某一个rightx不小于leftx的时候,因为是小跟堆,堆中所有的元素都是不小于leftx的,此时堆的size就是当前leftx的答案。

因此整体的算法如下:

  1. 把所有的线段,按照左端点从小到大排序
  2. 设置一个小跟堆,遍历每一个线段,对于每一个线段line:
    • 把line的右端点rightx放入小跟堆;
    • 只要堆顶元素小于当前线段的左端点leftx,则弹出
    • 把堆此时的size作为子问题的答案,和全局答案pk

3.2. 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.*;

public class MaxLineCoincide2 {

public static int maxLineCoincide(int[][] lines) {
int len = lines.length;
if(len == 1) return 1;
int ans = 1;
Arrays.sort(lines,(a,b)->a[0]-b[0]);
PriorityQueue<Integer> q = new PriorityQueue<>();
for(int i = 0; i < len; ++i) {
q.offer(lines[i][1]);
while(!q.isEmpty() && q.peek() < lines[i][0]) q.poll();
ans = Math.max(ans,q.size());
}
return ans;
}

public static void main(String[] args) {
int[][] lines = {
{1,3},
{3,4}
};
System.out.println(maxLineCoincide(lines));
}

}

时间复杂度$O(N\log_2N)$

4. 测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.*;

public class MaxLineCoincideTest {

public static void main(String[] args) {
int testTimes = 100_0000;
int maxArraySize = 100;
int maxPoint = 10_0000;
while(testTimes-- > 0) {
Random r = new Random();
int size = r.nextInt(maxArraySize) + 1;
int[][] data = new int[size][2], data2 = new int[size][2];
for(int i = 0; i < size; ++i) {
data[i][0] = r.nextInt(maxPoint);
//保证data[i][1]不小于data[i][0]
data[i][1] = data[i][0] + r.nextInt(Integer.MAX_VALUE - data[i][0]);
data2[i][0] = data[i][0];
data2[i][1] = data[i][1];
}
int ans0 = MaxLineCoincide.maxLineCoincide(data);
int ans1 = MaxLineCoincide2.maxLineCoincide(data2);
if(ans0 != ans1) {
System.out.println("ans0: " + ans0 + " ans1: " + ans1);
for(int i = 0; i < size; ++i) {
System.out.println(Arrays.toString(data[i]));
}
throw new RuntimeException("Oops");
}
}
System.out.println("Done successfully!");
}

}