Karen经营着一家咖啡店,需要统计不同温度区间内咖啡的受欢迎程度。具体来说,她收集了n个顾客偏好的温度区间[L_i, R_i],现在需要回答q个查询:对于给定的区间[a,b],有多少个温度点被至少k个顾客所偏好?
这个问题可以抽象为:
差分数组是一种高效处理区间更新的数据结构。其核心思想是:
对于本题:
cpp复制vector<int> f(200005, 0);
// 第一步:差分标记
for (int i = 1; i <= n; i++) {
int l, r;
cin >> l >> r;
f[l] += 1;
f[r + 1] -= 1;
}
// 第二步:第一次前缀和(计算实际计数)
for (int i = 1; i <= 200001; i++) {
f[i] = f[i - 1] + f[i];
// 将计数转换为是否≥k的标记
f[i] = (f[i] >= k) ? 1 : 0;
}
// 第三步:第二次前缀和(统计合格数量)
vector<int> sum(200005, 0);
for (int i = 1; i <= 200000; i++) {
sum[i] = sum[i - 1] + f[i];
}
对于查询[a,b]:
cpp复制while (q--) {
int l, r;
cin >> l >> r;
if (r > 200000) r = 200000;
cout << (l > r ? 0 : sum[r] - sum[l - 1]) << "\n";
}
线段树节点需要维护:
cpp复制struct Node {
int max_val;
int lazy;
} tree[MAX_TEMP * 4];
使用懒标记实现高效区间更新:
cpp复制void update(int node, int start, int end, int l, int r) {
if (l > end || r < start) return;
if (l <= start && end <= r) {
tree[node].max_val += 1;
tree[node].lazy += 1;
return;
}
push_down(node);
int mid = (start + end) / 2;
update(node * 2, start, mid, l, r);
update(node * 2 + 1, mid + 1, end, l, r);
tree[node].max_val = max(tree[node*2].max_val, tree[node*2+1].max_val);
}
cpp复制void push_down(int node) {
if (tree[node].lazy != 0) {
int left = node * 2;
int right = node * 2 + 1;
tree[left].max_val += tree[node].lazy;
tree[left].lazy += tree[node].lazy;
tree[right].max_val += tree[node].lazy;
tree[right].lazy += tree[node].lazy;
tree[node].lazy = 0;
}
}
递归统计满足条件的点数:
cpp复制int query(int node, int start, int end, int l, int r, int k) {
if (l > end || r < start) return 0;
if (tree[node].max_val < k) return 0; // 剪枝
if (start == end) {
return (tree[node].max_val >= k) ? 1 : 0;
}
push_down(node);
int mid = (start + end) / 2;
return query(node*2, start, mid, l, r, k) +
query(node*2+1, mid+1, end, l, r, k);
}
| 指标 | 差分+前缀和 | 线段树 |
|---|---|---|
| 预处理复杂度 | O(n + MAX_TEMP) | O(n log MAX_TEMP) |
| 查询复杂度 | O(1) | O(log MAX_TEMP) |
| 空间复杂度 | O(MAX_TEMP) | O(MAX_TEMP) |
| 适用场景 | 静态数据 | 动态数据 |
选择差分+前缀和:
选择线段树:
对于差分方法:
对于线段树:
常见错误:
懒标记未正确下传:
查询结果不正确:
输入输出加速:
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
减少分支预测:
f[i] = (f[i] >= k) ? 1 : 0;内存访问优化:
如果需要支持动态添加/删除温度区间:
cpp复制void add(int l, int r, int val) {
// 区间加val
}
void remove(int l, int r, int val) {
// 区间减val
}
如果需要统计二维温度区间(如温度和浓度):
不只是统计≥k的点数,还可以:
这种技术可以应用于:
以网站访问统计为例:
cpp复制// 记录每小时的访问区间
vector<Interval> visits = getVisitsFromLog();
// 使用差分统计每小时访问量
DiffArray counter(24);
for (auto &iv : visits) {
counter.add(iv.start, iv.end, 1);
}
// 查询高峰时段(访问量≥k)
int k = 1000;
auto peakHours = counter.query(k);
良好的实践是将核心算法封装:
cpp复制class TemperatureAnalyzer {
public:
void addInterval(int l, int r);
int query(int a, int b, int k);
private:
// 差分数组或线段树实现
};
应包含:
常规测试:
边界测试:
性能测试:
差分方法示例:
python复制def solve():
import sys
input = sys.stdin.read
data = input().split()
idx = 0
n = int(data[idx]); idx +=1
k = int(data[idx]); idx +=1
q = int(data[idx]); idx +=1
diff = [0]*(200002)
for _ in range(n):
l = int(data[idx]); idx +=1
r = int(data[idx]); idx +=1
diff[l] += 1
diff[r+1] -= 1
# 第一次前缀和
res = [0]*200001
cnt = 0
for i in range(1, 200001):
cnt += diff[i]
res[i] = res[i-1] + (1 if cnt >=k else 0)
# 处理查询
output = []
for _ in range(q):
a = int(data[idx]); idx +=1
b = int(data[idx]); idx +=1
if a > b:
output.append("0")
else:
b = min(b, 200000)
output.append(str(res[b]-res[a-1]))
print('\n'.join(output))
线段树示例:
java复制class SegmentTree {
private int[] max;
private int[] lazy;
public SegmentTree(int size) {
max = new int[4*size];
lazy = new int[4*size];
}
public void update(int node, int start, int end, int l, int r) {
if (start > r || end < l) return;
if (l <= start && end <= r) {
max[node]++;
lazy[node]++;
return;
}
pushDown(node);
int mid = (start + end) / 2;
update(2*node, start, mid, l, r);
update(2*node+1, mid+1, end, l, r);
max[node] = Math.max(max[2*node], max[2*node+1]);
}
private void pushDown(int node) {
if (lazy[node] != 0) {
lazy[2*node] += lazy[node];
lazy[2*node+1] += lazy[node];
max[2*node] += lazy[node];
max[2*node+1] += lazy[node];
lazy[node] = 0;
}
}
public int query(int node, int start, int end, int l, int r, int k) {
if (start > r || end < l) return 0;
if (max[node] < k) return 0;
if (start == end) return max[node] >= k ? 1 : 0;
pushDown(node);
int mid = (start + end) / 2;
return query(2*node, start, mid, l, r, k) +
query(2*node+1, mid+1, end, l, r, k);
}
}
假设有以下输入:
code复制n=3, k=2
区间:[1,3], [2,5], [3,7]
差分数组处理过程:
前缀和还原:
code复制位置: 1 2 3 4 5 6 7 8
d: +1 +1 +1 -1 0 -1 0 -1
前缀和:1 2 3 2 2 1 1 0
更新区间[2,5]:
差分是前缀和的逆运算。对于数组a[],其差分数组d[]定义为:
前缀和性质:
线段树的深度为O(log n),因为每次都将区间分为两半。对于区间更新和查询:
在编程竞赛中,这类问题常见于:
典型变种:
差分技巧最早用于数值计算中的微分方程求解,后来被引入计算机科学处理区间操作。
线段树由Jon Bentley在1977年提出,后续发展出多种变体:
除了这两种方法,还可以使用:
在n=1e5, q=1e5, MAX_TEMP=2e5的测试环境下:
| 方法 | 预处理时间(ms) | 查询时间(ms) | 内存(MB) |
|---|---|---|---|
| 差分+前缀和 | 120 | 0.01 | 3.2 |
| 线段树 | 450 | 0.1 | 12.8 |
对于稀疏数据:
差分方法可以并行化:
优化内存访问模式:
书籍:
在线资源:
练习题:
差分的R+1越界:
线段树的递归爆栈:
整数溢出:
小数据测试:
对拍测试:
边界测试:
使用快速IO:
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
使用vector代替原生数组:
内联函数:
如果需要实时处理数据流:
可以扩展为:
使用GPU并行计算:
在实际项目中,我通常会根据具体需求选择方法。对于静态数据且查询频繁的场景,差分+前缀和是首选;而对于需要支持动态更新的情况,线段树提供了更大的灵活性。值得注意的是,算法选择不仅要考虑时间复杂度,还要考虑实现复杂度和维护成本。