1. 题目背景与问题解析
《P5445 [APIO2019] 路灯》是2019年亚太信息学奥林匹克竞赛(APIO)的一道经典题目。这道题考察了选手对数据结构与区间操作的掌握程度,特别是对线段树或树状数组等高级数据结构的灵活运用能力。
题目描述了一个由n个路灯组成的街道,每个路灯有亮和灭两种状态。题目要求实现以下两种操作:
- 切换某个路灯的状态(亮变灭或灭变亮)
- 查询从位置a到位置b的连续亮灯区间数量
这类问题在实际中有广泛的应用场景,比如城市路灯管理系统、网络连接状态监控等。理解并掌握这类问题的解法,对于提升算法竞赛水平和解决实际问题都有重要意义。
2. 核心算法思路分析
2.1 问题转化与建模
首先我们需要将路灯问题转化为数学模型。我们可以用一个二进制数组L[1...n]表示路灯状态,其中L[i]=1表示第i个路灯亮着,L[i]=0表示熄灭。
查询操作要求统计区间[a,b]内连续亮灯区间的数量。这等价于统计在区间[a,b]内有多少个位置i满足:
- L[i]=1
- L[i-1]=0或者i=a
换句话说,我们统计的是每个连续亮灯区间的起始点。
2.2 暴力解法及其局限性
最直观的解法是每次查询时遍历区间[a,b],统计满足上述条件的点。这种方法:
- 切换操作时间复杂度:O(1)
- 查询操作时间复杂度:O(n)
当n很大(比如1e5)且查询次数很多(比如1e5)时,这种解法的时间复杂度O(nq)显然无法通过时间限制。
2.3 高效解法思路
我们需要一种数据结构,能够:
- 高效更新单个路灯状态(O(logn))
- 快速查询区间内连续亮灯区间数量(O(logn))
线段树是解决这类区间查询和单点更新问题的理想选择。我们需要设计合适的线段树节点结构来维护必要的信息。
3. 线段树设计与实现
3.1 线段树节点设计
每个线段树节点需要维护以下信息:
- cnt:当前区间内连续亮灯区间的数量
- left:区间最左端路灯的状态
- right:区间最右端路灯的状态
- len:区间长度
此外,为了合并子区间的信息,我们还需要考虑中间交界处的状态。
3.2 线段树合并操作
当合并左右子区间的信息时,需要考虑以下情况:
- 如果左子区间的右端和右子区间的左端都亮着,则合并后的连续亮灯区间数量需要减1(因为中间连接起来了)
- 否则,直接相加即可
具体合并逻辑如下:
cpp复制node merge(node left, node right) {
node res;
res.len = left.len + right.len;
res.left = left.left;
res.right = right.right;
res.cnt = left.cnt + right.cnt;
if(left.right == 1 && right.left == 1) {
res.cnt--;
}
return res;
}
3.3 线段树实现细节
完整的线段树实现包括以下部分:
- 建树(build)
- 单点更新(update)
- 区间查询(query)
每个操作的时间复杂度都是O(logn),完全满足题目要求。
4. 完整代码实现与解析
4.1 数据结构定义
cpp复制struct Node {
int cnt; // 连续亮灯区间数量
int left; // 最左端状态
int right; // 最右端状态
int len; // 区间长度
};
vector<Node> tree;
vector<int> lights;
int n, q;
4.2 线段树构建
cpp复制void build(int node, int start, int end) {
if(start == end) {
tree[node].cnt = lights[start];
tree[node].left = tree[node].right = lights[start];
tree[node].len = 1;
return;
}
int mid = (start + end) / 2;
build(2*node, start, mid);
build(2*node+1, mid+1, end);
tree[node] = merge(tree[2*node], tree[2*node+1]);
}
4.3 单点更新操作
cpp复制void update(int node, int start, int end, int idx) {
if(start == end) {
lights[idx] ^= 1; // 切换状态
tree[node].cnt = tree[node].left = tree[node].right = lights[idx];
return;
}
int mid = (start + end) / 2;
if(idx <= mid) {
update(2*node, start, mid, idx);
} else {
update(2*node+1, mid+1, end, idx);
}
tree[node] = merge(tree[2*node], tree[2*node+1]);
}
4.4 区间查询操作
cpp复制Node query(int node, int start, int end, int l, int r) {
if(r < start || end < l) {
return {0, 0, 0, 0};
}
if(l <= start && end <= r) {
return tree[node];
}
int mid = (start + end) / 2;
Node left = query(2*node, start, mid, l, r);
Node right = query(2*node+1, mid+1, end, l, r);
if(left.len == 0) return right;
if(right.len == 0) return left;
return merge(left, right);
}
5. 算法优化与边界处理
5.1 查询边界处理
在实际查询时,需要注意查询区间可能超出有效范围的情况。我们的线段树实现已经通过返回空节点({0,0,0,0})来处理这种情况。
5.2 空间优化
对于n很大的情况,可以采用动态开点线段树来节省空间。但在竞赛中,通常预先分配4*n的空间就足够了。
5.3 常数优化
在竞赛中,可以采用以下优化技巧:
- 使用位运算代替除法计算mid
- 使用数组而非结构体实现线段树
- 使用快速输入输出方法
6. 复杂度分析与正确性验证
6.1 时间复杂度分析
- 建树:O(n)
- 每次更新:O(logn)
- 每次查询:O(logn)
对于q次操作,总时间复杂度为O(n + qlogn),完全能够处理n,q=1e5的情况。
6.2 正确性验证
可以通过以下测试用例验证算法正确性:
- 所有灯都亮着:查询任意区间结果应为1
- 所有灯都熄灭:查询任意区间结果应为0
- 交替亮灭:查询全区间结果应为n/2(n为偶数)
- 随机测试:生成随机灯状态和随机查询,与暴力解法结果对比
7. 实际应用与扩展
7.1 实际应用场景
这种算法可以应用于:
- 城市路灯管理系统:实时监控路灯状态和故障区间
- 网络连接监控:检测网络节点的连通性
- 生产线监控:检测连续工作设备区段
7.2 问题扩展
可以尝试解决以下扩展问题:
- 支持区间切换操作(将区间内所有灯状态反转)
- 查询最长的连续亮灯区间
- 支持动态增加/删除路灯
这些扩展问题可以通过增强线段树节点维护的信息来实现。
