P3810 【模板】三维偏序 / 陌上花开 cdq分治+树状数组

P3810 【模板】三维偏序 / 陌上花开

题目背景

这是一道模板题,可以使用 bitset,CDQ 分治,KD-Tree 等方式解决。

题目描述

n 个元素,第 i 个元素有 a_i,b_i,c_i 三个属性,设 f(i) 表示满足 a_j \\leq a_i b_j \\leq b_i c_j \\leq c_i j \\ne i 的 j j j 的数量。

对于所有 d \\in \[0, n) ,求 f(i) = d 的数量。

输入格式

第一行两个整数 n,k ,表示元素数量和最大属性值。

接下来 n 行,每行三个整数 a_i ,b_i,c_i ,分别表示三个属性值。

输出格式

n 行,第 d + 1 行表示 f(i) = d i 的数量。

输入输出样例 #1

输入 #1

复制代码
10 3
3 3 3
2 3 3
2 3 1
3 1 1
3 1 2
1 3 1
1 1 2
1 2 2
1 3 2
1 2 1

输出 #1

复制代码
3
1
3
0
1
0
1
0
0
1

说明/提示

对于所有数据,保证 1 \\leq n \\leq 10\^51 \\leq a_i, b_i, c_i \\le k \\leq 2 \\times 10\^5

纯暴力(30分)

对于每个 i i i 求出 f ( i ) f(i) f(i) ,然后用个桶统计一下即可

cpp 复制代码
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define ll long long
#define N 100005
using namespace std;
int n,k,f[N],a[N],b[N],c[N],t[N];
int main(){
	IOS;
	cin>>n>>k;
	for(int i=1;i<=n;i++)cin>>a[i]>>b[i]>>c[i];
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(i==j)continue;
			if(a[j]<=a[i]&&b[j]<=b[i])
				if(c[j]<=c[i])f[i]++;
		}
		t[f[i]]++;
	}
	for(int i=0;i<n;i++)cout<<t[i]<<"\n";
	return 0;
}

暴力+一点点优化(40分)

一共有3个属性,能不能减少一个呢???

我们可以对 a a a 从小到大排序,然后每个 i i i 就只需要看 ≤ ≤ ≤ 它的 a a a 的数的 b b b 属性了。

但是要注意一点,需要看 ≤ ≤ ≤ 它的 a a a 的数的 b b b 属性 。也就是说,不仅是所有在它前面的数!!!
可以用二分找到最后一个小于等于它的数

cpp 复制代码
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define ll long long
#define N 100005
using namespace std;
struct inf{
	int a,b,c;
	bool operator <(const inf &z)const{
		return a<z.a;
	}
}p[N];
int n,k,t[N];
int find(int x){/*二分*/
	int l=0,r=n+1,mid;
	while(l+1<r){
		mid=(l+r)>>1;
		if(p[mid].a<=p[x].a)l=mid;
		else r=mid;
	}
	return l;
}
int main(){
	IOS;
	cin>>n>>k;
	for(int i=1;i<=n;i++)
		cin>>p[i].a>>p[i].b>>p[i].c;
	sort(p+1,p+1+n);
	for(int i=1;i<=n;i++){
		int x=0,y=find(i);
		for(int j=1;j<=y;j++)
			if(j!=i&&p[j].b<=p[i].b)
				if(p[j].c<=p[i].c)x++;
		t[x]++;
	}
	for(int i=0;i<n;i++)cout<<t[i]<<"\n";
	return 0;
}

正解之一(cdq分治)

1.如果是二维偏序(类似于逆序对)

我们可以用一个树状数组解决,具体:

按 a a a 排序,拆掉一维

根据 b b b 为树状数组的下标,求答案时看自己所在位置前面有多少个数即可!

给出逆序对的代码:

题目可见:P1908 逆序对

cpp 复制代码
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define ll long long
#define N 500005
using namespace std;
struct inf{
	ll x,xb;
	bool operator <(const inf &z)const{
		if(x==z.x)return xb<z.xb;
		return x<z.x;
	}
}a[N];
ll n,b[N],t[N],ans;
void add(ll x){for(;x<=n;x+=(x&-x))t[x]++;}
ll sum(ll x){
	ll sum=0;
	for(;x>0;x-=(x&-x))sum+=t[x];
	return sum;
}
int main(){
	IOS;/*
  思路:
  1.暴力解是:对于第 $i$ 数,求出 $[1,i-1]$ 区间内有几个数与 $i$ 组成逆序对(想到了)
  2.优化:用一个桶记录这个数以前的数,然后前缀和求,但是,每次前缀和仍然会超(想到了)
  3.继续优化:用树状数组代替桶(其实也是桶) ,这样,前缀和的时间就是O(logN))了(想不到了T-T)
  4.一个问题:数太大了,桶空间不够,于是离散化:排序,按顺序离散化成:1,2...n(更想不到了)
  5.另一个问题:相同的数,这是直接在排序时以下标为第二关键字就行啦(额)*/
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i].x,a[i].xb=i;
	/*对a数组离散化*/
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++)b[a[i].xb]=i;
	/*计算答案*/
	for(int i=n;i>=1;i--)
		ans+=sum(b[i]),add(b[i]);
	cout<<ans;
	return 0;
}

二维偏序只需要在这个基础上反过来即可

2.扩展到三维偏序

如果我们也用类似二维偏序的方法处理三维:

对 a a a 排序,对于 i i i 前面的 j j j 都满足 a j ≤ a i a_j≤a_i aj≤ai ,然后我们对 b b b 排序,再用树状数组处理 c c c

这时,就会有一个问题:按 b b b 排序时,会打乱 a a a 的顺序

如果我们把数组分成两半:左半部分和右半部分。满足:

1.左半部分的 a a a 都 ≤ ≤ ≤ 右半部分的 a a a

2.分别对左右两部分按 b b b 排序

3.然后处理左半部分对右半部分的贡献

这样,对于右半部分的每个元素 i i i:
· 左半部分的所有 j j j 都满足 a j ≤ a i a_j ≤ a_i aj≤ai(因为左半部分的 a a a 都小)
· 左半部分已经按 b b b 排序,我们可以用树状数组处理 c c c

下面两个代码思路是一样的,但是不太相同

详细注释版代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

const int N = 100005;  // 最大元素数量
const int M = 200005;  // 最大属性值

// 结构体存储每个元素的信息
struct inf {
	int a, b, c;        // 三个属性值
	int id;             // 原始编号
	int tj;            // 相同元素的数量(用于去重)
	int ans;            // f(i)的值
} p[N], jl[N];    // 临时数组用于归并排序

int n, k, m;            // n:元素数量, k:最大属性值, m:去重后的元素数量
int t[M];            // 树状数组(用于c维度)
int cnt[N];             // 最终结果数组,cnt[d]表示f(i)=d的数量

// 比较函数1:按a,b,c三维排序(用于初始排序)
bool cmpa(inf x, inf y) {
	if (x.a != y.a) return x.a < y.a;      // 第一关键字:a
	if (x.b != y.b) return x.b < y.b;      // 第二关键字:b
	return x.c < y.c;                      // 第三关键字:c
}
// 树状数组更新操作
void update(int x, int v) {
	while (x <= k) {            // k是c的最大值
		t[x] += v;           // 在位置x增加v
		x += (x&-x);         // 移动到父节点
	}
}
// 树状数组查询操作(前缀和)
int query(int x) {
	int sum = 0;
	while (x > 0) {
		sum += t[x];         // 累加
		x -= (x&-x);         // 移动到前一个区间
	}
	return sum;
}
// CDQ分治核心函数
void cdq(int l, int r) {
	// 递归边界:只有一个元素
	if (l >= r) return;
	int mid = (l + r) >> 1;     // 分治中点
	// 递归处理左右子问题
	cdq(l, mid);                // 处理左半部分内部的关系
	cdq(mid + 1, r);            // 处理右半部分内部的关系
	/*
	 * 关键部分:处理左半部分对右半部分的贡献
	 * 由于整个数组是按a排序的,所以:
	 *   左半部分的a值 ≤ 右半部分的a值
	 * 因此,左半部分的元素可能对右半部分的元素有贡献
	 * 右半部分的元素不可能对左半部分的元素有贡献
	 */
	// 对左右两部分分别按b排序(临时复制到jl数组)
	int i = l, j = mid + 1, idx = 0;
	// 归并排序的合并过程,同时按b排序
	while (i <= mid && j <= r) {
		if (p[i].b <= p[j].b) {
			jl[idx++] = p[i++];
		} else {
			jl[idx++] = p[j++];
		}
	}

	// 处理剩余部分
	while (i <= mid) jl[idx++] = p[i++];
	while (j <= r) jl[idx++] = p[j++];

	// 将排序后的结果复制回原数组
	for (i = 0; i < idx; i++) {
		p[l + i] = jl[i];
	}

	// 现在[l, r]区间已按b排序
	// 遍历右半部分,统计左半部分对它的贡献
	for (i = l; i <= r; i++) {
		if (p[i].id <= mid)  // 左半部分的元素
			// 将左半部分的c值加入树状数组
			update(p[i].c, p[i].tj);
		else                     // 右半部分的元素
			// 查询树状数组中≤p[i].c的数量
			// 这些就是左半部分中满足b≤当前b且c≤当前c的元素数量
			p[i].ans += query(p[i].c);
	}
	// 清空树状数组(非常重要!)
	for (i = l; i <= r; i++)
		if (p[i].id <= mid)
			update(p[i].c, -p[i].tj);  // 减去之前加的值
}
int main() {
	// 输入数据
	cin >> n >> k;
	for (int i = 1; i <= n; i++) {
		cin >> p[i].a >> p[i].b >> p[i].c;
		p[i].id = i;        // 记录原始编号
		p[i].tj = 1;       // 初始每个元素出现1次
		p[i].ans = 0;       // 初始答案为0
	}

	// 第一步:按a,b,c三维排序
	sort(p + 1, p + n + 1, cmpa);

	// 第二步:合并完全相同的元素
	m = 1;  // m记录去重后的元素数量
	for (int i = 2; i <= n; i++) {
		// 如果当前元素与上一个元素完全相同
		if (p[i].a == p[m].a &&
		        p[i].b == p[m].b &&
		        p[i].c == p[m].c) {
			p[m].tj++;      // 增加计数
			p[m].ans = 0;    // 重置答案
		} else {
			m++;                 // 新元素
			p[m] = p[i]; // 复制
		}
	}

	// 重新分配id,用于分治时区分左右部分
	for (int i = 1; i <= m; i++)
		p[i].id = i;

	// 第三步:进行CDQ分治
	cdq(1, m);

	// 第四步:处理相同元素的相互贡献
	// 在CDQ分治中,相同元素之间没有统计(因为id相近,在递归中会跳过)
	// 但实际上,相同的元素应该互相贡献
	for (int i = 1; i <= m; i++)
		// 对于tj个相同元素,每个元素会受到其他(tj-1)个相同元素的贡献
		p[i].ans += p[i].tj - 1;

	// 第五步:统计最终答案
	// cnt[d]表示f(i)=d的元素数量
	for (int i = 1; i <= m; i++)
		cnt[p[i].ans] += p[i].tj;  // 有tj个元素的答案都是p[i].ans

	// 第六步:输出结果
	for (int i = 0; i < n; i++)
		cout << cnt[i] << "\n";
	return 0;
}

纯代码

cpp 复制代码
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define ll long long
#define N 100005
using namespace std;
struct inf{int a,b,c,id,tj,ans;}p[N],jl[N];
int n,k,xb,t[4*N],cnt[N];
bool cmpa(inf x,inf y){
	if(x.a!=y.a)return x.a<y.a;
	if(x.b!=y.b)return x.b<y.b;
	return x.c<y.c;
}
void add(int x,int y){while(x<=k)t[x]+=y,x+=(x&-x);}
int sum(int x){
	int sum=0;
	while(x){sum+=t[x],x-=(x&-x);}
	return sum;
}
void cdq(int l,int r){
	if(l>=r)return;
	int mid=(l+r)>>1;
	cdq(l,mid),cdq(mid+1,r);
	int i=l,j=mid+1,kn=l;
	while(i<=mid&&j<=r){
		if(p[i].b<=p[j].b)jl[kn++]=p[i++];
		else jl[kn++]=p[j++];
	}
	while(i<=mid)jl[kn++]=p[i++];
	while(j<=r)jl[kn++]=p[j++];
	for(int h=l;h<=r;h++)p[h]=jl[h];
	for(int h=l;h<=r;h++){
		if(p[h].id<=mid)
			add(p[h].c,p[h].tj);
		else p[h].ans+=sum(p[h].c);
	}
	for(int h=l;h<=r;h++)
		if(p[h].id<=mid)
			add(p[h].c,-p[h].tj);
}
int main(){
	IOS;
	cin>>n>>k;
	for(int i=1;i<=n;i++)
		cin>>p[i].a>>p[i].b>>p[i].c,p[i].tj=1;
	sort(p+1,p+1+n,cmpa);
	for(int i=1;i<=n;i++){
		if(xb!=0&&p[i].a==p[xb].a&&p[i].b==p[xb].b&&p[i].c==p[xb].c)
			p[xb].tj++;
		else p[++xb]=p[i];
	}
	for(int i=1;i<=xb;i++)p[i].id=i;
	cdq(1,xb);
	for(int i=1;i<=xb;i++)
		p[i].ans+=p[i].tj-1;
	for(int i=1;i<=xb;i++)
		cnt[p[i].ans]+=p[i].tj;
	for(int i=0;i<n;i++)cout<<cnt[i]<<"\n";
	return 0;
}
相关推荐
Chrikk7 小时前
基于 RAII 的分布式通信资源管理:NCCL 库的 C++ 封装
开发语言·c++·分布式
LYFlied7 小时前
【每日算法】LeetCode 20. 有效的括号
数据结构·算法·leetcode·面试
阿沁QWQ7 小时前
C++哈希表设计
开发语言·c++·散列表
涛涛北京7 小时前
【强化学习实验】- Actor-Critic
算法
啊阿狸不会拉杆7 小时前
《数字图像处理》第 6 章 - 彩色图像处理
图像处理·人工智能·opencv·算法·计算机视觉·数字图像处理
油泼辣子多加7 小时前
【信创】中间件对比
人工智能·深度学习·算法·中间件
RickyWasYoung7 小时前
【笔记】矩阵的谱半径
笔记·算法·矩阵
一分之二~7 小时前
回溯算法--递增子序列
开发语言·数据结构·算法·leetcode
m0_639397297 小时前
代码随想录算法训练营第五十天|图论理论基础,深搜理论基础,98. 所有可达路径,广搜理论基础
算法·图论