刷题刷到第1577题的时候,我愣了一下——铺地毯?这不就是按顺序往地板上盖毯子,最后问某个点被哪张毯子盖住吗?听起来像模拟题,可真动手写的时候,四面八方都能踩坑。这道题在蓝桥杯的"算法提高VIP"题库里,实际上是把一个很经典的“区间覆盖 + 点查询”问题包装成了生活场景,考的不是你会不会铺地毯,而是你会不会倒着想问题。
先说结论:这道题的最优解非常简单,思路顺过来以后代码不超过二十行,但如果你顺着“铺地毯”这个动词去模拟,很容易掉进二维数组和超时的坑里。这篇文章我会从题面解读开始,一步步讲清楚为什么暴力模拟不行、倒序查找为什么是对的、C++和Python分别怎么写,还会延展一下如果题目改成“多个查询点”该怎么办。无论你是刚开始备战蓝桥杯,还是想借这道题补一补逆向思维,都可以放心往下看。
1. 读完题面先别动手:数据范围已经告诉你该用哪种思路
1.1 题面到底在问什么
这道题的场景很直白:会场地上要铺若干张矩形地毯,编号从1到n,按顺序往地上铺。后面的地毯会压在先前的地毯上面,所以当查询某个点时,问题答案应该是“覆盖这个点的所有地毯里,编号最大(也就是最在上面)的那一张”。
每张地毯用四个整数描述:a b g k,表示地毯左下角坐标是 (a, b),在x轴方向长度为g,在y轴方向长度为k。换句话说,这张地毯覆盖的矩形区域是:
code复制a <= x <= a + g
b <= y <= b + k
注意这里的边界是闭区间,也就是说地毯四条边的位置也算被覆盖。这是很多同学写错的第一处细节,后面我会专门说。
最后题目给出一个查询点 (x, y),要求输出覆盖这个点的最上层地毯编号;如果没有任何一张地毯覆盖这个点,输出 -1。
1.2 先别急着写代码,看一眼数据范围
很多同学一看到“铺地毯”三个字,第一反应是:开一个二维数组模拟地面,每来一张地毯就把对应区域涂成当前编号,最后直接输出 g[x][y]。
这个思路在平面很小的时候能过,但在竞赛题里几乎必死。原因是二维数组的规模取决于坐标系的范围,而不是地毯的数量。
假设坐标系范围到了 100000 × 100000,你要开的 bool 数组就是:
code复制100000 * 100000 = 1e10 个元素
C++ 里 bool 类型实际占1字节,也就是说光是数组就要约 10GB 内存。即使你把 bool 用位运算压缩到一个bit,也需要约 1.25GB,大多数OJ的内存限制只有 128MB 或者 256MB,内存直接爆掉。
就算你狠下心用离散化压缩坐标,模拟每一张地毯的矩形填充也是一个隐藏炸弹。每张地毯如果覆盖了很大的面积,填充一次就要循环很多次,n张地毯叠起来,时间开销完全不可控。
所以这道题真正考察的第一个点就是:你得意识到二维模拟是死路,真正的信息根本不需要铺满整个平面。
1.3 “从前往后查”和“从后往前查”的差别在哪里
既然不能开平面数组,另一个朴素想法是:把每张地毯的参数存下来,然后从头到尾遍历一遍,判断查询点是否在每张地毯内部。
这个思路其实已经接近正解了,只是遍历方向还有讲究。如果从1号地毯查到n号地毯,你会发现有可能多张地毯都覆盖了这个点,但最终答案要的是编号最大的那张,所以你得把所有覆盖到的编号都记下来,取最大值。这样做没问题,但多了一个变量,逻辑上也绕了一下。
更自然的做法是反过来:从n号地毯往1号查,第一次遇到能覆盖查询点的地毯,它一定是最上层的,直接输出编号。因为后铺的地毯编号大,既然编号大的都覆盖这个点,那更早铺的地毯根本没机会露出来。
这个“倒着查”的思路,就是本题的核心。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心解法:倒着数地毯,命中即停
2.1 为什么“后铺设的地毯优先”是天然的解题顺序
覆盖类问题有一个共性:后发生的操作会“盖住”先发生的操作。你往桌面上先放一张白纸,再放一张红纸,最后看到的只可能是红纸覆盖范围内的红色。找“最上面的一层”,本质上就是找“最后执行的那个状态”。
地毯问题的编号规则刚好符合这个逻辑:编号越大,铺设时间越晚,越在最上面。所以查询某个点的时候,根本不需要关心所有能覆盖它的地毯,只需要找到其中编号最大的那张。
如果倒序遍历,从最大编号开始检查,第一张能覆盖查询点的地毯就是答案。一旦找到就立刻停止,后面的地毯不用再看了。
你甚至可以这样理解:编号最大的地毯相当于“最终赢家”,任何能覆盖住查询点的更晚地毯,都会把早先的地毯完全压住。所以先查赢家,赢不了再往早的查,这是一种很节省思考的方式。
2.2 点在矩形内的判定条件
判断一个点是否在矩形内,其实就是四个不等式:
code复制x >= a
x <= a + g
y >= b
y <= b + k
四个条件同时满足,点就在地毯内。
这里尤其要注意的是等号。地毯是一条边界闭合的矩形,所以点恰好落在右边界 x == a + g、上边界 y == b + k,或者角落上,都算被覆盖。
举个最极端的例子:输入一张地毯 0 0 1 1,查询点 (1, 1)。正确答案是这张地毯的编号,因为 (1,1) 是地毯右上角。但如果你判断条件里写成了 x < a + g,这个点会被漏掉,导致输出 -1,白丢一题的分。
cpp复制// 错误写法
if (x >= a && x < a + g && y >= b && y < b + k) // 少了右边界和上边界
// 正确写法
if (x >= a && x <= a + g && y >= b && y <= b + k)
2.3 复杂度分析:为什么O(n)反而是这题的下界
单次查询的情况下,最坏可能的复杂度是多少?
假设地毯一张都不覆盖查询点,那么无论从前往后还是从后往前,都得把所有n张地毯全部检查一遍才知道答案为-1。这个场景下,任何算法至少需要遍历n张地毯,因为每张地毯的数据都可能改变结果。
所以,无论如何,单次查询的下界就是O(n)。你不可能比O(n)更快了,除非你有某种预处理,但预处理本身也要花钱。
倒序查找的复杂度:
- 时间复杂度:最多遍历n张地毯,每张O(1)判断,总O(n)
- 空间复杂度:只要存n张地毯的参数,O(n)
所以这道题不需要任何花哨的高级数据结构,O(n)就是满分解。你要做的只是把倒序遍历写对。
提示:竞赛里“看起来很简单”的题,往往卡的是思路和边界。不要一上来就追求线段树、压位这些高级东西,先用数据范围推导出最朴素的可行解,往往才是拿分最快的路。
3. 代码落地:C++和Python两种写法以及最容易翻车的边界
3.1 C++数组版本
C++最直接的写法是用四个一维数组,分别存a、b、g、k。下标从1开始,方便输出地毯编号时直接对应。
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
int a[N], b[N], g[N], k[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i] >> b[i] >> g[i] >> k[i];
}
int x, y;
cin >> x >> y;
for (int i = n; i >= 1; i--) {
if (x >= a[i] && x <= a[i] + g[i] &&
y >= b[i] && y <= b[i] + k[i]) {
cout << i << '\n';
return 0;
}
}
cout << -1 << '\n';
return 0;
}
几个细节值得说:
ios::sync_with_stdio(false); cin.tie(nullptr);是C++刷题标配,能明显加快cin、cout的速度。虽然这题数据量不大,但好习惯可以带到后面的题。for (int i = n; i >= 1; i--)是倒序遍历,注意不要把边界写成i > 0然后循环体里用i-1,很容易把自己绕晕。- 数组大小
N宁可开大一点,也不要刚好卡着题目上限。如果题目n能到10000,N = 10005或10010都行。多开10个元素对内存没有影响,但能防止手滑越界。
3.2 Python版本
Python写起来更短,核心是利用元组存储地毯数据,再逆序遍历。
python复制n = int(input())
carpets = []
for _ in range(n):
a, b, g, k = map(int, input().split())
carpets.append((a, b, g, k))
x, y = map(int, input().split())
for i in range(n - 1, -1, -1):
a, b, g, k = carpets[i]
if a <= x <= a + g and b <= y <= b + k:
print(i + 1)
break
else:
print(-1)
Python里的 for...else 是一个很实用的语法:如果for循环正常结束(没有触发break),就执行else里的内容。所以这里如果倒序遍历了一圈都没找到覆盖点,就输出 -1。
注意Python下标从0开始,存储的时候第 i 张地毯其实是 carpets[i-1]。所以判断到 carpets[i] 时,输出的编号应该是 i+1,别写成了 i。
3.3 最容易翻车的三个地方
结合我自己的刷题经历,这道题翻车点集中在下面三个地方:
第一,边界等号。 前面已经说过,<= 和 < 之间就差一个点。建议写的时候默念“矩形包含边界”,检查代码时专门看所有等号是否齐全。
第二,读入顺序搞混。 题目给的四个参数是 a b g k,分别代表左下角横坐标、左下角纵坐标、x方向长度、y方向长度。有些人读成 x、y、width、height 后,用 x、y 去存地毯左下角,然后查询点的坐标也是 x、y,变量名冲突了还不好查。建议变量名起得清晰一些,或者把地毯参数叫 x1、y1、w、h。
第三,数组/列表下标错位。 C++版本从1开始存,输出 i 没毛病;Python从0开始存,输出要 i+1。两种语言各有各的坑,写完代码后最好自己用题目样例跑一遍。
3.4 稍微“工程化”一点的结构体写法
如果你不喜欢四个数组满天飞,可以定义结构体,代码读起来更清晰:
cpp复制#include <bits/stdc++.h>
using namespace std;
struct Carpet {
int a, b, g, k;
};
bool inside(const Carpet& c, int x, int y) {
return x >= c.a && x <= c.a + c.g &&
y >= c.b && y <= c.b + c.k;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<Carpet> carpets(n + 1);
for (int i = 1; i <= n; i++) {
cin >> carpets[i].a >> carpets[i].b >> carpets[i].g >> carpets[i].k;
}
int x, y;
cin >> x >> y;
for (int i = n; i >= 1; i--) {
if (inside(carpets[i], x, y)) {
cout << i << '\n';
return 0;
}
}
cout << -1 << '\n';
return 0;
}
竞赛里简洁和清晰之间要平衡。如果你对结构体没那么熟,用四个数组其实完全能过,考试时选择自己最有把握的写法最重要。
4. 进阶联想:如果从单次查询变成多点查询
4.1 当查询点变多,O(n*m)会给你颜色看
原题只查一个点,所以O(n)的思路已经是最优。但蓝桥杯很多题很喜欢在下一问里加入“多组询问”的变体。
假设题目改成:给m个点,每个点都要输出覆盖它的最上层地毯编号,n和m都到了10^5级别。如果你对每个点都做一次倒序扫描,复杂度就是O(n*m),天文数字,肯定超时。
这时候就需要换个角度思考:既然地毯编号越大的越优先,能不能把所有查询点也当成“有状态”的容器,让每张地毯只在第一次覆盖查询点时发挥作用?
一个比较自然的想法是这样:倒着遍历地毯,如果某张地毯覆盖了某些尚未得到答案的查询点,就把这些点的答案统一设为当前地毯编号,然后把这些点从待处理点集中删掉。这样一来,每个查询点最多被标记一次,整个处理过程只跟“命中的查询点数量”有关,而不是每张地毯都遍历所有点。
但这个想法实现起来需要快速完成“找出矩形区域内所有未回答的点”操作。于是就可能用到二维线段树、扫描线、或者离线分治等技巧,复杂度可以优化到O((n+m)logn)级别。当然,这些都是进阶内容,原题完全用不上,了解思路即可,不必强行套用。
4.2 “倒着处理”思想在多点查询中的威力
哪怕你暂时不会写二维线段树,“倒着处理”这个思想本身也能帮助你写出更高效的暴力。
举个例子:如果查询点很多但坐标范围不大,你可以把所有查询点放到一个哈希集合里。倒序遍历每张地毯,对于当前地毯,遍历它的矩形区域内的所有点,如果某个点在哈希集合里,就把答案记下来并删除。这样一来,即使矩形有大有小,每个查询点也只会在某一次边角扫描中被遇到后删除,不会反复被扫。这种“每点只回答一次”的均摊思想,在竞赛里非常常见。
当然,如果矩形太大,遍历所有点也会爆炸。真正通用的解法是上二维数据结构,那是另外一篇几千字的文章了。对于这道题,你只需要记住一个规律:覆盖类问题里,“从后往前”配合“命中即删”,往往能带来数量级上的优化。
4.3 原题为什么不需要这些高级货
再回头看原题,单次查询、n张地毯、O(n)遍历就是解,没有任何必要上高级数据结构。
这不是蓝桥杯不考数据结构,而是这道题本身就定位在“算法提高”的思维训练。它想让你体会的是:面对一个看起来像模拟的问题,先判断能不能模拟,再思考最优的检查顺序。 当你把一串数据倒过来看时,原本复杂的问题可能瞬间变得很简单。
5. 从这道题看蓝桥杯的“逆向思维”题该怎么认
5.1 这类题有什么特征
刷多了你会发现,蓝桥杯非常喜欢考逆向思维,题目特征也很明显:
- 描述里有“覆盖”“叠加”“涂改”“染色”“替换”这类动作
- 操作有明确的先后顺序
- 最终问的是“最后的状态”“最上面的一层”“最终剩下什么”
只要你看到这些关键词,先别急着从前往后模拟,停下来问自己一句:如果我从最后一个操作往前推,会不会更简单?
地毯问题就是最典型的例子。编号越大越靠上,“最上面的地毯”用倒序搜索天然契合。还有类似“多个矩形依次涂色,问最终某个位置的颜色”,这类题倒序遍历同样能秒杀。
5.2 怎么举一反三用到下一题
当你遇到一道覆盖类新题时,可以按照这个流程走:
- 读题,找到“覆盖”或“最终状态”这类关键词。
- 确认数据范围,判断能不能直接模拟。
- 如果直接模拟很难,考虑把操作倒过来处理。
- 倒序处理时,想清楚“后发生的操作优先”能否直接对应到答案。
这个过程在赛场上非常管用。很多看起来需要线段树、差分数组的题,偶尔用倒序暴力反而能轻松过关。
5.3 这道题给我留下最深印象的一点
我记得自己第一次做这道题,写完正序遍历版本后评测过了,但总感觉写得别扭,因为维护了一个 ans 变量,还要在每次覆盖时更新。后来看到别人的倒序写法,恍然大悟:原来这题的“标准答案”不是让你找所有覆盖点,而是让你找第一个从后往前命中的点。
从此我养成了一个习惯:凡是和“层叠”“覆盖”有关的问题,先试着把循环反过来写一遍。很多代码写出来不仅更短,逻辑上也更贴近题目的真实语义。
最后分享一个小技巧:写完倒序遍历后,可以手动构造一个最刁钻的测试用例——查询点落在所有地毯都不覆盖的位置,确认输出-1;再构造一个查询点正好落在某张地毯边界上的用例,确认等号没有漏。这两个用例过了,这道题基本就稳了。
