## P11048 [蓝桥杯 2024 省 B] 拼十字 解题报告
P11048 [蓝桥杯 2024 省 B] 拼十字 解题报告
一、题目大意
简单来说,这道题给了我们 N 个矩形,每个矩形有三个属性:长度 l
、宽度 w
和颜色 c
。我们需要找出其中有多少“对”矩形可以“拼十字”。
一对矩形(我们称之为矩形 A 和矩形 B)能“拼十字”的条件是:
- 颜色不同:
A.c ≠ B.c
- 一个严格包住另一个的维度:
A.l > B.l
并且A.w < B.w
。
注意,矩形的长宽是固定的,不能旋转。我们需要计算出满足条件的矩形对的总数,并对 \(10^9 + 7\) 取模。
二、思路分析
1. 暴力解法(为什么不行)
最直观的想法是,我们枚举每一对可能的矩形组合。用两层 for
循环,第一层循环遍历每个矩形 i
,第二层循环遍历每个矩形 j
。在内层循环里,我们检查 (i, j)
是否满足“拼十字”的两个条件。
- 检查
c[i] != c[j]
- 检查是否
l[i] > l[j]
且w[i] < w[j]
,或者l[j] > l[i]
且w[j] < w[i]
。
如果满足,计数器加一。
这种方法的时间复杂度是 \(O(N^2)\)。根据题目数据范围,\(N\) 最大可达 \(10^5\)。\(N^2\) 就会达到 \((10^5)^2 = 10^{10}\),这会远超时间限制,所以暴力解法行不通。我们需要更高效的算法。
2. 优化思路:降维与转化
\(O(N^2)\) 的瓶颈在于,对于每个矩形,我们都“盲目地”去和其他所有矩形进行比较。我们得想办法,在考察一个矩形时,能快速地“数出”有多少个其他矩形能和它配对,而不是一个一个地去比。
让我们把“拼十字”的条件重新整理一下。我们要找的是满足以下三个条件的矩形对 (A, B)
:
A.c != B.c
A.l > B.l
A.w < B.w
这是一个带有三个限制条件的计数问题。这类问题通常的解决思路是:先处理掉简单的限制,然后把复杂的问题转化为一个经典的数据结构问题。
第一步:处理颜色限制
A.c != B.c
这个条件有点麻烦,因为它不是一个确定性的比较(比如 A.c > B.c
)。我们可以把它简化。
我们可以枚举其中一个矩形 A 的颜色,比如我们固定 A 的颜色为红色 (0)。那么,我们只需要在所有非红色(黄色 1 或蓝色 2)的矩形 B 中,寻找满足 B.l < A.l
且 B.w > A.w
的矩形。
我们可以把这个思路推广:
- 计算所有 红色 矩形与 非红色 矩形能组成的十字。
- 计算所有 黄色 矩形与 非黄色 矩形能组成的十字。
- 计算所有 蓝色 矩形与 非蓝色 矩形能组成的十字。
把这三种情况的总数加起来,就是最终的答案。这样,每次我们都只需要处理两个阵营的矩形:一个是我们选定的颜色(比如红色),另一个是“其他所有颜色”。
第二步:处理维度限制(二维偏序问题)
现在,问题转化成了:给定两个矩形集合(比如,集合 E
是红色矩形,集合 D
是非红色矩形),对于 E
中的每一个矩形 e
,找出 D
中有多少个矩形 d
满足 d.l < e.l
且 d.w > e.w
。
这是一个经典的 二维偏序 问题。解决二维偏序问题的标准技巧是:“排序”处理一维,“数据结构”处理另一维。
-
用“排序”搞定长度
l
:
我们将两个集合E
和D
中的所有矩形都按照长度l
从小到大排序。 -
用“数据结构”搞定宽度
w
:
我们遍历排好序的集合E
中的每个矩形e
。当我们考察e
时,我们希望能快速知道在D
集合中,有多少个d
满足d.l < e.l
且d.w > e.w
。由于我们已经对所有矩形按
l
排序了,我们可以利用这一点。我们使用一个“双指针”的思路来协同遍历E
和D
。- 用一个指针
i
遍历E
。 - 用一个指针
j
遍历D
。
当我们用
i
考察到矩形e[i]
时,我们把j
指针向前移动,直到d[j].l
不再小于e[i].l
。在这个过程中,所有被j
指针路过的d
矩形,都满足了d.l < e[i].l
这个条件。对于这些满足长度条件的
d
矩形,我们现在只需要统计其中有多少个还满足d.w > e[i].w
。这是一个动态的查询问题:
- 操作1 (添加):当
j
指针路过一个d
矩形时,我们需要把它“登记”在案,记录下它的宽度d.w
。 - 操作2 (查询):当考察
e[i]
时,我们需要在所有已登记的宽度中,查询有多少个宽度值大于e[i].w
。
能够高效完成这种“单点更新”和“区间查询”操作的数据结构就是 树状数组 (Fenwick Tree) 或线段树。树状数组代码更简洁,效率也高。
我们可以用一个树状数组来维护宽度的信息。树状数组的下标代表宽度值,存储的值是这个宽度值出现了多少次。
- 添加
d.w
:add(d.w, 1)
,表示宽度为d.w
的矩形多了一个。 - 查询 大于
e[i].w
的数量:这等价于(总数) - (小于等于 e[i].w 的数量)
。在树状数组中,这可以表示为query(最大宽度) - query(e[i].w)
。
- 用一个指针
三、算法步骤总结
综合以上分析,我们的完整算法步骤如下:
-
主循环:遍历三种颜色
C = 0, 1, 2
。在每次循环中,我们计算颜色为C
的矩形作为矩形 A 时,能组成的十字数量。 -
分组:将所有矩形分为两组:
E
组(颜色为C
)和D
组(颜色不为C
)。 -
排序:将
E
组和D
组都按照长度l
从小到大排序。 -
双指针 + 树状数组:
- 初始化一个空的树状数组
t
和一个指针j = 1
(指向D
组的开头)。 - 用指针
i
从1
到|E|
遍历E
组中的每个矩形e[i]
。 - 在循环内部,移动
j
指针:while (j <= |D| && d[j].l < e[i].l)
,将所有满足l
条件的d[j]
的宽度d[j].w
加入树状数组中(add(d[j].w, 1)
),然后j++
。 - 此时,树状数组中存储了所有
l
小于e[i].l
的D
组矩形的宽度信息。 - 查询树状数组,计算宽度大于
e[i].w
的矩形数量:count = query(最大宽度) - query(e[i].w)
。 - 将
count
累加到总答案ans
中。
- 初始化一个空的树状数组
-
清理:完成一次主循环后(比如计算完红色vs非红色的情况),树状数组中还留有
D
组矩形的信息。需要将其清空,以便下一次主循环(比如黄色vs非黄色)能正确计算。 -
输出:所有循环结束后,
ans
就是最终答案,输出时别忘了取模。
四、代码解读
#include<bits/stdc++.h>
// ... 一些宏定义和命名空间 ...
using namespace std;
// 结构体,方便存储矩形的长和宽
struct node{
int l,w;
};
// e[]: 存放当前考察颜色的矩形
// d[]: 存放其他颜色的矩形
node e[N], d[N];
const int mx=1e5, M=1e9+7; // mx是宽度的最大值,M是模数
int n, l[N], w[N], c[N], cnte, cntd, t[N], ans; // cnte, cntd是e和d的计数器
// 排序的比较函数,按长度 l 从小到大
bool cmp(node x, node y){
return x.l < y.l;
}
// === 树状数组模板 ===
// lowbit(x): 树状数组核心函数
int lowbit(int x) { return x & (-x); }
// add(x, y): 在位置x增加y
void add(int x, int y){
for(int i=x; i<=mx; i+=lowbit(i)) t[i] += y;
}
// query(x): 查询从1到x的前缀和
int query(int x){
int res=0;
for(int i=x; i; i-=lowbit(i)) res += t[i];
return res;
}
// === 树状数组结束 ===
// 核心处理函数,x是当前我们固定的颜色
void solve(int x){
cnte = cntd = 0; // 计数器清零
// 1. 分组
for(int i=1; i<=n; i++){
if(c[i] == x) e[++cnte] = {l[i], w[i]};
else d[++cntd] = {l[i], w[i]};
}
// 2. 排序
sort(d+1, d+cntd+1, cmp);
sort(e+1, e+cnte+1, cmp);
int j = 1; // 指向d数组的指针
// 3. 双指针 + 树状数组
for(int i=1; i<=cnte; i++){ // 遍历e数组
// 将所有 l < e[i].l 的 d 矩形加入树状数组
while(j <= cntd && d[j].l < e[i].l){
add(d[j].w, 1); // 登记宽度
j++;
}
// 查询有多少个已登记的d矩形,其宽度 w > e[i].w
// query(mx)是总数,query(e[i].w)是w<=e[i].w的数量
ans = (ans + query(mx) - query(e[i].w)) % M;
}
// 4. 清理树状数组,为下一次solve做准备
for(int i=1; i<j; i++) {
// j指针已经移动到了j的位置,说明1到j-1的d矩形都被加入了树状数组
add(d[i].w, -1);
}
}
signed main(){
ios::sync_with_stdio(false);
cin >> n;
for(int i=1; i<=n; i++) cin >> l[i] >> w[i] >> c[i];
// 对每种颜色都调用一次solve
for(int T=0; T<=2; T++) solve(T);
cout << ans;
return 0;
}
这份代码完美地实现了我们上面分析的算法。通过 分组 -> 排序 -> 双指针+树状数组 的流程,将原本 \(O(N^2)\) 的问题优化到了 \(O(N \log W_{max})\)(其中 \(W_{max}\) 是最大宽度),可以轻松通过本题。