树状数组
应用:
1.修改单个数询问区间和。
2.整个区间加(即整个区间每个数都修改),然后去查单点的值。——》差分
树状数组 模板:
修改单个数询问区间和
// 固定的三个函数
ll lowbit(ll x)
{
return x&(-x);
}
void add(ll i, ll val)
{// 对c[i]加val,对管辖它的每个数都加val,譬如对 c[1]加1,则对c[2]c[4]c[8]都要加1
while(i <= n)
{
c[i] += val;
i += lowbit(i);
}
}
ll sum(ll i) // 求前i个数的和 // 区间和就相减咯
{
ll s = 0;
while(i > 0)
{
s += c[i];
i -= lowbit(i); // 减掉它管辖的区域 // lowbit(i)就是求出它管辖区域的元素个数
}
return s;
}
// 练练手:
https://www.luogu.com.cn/problem/P3374
// 树状数组
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6;
ll n, m, opt, t, t1, t2, c[N];
ll lowbit(ll x)
{
return x&(-x);
}
void add(ll i, ll val)
{// 对c[i]加val,对管辖它的每个数都加val,譬如对 c[1]加1,则对c[2]c[4]c[8]都要加1
while(i <= n)
{
c[i] += val;
i += lowbit(i);
}
}
ll sum(ll i) // 求前i个数的和 // 区间和就相减咯
{
ll s = 0;
while(i > 0)
{
s += c[i];
i -= lowbit(i); // 减掉它管辖的区域 // lowbit(i)就是求出它管辖区域的元素个数
}
return s;
}
int main()
{
scanf("%d%d",&n,&m);
for(ll i = 1; i <= n; i++)
{
scanf("%d",&t);
add(i, t); // 构造过程 等同于 添加过程
}
for(ll i = 1; i <= m; i++)
{
scanf("%d%d%d",&opt,&t1,&t2);
if(opt == 1)
add(t1, t2);
else
printf("%d\n",sum(t2)-sum(t1-1)); // 这里不必判断t1-1是否会为0啥的
}
return 0;
}
修改整个区间,查单点——》差分
总结:差分实现了**“修改区间”——》“修改单点”、“查单点”——》查前缀和**
参考代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=5e5+1;
ll n,m,a[N],c[N];
inline ll lowbit(ll x)
{
return x&(-x);
}
void add(ll i,ll val)
{
while(i<=n)
{
c[i]+=val;
i+=lowbit(i);
}
}
ll sum(ll i)
{
ll s=0;
while(i>0)
{
s+=c[i];
i-=lowbit(i);
}
return s;
}
int main()
{
scanf("%lld %lld",&n,&m);
for(int i=1;i<=n;++i)
{
scanf("%lld",&a[i]);
add(i,a[i]-a[i-1]);
}
ll opt,x,y,z;
while(m--)
{
scanf("%lld",&opt);
if(opt==1)
{
scanf("%lld %lld %lld",&x,&y,&z);
add(x,z);
add(y+1,0-z);
}
else
{
scanf("%lld",&x);
printf("%lld\n",sum(x));
}
}
}
树状数组——维护区间最值
两种操作:
1.末尾插入一个元素
2.查询当前数列中末尾L个数中的最大的数
参考博客:(2条消息) 树状数组之区间最值_想谈恋爱的范德川的博客-优快云博客_树状数组维护区间最值
参考代码:
树状数组做法:
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int m,mod,t,a[N],len,last;
char ch;
int c[N];
inline int lowbit(int x)
{
return x&(-x);
}
//求区间最值:
int query(int x,int y){
int ans=0;
while(x<=y)
{
ans=max(ans,a[y]),--y; //x在y的管辖区域里
while(y-lowbit(y)>=x){ //x不在y的管辖区域内
ans=max(ans,c[y]);
y-=lowbit(y);
}
}
return ans;
}
int main()
{
scanf("%d %d",&m,&mod);getchar(); //输入字符之前getchar()一下
while(m--)
{
scanf("%c %d",&ch,&t);
if(ch=='A')
{
a[++len]=(t+last)%mod;
c[len]=max(query(len-lowbit(len)+1,len-1),a[len]); //c[pos]维护它管辖区域的最值
}
else
printf("%d\n",last=query(len-t+1,len));
getchar(); //输入字符之前getchar()一下
}
return 0;
}
单调栈做法://单调栈可以维护 末尾一段元素最大值
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=2e5+10;
int m,mod,last,t,st[N],top,a[N],len,pos;
char c;
int main()
{
scanf("%d %d",&m,&mod); getchar();
for(int i=1;i<=m;++i)
{
scanf("%c %d",&c,&t);
if(c=='A')
{
t=(t+last)%mod;
a[++len]=t;
while(top&&a[st[top]]<=t) top--;
st[++top]=len;
}
else
{
pos=lower_bound(st+1,st+1+top,len-t+1)-st;
printf("%d\n",a[st[pos]]);
last=a[st[pos]];
}
getchar();
}
}
类似题目:
管家的忠诚
//维护区间最小值
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int m,n,a[N],c[N];
inline int lowbit(int x)
{
return x&(-x);
}
int qry(int x,int y)
{
int ans=INT_MAX;
while(x<=y)
{
ans=min(ans,a[y]),--y;
while(y-lowbit(y)>=x)
{
ans=min(ans,c[y]);
y-=lowbit(y);
}
}
return ans;
}
int main()
{
scanf("%d %d",&m,&n);
for(int i=1;i<=m;++i)
{
scanf("%d",&a[i]);
if(i==1) c[i]=a[i];
else c[i]=min(qry(i-lowbit(i)+1,i-1),a[i]);
}
int x,y;
while(n--)
{
scanf("%d %d",&x,&y);
printf("%d",qry(x,y));
if(n==0) printf("\n");
else printf(" ");
}
}
逆序对
暴力做法:
时间复杂度:
O
(
N
2
)
O(N^2)
O(N2)
优点:
O
(
1
)
O(1)
O(1)访问任意区间的逆序对数
逆序对计数
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=6e3+3;
int n,q,a[N],f[N][N];
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;++i) scanf("%lld",&a[i]);
for(int i=1;i<=n;++i){
int s=0;
for(int j=i-1;j>=1;--j){
if(a[i]<a[j]) s++;
f[j][i]=f[j][i-1]+s;
}
}
scanf("%lld",&q);
int l,r;
while(q--) {
scanf("%lld%lld",&l,&r);
printf("%lld\n",f[1][n]+(r-l)*(r-l+1)/2-2*f[l][r]);
}
}
归并排序做法
------------------------------2023.3.30 更新--------------------------
归并排序
//辅助数组
int b[N];
//merge()函数功能:将有序表A[low...mid]和A[mid+1...high]合并为一个有序表
void merge(int a[],int low,int mid,int high){
int i,j,k;
for(k=low;k<=high;++k) b[k]=a[k];
for(i=low,j=mid+1,k=low;i<=mid&&j<=high;++k){
if(b[i]<=b[j]){
a[k]=b[i];++i;
}else {
a[k]=b[j];++j;
}
}
while(i<=mid) {
a[k]=b[i];++k;++i;
}
while(j<=high) {
a[k]=b[j];++k;++j;
}
}
void mergesort(int a[],int low,int high){
if(low<high){ //特别注意这句是小于不是小于等于!因为左子树取的是[low,mid]
int mid=((low+high)>>1);
mergesort(a,low,mid);
mergesort(a,mid+1,high);
merge(a,low,mid,high);
}
}
练手题:洛谷 P1908 逆序对
参考代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+5;
int n,a[N],b[N],ans=0;
void merge(int a[],int low,int mid,int high){
int i,j,k;
for(k=low;k<=high;++k) b[k]=a[k];
for(i=low,j=mid+1,k=low;i<=mid&&j<=high;++k){
if(b[i]<=b[j]){
a[k]=b[i];++i;
}else {
a[k]=b[j];++j;
ans+=(mid-i+1);
}
}
while(i<=mid) {
a[k]=b[i];++k;++i;
}
while(j<=high) {
a[k]=b[j];++k;++j;
}
}
void mergesort(int a[],int low,int high){
if(low<high){
int mid=((low+high)>>1);
mergesort(a,low,mid);
mergesort(a,mid+1,high);
merge(a,low,mid,high);
}
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i];
mergesort(a,1,n);
cout<<ans<<'\n';
}
--------------------旧版代码:自我觉得可读性不高。。。
/*
归并排序有个特点:前一个大于后一个的才交换。
*/
#include <iostream>
using namespace std;
int r[1000005], rs[1000005], n;
long long num;
void mergesort(int *a, int *b, int x, int y);
int main()
{
scanf("%d",&n); // 输入
for(int i = 1; i <= n; i++)
scanf("%d",&r[i]);
mergesort(r, rs, 1, n);
printf("%lld\n",num);
return 0;
}
/*
参数说明:a为原数组,b为辅助数组,排序范围为 [x, y]
函数功能说明:使用归并排序算法使 a 数组有序
参考书籍:刘汝佳《算法竞赛入门经典(第二版)》
*/
void mergesort(int *a, int *b, int x, int y)
{
// 没有返回值,当x=y不必做任何操作(当个元素即有序!)
if(y > x)
{
int mid = (x + y) / 2; // 划分
int l = x, h = mid + 1, i = x; // 这个是算法细节! l记左序列,h记右序列,i记总序列
mergesort(a, b, x, mid); // 归并排序左边
mergesort(a, b, mid + 1, y); // 归并排序右边
while(l <= mid || h <= y) // 归并左右两个有序序列
{
if(h > y || (l <= mid && a[l] <= a[h])) // 技巧:巧妙利用"||"(短路运算符)把两个条件链接起来:如果条件1满足,不会计算条件2;如果条件1不满足,就一定会计算条件2
b[i++] = a[l++]; // 提示:要熟悉这种写法,复制后移动下标。
else
{
num += mid - l + 1;
b[i++] = a[h++];
}
}
for(i = x; i <= y; i++) // 将归并的结果(存在辅助数组 b 中)复制到 a 中
a[i] = b[i];
}
}
树状数组做法:
时间复杂度:
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)
优点:适合于数据量比较大的情况。
缺点:只能一次统计数组中的逆序对数量。
逆序对
统计数组中的逆序对数量,可能有重复元素。
参考代码:
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,a[N],b[N],c[N];
inline int lowbit(int x){
return x&-x;
}
void update(int id,int val) //在id号点加上val
{
while(id<=n){
c[id]+=val;
id+=lowbit(id);
}
}
int sum(int id) {
int ans=0;
while(id>0) {
ans+=c[id];
id-=lowbit(id);
}
return ans;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
b[i]=a[i];
}
//离散化——排序、去重、找位置
sort(b+1,b+1+n);
int d=unique(b+1,b+1+n)-b-1; //元素个数
a[0]=-1;
long long ans=0;
for(int i=1;i<=n;++i){
int pos=lower_bound(b+1,b+1+d,a[i])-b;
update(pos,1);
ans+=(i-sum(pos)); //减去小于等于这个点的点
}
printf("%lld\n",ans);
}
好题:
火柴排队(逆序对变形)
有长度为
n
n
n的
a
,
b
a,b
a,b两个数组,每个数组中任意相邻两个数的位置都可以交换。问要使得
∑
i
=
1
n
(
a
i
−
b
i
)
2
\sum_{i=1}^{n} (a_{i}−b_{i})^2
∑i=1n(ai−bi)2最小的最少交换次数。其实就是求一个数列的逆序对数量。
参考博客:(2条消息) NOIp2013提高组 火柴排队————逆序对+贪心_wly127的博客-优快云博客
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int MOD=99999997;
#define pii pair<int,int>
#define pb push_back
#define se second
#define fi first
#define ll long long
vector<pii> a,b;
int n,num[N],t,c[N];
ll ans;
bool cmp(pii x,pii y)
{
return x.fi<y.fi;
}
inline int lowbit(int x)
{
return x&(-x);
}
void update(int id,int val)
{
while(id<=n)
{
c[id]+=val;
id+=lowbit(id);
}
}
int sum(int id)
{
int ans=0;
while(id>0)
{
ans+=c[id];
id-=lowbit(id);
}
return ans;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
{
scanf("%d",&t);
a.pb({t,i});
}
for(int i=1;i<=n;++i)
{
scanf("%d",&t);
b.pb({t,i});
}
sort(a.begin(),a.end(),cmp);
sort(b.begin(),b.end(),cmp);
for(int i=1;i<=n;++i)
num[a[i-1].se]=b[i-1].se;
for(int i=1;i<=n;++i) //统计num中的逆序对
{
update(num[i],1);
ans+=(i-sum(num[i])); //树状数组求逆序对操作~
ans%=MOD;
}
printf("%lld\n",ans);
}
好题:2019河北省大学生程序设计竞赛 E-Paper Plane Fly Away
题意:给定一个数
n
n
n ,接下来给出
n
n
n 行,每行两个整数,分别代表坐在第
i
i
i 号男生前面的女生的下标和第
i
i
i 号男生喜欢的女生的下标。每个男生都要给自己喜欢的女生传纸飞机(直线传送)。问每个男生在传纸飞机时会被多少架其他纸飞机撞到。
思路:
1.对于样例,就是以下这么一幅图:
然后求每条线的交点数。
2.线 l l l 会和线 l 1 l_1 l1 产生交点,当且仅当 ( l 1 l_1 l1 的起点在 l l l 的左边&& l 1 l_1 l1 的终点在 l l l 的右边) || ( l 1 l_1 l1 的起点在 l l l 的右边&& l 1 l_1 l1 的终点在 l l l 的左边) 。
3.考虑用树状数组插入点(思想 重要),正着扫一遍,反着扫一遍,可以求出答案。
代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5;
int n,a[N],b[N],l[N],c[N],cnt[N];
inline int lowbit(int x){
return x&-x;
}
void add(int id,int val){
while(id<=n){
c[id]+=val;
id+=lowbit(id);
}
}
int sum(int id){
int ans=0;
while(id>0){
ans+=c[id];
id-=lowbit(id);
}
return ans;
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n;
int t1,t2;
for(int i=1;i<=n;++i){
cin>>t1>>t2;
a[i]=t2-n;
b[t1-n]=i;
}
for(int i=1;i<=n;++i) l[i]=b[a[i]]; //第i号男孩喜欢的女孩坐在什么位置
//正着扫一遍
for(int i=0;i<=n;++i) c[i]=0;
for(int i=1;i<=n;++i){
cnt[i]+=sum(n)-sum(l[i]);
add(l[i],1);
}
//反着扫一遍
for(int i=0;i<=n;++i) c[i]=0;
for(int i=n;i>=1;--i){
cnt[i]+=sum(l[i]-1);
add(l[i],1);
}
for(int i=1;i<=n;++i) cout<<cnt[i]<<'\n';
}
离线处理+树状数组
统计区间种类数
#include <bits/stdc++.h>
using namespace std;
const int N=5e4+10;
struct Node{
int l,r,i;
}q[200005];
int n,m,a[N],c[N],vis[1000007],ans[200005];
bool cmp(Node x,Node y)
{
return x.r<y.r;
}
inline int lowbit(int x)
{
return x&(-x);
}
void add(int id,int val)
{
while(id<=n)
{
c[id]+=val;
id+=lowbit(id);
}
}
int sum(int id)
{
int ans=0;
while(id>0)
{
ans+=c[id];
id-=lowbit(id);
}
return ans;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]);
scanf("%d",&m);
for(int i=1;i<=m;++i)
{
scanf("%d %d",&q[i].l,&q[i].r);
q[i].i=i;
}
sort(q+1,q+1+m,cmp);
int j=1;
for(int i=1;i<=m;++i)
{
while(j<=q[i].r)
{
if(vis[a[j]]) add(vis[a[j]],-1); //把前面的记录删了~~
add(j,1);
vis[a[j]]=j;
++j;
}
ans[q[i].i]=sum(q[i].r)-sum(q[i].l-1);
}
for(int i=1;i<=m;++i) printf("%d\n",ans[i]);
}
采花
查询[l,r]区间内出现至少两次的元素种类
#include <bits/stdc++.h>
using namespace std;
#define io(x) scanf("%d",&x);
const int N=1e6+6;
int n,k,m,a[N],c[N],ans[N],vis[N],cnt[N],last[N];
struct Node{
int l,r,id;
}q[N];
bool cmp(Node x,Node y)
{
return x.r<y.r;
}
inline int lowbit(int x)
{
return x&(-x);
}
void add(int id,int val)
{
while(id<=n)
{
c[id]+=val;
id+=lowbit(id);
}
}
int sum(int id)
{
int s=0;
while(id>0)
{
s+=c[id];
id-=lowbit(id);
}
return s;
}
int main()
{
io(n);io(k);io(m);
for(int i=1;i<=n;++i) io(a[i]);
for(int i=1;i<=m;++i)
{
io(q[i].l);
io(q[i].r);
q[i].id=i;
}
sort(q+1,q+1+m,cmp);
int j=1;
for(int i=1;i<=m;++i)
{
while(j<=n && j<=q[i].r)
{
cnt[a[j]]++;
if(cnt[a[j]]>=2)
{
if(last[a[j]]) add(last[a[j]],-1); //把上上一个位置除掉
add(vis[a[j]],1); //在上一个点的位置加1
last[a[j]]=vis[a[j]];
}
vis[a[j]]=j;//修改为当前的点
++j;
}
ans[q[i].id]=sum(q[i].r)-sum(q[i].l-1);
}
for(int i=1;i<=m;++i)
printf("%d\n",ans[i]);
}
配对统计
/*
配对统计
离线查询
1.先把对统计出来
2.对查询右端点排序,对 成对 的右端点排序
*/
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define fi first
#define se second
#define pii pair<int,int>
#define ll long long
const int N=3e5+5;
vector<pii> a,couple;
int n,m,c[N];
ll ans=0;
bool cmp(pii x,pii y)
{
return x.fi<y.fi;
}
struct Node{
int l,r,id;
}q[N];
bool cmp1(Node x,Node y)
{
return x.r<y.r;
}
inline int lowbit(int x)
{
return x&(-x);
}
void add(int id,int val)
{
while(id<=n)
{
c[id]+=val;
id+=lowbit(id);
}
}
int qry(int id)
{
int sum=0;
while(id>0)
{
sum+=c[id];
id-=lowbit(id);
}
return sum;
}
bool cmp2(pii x,pii y)
{
return x.se<y.se;
}
int main()
{
scanf("%d %d",&n,&m);
int t;
for(int i=1;i<=n;++i)
{
scanf("%d",&t);
a.pb({t,i});
}
for(int i=1;i<=m;++i)
{
scanf("%d %d",&q[i].l,&q[i].r);
q[i].id=i;
}
sort(q+1,q+1+m,cmp1);
sort(a.begin(),a.end(),cmp);
if(n==1){
cout<<0<<endl;
return 0;
}
for(int i=0;i<a.size();++i)
{
int t1,t2;
if(i==0)
{
t1=a[i].se,t2=a[i+1].se;
if(t1>t2) swap(t1,t2);
couple.pb({t1,t2});
}
if(i==a.size()-1)
{
t1=a[i-1].se,t2=a[i].se;
if(t1>t2) swap(t1,t2);
couple.pb({t1,t2});
}
if(i!=0 && i!=a.size()-1)
{
if(abs(a[i].fi-a[i-1].fi)<=abs(a[i].fi-a[i+1].fi))
{
t1=a[i-1].se,t2=a[i].se;
if(t1>t2) swap(t1,t2);
couple.pb({t1,t2});
}
if(abs(a[i].fi-a[i-1].fi)>=abs(a[i].fi-a[i+1].fi))
{
t1=a[i].se,t2=a[i+1].se;
if(t1>t2) swap(t1,t2);
couple.pb({t1,t2});
}
}
}
sort(couple.begin(),couple.end(),cmp2);
// cout<<couple.size()<<endl;
int j=0;
for(int i=1;i<=m;++i) //遍历需求
{
while(j<couple.size() && couple[j].se<=q[i].r)
{
add(couple[j].fi,1);
++j;
}
ans+=(long long)(q[i].id)*(long long)(qry(q[i].r)-qry(q[i].l-1));
}
printf("%lld\n",ans);
}
找规律+树状数组
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e5+5;
int n,m,c1[N],c2[N],a[N];
inline int lowbit(int x)
{
return x&-x;
}
int sum1(int id)
{
int sum=0;
while(id>0)
{
sum^=c1[id];
id-=lowbit(id);
}
return sum;
}
int sum2(int id)
{
int sum=0;
while(id>0)
{
sum^=c2[id];
id-=lowbit(id);
}
return sum;
}
void add1(int id,int val)
{
while(id<=n)
{
c1[id]^=val;
id+=lowbit(id);
}
}
void add2(int id,int val)
{
while(id<=n)
{
c2[id]^=val;
id+=lowbit(id);
}
}
signed main(void)
{
scanf("%lld %lld",&n,&m);
int t;
for(int i=1;i<=n;++i)
{
scanf("%lld",&a[i]);
if(i%2) add1(i,a[i]);
else add2(i,a[i]);
}
int opt,x,y;
while(m--)
{
scanf("%lld %lld %lld",&opt,&x,&y);
if(opt==1)
{
if(x%2)
{
add1(x,a[x]);
add1(x,y);
a[x]=y;//要修改一下!!!
}
else
{
add2(x,a[x]);
add2(x,y);
a[x]=y;//要修改一下!!!
}
}
else
{
if(x>y) swap(x,y);
if(x%2 && y%2)
{
printf("%lld\n",sum1(y)^sum1(x-1));
}
else if(x%2==0 && y%2==0)
{
printf("%lld\n",sum2(y)^sum2(x-1));
}
else
printf("0\n");
}
}
}