在我之前的几篇博客中整理了树状数组、线段树相关的笔记,我最近又刷了一些题,觉得有几道挺好的,可以巩固这些知识,这几道题重在逆序对的学习,比较经典,所以我决定分享一下。
目录
注:本文例题均来自蓝桥杯官网公开真题,仅用于技术学习与算法讲解,里面的代码均由本人做出来并附上解析!
我的详细解析在第三题,大家可以先看第三题。
例题一:蓝桥杯官网------逆序对的数量
问题描述
给定长度为 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;
}
