https://leetcode.com/problems/the-skyline-problem/
给定若干大楼的左右边界及其高度(每个大楼都是矩形),大楼可能有重叠,要求返回一系列点的坐标表示这些大楼的边际线,这些点要从左向右输出,例如,对于相邻的两个点 ( a 1 , h 1 ) , ( a 2 , h 2 ) (a_1,h_1),(a_2,h_2) (a1,h1),(a2,h2),边际线的一部分是 ( a 1 , a 2 , h 1 ) (a_1,a_2,h_1) (a1,a2,h1),代表区间 [ a 1 , a 2 ] [a_1,a_2] [a1,a2]的边际线上的高度是 h 1 h_1 h1。相邻的两个点的高度不应该相同。输入是若干三元组,每个三元组 ( l , r , h ) (l,r,h) (l,r,h)代表 [ l , r ] [l,r] [l,r]这一段有个高 h h h的大楼。例如下图,右图中红色的点的坐标即为所求。
可以用线段树来做,每次读入一个大楼 b b b,相当于将 [ b [ 0 ] , b [ 1 ] − 1 ] [b[0],b[1]-1] [b[0],b[1]−1]这一段的高度变为 b [ 2 ] b[2] b[2]与旧值的更大值,这可以用线段树做。但是注意,由于这题的大楼左右边界的取值范围是 [ 0 , 2 31 − 1 ] [0,2^{31}-1] [0,231−1],所以要采用动态开点的方式,而不能一下子把所有节点都开出来,这样,只有叶子才是真正有效的值。并且,如果某个节点的左右儿子都是叶子,且它们的高度相同,则可以将这两个叶子删除,自己高度更新,并且成为新的叶子,这样可以减少节点个数。最后只需要DFS一遍就能知道边界。代码如下:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
class Node {
int st, ed, h;
Node left, right;
public Node(int st, int ed, int h) {
this.st = st;
this.ed = ed;
this.h = h;
}
}
// 如果cur的左右儿子都是叶子并且高度相同,则删除左右儿子,自己高度更新,成为新的叶子
void pushup(Node cur) {
if (cur.left != null && cur.right != null) {
if (cur.left.left == null && cur.right.left == null && cur.left.h == cur.right.h) {
cur.h = cur.left.h;
cur.left = cur.right = null;
}
}
}
void update(Node root, int st, int ed, int h) {
if (root == null) {
return;
}
if (root.st > ed || root.ed < st) {
return;
}
// 如果是叶子,看一下要不要分裂,如果不需要则直接赋值;否则分裂
if (root.left == null && root.right == null) {
if (st <= root.st && root.ed <= ed) {
root.h = Math.max(root.h, h);
return;
}
// root.st, st - 1 st, root.ed
if (root.st < st) {
root.left = new Node(root.st, st - 1, root.h);
root.right = new Node(st, root.ed, root.h);
update(root.right, st, ed, h);
} else {
root.left = new Node(root.st, ed, root.h);
root.right = new Node(ed + 1, root.ed, root.h);
update(root.left, st, ed, h);
}
} else {
// 如果不是叶子,则直接更新左右儿子
update(root.left, st, ed, h);
update(root.right, st, ed, h);
}
pushup(root);
}
Node root;
int last;
void dfs(Node cur, List<List<Integer>> res) {
if (cur == null) {
return;
}
dfs(cur.left, res);
if (cur.left == null && cur.right == null && cur.h != last) {
res.add(Arrays.asList(cur.st, cur.h));
last = cur.h;
}
dfs(cur.right, res);
}
public List<List<Integer>> getSkyline(int[][] buildings) {
List<List<Integer>> res = new ArrayList<>();
root = new Node(0, Integer.MAX_VALUE, 0);
for (int[] b : buildings) {
update(root, b[0], b[1] - 1, b[2]);
}
dfs(root, res);
return res;
}
}
时间复杂度 O ( n log n ) O(n\log n) O(nlogn),空间 O ( n ) O(n) O(n)。
下面给出离散化的做法。由于区间的取值范围特别大,但区间的个数很少,所以想到离散化的方法。先将所有端点排序,然后去重,接着用静态开点的办法,一次性开出所有点。每次更新的时候,需要二分出离散化后的下标是什么然后在线段树里更新。先将大楼的区间按照高度从小到大排序,接着线段树事实上只是在执行将某段区间变为 x x x的操作,可以带懒标记做。代码如下:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
class Node {
// h是懒标记,表示当前区间要变为h
int l, r, h;
public Node(int l, int r) {
this.l = l;
this.r = r;
}
}
Node[] tr;
void pushdown(int u) {
if (tr[u].h > 0) {
tr[u << 1].h = tr[u << 1 | 1].h = tr[u].h;
tr[u].h = 0;
}
}
void build(int u, int l, int r) {
tr[u] = new Node(l, r);
if (l == r) {
return;
}
int mid = l + (r - l >> 1);
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
}
void modify(int u, int l, int r, int h) {
if (l <= tr[u].l && tr[u].r <= r) {
tr[u].h = h;
return;
}
pushdown(u);
int mid = tr[u].l + (tr[u].r - tr[u].l >> 1);
if (l <= mid) {
modify(u << 1, l, r, h);
}
if (r > mid) {
modify(u << 1 | 1, l, r, h);
}
}
public List<List<Integer>> getSkyline(int[][] buildings) {
Arrays.sort(buildings, (b1, b2) -> Integer.compare(b1[2], b2[2]));
int[] idx = new int[buildings.length << 1];
int r = 0;
for (int[] b : buildings) {
idx[r++] = b[0];
idx[r++] = b[1];
}
Arrays.sort(idx);
r = 0;
for (int i = 0; i < idx.length; i++) {
if (r == 0 || idx[i] != idx[r - 1]) {
idx[r++] = idx[i];
}
}
tr = new Node[r + 1 << 2];
build(1, 0, r);
for (int[] b : buildings) {
modify(1, Arrays.binarySearch(idx, 0, r, b[0]), Arrays.binarySearch(idx, 0, r, b[1]) - 1, b[2]);
}
List<List<Integer>> res = new ArrayList<>();
dfs(1, res, idx);
return res;
}
int last;
void dfs(int u, List<List<Integer>> res, int[] idx) {
// 如果走到叶子,或者当前节点有懒标记,则考虑将该线段的左端点及其高度加入答案
if (tr[u].l == tr[u].r || tr[u].h > 0) {
if (tr[u].h != last) {
res.add(Arrays.asList(idx[tr[u].l], tr[u].h));
}
last = tr[u].h;
return;
}
dfs(u << 1, res, idx);
dfs(u << 1 | 1, res, idx);
}
}
时空复杂度一样。
C++:
class Solution {
public:
struct Node {
int l, r, h;
};
vector<Node> tr;
void pushdown(int u) {
auto &t = tr[u], &l = tr[u << 1], &r = tr[u << 1 | 1];
// 注意这里懒标记下传的方式,我们默认懒标记非0,如果非0则直接下传,然后清空;
// 下传懒标记的含义是当前区间的高度不是一个定值
if (t.h) {
l.h = r.h = t.h;
t.h = 0;
}
}
void build(int u, int l, int r) {
tr[u] = {l, r};
if (l == r) return;
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}
void update(int u, int l, int r, int h) {
if (l <= tr[u].l && tr[u].r <= r) {
tr[u].h = max(tr[u].h, h);
return;
}
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) update(u << 1, l, r, h);
if (r > mid) update(u << 1 | 1, l, r, h);
}
vector<vector<int>> getSkyline(vector<vector<int>> &bs) {
// 一定要先对高度排序,这样后面的高度才能覆盖掉前面的高度,更新懒标记才能正确
sort(bs.begin(), bs.end(),
[&](const vector<int> &v1, const vector<int> &v2) {
return v1[2] < v2[2];
});
vector<int> v;
for (auto &b : bs) {
v.push_back(b[0]);
v.push_back(b[1]);
}
sort(v.begin(), v.end());
int n = v.erase(unique(v.begin(), v.end()), v.end()) - v.begin();
unordered_map<int, int> mp;
for (int i = 0; i < n; i++) mp[v[i]] = i;
tr.resize(n << 2);
build(1, 0, n);
// 注意这里的写法,mp[b[1]] - 1
for (auto &b : bs) update(1, mp[b[0]], mp[b[1]] - 1, b[2]);
vector<vector<int>> res;
int last = 0;
dfs(1, last, v, res);
return res;
}
void dfs(int u, int &last, vector<int> &v, vector<vector<int>> &res) {
if (tr[u].l == tr[u].r || tr[u].h) {
if (tr[u].h != last) {
res.push_back({v[tr[u].l], tr[u].h});
last = tr[u].h;
}
return;
}
dfs(u << 1, last, v, res);
dfs(u << 1 | 1, last, v, res);
}
};
时空复杂度一样。