## P11048 [蓝桥杯 2024 省 B] 拼十字 解题报告


P11048 [蓝桥杯 2024 省 B] 拼十字 解题报告

一、题目大意

简单来说,这道题给了我们 N 个矩形,每个矩形有三个属性:长度 l、宽度 w 和颜色 c。我们需要找出其中有多少“对”矩形可以“拼十字”。

一对矩形(我们称之为矩形 A 和矩形 B)能“拼十字”的条件是:

  1. 颜色不同A.c ≠ B.c
  2. 一个严格包住另一个的维度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)

  1. A.c != B.c
  2. A.l > B.l
  3. A.w < B.w

这是一个带有三个限制条件的计数问题。这类问题通常的解决思路是:先处理掉简单的限制,然后把复杂的问题转化为一个经典的数据结构问题

第一步:处理颜色限制

A.c != B.c 这个条件有点麻烦,因为它不是一个确定性的比较(比如 A.c > B.c)。我们可以把它简化。
我们可以枚举其中一个矩形 A 的颜色,比如我们固定 A 的颜色为红色 (0)。那么,我们只需要在所有非红色(黄色 1 或蓝色 2)的矩形 B 中,寻找满足 B.l < A.lB.w > A.w 的矩形。

我们可以把这个思路推广:

  • 计算所有 红色 矩形与 非红色 矩形能组成的十字。
  • 计算所有 黄色 矩形与 非黄色 矩形能组成的十字。
  • 计算所有 蓝色 矩形与 非蓝色 矩形能组成的十字。

把这三种情况的总数加起来,就是最终的答案。这样,每次我们都只需要处理两个阵营的矩形:一个是我们选定的颜色(比如红色),另一个是“其他所有颜色”。

第二步:处理维度限制(二维偏序问题)

现在,问题转化成了:给定两个矩形集合(比如,集合 E 是红色矩形,集合 D 是非红色矩形),对于 E 中的每一个矩形 e,找出 D 中有多少个矩形 d 满足 d.l < e.ld.w > e.w

这是一个经典的 二维偏序 问题。解决二维偏序问题的标准技巧是:“排序”处理一维,“数据结构”处理另一维

  1. 用“排序”搞定长度 l
    我们将两个集合 ED 中的所有矩形都按照长度 l 从小到大排序。

  2. 用“数据结构”搞定宽度 w
    我们遍历排好序的集合 E 中的每个矩形 e。当我们考察 e 时,我们希望能快速知道在 D 集合中,有多少个 d 满足 d.l < e.ld.w > e.w

    由于我们已经对所有矩形按 l 排序了,我们可以利用这一点。我们使用一个“双指针”的思路来协同遍历 ED

    • 用一个指针 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.wadd(d.w, 1),表示宽度为 d.w 的矩形多了一个。
    • 查询 大于 e[i].w 的数量:这等价于 (总数) - (小于等于 e[i].w 的数量)。在树状数组中,这可以表示为 query(最大宽度) - query(e[i].w)

三、算法步骤总结

综合以上分析,我们的完整算法步骤如下:

  1. 主循环:遍历三种颜色 C = 0, 1, 2。在每次循环中,我们计算颜色为 C 的矩形作为矩形 A 时,能组成的十字数量。

  2. 分组:将所有矩形分为两组:E 组(颜色为 C)和 D 组(颜色不为 C)。

  3. 排序:将 E 组和 D 组都按照长度 l 从小到大排序。

  4. 双指针 + 树状数组

    • 初始化一个空的树状数组 t 和一个指针 j = 1(指向 D 组的开头)。
    • 用指针 i1|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].lD 组矩形的宽度信息。
    • 查询树状数组,计算宽度大于 e[i].w 的矩形数量:count = query(最大宽度) - query(e[i].w)
    • count 累加到总答案 ans 中。
  5. 清理:完成一次主循环后(比如计算完红色vs非红色的情况),树状数组中还留有 D 组矩形的信息。需要将其清空,以便下一次主循环(比如黄色vs非黄色)能正确计算。

  6. 输出:所有循环结束后,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}\) 是最大宽度),可以轻松通过本题。

posted @ 2025-07-17 15:03  surprise_ying  阅读(11)  评论(0)    收藏  举报