算法概述
坐标压缩(Coordinate Compression),又称离散化,是一种将大规模稀疏坐标映射到紧凑连续区间的预处理技术。在算法竞赛和工程实践中,当问题的坐标范围极大(如10⁹级别)但实际涉及的坐标点数量远小于范围时,直接以原坐标建立数据结构会导致巨大的空间浪费。坐标压缩通过将所有出现过的坐标排序并重新编号,将问题规模从O(坐标范围)降至O(坐标数量),同时保持坐标之间的相对顺序关系不变。
核心原理
坐标压缩的执行步骤:
- 收集坐标:遍历所有输入,提取需要处理的坐标值
- 排序去重:将坐标值排序并去除重复值
- 建立映射:为每个唯一坐标分配一个从1(或0)开始的连续编号
- 替换使用:将所有原坐标替换为对应的压缩编号后进行后续计算
关键性质:压缩后的坐标保持了原坐标的相对大小关系,即若原坐标a < b,则压缩后的编号也满足comp(a) < comp(b)。
经典问题:矩形面积并
给定N个与坐标轴平行的矩形,求它们覆盖的总面积。直接离散化x和y坐标后,将平面划分为O(N²)个网格单元,统计被覆盖的单元即可高效求解。
Java完整实现
import java.util.*;
/**
* 坐标压缩(离散化)实现
* 核心应用:矩形面积并、区间覆盖、前缀和优化、数据结构压缩
*/
public class CoordinateCompression {
/**
* 坐标压缩器:将任意坐标映射到连续的紧凑编号
*/
public static class Compressor {
private long[] sortedUnique; // 排序去重后的原坐标
private Map<Long, Integer> indexMap; // 原坐标到压缩编号的映射
/**
* 从所有待压缩的坐标值构建压缩器
*/
public Compressor(long[] coordinates) {
// 排序并去重
long[] temp = coordinates.clone();
Arrays.sort(temp);
int uniqueCount = 0;
for (int i = 0; i < temp.length; i++) {
if (i == 0 || temp[i] != temp[i - 1]) {
uniqueCount++;
}
}
sortedUnique = new long[uniqueCount];
indexMap = new HashMap<>();
int idx = 0;
for (int i = 0; i < temp.length; i++) {
if (i == 0 || temp[i] != temp[i - 1]) {
sortedUnique[idx] = temp[i];
indexMap.put(temp[i], idx);
idx++;
}
}
}
/**
* 获取坐标x的压缩编号(0-based)
*/
public int compress(long x) {
Integer idx = indexMap.get(x);
if (idx == null) {
// 如果x未在原始集合中,二分查找最接近的位置
int pos = Arrays.binarySearch(sortedUnique, x);
if (pos < 0) pos = -pos - 1;
return pos;
}
return idx;
}
/**
* 获取压缩编号的原坐标值
*/
public long decompress(int idx) {
return sortedUnique[idx];
}
/**
* 获取唯一坐标的数量
*/
public int size() {
return sortedUnique.length;
}
/**
* 获取所有唯一坐标(已排序)
*/
public long[] getSortedUnique() {
return sortedUnique.clone();
}
}
/**
* 矩形类
*/
static class Rectangle {
long x1, y1, x2, y2; // 左下角和右上角坐标
Rectangle(long x1, long y1, long x2, long y2) {
this.x1 = x1; this.y1 = y1;
this.x2 = x2; this.y2 = y2;
}
}
/**
* 扫描线事件:矩形边的进入/离开事件
*/
static class Event implements Comparable<Event> {
long x; // 事件发生的x坐标
int y1, y2; // 影响的y区间(压缩后的编号)
int type; // +1 表示矩形左边进入,-1 表示矩形右边离开
Event(long x, int y1, int y2, int type) {
this.x = x; this.y1 = y1; this.y2 = y2; this.type = type;
}
@Override
public int compareTo(Event other) {
return Long.compare(this.x, other.x);
}
}
/**
* 线段树:维护y轴方向的覆盖次数和覆盖长度
*/
static class SegmentTree {
int n;
int[] cover; // 区间被覆盖的次数
long[] length; // 区间被覆盖的总长度(需结合原始坐标差值)
long[] coordDiff; // 相邻压缩坐标之间的原始长度差
SegmentTree(int size, long[] sortedY) {
this.n = size;
this.cover = new int[4 * n];
this.length = new long[4 * n];
this.coordDiff = new long[n];
for (int i = 0; i < n - 1; i++) {
coordDiff[i] = sortedY[i + 1] - sortedY[i];
}
}
void update(int node, int l, int r, int ql, int qr, int val) {
if (ql >= r || qr <= l) return;
if (ql <= l && r <= qr) {
cover[node] += val;
} else {
int mid = (l + r) / 2;
update(node * 2, l, mid, ql, qr, val);
update(node * 2 + 1, mid, r, ql, qr, val);
}
if (cover[node] > 0) {
// 整个区间被完全覆盖
length[node] = 0;
for (int i = l; i < r && i < n; i++) {
length[node] += coordDiff[i];
}
} else if (l + 1 == r) {
// 叶子节点且未被覆盖
length[node] = 0;
} else {
// 未被完全覆盖,累加子节点
length[node] = length[node * 2] + length[node * 2 + 1];
}
}
long queryTotalLength() {
return length[1];
}
}
/**
* 计算矩形面积并
* @param rectangles 矩形数组
* @return 覆盖的总面积
*/
public static long unionArea(Rectangle[] rectangles) {
if (rectangles == null || rectangles.length == 0) return 0;
// 收集所有y坐标
List<Long> yCoords = new ArrayList<>();
List<Event> events = new ArrayList<>();
for (Rect r : rectangles) {
yCoords.add(r.y1);
yCoords.add(r.y2);
}
// y坐标压缩
long[] yArr = yCoords.stream().mapToLong(Long::longValue).toArray();
Compressor yComp = new Compressor(yArr);
long[] sortedY = yComp.getSortedUnique();
// 构建扫描线事件
for (Rectangle rect : rectangles) {
int y1 = yComp.compress(rect.y1);
int y2 = yComp.compress(rect.y2);
events.add(new Event(rect.x1, y1, y2, 1)); // 左边进入
events.add(new Event(rect.x2, y1, y2, -1)); // 右边离开
}
Collections.sort(events);
// 线段树维护y轴覆盖长度
SegmentTree st = new SegmentTree(yComp.size(), sortedY);
long prevX = events.get(0).x;
long area = 0;
for (Event e : events) {
long dx = e.x - prevX;
if (dx > 0) {
area += dx * st.queryTotalLength();
prevX = e.x;
}
st.update(1, 0, yComp.size() - 1, e.y1, e.y2, e.type);
}
return area;
}
/**
* 一维坐标压缩应用:区间去重后的计数
* 给定大量区间,求去重后覆盖的总长度
*/
public static long uniqueCoverageLength(long[][] intervals) {
Set<Long> coords = new TreeSet<>();
for (long[] interval : intervals) {
coords.add(interval[0]);
coords.add(interval[1]);
}
long[] allCoords = coords.stream().mapToLong(Long::longValue).toArray();
Compressor comp = new Compressor(allCoords);
int m = comp.size();
int[] diff = new int[m];
for (long[] interval : intervals) {
int l = comp.compress(interval[0]);
int r = comp.compress(interval[1]);
diff[l]++;
if (r < m) diff[r]--;
}
long total = 0;
int current = 0;
for (int i = 0; i < m - 1; i++) {
current += diff[i];
if (current > 0) {
total += allCoords[i + 1] - allCoords[i];
}
}
return total;
}
public static void main(String[] args) {
// 测试矩形面积并
Rectangle[] rects = {
new Rectangle(1, 1, 3, 3),
new Rectangle(2, 2, 4, 4),
new Rectangle(2, 0, 5, 2)
};
System.out.println("矩形面积并: " + unionArea(rects));
// 测试区间覆盖
long[][] intervals = {
{1, 5}, {3, 7}, {10, 15}, {12, 20}
};
System.out.println("区间去重覆盖长度: " + uniqueCoverageLength(intervals));
// 测试坐标压缩器
long[] coords = {1000000000L, 500, 1000000000L, 200, 500, 800, 200};
Compressor comp = new Compressor(coords);
System.out.println("\n原始坐标 -> 压缩编号:");
for (long c : new long[]{200, 500, 800, 1000000000L}) {
System.out.println(" " + c + " -> " + comp.compress(c));
}
System.out.println("压缩后坐标范围: 0 ~ " + (comp.size() - 1));
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 坐标压缩构建 | O(N log N) | O(N) | N为坐标点数量 |
| 单次查询映射 | O(log N) | – | 二分查找 |
| 矩形面积并 | O(N log N) | O(N) | 扫描线+线段树+坐标压缩 |
| 区间覆盖 | O(N log N) | O(N) | 差分数组+坐标压缩 |
应用场景
- 大规模网格问题:坐标范围10⁹但有效点仅10⁵的场景
- 图像处理:稀疏矩阵的紧凑存储与快速索引
- GIS系统:地理坐标的高精度离散化与空间查询
- 计算几何:矩形并、周长并、多边形覆盖等问题
- 机器学习:特征分桶与离散型特征编码
坐标压缩是处理大规模稀疏数据的必备技巧,将”无限”的问题空间转化为”有限”的可计算空间,是算法优化中的经典降维策略。