1. 题目背景与需求分析
这道来自洛谷的P1003题目,是NOIP2011提高组的真题之一。题目描述了一个看似简单却蕴含巧妙算法的场景:在一个矩形区域上按顺序铺设若干张不同大小的地毯,最后需要确定某一点被哪张地毯覆盖(或未被任何地毯覆盖)。
在实际编程竞赛和算法学习中,这类二维平面覆盖问题具有典型意义。它不仅考察选手对基础数据结构的掌握程度,更能检验对空间关系的抽象能力和算法优化意识。许多实际应用场景如地图图层叠加、UI元素层级管理等都与此题内核相通。
2. 解题思路与算法选择
2.1 暴力解法与优化空间
最直观的解法是创建一个二维数组模拟整个平面,依次铺设每张地毯并标记所属编号。当查询点时直接返回数组对应位置的值即可。这种方法时间复杂度为O(n^2)(n为地毯边长),当n较大时(如题目中的1e5量级),无论是时间还是空间复杂度都难以承受。
关键提示:在算法题中,当数据规模达到1e5量级时,O(n^2)的算法通常都会超时,必须寻找线性或更低复杂度的解法。
2.2 逆向思维的应用
更优的解法是利用"后来居上"的特性逆向处理:由于后铺设的地毯会覆盖先铺设的,我们只需要从最后一张地毯开始倒序检查,第一个包含目标点的地毯就是答案。这种方法将时间复杂度降为O(n),空间复杂度仅为O(1)。
struct Carpet { int a, b, g, k; // (a,b)为左下角坐标,(g,k)为延伸长度 }; vector<Carpet> carpets; int findCover(int x, int y) { for(int i = carpets.size()-1; i >=0; --i) { auto& c = carpets[i]; if(x >= c.a && x <= c.a + c.g && y >= c.b && y <= c.b + c.k) { return i + 1; // 题目中地毯编号从1开始 } } return -1; // 未被任何地毯覆盖 }3. 关键实现细节
3.1 数据结构设计
采用结构体数组存储每张地毯的参数是最直观的选择。在C++中,使用vector动态数组比原生数组更安全便捷。每个地毯只需存储其左下角坐标(a,b)和向右(g)、向上(k)延伸的长度即可。
3.2 边界条件处理
特别注意题目中的坐标描述:
- 地毯左下角坐标为(a,b)
- 地毯覆盖区域为[a, a+g]和[b, b+k]
- 判断点(x,y)是否在地毯内时,要包含等于边界的情况
3.3 输入输出优化
在竞赛编程中,当数据量较大时(如本题n可达1e4),建议使用快速的输入输出方法:
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);4. 算法复杂度分析
设地毯数量为n:
- 时间复杂度:O(n),最坏情况下需要检查所有地毯
- 空间复杂度:O(n),仅需存储n张地毯的信息
相比模拟整个平面的O(n^2)方法,这种解法在n=1e4时可节省约1e8量级的计算量。
5. 常见错误与调试技巧
5.1 典型错误案例
- 边界判断错误:将条件写成
x > a && x < a+g,漏掉了等于边界的情况 - 地毯编号混淆:题目中地毯编号从1开始,而数组通常从0开始存储
- 循环方向错误:正向遍历而非题目要求的逆向遍历
5.2 调试建议
对于此类问题,建议构造小型测试用例:
2 0 0 2 2 1 1 2 2 1 1预期输出应为2(第二张地毯覆盖了(1,1)点)
6. 算法扩展与应用
6.1 多查询优化
若题目改为需要回答多个点的覆盖情况,上述算法对每个查询都需要O(n)时间。可以考虑以下优化:
- 空间分区法:将平面划分为网格,预先计算每个网格被哪些地毯覆盖
- 线段树/树状数组:适用于一维情况,二维扩展较复杂
- 持久化数据结构:记录每个位置的地毯覆盖历史
6.2 实际应用场景
- 图形界面层级管理:确定鼠标点击位于哪个UI元素上方
- 地理信息系统:判断某坐标点位于哪个行政区域
- 游戏开发:检测物体碰撞或触发区域
7. 不同语言实现要点
7.1 Python实现
n = int(input()) carpets = [tuple(map(int, input().split())) for _ in range(n)] x, y = map(int, input().split()) result = -1 for i in range(n-1, -1, -1): a, b, g, k = carpets[i] if a <= x <= a + g and b <= y <= b + k: result = i + 1 break print(result)7.2 Java实现
import java.util.Scanner; class Main { static class Carpet { int a, b, g, k; Carpet(int a, int b, int g, int k) { this.a = a; this.b = b; this.g = g; this.k = k; } } public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); Carpet[] carpets = new Carpet[n]; for(int i=0; i<n; i++) { carpets[i] = new Carpet( sc.nextInt(), sc.nextInt(), sc.nextInt(), sc.nextInt() ); } int x = sc.nextInt(); int y = sc.nextInt(); int result = -1; for(int i=n-1; i>=0; i--) { Carpet c = carpets[i]; if(x >= c.a && x <= c.a + c.g && y >= c.b && y <= c.b + c.k) { result = i + 1; break; } } System.out.println(result); } }8. 性能优化进阶
对于极端情况(如n=1e5),可以考虑以下优化:
- 提前终止:找到第一个覆盖地毯后立即返回
- 输入优化:使用更快的输入方法(如C++的getchar_unlocked)
- 并行处理:将地毯分成多个区间并行检查(适用于多核环境)
9. 相似题目推荐
- 矩形覆盖问题:求多个矩形的并集面积
- 线段覆盖问题:一维区间覆盖查询
- 最近邻查询:寻找距离某点最近的特定对象
- 碰撞检测:判断多个物体是否相交
10. 总结与个人心得
这道题教会我们,在算法设计中,有时换个思考方向就能大幅提升效率。从正向模拟到逆向查询的转变,体现了算法思维的精妙之处。在实际编程竞赛中,我建议:
- 先理解题意,明确输入输出要求
- 分析数据规模,估算所需算法复杂度
- 考虑边界条件和特殊案例
- 先实现朴素解法,再思考优化空间
- 使用小规模测试数据验证逻辑正确性
对于初学者,建议从暴力解法开始编写,再逐步优化。理解为什么朴素解法不可行,比直接学习最优解更重要。这道题也提醒我们,在解决空间相关问题时,不一定要真的构建整个空间模型,有时只需记录关键信息即可。