线段树1
【模板】树状数组 2—区间修改,单个查找
题目描述
如题,已知一个数列,你需要进行下面两种操作:
-
将某区间每一个数加上 x x x;
-
求出某一个数的值。
输入格式
第一行包含两个整数 N N N、 M M M,分别表示该数列数字的个数和操作的总个数。
第二行包含 N N N 个用空格分隔的整数,其中第 i i i 个数字表示数列第 $i $ 项的初始值。
接下来 M M M 行每行包含 2 2 2 或 4 4 4个整数,表示一个操作,具体如下:
操作
1
1
1: 格式:1 x y k
含义:将区间
[
x
,
y
]
[x,y]
[x,y] 内每个数加上
k
k
k;
操作
2
2
2: 格式:2 x
含义:输出第
x
x
x 个数的值。
输出格式
输出包含若干行整数,即为所有操作 2 2 2 的结果。
样例 #1
样例输入 #1
5 5
1 5 4 2 3
1 2 4 2
2 3
1 1 5 -1
1 3 5 7
2 4
样例输出 #1
6
10
提示
样例 1 解释:
故输出结果为 6、10。
数据规模与约定
对于 30 % 30\% 30% 的数据: N ≤ 8 N\le8 N≤8, M ≤ 10 M\le10 M≤10;
对于 70 % 70\% 70% 的数据: N ≤ 10000 N\le 10000 N≤10000, M ≤ 10000 M\le10000 M≤10000;
对于 100 % 100\% 100% 的数据: 1 ≤ N , M ≤ 500000 1 \leq N, M\le 500000 1≤N,M≤500000, 1 ≤ x , y ≤ n 1 \leq x, y \leq n 1≤x,y≤n,保证任意时刻序列中任意元素的绝对值都不大于 2 30 2^{30} 230。
分析
这个题目是树状数组的一个拓展,在树状数组中可以用前 i 项的和来表示第 i 个数.那么当对 x ~ y 的区间进行修改的时候需要在树状数组中的第 x 个位置 + k, 第 y + 1 个位置 -k这样便维护了这个树状数组
输出时候直接输出查询即可
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 7;
int n, m, s, t;
int ans;
int a[maxn];
struct tree{
struct node{
int l,r,num;
}tr[maxn*4];
void build(int p,int l,int r){//递归建树
tr[p] = {l,r,0};
if(l==r){//如果这个节点是叶子节点
tr[p].num = a[l];
return ;
}
int mid = l+r>>1;
build(p<<1/* p*2 */,l,mid);//分别构造左子树和右子树
build(p<<1|1/* p*2+1 */,mid+1,r);
}
void modify(int p,int l,int r,int k){ //单点修改
if(tr[p].l >= l && tr[p].r <= r){//如果这个区间被完全包括在目标区间里面,直接返回这个区间的值
tr[p].num += k;
return ;
}
int mid = tr[p].l + tr[p].r >>1;
if(l <= mid ) modify(p<<1,l,r,k);//如果这个区间的左儿子和目标区间又交集,那么搜索左儿子
if(r > mid ) modify(p<<1|1,l,r,k);//如果这个区间的右儿子和目标区间又交集,那么搜索右儿子
}
void query(int p,int x){ //单点查询
ans += tr[p].num;
if(tr[p].l == tr[p].r) return;
int mid = tr[p].l + tr[p].r>>1;
if(x<=mid) query(p<<1,x);
else query(p<<1|1,x);
}
}st;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
st.build(1,1,n);//建树
for(int i=1;i<=m;i++){
int c;
cin>>c;
if(c==1){
int x,y,z;
cin>>x>>y>>z;
st.modify(1,x,y,z);
}
else {
ans = 0;
int x;
cin>>x;
st.query(1,x);
cout<<ans<<endl;
}
}
return 0;
}
# 【模板】线段树 1–区间修改,区间和
题目描述
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 k k k。
- 求出某区间每一个数的和。
输入格式
第一行包含两个整数 n , m n, m n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n n n 个用空格分隔的整数,其中第 i i i 个数字表示数列第 i i i 项的初始值。
接下来 m m m 行每行包含 3 3 3 或 4 4 4 个整数,表示一个操作,具体如下:
1 x y k
:将区间 [ x , y ] [x, y] [x,y] 内每个数加上 k k k。2 x y
:输出区间 [ x , y ] [x, y] [x,y] 内每个数的和。
输出格式
输出包含若干行整数,即为所有操作 2 的结果。
样例 #1
样例输入 #1
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
样例输出 #1
11
8
20
提示
对于
30
%
30\%
30% 的数据:
n
≤
8
n \le 8
n≤8,
m
≤
10
m \le 10
m≤10。
对于
70
%
70\%
70% 的数据:
n
≤
10
3
n \le {10}^3
n≤103,
m
≤
10
4
m \le {10}^4
m≤104。
对于
100
%
100\%
100% 的数据:
1
≤
n
,
m
≤
10
5
1 \le n, m \le {10}^5
1≤n,m≤105。
保证任意时刻数列中所有元素的绝对值之和 ≤ 10 18 \le {10}^{18} ≤1018。
【样例解释】
分析
1、建树与维护
由于二叉树的自身特性,对于每个父亲节点的编号 i,他的两个儿子的编号分别是 2i 和 2i+1,所以我们考虑写两个 O(1) 的取儿子函数:
int n;
int ans[MAXN*4];
inline int ls(int p){return p<<1;}//左儿子
inline int rs(int p){return p<<1|1;}//右儿子
1、此处的
inline
可以有效防止无需入栈的信息入栈,节省时间和空间。
2、二进制位左移一位代表着数值 ×2,而如果左移完之后再或上 1,由于左移完之后最后一位二进制位上一定会是 0 ,所以 ∣1 等价于 +1 。这个地方更多是为了方便,速度方面理论上是要比 +1 快,但其实编译器会帮你主动干这件事。
(还有,输入大数据一定不要用不加优化的 cin/cout
啊)。
代码(lazytag版)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n,m,a[N];
long long f[4*N],v[4*N];
void build(int k,int l,int r){//建树
v[k] = 0;//初始还没标记
if(l == r){//到底了返回
f[k] = a[l];
return ;
}
int mid = (l+r)>>1;
build(k*2,l,mid);//左递归
build(k*2+1,mid+1,r);//右递归
f[k] = f[k*2] + f[k*2+1];//区间和,不考虑标记
}
void insert(int k,int l,int r,int x,int y,long long z){
//当前这个点下标是k,对应区间【l,r】,想将【l,r】区间内的【x,y】区间上的每一个数都加上z
if(l == x && r == y){
v[k] += z;//当前k这个点对应的区间上的每一个数都要加上z
return ;
}
f[k] += (y - x + 1) * z;//当前区间的和
int mid = (l+r)>>1;
if(y<=mid) insert(k*2,l,mid,x,y,z);//完全在区间左边
else if(x>mid) insert(k*2+1,mid+1,r,x,y,z);//完全在右边
else insert(k*2,l,mid,x,mid,z),insert(k*2+1,mid+1,r,mid+1,y,z);//横跨左右两个区间
}
long long sum(int k,int l,int r,int x,int y,long long p){
//下标为k对应区间【l,r】内求区间【x,y】的和 p记录v的和
p += v[k];
if(l == x && r == y){
return p * (r-l+1)/*v的和*/ + f[k]/*v下面子树的和*/;
}
int mid = (l+r)>>1;
if(y<=mid) return sum(k*2,l,mid,x,y,p);//完全在左边
else if(x>mid) return sum(k*2+1,mid+1,r,x,y,p); //右边
else return sum(k*2,l,mid,x,mid,p)+sum(k*2+1,mid+1,r,mid+1,y,p);//横跨
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);//存数组
build(1,1,n);
for(int i=1;i<=m;i++){
int t;
scanf("%d",&t);
if(t == 1){
int x,y;
long long k;
scanf("%d%d%lld",&x,&y,&k);
insert(1,1,n,x,y,k);//插入
}
else{
int x,y;
scanf("%d%d",&x,&y);
printf("%lld\n",sum(1,1,n,x,y,0));
}
}
return 0;
}
Lost Cows–树状数组
题目链接:
https://cn.vjudge.net/problem/POJ-2182
题目大意
n头牛,给出每头牛前面有几个编号比其小的,求出该序列。
分析
倒着看,假设最后一个数前面有i个比它小的,那么它就是第i+1个数,接着将第i+1个数从这n个数中剔除,假设倒数第二个数前面有j个比它小的,那么它就是剩下的数中第j+1个数,以此类推,二分查找树状数组即可。
代码
#include<iostream>
#include<algorithm>
using namespace std;
int sum[32005],a[8005],b[8005],nl,nr;
void build(int id,int l,int r){
if(l==r) {
sum[id] = 1;//标记第二头牛
return ;
}
int mid = (l+r)/2;
build(id*2,l,mid);//左枝
build(id*2+1,mid+1,r);//右枝
sum[id] = sum[id*2] + sum[id*2+1];//递归
}
int quary(int id,int l,int r){
int mid = l+r >> 1;
if(l == r) return r;//保存答案
if(sum[id*2] >= nl){
return quary(id*2,l,mid);
}
else{
nl -=sum[id*2];
//将左儿子的数的个数减去
//因为我要接着访问右儿子,当然要将左儿子的个数减去
return quary(id*2+1,mid+1,r);
}
}
void update(int id,int l,int r){
if(l == r){
sum[id]--; //因为要从这一段中取走一个数,自然要-1
return ;
}
int mid = (l+r)/2;
if(nl > mid) update(id*2+1,mid+1,r);
else update(id*2,l,mid);
sum[id] = sum[id*2] + sum[id*2+1];
}
int main()
{
int n;
cin>>n;
for(int i=1;i<n;i++){
cin>>a[i];
}
a[0] = 0;
build(1,1,n);
for(int i=n-1;i>=0;i--){
nl = a[i] + 1;//如果第n头牛前面有an头牛比它低,那么它的身高必为an + 1
b[i] = quary(1,1,n);
nl = b[i];
update(1,1,n);
}
for(int i=0;i<n;i++){
cout<<b[i]<<endl;
}
return 0;
}
A Simple Problem with Integers(线段树区间修改和区间求和)
题目描述
You have N integers, A1, A2, … , AN. You need to deal with two kinds of operations. One type of operation is to add some given number to each number in a given interval. The other is to ask for the sum of numbers in a given interval.
Input
The first line contains two numbers N and Q. 1 ≤ N,Q ≤ 100000.
The second line contains N numbers, the initial values of A1, A2, … , AN. -1000000000 ≤ Ai ≤ 1000000000.
Each of the next Q lines represents an operation.
“C a b c” means adding c to each of Aa, Aa+1, … , Ab. -10000 ≤ c ≤ 10000.
“Q a b” means querying the sum of Aa, Aa+1, … , Ab.
Output
You need to answer all Q commands in order. One answer in a line.
Sample Input
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
Sample Output
4
55
9
15
Hint
The sums may exceed the range of 32-bit integers.
题意:
n个数进行Q次操作,对某一区间数据进行修改和查询某一区间的数据和,线段树模板题。
分析
区间修改+区间查询
代码
#include <iostream>//标记下传
using namespace std;
const int N = 1e5 + 5;
int n,m,a[N];
long long f[4*N],v[4*N];
void build(int k,int l,int r){//建树
v[k] = 0;//初始还没标记
if(l == r){//到底了返回
f[k] = a[l];
return ;
}
int mid = (l+r)>>1;
build(k*2,l,mid);//左递归
build(k*2+1,mid+1,r);//右递归
f[k] = f[k*2] + f[k*2+1];//区间和,不考虑标记
}
void insert(int k,int l,int r,int x,int y,long long z){
//当前这个点下标是k,对应区间【l,r】,想将【l,r】区间内的【x,y】区间上的每一个数都加上z
if(l == x && r == y){
v[k] += z;//当前k这个点对应的区间上的每一个数都要加上z
return ;
}
if(v[k]){//被标记过
v[k*2] += v[k];//分别下放标记
v[k*2+1] += v[k];
v[k] = 0;//当前清零
}
f[k] += (y - x + 1) * z;//当前区间的和
int mid = (l+r)>>1;
if(y<=mid) insert(k*2,l,mid,x,y,z);//完全在区间左边
else if(x>mid) insert(k*2+1,mid+1,r,x,y,z);//完全在右边
else{
//横跨左右两个区间
insert(k*2,l,mid,x,mid,z);
insert(k*2+1,mid+1,r,mid+1,y,z);
}
f[k] = f[k*2] + v[k*2] * (mid - l +1) + f[k*2+1] + v[k*2+1] * (r - mid);//更新f
}
long long sum(int k,int l,int r,int x,int y){
//下标为k对应区间【l,r】内求区间【x,y】的和
if(l == x && r == y){
return v[k] * (r-l+1)/*v的和*/ + f[k]/*v下面子树的和*/;
}
if(v[k]){//被标记过
v[k*2] += v[k];//分别下放标记
v[k*2+1] += v[k];
v[k] = 0;//当前清零
}
int mid = (l+r)>>1;
long long res = 0;
if(y<=mid) res = sum(k*2,l,mid,x,y);//完全在左边 ,不能直接return 因为f的值还没更新,先用res先存
else if(x>mid) res = sum(k*2+1,mid+1,r,x,y); //右边
else res = sum(k*2,l,mid,x,mid)+sum(k*2+1,mid+1,r,mid+1,y);//横跨
f[k] = f[k*2] + v[k*2] * (mid - l +1) + f[k*2+1] + v[k*2+1] * (r - mid);//更新f
return res;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);//存数组
build(1,1,n);
for(int i=1;i<=m;i++){
char t[5];
scanf("%s",t);
if(t[0] == 'C'){
long long x,y;
long long k;
scanf("%lld%lld%lld",&x,&y,&k);
insert(1,1,n,x,y,k);//插入
}
else{
int x,y;
scanf("%d%d",&x,&y);
printf("%lld\n",sum(1,1,n,x,y));
}
}
return 0;
}
Ultra-QuickSort(树状数组+逆序数)
题目描述
In this problem, you have to analyze a particular sorting algorithm.
The algorithm processes a sequence of n distinct integers by swapping
two adjacent sequence elements until the sequence is sorted in ascending order. For the input sequence
9 1 0 5 4 ,
Ultra-QuickSort produces the output
0 1 4 5 9 .
Your task is to determine how many swap operations Ultra-QuickSort needs to perform in order to sort a given input sequence.
题意
给你一个整型数组,用相邻两个元素两两交换的方式使得整个数列从小到大排列,求出需要交换的次数
题意简单来说就是用冒泡排序需要交换多少次,但是我们都知道冒泡排序的时间复杂度,暴力模拟肯定是会超时的。通过观察我们不难发现,交换次数的总和其实就是数列的逆序数之和(所谓逆序数就是在该元素之前比他大的元素个数),所以我们求出每一个元素的逆序数然后加起来就可以求出总共的交换次数。
输入
The input contains several test cases. Every test case begins with a line that contains a single integer n < 500,000 — the length of the input sequence. Each of the the following n lines contains a single integer 0 ≤ a[i] ≤ 999,999,999, the i-th input sequence element. Input is terminated by a sequence of length n = 0. This sequence must not be processed.
输出
For every input sequence, your program prints a single line containing an integer number op, the minimum number of swap operations necessary to sort the given input sequence.
样例
Input
5 9 1 0 5 4 3 1 2 3
0
Output
6
0
题解
我们首先通过快排求出所有元素应在的位置,用数组t来存放他们的出现状态,然后遍历原来的数组,每一次将每一个元素填入它应在的位置i(t[i]++),然后算出它前面出现过的n个数(包括自身),这n个数都是比它小的,我们用它当前位置减去n就可以知道在原序列中在他前面有多少个数比他大,也就是他的逆序数。
上面的单点修改区间求和这样的方法计算的话还是会超时,所以我们使用树状数组进行优化。
代码
#include <iostream>
#include <stdio.h>
#include <cstring>
#include <algorithm>
#define ll long long
using namespace std;
int n;
int b[500005];
struct node
{
int x;
int num;
}a[500005];
int lowbit(int t)
{
return t&(-t);
}//取出t的最低位1
int sum(int x)//区间查询
{
int ans=0;
for(int i=x;i>0;i-=lowbit(i))
ans+=b[i];
return ans;
}
void update(int x,int y)//单点更新
{
for(int i=x;i<=n;i+=lowbit(i))
b[i]+=y;
}
bool cmp1(node a,node b)
{
return a.x<b.x;
}
bool cmp2(node a,node b)
{
return a.num<b.num;
}
int main()
{
while(scanf("%d",&n)!=EOF)
{
if(n==0)
break;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i].x);
a[i].num=i;
}
sort(a+1,a+1+n,cmp1);
for(int i=1;i<=n;i++)
{
a[i].x=i;
}
sort(a+1,a+1+n,cmp2);
memset(b,0,sizeof(b));
long long ans=0;
for(int i=1;i<=n;i++)
{
ans=ans+i-sum(a[i].x)-1;
update(a[i].x,1);
}
printf("%lld\n",ans);
}
return 0;
}
GDUT_专题五:H - Buy Tickets 【线段树,单点修改】
题目链接
题意:
输入n,表示插队的人数(一开始没有有排队)0号位置为售票处
接下来n行分别输入一个pos,val,表示插队的位置和插队的人的标号。插队的位置只能在已排队的人的位置插队。
求最终的队伍的排列顺序。
分析:
每次插队后,都插在插队位置的后面,那么位置后面的人都需要后退一个空位,才能成功插入,显然这样维护起来十分麻烦。所以我们不如倒过来排,对于先排的人,他的位置便不会再改变,对于后排的人,如果已经有人排了,那么就只能寻找后面的空位。
那为了方便找到想排的位置已经被占的情况下,后面的空位,对此我们可以选择无视已经在队伍中的人,那么对于我们最终插入的位置x+1,那么前面一定就有x个空位
对于插入顺序为:
3
0 1
0 3
1 2
原始的队伍中是没有人的,0 0 0
倒序插入 ,首先是1 2,插入后队伍为 0 2 0
然后是0 3 ,插入后 3 2 0
最后0 1,插入后为 3 2 1 (这里因为原来的位置已经有人了,那么我们无视掉人的话,只有位置3的前面有0个空位,所以插入位置3。
所以我们为线段树的每个子叶标记上空位1。更新时,若排入子叶后将其标记清除为0,表示无空位。对于每个子叶单点维护后,最终查询即可。
代码
#include<iostream>
#include<cstring>
#define ll long long
using namespace std;
const double eps=1e-6;
const int N=2e5+10;
const int mod=1e9+7;
int n;
int pos[N],val[N],ans[N];
struct tree{
int l,r,sum;
int mid(){
return l+r>>1;
}
}t[N<<2];
inline void update(int node){//更新空位数
t[node].sum=t[node<<1].sum+t[node<<1|1].sum;
}
void build(int node,int l,int r){
t[node].l=l;t[node].r=r;
if(l==r){
t[node].sum=1;//标记空位
return ;
}
int mid=t[node].mid();
build(node<<1,l,mid);
build(node<<1|1,mid+1,r);
update(node);
}
//单点修改
void change(int node,int x,int y){
if(t[node].l==t[node].r){
t[node].sum=0;//标记这个点被插队
ans[t[node].l]=y;//记录下该位置的人
return ;
}
if(x<=t[node<<1].sum) change(node<<1,x,y);//如果想插入的位置小于子节点的可插空数
else change(node<<1|1,x-t[node<<1].sum,y); //否则需要减去左结点能插空的人数,得到相对右结点的插空位置
update(node);
}
int main() {
while(~scanf("%d",&n)){
memset(ans,0,sizeof(ans));
for(int i=1;i<=n;i++){
scanf("%d%d",&pos[i],&val[i]);
}
build(1,1,n);
for(int i=n;i>=1;i--){//因为后面插完队后不会被前面的影响,所以逆序改变
change(1,pos[i]+1,val[i]);
}
for(int i=1;i<=n;i++) printf("%d ",ans[i]);
printf("\n");
}
return 0;
}
HDU 1166 敌兵布阵-线段树-(单点更新,区间查询)
题意:
n个敌兵的阵营,多个操作:add a ,x:第a个阵营增加x个人;sub a,x:第a个阵营减少x;query a,b:查询区间[a,b]的总人数。
分析:
单点更新,区间查询。直接写。
代码
#include <iostream>
#include <cstdio>
#include <string.h>
using namespace std;
int n[200000];
void built(int t,int l,int r)
{
if(l==r){
scanf("%d",&n[t]);
return ;
}
int mid=(l+r)/2;
built(2*t,l,mid);
built(2*t+1,mid+1,r);
n[t]=n[2*t]+n[2*t+1];
}
int sum(int L,int R,int l,int r,int t)
{
int ans=0;
if(L<=l&&R>=r){
return n[t];
}
int mid=(l+r)/2;
if(L<=mid){
ans=ans+sum(L,R,l,mid,2*t);
}
if(R>mid){
ans=ans+sum(L,R,mid+1,r,2*t+1);
}
return ans;
}
void add(int p,int v,int l,int r,int t)
{
if(l==r) {
n[t]=n[t]+v;
return;
}
int mid=(l+r)/2;
if(p<=mid){
add(p,v,l,mid,2*t);
}
else{
add(p,v,mid+1,r,2*t+1);
}
n[t]=n[2*t]+n[2*t+1];
}
int main()
{
int a,b,k=0,t=0,p,L,R;
int v;
scanf("%d",&a);
while(a--){
t=0;
k++;
scanf("%d",&b);
built(1,1,b);
while(1){
char s[50];
scanf("%s",s);
if(strcmp(s,"Query")==0) {
if(t==0){
cout << "Case " << k << ":" << endl;
t=1;
}
scanf("%d%d",&L,&R);
printf("%d\n",sum(L,R,1,b,1));
}
if(strcmp(s,"Add")==0){
scanf("%d%d",&p,&v);
add(p,v,1,b,1);
}
if(strcmp(s,"Sub")==0){
scanf("%d%d",&p,&v);
add(p,-v,1,b,1);
}
if(strcmp(s,"End")==0) break;
}
}
return 0;
}
Minimum Inversion Number(最小逆序数对 树状数组 HDU 1394)
Problem Description
The inversion number of a given number sequence a1, a2, …, an is the number of pairs (ai, aj) that satisfy i < j and ai > aj.
For a given sequence of numbers a1, a2, …, an, if we move the first m >= 0 numbers to the end of the seqence, we will obtain another sequence. There are totally n such sequences as the following:
a1, a2, …, an-1, an (where m = 0 - the initial seqence)
a2, a3, …, an, a1 (where m = 1)
a3, a4, …, an, a1, a2 (where m = 2)
…
an, a1, a2, …, an-1 (where m = n-1)
You are asked to write a program to find the minimum inversion number out of the above sequences.
Input
The input consists of a number of test cases. Each case consists of two lines: the first line contains a positive integer n (n <= 5000); the next line contains a permutation of the n integers from 0 to n-1.
Output
For each case, output the minimum inversion number on a single line.
Sample Input
10 1 3 6 9 0 8 5 7 4 2
Sample Output
16
分析
第一组:
序列: 1 3 6 9 0 8 5 7 4 2
逆序对: 0 0 0 0 4 1 3 2 5 7 sum1=22
第二组:
序列: 3 6 9 0 8 5 7 4 2 1
逆序对: 0 0 0 3 1 3 2 5 7 8 sum2=29
通过比较发现,一个数num的移动会使得:
比num小的——个数减小——减少num
比num大的——个数增加——增加n-(num-1)
总量变化就是n-2*num+1。
树状数组的插入函数(假设为 void insert(int x,int num) )的含义:在求逆序数这个问题中,我们的插入函数通常使用为insert( i , 1 ),即将数组A[i]的值加1 (A数组开始应该初始化为0,所以也可以理解为设置A[ i ]的值为1,即将数字i 加入到序列的意思 )。,同时维护c数组的值。
树状数组中区间求和函数(假设函数定义为: int sum(int x ) )的含义:该函数的作用是用于求序列中小于等于数字 i 的元素的个数。这个是显而易见的,因为树状数组c 维护的是数组A的值,则该求和函数即是用于求下标小于等于 i 的数组A的和,而数组A中元素的值要么是0要么是1,所以最后求出来的就是小于等于i的元素的个数。
假设序列开始一个数都没有,每添加一个数之前计算序列有多少数大于该数。程序中s=s+(i-sum(a[i]))就是表达的这个意思。——这样就求出了序列中一个数的逆序数。然后把a[i]加入到序列中,重复循环即可求得整个序列的。
参考:点击转到
3.注意:
虽然题目中样例是一组数据,但题目要求的多组读入。
————————————————
版权声明:本文为CSDN博主「SY_Pistachio」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/SSYITwin/article/details/81977836
代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=5e3+5;
struct node {
int l,r;
int sum;
}tr[maxn*4];
int a[maxn];
void build(int u,int l,int r){
tr[u].l=l;
tr[u].r=r;
tr[u].sum=0;
if(l==r) return ;
int mid=l+r>>1;
build(u*2,l,mid);
build(u*2+1,mid+1,r);
}
void updata(int u,int pos)
{
if(tr[u].l==tr[u].r&&tr[u].l==pos)
{
tr[u].sum=1;
return ;
}
int mid=(tr[u].l+tr[u].r)/2;
if(pos<=mid) updata(u*2,pos);
else updata(u*2+1,pos);
tr[u].sum=tr[u*2].sum+tr[u*2+1].sum ;
}
int quary(int u,int l,int r)
{
if(tr[u].l>=l&&tr[u].r<=r)
{
return tr[u].sum;
}
int mid=(tr[u].l+tr[u].r)/2;
int res=0;
if(l<=mid) res+=quary(u*2,l,r);
if(r>mid) res+=quary(u*2+1,l,r);
return res;
}
int main()
{
int n;
while(cin>>n)
{
int minn=99999999;
build(1,1,n);
int ans=0;
for(int i=0;i<n;i++)
{
scanf("%d",&a[i]);
ans+=quary(1,a[i]+1,n);
updata(1,a[i]+1);
}
minn=min(minn,ans);
for(int i=0;i<n;i++)
{
ans=ans+(n - 2 * a[i] - 1);//此时逆序数ans=ans+(顺序对)-(逆序对)=ans+(n-x)-x
minn=min(ans,minn);
}
printf("%d\n",minn);
}
return 0;
}
I Hate It-区间最值
题目链接
题意:
给你N个已经排好的学生成绩,然后有M条指令,输出对应指令的结果。指令有两种:
1.Q i j:询问i到j的最值
2.U i j:把学生编号为i的成绩改为j
Sample Input
5 6 //第一行两个整数N,M,表示N个学生和M条指令( 0<N<=200000,0<M<5000 )
1 2 3 4 5 //第二行是N个学生的成绩(对应学生编号)
Q 1 5
U 3 6
Q 3 4
Q 4 5
U 2 9
Q 1 5
Sample Output
5
6
5
9
分析
又是一个线段树的应用,不过跟上一题(D-排兵布阵)不同的是,这次是求某段区间上的最值,而不是某段区间和。当然,数据更新是必须的。D题注释已经很详细了,所以这题注释少点。
代码
#include<stdio.h>
#include<string.h>
int max(int a,int b)//首先定义一个取最大值的函数
{
return a>b?a:b;
}
struct tree
{
int l,r,s;
};
char s[5];
int m,n,x,y;
tree t[600010];//一般存节点的数组是叶子节点的三倍就够了
void Init(int l,int r,int k)//构造线段树
{
t[k].l=l;
t[k].r=r;
if(l==r)//叶子节点
{
int x;
scanf("%d",&x);
t[k].s=x;//输入该叶子节点(学生)的分数
return ;
}
int mid=(l+r)/2;
Init(l,mid,k*2);//构造左子树
Init(mid+1,r,k*2+1);//构造右子树
t[k].s=max(t[k*2].s,t[k*2+1].s);//更新父节点数据(取最大值)
}
void updata(int c,int x,int k)//数据的更新操作
{
if(t[k].l==t[k].r&&t[k].l==c)
{
t[k].s=x;
return ;
}
if(c<t[k].l||c>t[k].r)
return ;
updata(c,x,k*2);
updata(c,x,k*2+1);
t[k].s=max(t[k*2].s,t[k*2+1].s);
}
int query(int l,int r,int k)//数据查询 注意范围落在哪个区间就行了
{
if(t[k].l==l&&t[k].r==r)
return t[k].s;
int mid=(t[k].l+t[k].r)/2;
if(r<=mid)
return query(l,r,k*2);
else if(l>=mid+1)
return query(l,r,k*2+1);
else
return max(query(l,mid,k*2),query(mid+1,r,k*2+1));
}
int main()
{
while(scanf("%d %d",&n,&m)!=EOF)
{
Init(1,n,1);
while(m--)
{
scanf("%s %d %d",&s,&x,&y);
if(s[0]=='U')
updata(x,y,1);
else
printf("%d\n",query(x,y,1));
}
}
return 0;
}