C++树形数据结构————树状数组、线段树中“逆序对”的问题

在我之前的几篇博客中整理了树状数组、线段树相关的笔记,我最近又刷了一些题,觉得有几道挺好的,可以巩固这些知识,这几道题重在逆序对的学习,比较经典,所以我决定分享一下。

目录

例题一:蓝桥杯官网------逆序对的数量

问题描述

输入格式

输出格式

样例输入

样例输出

代码解析

方法一:树状数组+离散化

方法2:线段树+离散化

方法三:01trie

例题二:蓝桥杯官网------逆序对数

题目描述

代码详解:

方法一:暴力解法

方法二:树状数组+离散化

例题三:蓝桥杯官网------压制二元组的总价值

问题描述

输入格式

输出格式

样例输入

样例输出

说明

注:本文例题均来自蓝桥杯官网公开真题,仅用于技术学习与算法讲解,里面的代码均由本人做出来并附上解析!

我的详细解析在第三题,大家可以先看第三题。

例题一:蓝桥杯官网------逆序对的数量

问题描述

给定长度为 n 的序列 a,输出 a 中逆序对的数量。逆序对:对于 1 ≤ i <j ≤ n,若 ai> aj,则 <ai, aj> 为一对逆序对。

输入格式

第一行输入一个正整数 n。(1 < n ≤ 10^5)第二行输入 n 个正整数表示序列 a。(1 ≤ ai≤ 10^9, 1 ≤ i ≤ n)

输出格式

输出一个整数,表示 a 中逆序对的数量。

样例输入

cpp 复制代码
8
1 4 7 2 5 7 9 2

样例输出

cpp 复制代码
8

代码解析

方法一:树状数组+离散化
cpp 复制代码
#include <iostream>
#include<vector>
#include<algorithm>
using namespace std;
using ll=long long;
const int N=1e5+9;
//方法一:树状数组
ll t[4*N],a[N];
vector<int>X;

int getidx(int x)
{
  return lower_bound(X.begin(),X.end(),x)-X.begin()+1;
}

void pushup(int o)
{
  t[o]=t[o<<1]+t[o<<1|1];
}

void update(int l,int r,int k,int s=1,int e=X.size(),int o=1)
{
  if(l<=s&&e<=r)
  {
    t[o]+=k;
    return;
  }
  int mid=(s+e)>>1;
  if(mid>=l)update(l,r,k,s,mid,o<<1);
  if(mid+1<=r)update(l,r,k,mid+1,e,o<<1|1);
  pushup(o);
}

ll query(int l,int r,int s=1,int e=X.size(),int o=1)
{
  ll res=0;
  if(l<=s&&e<=r)
  {
    return t[o];
  }
  int mid=(s+e)>>1;
  if(mid>=l) res+=query(l,r,s,mid,o<<1);
  if(mid+1<=r) res+=query(l,r,mid+1,e,o<<1|1);
  return res;
}

int main()
{
  ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
  ll ans=0;
  int n;cin>>n;
  for(int i=1;i<=n;i++)
  {
    cin>>a[i];
    X.push_back(a[i]);
  }
  //排序和去重
  sort(X.begin(),X.end());
  X.erase(unique(X.begin(),X.end()),X.end());
  //更新
  for(int i=1;i<=n;i++)
  {
    update(getidx(a[i]),getidx(a[i]),1);
    ans+=query(1,X.size())-query(1,getidx(a[i]));
  }
  cout<<ans<<'\n';
  return 0;
}
方法2:线段树+离散化
cpp 复制代码
#include <iostream>
#include<vector>
#include<algorithm>
using namespace std;
using ll=long long;
const int N=1e5+9;
//方法一:树状数组
ll t[4*N],a[N];
vector<int>X;

int getidx(int x)
{
  return lower_bound(X.begin(),X.end(),x)-X.begin()+1;
}

void pushup(int o)
{
  t[o]=t[o<<1]+t[o<<1|1];
}

void update(int l,int r,int k,int s=1,int e=X.size(),int o=1)
{
  if(l<=s&&e<=r)
  {
    t[o]+=k;
    return;
  }
  int mid=(s+e)>>1;
  if(mid>=l)update(l,r,k,s,mid,o<<1);
  if(mid+1<=r)update(l,r,k,mid+1,e,o<<1|1);
  pushup(o);
}

ll query(int l,int r,int s=1,int e=X.size(),int o=1)
{
  ll res=0;
  if(l<=s&&e<=r)
  {
    return t[o];
  }
  int mid=(s+e)>>1;
  if(mid>=l) res+=query(l,r,s,mid,o<<1);
  if(mid+1<=r) res+=query(l,r,mid+1,e,o<<1|1);
  return res;
}

int main()
{
  ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
  ll ans=0;
  int n;cin>>n;
  for(int i=1;i<=n;i++)
  {
    cin>>a[i];
    X.push_back(a[i]);
  }
  //排序和去重
  sort(X.begin(),X.end());
  X.erase(unique(X.begin(),X.end()),X.end());
  //更新
  for(int i=1;i<=n;i++)
  {
    update(getidx(a[i]),getidx(a[i]),1);
    ans+=query(1,X.size())-query(1,getidx(a[i]));
  }
  cout<<ans<<'\n';
  return 0;
}
方法三:01trie
cpp 复制代码
#include <iostream>
using namespace std;
using ll=long long;
const int N=32*1e5+9,M=1e5+9;
int e[N];
int son[N][2],tot=1;
int a[M];
//插入

void insert(int x)
{
  int o=1;
  e[o]++;
  for(int i=30;i>=0;i--)
  {
    int y=x>>i&1;
    if(!son[o][y])son[o][y]=++tot;
    o=son[o][y]; 
    e[o]++;
  }
}
int query(int x)
{
    int res = 0, o = 1;  // res存储结果,o表示当前节点,从根节点1开始
    for (int i = 30; i >= 0; i--)  // 从最高位(第30位)到最低位(第0位)遍历
    {
        int y = x >> i & 1;  
        if (y == 0) res += e[son[o][1]];  
        if (!son[o][y]) break;
        o = son[o][y];  // 移动到下一位的节点
    }
    return res;  
} 
int main()
{
  ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
  int n;cin>>n;
  for(int i=1;i<=n;i++)cin>>a[i];
  ll ans=0;
  for(int i=1;i<=n;i++)
  {
    ans+=1ll*query(a[i]);
    insert(a[i]);
  }
  cout<<ans<<'\n';
  return 0;
}

例题二:蓝桥杯官网------逆序对数

这题和上一题几乎一模一样,大家快速过即可

题目描述

本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。

在一个序列 a=(a[1],a[2],...,a[n]) 中,如果 (i,j) 满足 i<j 且 a[i]>a[j],则称为一个逆序对。

例如:(3,2,2,1) 中包含 6 个逆序对。

请问,(87,39,35,1,99,10,54,1,46,24,74,62,49,13,2,80,24,58,8,14,83,23,97,85,3,2,86,10,71,15) 中包含多少个逆序对?

代码详解:

方法一:暴力解法

本体数据量小,可直接暴力

cpp 复制代码
#include <iostream>
using namespace std;
//方法1:暴力解法
int main()
{
  int count=0;
  int arr[]={0,87,39,35,1,99,10,54,1,46,24,74,62,49,13,2,80,24,58,8,14,83,23,97,85,3,2,86,10,71,15};
  int n=sizeof(arr)/sizeof(arr[0])-1;
  for(int i=1;i<n;i++)
  {
    for(int j=i+1;j<=n;j++)
    {
      if(arr[i]>arr[j]) count++;
    }
  }
  cout<<count<<'\n';
  return 0;
}
方法二:树状数组+离散化
cpp 复制代码
//方法2:树状数组
//注意这里,是我大E了,不能直接对这个a数组进制树状数组操作!要进行离散化处理!
//核心原因:你的数组 a 中元素的值远超树状数组的下标范围,树状数组的 update/getprefix 是基于「值作为下标」操作的,
//而当前代码的逻辑完全依赖 "值的范围 ≤ 树状数组大小",但是数据不满足这个条件
/*int t[N];//树状数组
int a[]={0,87,39,35,1,99,10,54,1,46,24,74,62,49,13,2,80,24,58,8,14,83,23,97,85,3,2,86,10,71,15};
int n=sizeof(a)/sizeof(a[0])-1;

int lowbit(int x){return x&-x;}

void update(int a,int k)
{
  for(int i=a;i<=n;i+=lowbit(i)) t[i]+=k;
}

int getprefix(int k)
{
  int res=0;
  for(int i=k;i>=1;i-=lowbit(i)) res+=t[i];
  return res;
}

int main()
{
  int count=0;
  for(int i=1;i<=n;i++)
  {
    update(a[i],1);
    //计算每一步前面比a[i]大的数!
    count+=(i-1)-getprefix(a[i]-1);
  }
  cout<<count<<'\n';
  return 0;
}*/

#include <iostream>
#include<algorithm>
#include<vector>
using namespace std;
vector<int>X;
const int N=109;
int t[N];
//离散化操作
int getidx(int x)//读取x在X中的下标
{
  return lower_bound(X.begin(),X.end(),x)-X.begin()+1;
}

//树状数组相关操作:
int lowbit(int x){return x&-x;}

void update(int idx,int k)
{
  for(int i=idx;i<=X.size();i+=lowbit(i)) t[i]+=k;
}

int getprefix(int k)
{
  int res=0;
  for(int i=k;i>=1;i-=lowbit(i)) res+=t[i];
  return res;
}

int main()
{
  ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
  int ans=0;
  int a[]={0,87,39,35,1,99,10,54,1,46,24,74,62,49,13,2,80,24,58,8,14,83,23,97,85,3,2,86,10,71,15};
  int n=sizeof(a)/sizeof(a[0])-1;
  for(int i=1;i<=n;i++) X.push_back(a[i]);
  //排序和去重
  sort(X.begin(),X.end());
  X.erase(unique(X.begin(),X.end()),X.end());
  for(int i=1;i<=n;i++)
  {
    update(getidx(a[i]),1);
    ans+=getprefix(X.size())-getprefix(getidx(a[i]));
  }
  cout<<ans<<'\n';
  return 0;
}

例题三:蓝桥杯官网------压制二元组的总价值

我认为最好的一道题

问题描述

给定两个长度为 N 的排列 A 和 B。若一对二元组下标 (i,j) 满足以下关系则被称之为压制二元组:

  • 1≤i<j≤N。
  • PAi<PAj,其中 Px 表示值 x 在数组 B 中的下标。

一对压制二元组的价值被定义为 j−i,请你计算出所有压制二元组的价值之和。

排列的定义:长度为 N 的排列表示一个长度为 N 的数组,其中 [1,N] 每个数都恰好出现一次。

输入格式

第一行输入一个整数 N(1≤N≤2×105) 表示排列的长度。第二行输入 N 个整数 A1​,A2​,A3​,...,AN​ 表示排列 A。第三行输入 N 个整数 B1​,B2​,B3​,...,BN​ 表示排列 B。

保证 A,B 是一个排列。

输出格式

输出一个整数表示答案。

样例输入

cpp 复制代码
4
2 4 1 3
4 1 2 3

样例输出

cpp 复制代码
7

说明

样例中有效的压制二元组有 (1,4),(2,3),(2,4),(3,4),总价值为 4−1+3−2+4−2+4−3=7。

cpp 复制代码
//树状数组的另类用法,不是简单的单点修改,区间求和
#include <iostream>
using ll=long long;
using namespace std;
const int N=2e5+9;
ll n,h[N],a[N],b[N],c[N];
//b,c均是树状数组,其中
//b维护的是映射数组a中小于a[i]的和
//c维护的是a中小于a[i]的个数
/*
代码的核心目标:遍历到第 i 个元素 a[i] 时,需要计算所有已遍历的、
比 a[i] 小的元素 对应的 (a[i] - 该元素值) 之和(这等价于最终统计的 "带权逆序对和")。
把这个求和公式展开:
  总和 = (a[i] - a[j1]) + (a[i] - a[j2]) + ... + (a[i] - a[jk])  
  (其中 j1,j2...jk 是 i 之前的位置,且 a[j] < a[i],k 是这样的元素个数)

进一步化简(提取公因子 a[i]):
  总和 = a[i] * k  - (a[j1] + a[j2] + ... + a[jk])

从这个公式能直接看出,我们需要两个关键数据:
k:已遍历元素中比 a[i] 小的元素个数(对应公式里的 k);
sum_small:已遍历元素中比 a[i] 小的元素值的总和(对应公式里的 a[j1]+a[j2]+...+a[jk])。
第二步:为什么用两个树状数组分别维护这两个量?
树状数组的核心能力是:支持单点更新(加入新元素)+ 前缀和查询(统计区间内的总和)。
对于本题的场景:
我们遍历元素是 "逐个加入" 的(动态过程),需要每次加入 a[i] 后,
快速查询 [1, a[i]-1] 区间内的统计量(因为 [1, a[i]-1] 就是 "比 a[i] 小的数的范围");
两个统计量(个数、值的和)是独立的,无法用一个数组同时维护,因此需要两个树状数组:

c[]    元素个数    加入 1 个元素 -> update(a[i], 1, c)    查询结果 = 比 a[i] 小的元素个数 k
b[]    元素值的和    加入值 a[i] -> update(a[i], a[i], b)    查询结果 = 比 a[i] 小的元素值的和 sum_small

//核心思想:转化:
由题目的样例
A: 2 4 1 3 
B: 4 1 2 3
我们把A映射为1 2 3 4 
即h[2] = 1, h[4] = 2, h[1] = 3, h[3] = 4

那么这时B就变为2 3 1 4 记为a[N],表示b中元素在A中的位置
我们要求的就是2 3 1 4这个数组中所有满足i<j&&a[i]<a[j]的a[j]-a[i]之和

根据题意,此时下标之差就转化为了值之差:
即(2,3), (2,4), (3,4), (1,4)
(3-2) + (4-1) + (4-2) + (4-3)
= (3-2) + 4*3 - (1+2+3)
= 7
*/

int lowbit(ll x){return x&-x;}

void update(ll a,ll b,ll*arr)//将arr数组包含位置a的管辖区间都更新+b!!!
{
  //for(ll i=1;i<=a;i+=lowbit(i)) arr[i]+=b;遍历方式写错了!
  for(ll i=a;i<=n;i+=lowbit(i)) arr[i]+=b;
}

ll getprefix(int k,ll*arr)//求arr区间[1,k]的和
{
  ll res=0;
  for(int i=k;i>=1;i-=lowbit(i)) res+=arr[i];
  return res;
}

ll query(ll l,ll r,ll*arr)//求区间和
{
  return getprefix(r,arr)-getprefix(l-1,arr);
}

int main()
{
  ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
  int x;cin>>n;
  for(int i=1;i<=n;i++)
  {
    //输入A数组,并将其反向映射到h数组
    cin>>x;h[x]=i;//h表示A中出现的每一个数啊位置(编号!)
  }
  for(int i=1;i<=n;i++)
  {
    //输入B数组,并将其映射到A,表示B在A中出现的位置
    cin>>x;a[i]=h[x];
  }
  ll ans=0;//计算结果
  for(int i=1;i<=n;i++)
  {
    update(a[i],a[i],b);
    update(a[i],1,c);
    ans += a[i]*query(1, a[i]-1, c) - query(1, a[i]-1, b);
  }
  cout<<ans<<'\n';
  return 0;
}

创作不易,不妨点赞+关注支持一下(づ ̄3 ̄)づ╭❤~

相关推荐
❥ღ Komo·2 小时前
K8s蓝绿发布实战:零停机部署秘籍
java·开发语言
梨落秋霜2 小时前
Python入门篇【函数】
开发语言·python
FMRbpm2 小时前
用栈实现队列
数据结构·c++·新手入门
添砖java‘’2 小时前
常见的进程间通信方式详解
linux·c++·操作系统·信息与通信·进程通信
AA陈超2 小时前
LyraStarterGame_5.6 Experience系统加载流程详细实现
c++·笔记·学习·ue5·虚幻引擎·lyra
电饭叔2 小时前
利用类来计算点是不是在园内《python语言程序设计》2018版--第8章18题第3部分
开发语言·python
一韦以航.2 小时前
C【指针】详解(上)
c语言·数据结构·c++·算法
martian6653 小时前
深入解析C++驱动开发实战:优化高效稳定的驱动应用
开发语言·c++·驱动开发
HappRobot3 小时前
python类和对象
开发语言·python