【6】树状数组学习笔记
前言
树状数组是我学的第一个高级数据结构,属于 log \log log 级数据结构。
其实现在一般不会单独考察数据结构,主要是其在其他算法(如贪心,DP)中起到优化作用。
长文警告:本文一共 995 995 995 行,请合理安排阅读时间。
lowbit函数
lowbit函数用于求解一个数二进制位最右边的 1 1 1 表示的权值,可以使用下面函数计算。
int lowbit(int x)
{return (x&(-x));
}
举个例子:
x = 1010100100 \;\;x=1010100100 x=1010100100
− x = 0101011100 -x=0101011100 −x=0101011100
x & ( − x ) = 0000000100 = 4 x\&(-x)=0000000100=4 x&(−x)=0000000100=4
树状数组本质上是一个数组,属于线性数据结构。树状数组中,把数按照其 l o w b i t lowbit lowbit 值进行分层,分层虽然没有明显的操作与现象,但导致画出来的层次像树一样,所以叫做树状数组。
单点修改+区间查询
树状数组支持单点修改,区间查询(一般是求和)。
对于 c c c 数组的定义(初始化):
c k = ∑ i = k − l o w b i t ( k ) + 1 k a i c_k=\sum_{i=k-lowbit(k)+1}^{k}a_i ck=i=k−lowbit(k)+1∑kai
原理类似于倍增,把原数组下标 k k k 按照二进制每位进行拆分,通过每个二进制位求出一段区间的和,最后加和得到区间前缀和。
注意:下标从 1 1 1 开始存储。
查询前缀和
使用 l o w b i t lowbit lowbit 求出下标最右边二进制位为 1 1 1 的数位表示的值,然后 c c c 数组对应元素累加到结果,再减去这个 l o w b i t lowbit lowbit 值,使之后可以求出下标第二右边二进制位为 1 1 1 的数位表示的值。重复这个过程,直到下标为 0 0 0 。
举个例子:
求 a a a 中 [ 1 , 6 ] [1,6] [1,6] 的前缀和。
首先,因为 6 = 110 , l o w b i t ( 6 ) = 2 6=110,lowbit(6)=2 6=110,lowbit(6)=2 ,所以 c 6 = a 5 + a 6 c_6=a_5+a_6 c6=a5+a6 。
然后,将最后一位的 1 1 1 减掉,得到 4 = 100 , l o w b i t ( 4 ) = 4 4=100,lowbit(4)=4 4=100,lowbit(4)=4 ,所以 c 4 = a 1 + a 2 + a 3 + a 4 c_4=a_1+a_2+a_3+a_4 c4=a1+a2+a3+a4 。
最后, c 4 + c 6 = a 1 + a 2 + a 3 + a 4 + a 5 + a 6 c_4+c_6=a_1+a_2+a_3+a_4+a_5+a_6 c4+c6=a1+a2+a3+a4+a5+a6 ,也就是 a a a 中 [ 1 , 6 ] [1,6] [1,6] 的前缀和。
正确性证明:
设原下标为 k k k ,由 l o w b i t lowbit lowbit 和 c c c 数组的定义得求出 [ k − l o w b i t ( k ) + 1 , k ] [k-lowbit(k)+1,k] [k−lowbit(k)+1,k] 的和。设减去 l o w b i t ( k ) lowbit(k) lowbit(k) 得 l l l ,求出 [ l − l o w b i t ( l ) + 1 , k − l o w b i t ( k ) ] [l-lowbit(l)+1,k-lowbit(k)] [l−lowbit(l)+1,k−lowbit(k)] 。观察得这两个区间是相接且不交叉的,可以合并为 [ l − l o w b i t ( l ) + 1 , k ] [l-lowbit(l)+1,k] [l−lowbit(l)+1,k] ,这样递推下去。由于最后下标为 0 0 0 ,故一定可以合并成 [ 1 , k ] [1,k] [1,k] 。证毕。
int getsum(int x)
{int t=0;while(x>0)t+=c[x],x-=lowbit(x);return t;
}
时间复杂度: O ( log n ) O(\log n) O(logn)
单点修改
单点修改一个下标后,受影响的必然是减去 l o w b i t lowbit lowbit 后是这个下标,所以可以把 l o w b i t lowbit lowbit 加回来,同时把 c c c 数组增加。当然,如果超过了数组元素个数 n n n 就没必要再加了。
void add(int x,int d)
{while(x<=n)c[x]+=d,x+=lowbit(x);
}
时间复杂度: O ( log n ) O(\log n) O(logn)
初始化
不需要按照 c c c 数组的定义来,可以把每一个数都进行一次单点修改操作。
void init()
{for(int i=1;i<=n;i++)add(i,a[i]);
}
时间复杂度: O ( n log n ) O(n\log n) O(nlogn)
区间求和
使用查询前缀和查询出前缀和,然后相减就是区间的和。
int ask(int x,int y)
{return getsum(y)-getsum(x-1);
}
时间复杂度: O ( log n ) O(\log n) O(logn)
单点修改+区间查询例题
例题 1 1 1 :
P3374 【模板】树状数组 1
单点修改+区间查询模板题,不多赘述。
#include <bits/stdc++.h>
using namespace std;
int n,m,a[600000],c[600000];
int lowbit(int x)
{return (x&(-x));
}void add(int x,int d)
{while(x<=n)c[x]+=d,x+=lowbit(x);
}void init()
{for(int i=1;i<=n;i++)add(i,a[i]);
}int getsum(int x)
{int t=0;while(x>0)t+=c[x],x-=lowbit(x);return t;
}int ask(int x,int y)
{return getsum(y)-getsum(x-1);
}int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)scanf("%d",&a[i]);init(); for(int i=0;i<m;i++){int op,x,y;scanf("%d%d%d",&op,&x,&y);if(op==1)add(x,y);else if(op==2)printf("%d\n",ask(x,y));}return 0;
}
例题 2 2 2 :
P2068 统计和
类似例题 1 1 1 ,但是不用初始化,注意字符的输入。
#include <bits/stdc++.h>
using namespace std;
long long n,m,a[600000],c[600000];
long long lowbit(long long x)
{return (x&(-x));
}void add(long long x,long long d)
{while(x<=n)c[x]+=d,x+=lowbit(x);
}long long getsum(long long x)
{long long t=0;while(x>0)t+=c[x],x-=lowbit(x);return t;
}long long ask(long long x,long long y)
{return getsum(y)-getsum(x-1);
}int main()
{cin>>n>>m;for(int i=0;i<m;i++){char op;int x,y;cin>>op>>x>>y;if(op=='x')add(x,y);else if(op=='y')printf("%lld\n",ask(x,y));}return 0;
}
例题 3 3 3 :
P2982 [USACO10FEB]Slowing down G
树上树状数组。
由于每次每头牛走完之后会单点修改一个值,而放慢速度的次数又取决于从根到目标节点的区间和,自然联想到树状数组。
首先预处理出每个节点的层次,一方面方便遍历树,另一方面方便树上树状数组。
对于单点修改的操作,考虑预处理出修改每个节点之后会影响的点,可以通过先搜索层次深的节点,搜索更浅的节点时记忆化,做到将近 O ( n log n ) O(n\log n) O(nlogn) 的复杂度。
对于区间查询的操作,考虑预处理出查询每个节点之前会计算的点,可以通过先搜索层次浅的节点,搜索更深的节点时记忆化,同样做到将近 O ( n log n ) O(n\log n) O(nlogn) 的复杂度。
然后,由于已经预处理了每个点的影响与计算,所以树状数组操作时只需要遍历这些预处理的记录就行了。
注意一定要记忆化,否则很容易退化回 O ( n 2 ) O(n^2) O(n2) 。
#include <bits/stdc++.h>
#define lowbit(x) (x&(-x))
using namespace std;
struct edge
{int to,next;
}e[300000];
struct order
{int p,c;
}xu[100001];
struct node
{int b;vector<int>low;vector<int>high;
}retree[100001];
int n,h[100001],tol=0,c[100001],book[100001];
inline int read()
{int x=0,f=1;char ch=getchar();while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}return x*f;
}bool cmp(struct order a,struct order b)
{return a.c>b.c;
}void add_edge(int u,int v)
{e[++tol].next=h[u];e[tol].to=v;h[u]=tol;
}void dfs1(int root,int ce)
{retree[root].b=ce;for(register int i=h[root];i;i=e[i].next){if(book[e[i].to])continue;book[e[i].to]=1;dfs1(e[i].to,ce+1);}
}void dfs2(int root,int from,int want)
{if(retree[root].b==want){int l=retree[root].low.size();for(int i=0;i<l;i++)retree[from].low.push_back(retree[root].low[i]);return;}if(want>n)return;for(register int i=h[root];i;i=e[i].next){if(retree[e[i].to].b<=retree[root].b)continue;dfs2(e[i].to,from,want);}
}void dfs3(int root,int from,int want)
{if(retree[root].b==want){int l=retree[root].high.size();for(int i=0;i<l;i++)retree[from].high.push_back(retree[root].high[i]);want-=lowbit(want);return;}if(want<=0)return;for(register int i=h[root];i;i=e[i].next){if(retree[e[i].to].b>=retree[root].b)continue;dfs3(e[i].to,from,want);}
}void add(int x)
{int l=retree[x].low.size();for(register int i=0;i<l;i++)c[retree[x].low[i]]++;
}int query(int x)
{int t=0,l=retree[x].high.size();for(register int i=0;i<l;i++)t+=c[retree[x].high[i]];return t;
}int main()
{n=read();for(register int i=0;i<n-1;i++){int u,v;u=read();v=read();add_edge(u,v);add_edge(v,u);}for(register int i=1;i<=n;i++){retree[i].low.push_back(i);retree[i].high.push_back(i);}book[1]=1;dfs1(1,1);for(register int i=1;i<=n;i++)xu[i].p=i,xu[i].c=retree[i].b;sort(xu+1,xu+n+1,cmp);for(register int i=1;i<=n;i++)dfs2(xu[i].p,xu[i].p,retree[xu[i].p].b+lowbit(retree[xu[i].p].b));for(register int i=n;i>=1;i--)dfs3(xu[i].p,xu[i].p,retree[xu[i].p].b-lowbit(retree[xu[i].p].b));for(register int i=1;i<=n;i++){int p;p=read();printf("%d\n",query(p));add(p);}return 0;
}
当然,每次修改时计算也是可以的,而且可以大大减少码量,这里不多赘述。
例题 4 4 4 :
P4054 [JSOI2009] 计数问题
由于权值值域很小( c ≤ 100 c\le100 c≤100 ),可以对每一种权值建立一个二维树状数组,每次更新时单独更新。一个某种权值的格子会对这种权值的答案造成 1 1 1 的贡献,所以可以把每个这种权值的格子视为 1 1 1 ,其他的格子视为 0 0 0 。统计个数时加起来就行了。
操作 1 1 1 的处理:
首先,如果把 a a a 修改成 b b b ,那么会影响 a a a 和 b b b 两种权值的二维树状数组。可以把 a a a 的树状数组影响到的值都加 1 1 1 ,而 b b b 的树状数组影响到的值都减 1 1 1 ,从而达到修改的目的。
注意:警示后人(0pts,仅过Subtask #1)
对于影响到的值,可以参考一维树状数组的解决方式。将横坐标与纵坐标分别加上其 l o w b i t lowbit lowbit ,把原区间划分为四个小区间。
void add(int x,int y,int k,int s)
{int i=x,j=y;while(x<=n){y=j;while(y<=m)c[x][y][s]+=k,y+=lowbit(y);x+=lowbit(x);}
}void insert()
{int x=0,y=0,c=0;scanf("%d%d%d",&x,&y,&c);add(x,y,1,c);add(x,y,-1,a[x][y]);a[x][y]=c;
}
操作 2 2 2 的处理:
设 s [ i ] [ j ] s[i][j] s[i][j] 表示二维右下点为 ( i , j ) (i,j) (i,j) 的矩形的前缀和,利用容斥原理得到矩形 ( x 1 , y 1 , x 2 , y 2 ) (x_1,y_1,x_2,y_2) (x1,y1,x2,y2) 的加和值为:
s [ x 2 ] [ y 2 ] − s [ x 1 − 1 ] [ y 2 ] − s [ x 2 ] [ y 1 − 1 ] + s [ x 1 − 1 ] [ y 1 − 1 ] s[x_2][y_2]-s[x_1-1][y_2]-s[x_2][y_1-1]+s[x_1-1][y_1-1] s[x2][y2]−s[x1−1][y2]−s[x2][y1−1]+s[x1−1][y1−1]
对于前缀和的处理,可以参考一维树状数组的解决方式。将横坐标与纵坐标分别减去其 l o w b i t lowbit lowbit ,把原区间划分为四个小区间。
int getsum(int x,int y,int k)
{int t=0,i=x,j=y;while(x>0){y=j;while(y>0)t+=c[x][y][k],y-=lowbit(y);x-=lowbit(x);}return t;
}int query()
{int x1,y1,x2,y2,c;scanf("%d%d%d%d%d",&x1,&x2,&y1,&y2,&c);return (getsum(x2,y2,c)-getsum(x1-1,y2,c)-getsum(x2,y1-1,c)+getsum(x1-1,y1-1,c));
}
最后,把这些操作拼起来,再加上输入输出与初始化即可。
#include <bits/stdc++.h>
using namespace std;
int n,m,q,a[301][301],c[301][301][101],op;
int lowbit(int x)
{return (x&(-x));
}void add(int x,int y,int k,int s)
{int i=x,j=y;while(x<=n){y=j;while(y<=m)c[x][y][s]+=k,y+=lowbit(y);x+=lowbit(x);}
}void insert()
{int x=0,y=0,c=0;scanf("%d%d%d",&x,&y,&c);add(x,y,1,c);add(x,y,-1,a[x][y]);a[x][y]=c;
}int getsum(int x,int y,int k)
{int t=0,i=x,j=y;while(x>0){y=j;while(y>0)t+=c[x][y][k],y-=lowbit(y);x-=lowbit(x);}return t;
}int query()
{int x1,y1,x2,y2,c;scanf("%d%d%d%d%d",&x1,&x2,&y1,&y2,&c);return (getsum(x2,y2,c)-getsum(x1-1,y2,c)-getsum(x2,y1-1,c)+getsum(x1-1,y1-1,c));
}int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[i][j]);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)add(i,j,1,a[i][j]); scanf("%d",&q);for(int i=0;i<q;i++){scanf("%d",&op);if(op==1)insert(); else if(op==2)printf("%d\n",query());}return 0;
}
区间修改+单点查询
树状数组支持单点查询,区间修改(一般是求和)。
单点修改,区间查询使用的是前缀和思想,把它反过来,变成差分思想,就能够实现单点查询,区间修改。
首先建立一个差分数组,其中每个值定义为:
b i = a i ( i = 1 ) b_i=a_i(i=1) bi=ai(i=1)
b i = a i − a i − 1 ( i ≠ 1 ) b_i=a_i-a_{i-1}(i\neq1) bi=ai−ai−1(i=1)
之后,在差分数组上建立树状数组。
单点查询
同单点修改,区间查询的查询前缀和。
因为由差分数组的定义,可以知道差分数组前 i i i 项的和为:
b i + b i − 1 ⋯ + b 2 + b 1 = a i − a i − 1 + a i − 1 − a i − 2 ⋯ + a 2 − a 1 + a 1 = a i \begin{aligned} b_i+b_{i-1}\dots+b_2+b_1 = a_i&-a_{i-1}+a_{i-1}-a_{i-2}\dots+a_{2}-a_{1}+a_{1}\\=a_i\end{aligned} bi+bi−1⋯+b2+b1=ai=ai−ai−1+ai−1−ai−2⋯+a2−a1+a1
所以,可以直接在差分数组上查询前 i i i 项的前缀和,就是 a i a_i ai 的值。
int getsum(int x)
{int t=0;while(x>0)t+=c[x],x-=lowbit(x);return t;
}
时间复杂度: O ( log n ) O(\log n) O(logn)
区间修改
对于 [ l , r ] [l,r] [l,r] 区间增加 k k k 的修改,可以把位置分为一下几类:
1 1 1 : l < i ≤ r l\lt i\le r l<i≤r
b i = b i + k − ( b i − 1 + k ) = b i − b i − 1 b_i=b_i+k-(b_{i-1}+k)=b_i-b_{i-1} bi=bi+k−(bi−1+k)=bi−bi−1
没有变化,无需修改。
2 2 2 : i = l i=l i=l
b i = b i + k − b i − 1 = b i − b i − 1 + k b_i=b_i+k-b_{i-1}=b_i-b_{i-1}+k bi=bi+k−bi−1=bi−bi−1+k
按照之前的修改操作将第 l l l 项 + k +k +k 即可。
3 3 3 : i = r + 1 i=r+1 i=r+1
b i = b i − ( b i − 1 + k ) = b i − b i − 1 − k b_i=b_i-(b_{i-1}+k)=b_i-b_{i-1}-k bi=bi−(bi−1+k)=bi−bi−1−k
按照之前的修改操作将第 r + 1 r+1 r+1 项 − k -k −k 即可。
void add(int x,int d)
{while(x<=n)c[x]+=d,x+=lowbit(x);
}void pluss()
{int x,y,k;scanf("%d%d%d",&x,&y,&k);add(x,k);add(y+1,-k);
}
时间复杂度: O ( log n ) O(\log n) O(logn)
初始化
在差分数组上建立树状数组。
void init()
{for(int i=1;i<=n;i++)add(i,b[i]);
}
时间复杂度: O ( n log n ) O(n\log n) O(nlogn)
区间修改+单点查询例题
例题 5 5 5 :
P3368 【模板】树状数组 2
区间修改+单点查询模板题,不多赘述。
#include <bits/stdc++.h>
using namespace std;
int n,m,a[600000],b[600000],c[600000];
int lowbit(int x)
{return (x&(-x));
}void add(int x,int k)
{while(x<=n)c[x]+=k,x+=lowbit(x);
}void init()
{for(int i=1;i<=n;i++)add(i,b[i]);
}int getsum(int x)
{int ans=0;while(x>0)ans+=c[x],x-=lowbit(x);return ans;
}void pluss()
{int x,y,k;scanf("%d%d%d",&x,&y,&k);add(x,k);add(y+1,-k);
}int ask()
{int x;scanf("%d",&x);return getsum(x);
}int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)scanf("%d",&a[i]);for(int i=1;i<=n;i++)b[i]=a[i]-a[i-1];init();for(int i=1;i<=m;i++){int op;scanf("%d",&op);if(op==1)pluss();else if(op==2)printf("%d\n",ask());}return 0;
}
例题 6 6 6 :
P5057 [CQOI2006]简单题
同例题 5 5 5 ,不用初始化。
对于数字反转,可以修改时直接将数字加 1 1 1 ,查询时利用对 2 2 2 取模的周期性来解决。
#include <bits/stdc++.h>
using namespace std;
int n,m,b[600000],c[600000];
int lowbit(int x)
{return (x&(-x));
}void add(int x,int k)
{while(x<=n)c[x]+=k,x+=lowbit(x);
}int getsum(int x)
{int ans=0;while(x>0)ans+=c[x],x-=lowbit(x);return ans;
}void pluss()
{int x,y,k=1;scanf("%d%d",&x,&y);add(x,k);add(y+1,-k);
}int ask()
{int x;scanf("%d",&x);return (getsum(x)+2)%2;
}int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=m;i++){int op;scanf("%d",&op);if(op==1)pluss();else if(op==2)printf("%d\n",ask());}return 0;
}
树状数组求逆序对
一般来说,可以使用归并排序来求一个序列中的逆序对数。但是,树状数组也可以完成。
其中步骤如下:
1 1 1 :将数组离散化,映射到 1 ∼ n 1\sim n 1∼n 。
教练推荐的离散化博客:浅谈数据的离散化
2 2 2 :建立一个树状数组, c i c_i ci 表示离散化后数字 i i i 出现的次数。
3 3 3 :从左到右依次遍历数组,设这个 a i a_i ai 为 k k k 每次修改对应 c k c_k ck ,进行一次 + 1 +1 +1 。然后根据数组有序时的要求是升序还是降序,查询 [ k + 1 , n ] [k+1,n] [k+1,n] 或 [ 1 , k − 1 ] [1,k-1] [1,k−1] 的区间和,累加进 a n s ans ans 。
4 4 4 :结束,输出 a n s ans ans 。
树状数组求逆序对用到的是单点修改+区间查询的树状数组。
树状数组求逆序对例题
例题 7 7 7 :
P1908 逆序对
求逆序对数板子题,可以归并排序,亦可树状数组,这里使用树状数组,不多赘述。
#include <bits/stdc++.h>
using namespace std;
struct node
{long long x,y;
}a[600000];
long long n,m,ans=0,b[600000],c[600000];
bool cmp(struct node a,struct node b)
{return a.y<b.y;
}long long lowbit(long long x)
{return (x&(-x));
}void add(long long x,long long d)
{while(x<=n)c[x]+=d,x+=lowbit(x);
}void init()
{sort(a+1,a+n+1,cmp);b[a[1].x]=++m;for(long long i=2;i<=n;i++)if(a[i].y!=a[i-1].y)b[a[i].x]=++m;else b[a[i].x]=m;
}long long getsum(long long x)
{long long t=0;while(x>0)t+=c[x],x-=lowbit(x);return t;
}int main()
{scanf("%lld",&n);for(long long i=1;i<=n;i++){scanf("%lld",&a[i].y);a[i].x=i;}init();for(long long i=1;i<=n;i++){add(b[i],1);ans+=(i-getsum(b[i]));}printf("%lld",ans);return 0;
}
例题 8 8 8 :
P1774 最接近神的人
实质就是求一个数组的逆序对数,推导过程可以看 零碎知识点整理 杂项部分 3.2 3.2 3.2 的证明部分。
#include <bits/stdc++.h>
using namespace std;
struct node
{long long x,y;
}a[600000];
long long n,m,ans=0,b[600000],c[600000];
bool cmp(struct node a,struct node b)
{return a.y<b.y;
}long long lowbit(long long x)
{return (x&(-x));
}void add(long long x,long long d)
{while(x<=n)c[x]+=d,x+=lowbit(x);
}void init()
{sort(a+1,a+n+1,cmp);b[a[1].x]=++m;for(long long i=2;i<=n;i++)if(a[i].y!=a[i-1].y)b[a[i].x]=++m;else b[a[i].x]=m;
}long long getsum(long long x)
{long long t=0;while(x>0)t+=c[x],x-=lowbit(x);return t;
}int main()
{scanf("%lld",&n);for(long long i=1;i<=n;i++){scanf("%lld",&a[i].y);a[i].x=i;}init();for(long long i=1;i<=n;i++){add(b[i],1);ans+=(i-getsum(b[i]));}printf("%lld",ans);return 0;
}
例题 9 9 9 :
P1637 三元上升子序列
首先,为了计算方便,可以以中间那个数,也就是 a j a_j aj 为基准。设在在这个数之前有 n n n 个数比它小,在这个数之后有 m m m 个数比它大,那么由于前后各自任选一个就能构成一组 thair
,根据乘法原理得到 a n s ans ans 应该累加 n × m n\times m n×m 。
我们可以进行两次树状数组求逆序对:第一次求某个数之前比它小的数的个数,第二次反转序列,求某个数之后比它大的数的个数。
由于 a i a_i ai 的范围很小,所以不用离散化,但是 警示后人(73pts,WA on #4 #9 #10) 。
#include <bits/stdc++.h>
using namespace std;
long long n,ans=0,maxn=0,a[300000],c[300000],ls[300000],rb[300000];
long long lowbit(long long x)
{return (x&(-x));
}void add(long long x,long long d)
{while(x<=maxn)c[x]+=d,x+=lowbit(x);
}long long getsum(long long x)
{long long t=0;while(x>0)t+=c[x],x-=lowbit(x);return t;
}int main()
{scanf("%lld",&n);for(long long i=1;i<=n;i++){scanf("%lld",&a[i]);maxn=max(maxn,a[i]);}for(long long i=1;i<=n;i++){add(a[i],1);ls[i]=getsum(a[i]-1);}memset(c,0,sizeof(c));for(long long i=n;i>=1;i--){add(a[i],1);rb[i]=getsum(maxn)-getsum(a[i]);}for(long long i=1;i<=n;i++)ans+=ls[i]*rb[i];printf("%lld\n",ans);return 0;
}
例题 10 10 10 :
P1966 [NOIP2013 提高组] 火柴排队
排序题目的巅峰。
首先,由于距离是两数之差的平方,有一个显然的贪心:把两个数组从小到大排序,同一位置的数互相对应,此时距离最小。交换两数后,由于平方的影响,绝对值之差会较大,导致距离也较大,证明了贪心的正确性。
然后,将 a a a 数组的每个数与 b b b 数组建立映射关系,同时进行离散化。可以记录下数组 a a a 中每个数字排序前的位置,再以排序后其对应的 b b b 数组的元素的位置作为关键字进行离散化。这样相当于数组中记录的是在不改变 b b b 数组的情况下,每个数组元素在距离最小情况下的排名。
由于数组记录的是排名,那么最小情况必然是一个单升不降的序列。此时的序列是一个乱序序列,要求最小化将这个序列排好序的相邻两数交换次数。这点就很像例题 8 8 8 ,实质就是求一个数组的逆序对数,树状数组求解即可,推导过程可以看 零碎知识点整理 杂项部分 3.2 3.2 3.2 的证明部分。
由于每次交换,逆序对数量要么 + 1 +1 +1 ,要么 − 1 -1 −1 ,两个序列地位相同,可以只变换一个序列,最终最优解不受影响,再次保证了算法的正确性。
#include <bits/stdc++.h>
const int MOD=100000000-3;
using namespace std;
struct node
{int x,v;
}a[200000],b[200000];
long long n,m=1,ans,c[200000],d[200000];
bool cmp(struct node a,struct node b)
{return a.v<b.v;
}long long lowbit(long long x)
{return (x&(-x));
}void add(long long x,long long k)
{while(x<=m)c[x]+=k,x+=lowbit(x);
}long long getsum(long long x)
{long long ans=0;while(x>0)ans+=c[x],x-=lowbit(x);return ans;
}int main()
{scanf("%lld",&n);for(int i=1;i<=n;i++){scanf("%lld",&a[i].v);a[i].x=i;}for(int i=1;i<=n;i++){scanf("%lld",&b[i].v);b[i].x=i;}sort(a+1,a+n+1,cmp);sort(b+1,b+n+1,cmp);for(int i=1;i<=n;i++){if(i!=1&&a[i].v!=a[i-1].v)m++;d[a[i].x]=b[i].x;}for(int i=1;i<=n;i++){add(d[i],1);ans+=((getsum(m)%MOD-getsum(d[i])%MOD)+MOD)%MOD;ans%=MOD;}printf("%lld\n",ans%MOD);return 0;
}
后记
树状数组讲了两个星期,可见其内容之多,共 995 995 995 行。
数据结构果然毒瘤啊qaq
相关文章:
【6】树状数组学习笔记
前言 树状数组是我学的第一个高级数据结构,属于 log \log log 级数据结构。 其实现在一般不会单独考察数据结构,主要是其在其他算法(如贪心,DP)中起到优化作用。 长文警告:本文一共 995 995 995 行…...
【RISCV LAB】0x01-安装实验仿真辅助工具
安装实验辅助工具 实验环境搭建安装 Verilator编译依赖下载源码编译安装测试安装 安装 RISC-V 交叉编译工具链编译依赖下载源码编译安装编译并安装添加环境变量并测试 安装 GTKWave其他模拟器推荐RARSemulsiV FAQ 实验环境搭建 Verilator 是一款开源的支持 Verilog 和 SystemV…...
OSPF-2 邻接建立关系
上一期我们说了OSPF的邻居建立关系以及OSPF邻居关系建立中建立失败的因素以及相关实验案例 这一期我们来说说OSPF的邻接关系建立时需要交互哪些报文以及失败因素及原因和相关实验案例 一、概述 在运行了OSPF的网络当中为了交互链路状态信息和路由信息,互相之间需要建立邻接关…...
操作系统知识点29
1.当用户使用外部设备时,其控制设备的命令传递途径依次为用户应用层->设备独立层->设备驱动层->设备硬件 2.通常用于管理空闲物理内存的方法:空闲快链表法;位示图法;空闲页面表 3. 可用于文件的存取控制和保护的方法&a…...
【Java篇】行云流水,似风分岔:编程结构中的自然法则
文章目录 Java 程序逻辑控制:顺序、分支与循环结构全面解析一、顺序结构二、分支结构2.1 if 语句2.1.1 基本语法2.1.2 if-else 语句2.1.3 if-else if-else 语句 2.2 switch 语句 三、循环结构3.1 while 循环3.2 break 语句3.3 continue 语句3.4 for 循环 四、输入输…...
代码块与设计模式
文章目录 1.代码块1.1基本介绍基本语法 1.2代码块的好处和案例演示1.3代码块使用注意事项和细节讨论!!! 2.单例设计模式2.1什么是设计模式2.2什么是单例模式2.2.1饿汉式2.2.2懒汉式2.2.3比较 1.代码块 1.1基本介绍 代码化块又称为初始化块,属于类中的成员[即是类的一部分]&am…...
要登录的设备ip未知时的处理方法
目录 1 应用场景... 1 2 解决方法:... 1 2.1 wireshark设置... 1 2.2 获取网口mac地址,wireshark抓包前预过滤掉自身mac地址的影响。... 2 2.3 pc网口和设备对接... 3 2.3.1 情况1:... 3 2.3.2 情…...
CentOS 系统安装 docker 以及常用插件
博主用的的是WindTerm软件链接的服务器,因为好用 1.链接上服务器登入后,在/root/目录下 2.执行以下命令安装docker sudo yum install -y yum-utilssudo yum-config-manager \--add-repo \https://download.docker.com/linux/centos/docker-ce.reposudo…...
统计字符(字符串)(gets与fgets的区别)
统计字符 #include<stdio.h> #include<string.h> int main(){char str1[5],str2[80];while(gets(str1)){if(strcmp(str1,"#")0)break;gets(str2);for(int i0;i<strlen(str1);i){int sum0;for(int j0;j<strlen(str2);j){if(str1[i]str2[j])sum;}p…...
Node.js REPL 深入解析
Node.js REPL 深入解析 引言 Node.js 作为一种流行的 JavaScript 运行环境,在服务器端开发中扮演着重要角色。REPL(Read-Eval-Print Loop,读取-求值-打印循环)是 Node.js 的一个核心特性,它允许开发者在一个交互式环境中执行 JavaScript 代码。本文将深入探讨 Node.js R…...
【测试语言基础篇】Python基础之List列表
一、Python 列表(List) 序列是Python中最基本的数据结构。序列中的每个元素都分配一个数字 - 它的位置,或索引,第一个索引是0,第二个索引是1,依此类推。 Python有6个序列的内置类型,但最常见的是列表和元组。序列都可…...
中山六院团队发表可解释多模态融合模型Brim,可以在缺少分子数据时借助病理图像模拟生成伪基因组特征|顶刊解读·25-02-14
小罗碎碎念 在癌症诊疗领域,精准预测患者预后对临床决策意义重大。传统的癌症分期系统,如TNM分期,因无法充分考量肿瘤异质性,难以准确预测患者的临床结局。而基于人工智能的多模态融合模型虽有潜力,但在实际临床应用中…...
《基於Python的网络爬虫抓包技术研究与应用》
## 摘要 本文探讨了基于Python的网络爬虫抓包技术及其应用。随着互联网数据的快速增长,网络爬虫技术在数据采集和分析中扮演着越来越重要的角色。本研究首先介绍了网络爬虫的基本概念和Python在爬虫开发中的优势,然后深入分析了抓包技术的原理和常用工具…...
从零开始探索C++游戏开发:性能、控制与无限可能
一、为何选择C开发游戏? 在虚幻引擎5渲染的次世代画面背后,在《巫师3》的庞大开放世界中,在《毁灭战士》的丝滑60帧战斗里,C始终扮演着核心技术角色。这门诞生于1983年的语言,至今仍占据着游戏引擎开发语言使用率榜首…...
TypeScript 高级类型 vs JavaScript:用“杂交水稻”理解类型编程
如果把 JavaScript 比作乐高积木,TypeScript 就是一套智能积木系统。本文将用最生活化的比喻,带你理解 TypeScript 那些看似复杂的高级类型。 一、先看痛点:JavaScript 的“薛定谔类型” // 场景:用户信息处理 function getUserI…...
几款可用于绘制工艺原理图的开源框架
一、LogicFlow 由滴滴团队开发的开源流程图框架,支持高度定制的工艺原理图绘制。 • 核心特性: • 提供拖拽式界面和丰富的节点类型(矩形、圆形、多边形等),支持自定义节点形状、样式和交互逻辑。 • 支持插件扩展&am…...
STM32如何精准控制步进电机?
在工业自动化、机器人控制等场合,步进电机以其高精度、开环控制的特性得到了广泛应用。而在嵌入式系统中,使用STM32进行步进电机的精确控制,已成为开发者的首选方案之一。 本文将从嵌入式开发者的角度,深入探讨如何基于STM32 MCU…...
Go语言入门基础详解
一、语言历史背景 Go语言由Google工程师Robert Griesemer、Rob Pike和Ken Thompson于2007年设计,2009年正式开源。设计目标: 兼具Python的开发效率与C的执行性能内置并发支持(goroutine/channel)简洁的类型系统现代化的包管理跨…...
WPF窗口读取、显示、修改、另存excel文件——CAD c#二次开发
效果如下: using System.Data; using System.IO; using System.Windows; using Microsoft.Win32; using ExcelDataReader; using System.Text; using ClosedXML.Excel;namespace IfoxDemo {public partial class SimpleWindow : Window{public SimpleWindow(){Initi…...
Ubuntu 服务器安装 Python 环境 的详细指南
以下是 在 Ubuntu 上安装 Python 3.10 的详细步骤(兼容 Ubuntu 20.04/22.04): 方法一:通过 PPA 仓库安装(推荐) 1. 添加 deadsnakes PPA sudo apt update sudo apt install software-properties-common s…...
鸿蒙next 多行文字加图片后缀实现方案
需求 实现类似iOS的YYLabel之类的在文字后面加上图片作为后缀的样式,多行时文字使用…省略超出部分,但必须保证图片的展现。 系统方案 在当前鸿蒙next系统提供的文字排版方法基本没有合适使用的接口,包括imagespan和RichEditor,根据AI的回…...
STM32---FreeRTS队列集
一、简介 一个队列只允许任务间传递的消息为同一种数据类型,如果需要在任务间传递不同数据类型的消息时,那么就可以使用队列集 ! 作用:用于对多个队列或信号量进行“监听”,其中不管哪一个消息到来,都可让…...
oracle11.2.0.4 RAC 保姆级静默安装(二) DB数据库软件
1.响应文件配置 [rootdb11g1 software]# su - oracle [oracledb11g1 ~]$ cd /software/database/ [oracledb11g1 database]$ cd response/ [oracledb11g1 response]$ vi db_install.rsp oracle.install.optionINSTALL_DB_SWONLY ORACLE_HOSTNAMEdb11g1 UNIX_GROUP_NAME…...
用python代码将excel中的数据批量写入Json中的某个字段,生成新的Json文件
需求 需求: 1.将execl文件中的A列赋值给json中的TrackId,B列赋值给json中的OId 要求 execl的每一行,对应json中的每一个OId json 如下: {"List": [{"BatchNumber": "181-{{var}}",// "Bat…...
【每日学点HarmonyOS Next知识】状态变量、动画UI残留、Tab控件显示、ob前缀问题、文字背景拉伸
1、HarmonyOS 怎么用一个变量观察其他很多个变量的变化? 有一个提交按钮的颜色,需要很多个值非空才变为红色,否则变为灰色,可不可以用一个变量统一观察这很多个值,去判断按钮该显示什么颜色,比如Button().…...
【第五节】windows sdk编程:windows 控件基础
目录 一、控件概述 二、标准控件 三、通用控件 四、控件的创建 五、控件风格 六、控件相关的消息 6.1 控件控制消息 6.2 控件通知消息 一、控件概述 控件是 Windows 系统内置的窗口类,它们只能是某个窗口的子窗口。因此,创建控件时必须使用 WS_C…...
架构师论文《论云原生架构及其应用》
【摘要】 2022年3月,我作为系统架构师参与了某大型零售企业“智能化供应链管理平台”项目的设计与实施工作。该平台旨在整合企业分散在不同区域的仓储、物流、库存及订单系统,构建统一管理的云原生架构,以应对业务季节性峰值带来的弹性伸缩需…...
Centos 7 安装达梦数据库
一、环境准备 1. 确认操作系统的版本和数据库的版本是否一致 cat /etc/redhat-release 2. 关闭防火墙 查看防火墙状态 firewall-cmd --state 停止firewall systemctl stop firewalld.service 禁止firewall开机启动 systemctl disable firewalld.service 3. 修改文件l…...
46.全排列
46.全排列 力扣题目链接 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1: 输入:nums [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]示例 2:…...
RabbitMQ (Java)学习笔记
目录 一、概述 ①核心组件 ②工作原理 ③优势 ④应用场景 二、入门 1、docker 安装 MQ 2、Spring AMQP 3、代码实现 pom 依赖 配置RabbitMQ服务端信息 发送消息 接收消息 三、基础 work Queue 案例 消费者消息推送限制(解决消息堆积方案之一&#…...
2-002:MySQL 索引的最左前缀匹配原则是什么?
MySQL 索引的最左前缀匹配原则 最左前缀匹配原则(Leftmost Prefix Matching) 是指: 当 查询使用了复合索引(联合索引) 时,MySQL 会优先匹配索引的 最左列,然后逐步向右匹配,直到遇到…...
【Python 数据结构 15.哈希表】
目录 一、哈希表的基本概念 1.哈希表的概念 2.键值对的概念 3.哈希函数的概念 4.哈希冲突的概念 5.常用的哈希函数 Ⅰ、直接定址法 Ⅱ、平方取中法 Ⅲ、折叠法 Ⅳ、除留余数法 Ⅴ、位与法 6.哈希冲突的解决方案 Ⅰ、开放定址法 Ⅱ、链地址法 7.哈希表的初始化 8.哈希表的元素插…...
校园安全用电怎么保障?防触电装置来帮您
引言 随着教育设施的不断升级和校园用电需求的日益增长,校园电力系统的安全性和可靠性成为了学校管理的重要课题。三相智能安全配电装置作为一种电力管理设备,其在校园中的应用不仅能够提高电力系统的安全性,还能有效保障师生的用电安全&am…...
疗养院管理系统设计与实现(代码+数据库+LW)
摘 要 传统办法管理信息首先需要花费的时间比较多,其次数据出错率比较高,而且对错误的数据进行更改也比较困难,最后,检索数据费事费力。因此,在计算机上安装疗养院管理系统软件来发挥其高效地信息处理的作用…...
基于 Redis Stream 实现消息队列功能
好长时间没更新了。。。。。。 背景:举个例子在某个接口执行完成后只需要前半段返回结果,后半段可能是日志记录、下游系统调用等功能的情况下,将耗时的消息进行异步发送就显得很有必要,这时就有很多种选择,单体项目甚至…...
单元测试、系统测试、集成测试、回归测试的步骤、优点、缺点、注意点梳理说明
单元测试、系统测试、集成测试、回归测试的梳理说明 单元测试 步骤: 编写测试用例,覆盖代码的各个分支和边界条件。使用测试框架(如JUnit、NUnit)执行测试。检查测试结果,确保代码按预期运行。修复发现的缺陷并重新测…...
深入理解 HTML 中的<div>和元素:构建网页结构与样式的基石
一、引言 在 HTML 的世界里,<div>和元素虽看似普通,却扮演着极为关键的角色。它们就像网页搭建过程中的万能积木,能够将各种 HTML 元素巧妙地组合起来,无论是构建页面布局,还是对局部内容进行样式调整ÿ…...
网络安全信息收集[web子目录]:dirsearch子目录爆破全攻略以及爆破字典结合
目录 一、dirsearch 工具详细使用攻略 1. 安装 前提条件 安装步骤 可选:直接下载预编译版本 2. 基本用法 命令格式 参数说明 示例 3. 核心功能与高级用法 3.1 多线程加速 3.2 自定义字典 3.3 递归扫描 3.4 过滤响应 3.5 添加请求头 3.6 代理支持 3…...
Mybaties批量操作
1、批量插入 <!--批量操作-插入--><!-- 相当于INSERT INTO t_goods (c1,c2,c3) VALUES (a1,a2,a3),(b1,b2,b3),(d1,d2,d3),...--><insert id"batchInsert" parameterType"java.util.List">INSERT INTO t_goods (title,sub_title,origina…...
27.卷2的答案
CSP-J离我们不远了,加加油啦! 1.堆排序最坏时间复杂度是? 解析:平时多多练习可知,最坏时间复杂度是O(n log n)。 2.哪条能将s中的数值保留一位,并将第二位四舍五入? 解析:经过试…...
【 Manus平替开源项目】
文章目录 Manus平替开源项目1 OpenManus1.1 简介1.2 安装教程1.3 运行 2 OWL2.1 简介2.2 安装教程2.3 运行 3 OpenHands(原OpenDevin)3.1 简介3.2 安装教程和运行 Manus平替开源项目 1 OpenManus 1.1 简介 开发团队: MetaGPT 核心贡献者(5…...
【WEB APIs】DOM-事件基础
目录 1. 事件监听(绑定) 案例—关闭广告 案例-随机点名 2. 事件类型 2.1 鼠标事件 2.2 焦点事件 2.3 文本事件 3. 事件对象 案例—评论回车发布 4. 环境对象 5. 回调函数 6. 综合案例—tab栏切换 1. 事件监听(绑定) …...
66.Harmonyos NEXT 图片预览组件使用指南
温馨提示:本篇博客的详细代码已发布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下载运行哦! Harmonyos NEXT 图片预览组件使用指南 文章目录 Harmonyos NEXT 图片预览组件使用指南效果预览一、组件使用概述1. 组件功能特点2. 组件依赖关系 二…...
linux系统安装和激活conda
安装 wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.shbash ./Miniconda3-latest-Linux-x86_64.sh回车到最后按照输入yes,之后按提示操作。 激活 conda activate如果没有反应或者返回: bash: conda: command not found则…...
Java 集合框架大师课:集合框架的暗黑料理(六)
🔮Java 集合框架大师课:集合框架的暗黑料理(六)——弱引用与幽灵队列 第一章 弱引用:Java世界的塑料兄弟情 💔 四大引用类型生死簿 // 四类引用生死实验 Object strongObj new Object(); …...
LVI-SAM、VINS-Mono、LIO-SAM算法的阅读参考和m2dgr数据集上的复现(留作学习使用)
ROS一键安装参考: ROS的最简单安装——鱼香一键安装_鱼香ros一键安装-CSDN博客 opencv官网下载4.2.0参考:https://opencv.org/releases/page/3/ nvidia驱动安装:ubuntu18.04 安装显卡驱动 - 开始战斗 - 博客园 cuda搭配使用12 cuda安装1:Ub…...
京鲁医疗健康专家委员会聊城专家团成立
3月13日由京鲁医疗健康专家委员会指导,聊城市委人才工作领导小组办公室、聊城市卫生健康委员会、聊城市人才引进服务中心主办的"智链医脉.新启未来"聊城卫生健康产才共融发展交流会在北京人卫酒店召开。会上,京鲁医疗健康专家委员会…...
MySQL的事务机制
事务 事务概念:事务是一个完整的操作单元,不可分割,事务中的操作要么全部成功,要么全部失败。 1. 事务特性 ACID 1.1 原子性(A) 一个事务中所有操作是不能被分割的,要么所有的操作都成功&am…...
30、Vuex 为啥可以进行缓存处理
Vuex 状态管理基础与缓存的关联 Vuex 的核心概念: Vuex 主要由五个部分组成:state、mutations、actions、getters和modules。其中,state是存储数据的地方,类似于一个全局的数据仓库。在这个菜谱 APP 的例子中,缓存的数…...
OpenAI Agents SDK 中文文档 中文教程 (5)
英文文档原文详见 OpenAI Agents SDKhttps://openai.github.io/openai-agents-python/ 本文是OpenAI-agents-sdk-python使用翻译软件翻译后的中文文档/教程。分多个帖子发布,帖子的目录如下: (1) OpenAI 代理 SDK, 介绍及快速入门 (2)Open…...