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\^5,1 \\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;
}