线段树主要用来维护复杂的区间信息.只要满足区间可加性,线段树基本都可以解决.
1.线段树基本操作(单点更新,区间求和等不涉及lazy标记问题)
先来讲建树问题,线段树建树有很多种方法,本文介绍的是把一个区间划分成为[l,mid],[mid+1,r]的建树方法.
我们会把一个大区间分成若干个小区间,tree[1]是表示整个大区间.把它分成两个小区间.用下标tree[2×father], tree[2×father+1]来储存信息.
最基本的信息有两个.区间的左端点和右端点.其他的附加信息首先一定要满足区间可加性. 即father区间的信息可以由两个子区间的信息推出来.最简单的如sum.max,min这几种信息都是很明显的具有区间可加性.
建树代码
void build(int i,int l,int r){
tree[i] = {l,r};
if(l >= r){
tree[i].sum or max or min = a[l];
return;
}
int mid = l+r>>1;
build(i*2,l,mid);
build(i*2+1,mid+1,r);
push_up(i);
}
要注意递归的边界和区间的划分.一开始可能会敲出很多神奇的坑.敲多了就熟练了.
前面说了,满足区间可加性的信息可以用子区间的信息推出父亲区间的信息.这里用一个通用的push_up(i)操作来减少代码量和增加可调试性.这个函数在每次更新完子节点的信息之后就用一次.就可以了.
push_up代码
void push_up(int i){
tree[i].sum = tree[i*2].sum + tree[i*2+1].sum;
tree[i].max = max(tree[i*2].max,tree[i*2+1].max);
//min同上.
}
理解如何建树之后再引入最简单的一些操作.
单点更新(log(n)).
线段树一定要理解复杂度是如何来的.不然对后面的lazy标记的运用会很懵逼.单点更新的复杂度之所以是log(n)是因为我们只更新一个数.而这个更新的过程是不断二分的.故复杂度是log(n).虽然它很简单,但能对我们接下来理解lazy标记有帮助.
单点更新代码(在左边就往左找,在右边就往右找)
void add(int i,int p,int v){
if(tree[i].l == tree[i].r){
tree[i].sum or max or min = v; // 如果是表示增加v可以写成 += v;
return;
}
int mid = tree[i].l + tree[i].r >> 1;
if(p <= mid) add(i*2,p,v);
//这一步也很关键.因为区间是被划分成为[l,mid][mid+1,r]所以等于mid的时候要更新左边
else add(i*2+1,p,v);
push_up(i);//更新完别忘记push_up
}
区间求和(log(n))
区间求和的复杂度证明过程稍长.先给代码再简要的说一下.
int query(int i,int l,int r){
if(tree[i].l >= l && tree[i].r <= r) return tree[i].sum;
int mid = tree[i].l + tree[i].r >> 1;
if(l >= mid) return query(i*2,l,r);
else if(r < mid) return query(i*2+1,l,r);
else return query(i*2,l,mid)+query(i*2+1,mid+1,r);
}
我们来看这三条分支
第一条 虽然分成了两个小区间,但只用了一个,所以和单点更新是一样的.
第二条 同上
第三条 这一次两边都会递归. 但是有一个特点.只有在l,r位于tree[i].l,r中间的时候才会真正去递归两个子区间,而且这种情况只会出现至多一次(可以自己思考一下为什么,不理解也没关系,知道复杂度是log(n)就行)
所以总体来说复杂度是(2*log(n))的.
给几道例题感受一下线段树的强大.(不是基本的求和求最大值等问题,网上有很多基础的模板可以测试.我就不给题目了)
顺带提一嘴.不建议初学的时候敲了模板之后做题一直用模板.最好每一题都新敲一次.能更好的锻炼自己的代码能力.也能更深刻的理解线段树.
你能回答这些问题吗
题意很直接,就不当复读机了. 这题其实是由一个很经典的题目加了点新操作转化而来的.先引入一下这个问题的解法再回来看这题
即最大连续字段和这个问题.
给一个数组,求最大的区间和.有负数. 有很便捷的dp O(n)解法,但和线段树关系不大,讲另外一个没那么优秀但有启发性的分治算法.
我们把数组分为两段,可以知道的是.答案要么在左半边区间,要么在右半边区间,要么就是中间连着跨越了左右区间连续的一段. 具体一点说就是递归求完左右区间的最大值之后,从中点向两边延伸过程中得到的子序列也是可能的答案.
从这个做法我们可以联想到这题的解法.大区间和小区间就如同刚才分治时候分割的两段和被分割的区间的关系. 答案要么在左右两边要么在中间.为了方便得到中间的最大值,我们可以用两个变量 lmax表示贴着左边界向右延伸的最大值和rmax表示贴着右边界向左延伸的最大值.这样tree[L].rmax+tree[R].lmax(L和R表示2×i和2×i+1,代码里也用了一个宏去减少代码量)就是待选答案了.而lmax和rmax的更新方式可以自己独立思考一下.
顺带提一嘴的是线段树代码的出bug率是很高的.debug一下午甚至一天.两天都是很可能发生的事情.尽量在崩溃之前不要看题解.
具体代码
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#define L 2*i
#define R 2*i+1
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5+10;
const int mod = 9901;
inline int read(){
int x = 0,f=1;char ch = getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
struct Tree{
int l,r,v,sum,lmax,rmax;
}tree[N*4];
int a[N];
void push_up(int i){
tree[i].sum = tree[i*2].sum + tree[i*2+1].sum;
tree[i].lmax = max(tree[L].lmax,tree[L].sum+tree[R].lmax);
tree[i].rmax = max(tree[R].rmax,tree[R].sum+tree[L].rmax);
tree[i].v = max(tree[L].rmax + tree[R].lmax, max(tree[L].v, tree[R].v));
}
void build(int i,int l,int r){
tree[i] = {l,r};
if(l >= r){
tree[i] = {l,r,a[l],a[l],a[l],a[l]};
return;
}
int mid = l+r>>1;
build(i*2,l,mid);
build(i*2+1,mid+1,r);
push_up(i);
}
void add(int i,int p,int v){
if(tree[i].l == tree[i].r){
tree[i].sum = tree[i].lmax = tree[i].rmax = tree[i].v = v;
return;
}
int mid = tree[i].l + tree[i].r >> 1;
if(p <= mid) add(L,p,v);
else add(R,p,v);
push_up(i);
}
Tree query(int i,int l,int r){
if(tree[i].l >= l && tree[i].r <= r) return tree[i];
int mid = tree[i].l + tree[i].r >> 1;
if(r <= mid) return query(L,l,r);
if(l > mid) return query(R,l,r);
Tree t1,t2,t3;
t1 = query(L,l,mid);
t2 = query(R,mid+1,r);
t3.sum = t1.sum + t2.sum;
t3.lmax = max(t1.lmax,t1.sum+t2.lmax);
t3.rmax = max(t2.rmax,t2.sum+t1.rmax);
t3.v = max(t1.rmax + t2.lmax, max(t1.v, t2.v));
return t3;
}
int main(){
int n,m;
cin >> n >> m;
fir(i,1,n) cin >> a[i];
build(1,1,n);
while(m--){
int op,p,x;
cin >> op >> p >> x;
if(op == 1) cout << query(1,min(p,x),max(p,x)).v << endl;
else add(1,p,x);
}
return 0;
}
区间最大公约数
这题给出的两个操作.第二个操作显然是满足区间可加性的.可第一个操作的复杂度不如人意.可以看完下面的区间修改后再来思考这题为什么不能用lazy标记.
给区间的每一个数字都加x的话这个区间里面每一对数的gcd都会变化.所以要到跟节点修改每一个数的值.这个复杂度是O(n)的.不可以接受.所以需要转化.
数学知识:更相减损法. gcd(a,b) = gcd(a,b-a).还可以扩展到多个的情况.gcd(a,b,c) = gcd(a,b-a,c-b)… 这个式子是不是有点眼熟. 这不就是一个差分数组嘛. 再考虑区间加x其实就是在差分数组上d[l] += x, d[r+1] -= x,这样子我们就把修改区间的每一个数转化成了修改差分数组上的两个数.这样就是O(2×log(n))的操作了. 同时我们还需要用到a[i]的值. 这个可以用多一个线段树或者树状数组来维护.
顺带补充一下,这题会爆int需要用long long
具体代码:
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#define L 2*i
#define R 2*i+1
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5+10;
const int mod = 9901;
inline int read(){
int x = 0,f=1;char ch = getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
LL gcd(LL a,LL b){return b == 0 ? a : gcd(b,a%b);}
struct Tree{
int l,r;
LL g;
} tree[N*4];
LL a[N],c[N],b[N];
int n,m;
void add(int x,LL v){
for(;x<=n;x+=x&-x) c[x] += v;
}
LL ask(int x){
LL res = 0;
for(;x;x-=x&-x) res += c[x];
return res;
}
void push_up(int i){
tree[i].g = gcd(tree[L].g,tree[R].g);
}
void build(int i,int l,int r){
tree[i].l = l;tree[i].r = r;
if(l >= r){
tree[i].g = b[l];
return;
}
int mid = l+r >> 1;
build(L,l,mid);
build(R,mid+1,r);
push_up(i);
}
void change(int i,int p,LL v){
if(tree[i].l == tree[i].r){
tree[i].g += v;
return;
}
int mid = tree[i].l + tree[i].r >> 1;
if(p <= mid) change(L,p,v);
else change(R,p,v);
push_up(i);
}
LL query(int i,int l,int r){
if(tree[i].l >= l && tree[i].r <= r) return tree[i].g;
int mid = tree[i].l + tree[i].r >> 1;
if(r <= mid) return query(L,l,r);
if(l > mid) return query(R,l,r);
return gcd(query(L,l,mid),query(R,mid+1,r));
}
int main(){
cin >> n>> m;
fir(i,1,n) cin >> a[i],b[i] = a[i]-a[i-1];
build(1,1,n);
while(m--){
char op;
int l,r;
LL x;
cin >> op >> l >> r;
if(op == 'Q'){
if(l == r){
cout << a[l]+ask(l) << endl;
continue;
}
cout << abs(gcd(ask(l)+a[l],query(1,l+1,r))) << endl;
}
else{
cin >> x;
add(l,x);
if(r != n) add(r+1,-x);
change(1,l,x);
if(r != n) change(1,r+1,-x);
}
}
return 0;
}
2.lazy标记的运用
引入一个问题.要给一段区间都加上x. 这个操作如果按上面的做法来说的话是O(n)的.因为要遍历到每一个跟节点才可以修改.所以我们引入lazy标记.
什么是lazy标记?顾名思义:懒标记.怎么个懒法呢.我们可以发现线段树的一个特点.如果我不使用子区间的值.那么我就只修改父区间.不继续往下遍历线段树.而且告诉线段树,我曾经在这段区间加过lazy这么多的数值. 那什么时候这个lazy要被消掉呢?自然是要用到子区间的时候,我们用之前要引入一个和push_up相对应的操作push_down, 也就是从父区间向下传递信息. 这样就能节省大量的修改时间. 具体复杂度就不加证明了.是log(n)的.直接给push_down的代码(以区间加数为例)
代码
void push_down(int i){
int lz = tree[i].lazy;
if(lz){
tree[i].lazy = 0;
tree[L].lazy += lz;
tree[R].lazy += lz;
tree[L].sum += tree[L].length*lz;
tree[R].sum += tree[R].length*lz;
}
}
例题 洛谷的模板题
还有线段树1也可以去熟悉一下,这里就不说那题的代码了.直接上这题能更好的理解lazy标记的妙用.
这题除了区间加之外还有一个区间乘的操作.我们可以发现一个lazy标记是不够的.所以多加一个. 那么要先处理加法标记还是乘法标记呢?其实都可以.但因为精度和方便性.我们用先乘后加.也就是用乘法处理sum和add_lazy.然后直接做就好了.(其实这题可能要比上面两题还要简单一点.)
代码
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#define L 2*i
#define R 2*i+1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 9901;
inline int read(){
int x = 0,f=1;char ch = getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
struct Tree{
int l,r;
LL add,mul,sum,len;
}tree[N*4];
LL a[N];
int n,m,p;
void push_up(int i){
tree[i].sum = tree[L].sum + tree[R].sum;
tree[i].sum %= p;
}
void push_down(int i){
LL lz = tree[i].mul;
if(lz != 1){
tree[L].sum = tree[L].sum*lz%p;
tree[L].add = tree[L].add*lz%p;
tree[L].mul = tree[L].mul*lz%p;
tree[R].sum = tree[R].sum*lz%p;
tree[R].add = tree[R].add*lz%p;
tree[R].mul = tree[R].mul*lz%p;
}
lz = tree[i].add;
if(lz != 0){
tree[L].sum = (tree[L].sum + lz*tree[L].len%p)%p ;
tree[L].add = (tree[L].add + lz)%p;
tree[R].sum = (tree[R].sum + lz*tree[R].len%p)%p;
tree[R].add = (tree[R].add + lz)%p;
}
tree[i].mul = 1;
tree[i].add = 0;
}
void build(int i,int l,int r){
tree[i].l = l;tree[i].r = r;tree[i].mul = 1; tree[i].add = 0;
if(l >= r){
tree[i].sum = a[l];
tree[i].len = 1;
return;
}
int mid = l+r>>1;
build(L,l,mid);
build(R,mid+1,r);
push_up(i);
tree[i].len = tree[L].len + tree[R].len;
}
void add(int i,int l,int r,LL v){
if(l <= tree[i].l && tree[i].r <= r){
tree[i].sum = (tree[i].sum + v*tree[i].len%p) %p;
tree[i].add = (tree[i].add + v)%p;
return;
}
push_down(i);
int mid = tree[i].l+tree[i].r >> 1;
if(r <= mid) add(L,l,r,v);
else if(l > mid) add(R,l,r,v);
else{
add(L,l,mid,v);
add(R,mid+1,r,v);
}
push_up(i);
}
void mul(int i,int l,int r,LL v){
if(l <= tree[i].l && tree[i].r <= r){
tree[i].sum = tree[i].sum * v % p;
tree[i].add = tree[i].add * v % p;
tree[i].mul = tree[i].mul * v % p;
return;
}
push_down(i);
int mid = tree[i].l+tree[i].r >> 1;
if(r <= mid) mul(L,l,r,v);
else if(l > mid) mul(R,l,r,v);
else{
mul(L,l,mid,v);
mul(R,mid+1,r,v);
}
push_up(i);
}
LL query(int i,int l,int r){
if(l <= tree[i].l && tree[i].r <= r) return tree[i].sum;
push_down(i);
int mid = tree[i].l+tree[i].r >> 1;
if(r <= mid) return query(L,l,r);
if(l > mid) return query(R,l,r);
return ( query(L,l,mid) + query(R,mid+1,r) )%p;
}
int main(){
cin >> n >> m >> p;
fir(i,1,n) cin >> a[i];
build(1,1,n);
while(m--){
int op,l,r;
LL d;
cin >> op >> l >> r;
if(op == 3) cout << query(1,l,r) << endl;
else{
cin >> d;
if(op == 2) add(1,l,r,d);
else mul(1,l,r,d);
}
}
return 0;
}
3.扫描线与线段树
这一块其实属于计算几何部分的内容了,学有余力的话可以看一下.
先引入一个问题
给定N个矩形,求这些矩形的面积并.
扫描线算法的具体讲解可以看这篇博客.有图理解起来很快.
概括一下就是从左往右扫描每一个矩形的边.有些是出边,有些是入边.我们需要维护这个线的哪些信息呢?仔细分析就能知道.需要的是覆盖的边长和各个区间被覆盖的次数.这样扫到每一条边我们就用x坐标差乘上覆盖的长度.就是一个小矩形的面积.一个一个加起来就是答案了.
不过这题的细节是很需要注意的. 因为我们维护的是线段长度.可是这题的坐标是有小数的.这个无法用数组来维护.所以我们需要离散化之后再用数组来维护.而且要注意的是离散化得到的是一个一个点.我们需要的不是点.而是线段.所以我们可以令离散后的数组a[i]表示链接了a[i]-a[i+1]这个线段.
然后一个矩形可以得到两个四元组(x,y1,y2,k) k是代表入边和出边.入边是1,出边是-1.我们这样维护cover的点. 按照上面的方法.就是链接 a[y1] 到 a[y2] 也就是让a[y1] 到 a[y2-1] 都+k. 这样子就能用线段树来O(logn)的维护这个扫描线了.
这题还有一个优化的点. 观察一下这些边都是成对出现的,也就是说对区间的修改也是成对出现的. 而且有一个很重要的性质.我们需要的只是跟节点的有效长度.而我们把这个信息push_down下去的话是没有意义的. 因为在遍历到出边之前这条边永远都是存在的. 所以即使push_down了也不会对答案有什么贡献. 我们就只记录两个信息点 cover 的次数cnt 和cover的长度len就好了.如果cnt != 0 说明这个区间的有效长度就是离散化之前的区间长度 否则就是左右两个区间之和.
代码(顺带一提这题的描述有点坑,矩形个数应该是1e5个)
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#define L 2*i
#define R 2*i+1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 9901;
inline int read(){
int x = 0,f=1;char ch = getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
struct Point{
db x,y1,y2;
int k;
bool operator<(const Point & w) const{
return x < w.x;
}
} p[N*2];
struct Tree{
int l,r,cnt;
double len;
} tree[N*8];
vector<db> all;
int getpos(double x){
return lower_bound(ALL(all),x)-all.begin();
}
void get_len(int i){
if(tree[i].cnt) tree[i].len = all[tree[i].r+1]-all[tree[i].l];
else tree[i].len = tree[L].len + tree[R].len;
}
void build(int i,int l,int r){
tree[i].l = l;tree[i].r = r;tree[i].cnt = tree[i].len = 0;
if(l >= r) return;
int mid = l+r>>1;
build(L,l,mid);
build(R,mid+1,r);
}
void change(int i,int l,int r,int v){
if(l <= tree[i].l && r >= tree[i].r){
tree[i].cnt += v;
get_len(i);
return;
}
int mid = tree[i].l + tree[i].r >> 1;
if(r <= mid) change(L,l,r,v);
else if(l > mid) change(R,l,r,v);
else{
change(L,l,mid,v);
change(R,mid+1,r,v);
}
get_len(i);
}
int main(){
int n;
int tot = 0;
while(cin >> n){
if(!n) break;
printf("Test case #%d\n",++tot);
int cnt = 0;
all.pb(-1);
fir(i,1,n){
db x1,y1,x2,y2;
cin >> x1 >> y1 >> x2 >> y2;
p[++cnt] = {x1,y1,y2,1};
p[++cnt] = {x2,y1,y2,-1};
all.pb(y1);
all.pb(y2);
}
sort(p+1,p+1+cnt);
sort(ALL(all));
all.erase(unique(ALL(all)),all.end());
double ans = 0;
build(1,1,(int)all.size()-1);
fir(i,1,cnt-1){
int y1 = getpos(p[i].y1),y2 = getpos(p[i].y2);
change(1,y1,y2-1,p[i].k);
ans += (p[i+1].x-p[i].x)*tree[1].len;
}
printf("Total explored area: %.2lf\n\n",ans);
}
return 0;
}
上面讲了面积并.再引入一个周长并来加深对扫描线的理解.
洛谷例题
这题比上面一题要难搞一点. 不过思想是一样的,还是维护一条扫描线.不过维护的信息不同. 我们可以在纸上模拟一下计算过程 可以发现的是 我们需要的最主要的信息有扫描线被分割成了几段.以及有效长度是多少.和被覆盖的次数.
为什么需要他们呢. 计算周长无非就是要长和宽. 长我们分析可以发现每次加入的线段对长的贡献为abs(tree[i].len-修改前的tree[i].len) 这个需要一点数学直觉. 而宽的贡献就比较好想. 假设线段被分割成了3段,那么宽的贡献就是32(宽). 所以比上题多了一个要维护的信息:分割的段数. 其他都是一样的.
具体来讲讲怎么维护这个信息. 如果父区间被覆盖了,那么线段树显然就是1.没有覆盖的情况最直接的就是左区间+右区间的线段数. 可是有一种情况会出错.如果左右区间链接在一起.是会多算一段的.所以引入两个新的变量.lpoint和rpoint表示这个区间右端点和左端点的覆盖情况. 如果都被覆盖了,那就是L.cut+R.cut - 1 .不然就是 L.cut +R.cut
还有一个很重要的细节.如果说有出边和入边重叠了一定要先算入边再算出边.不然的话答案会多算一部分.可以画图理解一下.
代码
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#define L 2*i
#define R 2*i+1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4+10;
const int mod = 9901;
inline int read(){
int x = 0,f=1;char ch = getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
struct Point{
int x,y1,y2,k;
bool operator<(const Point&w)const{
if(x == w.x) return k > w.k;
return x < w.x;
}
} p[N*2];
struct Tree{
int l,r,cnt,len,cut;
bool lpoint,rpoint;
}tree[N*8];
int n;
vi all;
int getpos(int x){
return lower_bound(ALL(all),x)-all.begin();
}
void push_up(int i){
if(tree[i].cnt != 0){
tree[i].cut = 1;
tree[i].len = all[tree[i].r+1] - all[tree[i].l];
tree[i].lpoint = tree[i].rpoint = true;
}
else{
tree[i].len = tree[L].len + tree[R].len;
tree[i].cut = tree[L].cut + tree[R].cut;
tree[i].lpoint = tree[L].lpoint;
tree[i].rpoint = tree[R].rpoint;
if(tree[L].rpoint && tree[R].lpoint) tree[i].cut --;
}
}
void build(int i,int l,int r){
tree[i] = {l,r,0,0,0,false,false};
if(l >= r) return;
int mid = l+r>>1;
build(L,l,mid);
build(R,mid+1,r);
}
void change(int i,int l,int r,int v){
if(l <= tree[i].l && tree[i].r <= r){
tree[i].cnt += v;
push_up(i);
return;
}
int mid = tree[i].l + tree[i].r >> 1;
if(r <= mid) change(L,l,r,v);
else if(l > mid) change(R,l,r,v);
else {
change(L,l,mid,v);
change(R,mid+1,r,v);
}
push_up(i);
}
int main(){
cin >> n;
int cnt = 0;
all.pb(-1e9);
fir(i,1,n){
int x1,x2,y1,y2;
cin >> x1 >> y1 >> x2 >> y2;
p[++cnt] = {x1,y1,y2,1};
p[++cnt] = {x2,y1,y2,-1};
all.pb(y1);
all.pb(y2);
}
sort(p+1,p+1+cnt);
sort(ALL(all));
all.erase(unique(ALL(all)),all.end());
int ans = 0;
build(1,1,all.size()-1);
fir(i,1,cnt-1){
int y1 = getpos(p[i].y1), y2 = getpos(p[i].y2);
int len = tree[1].len;
change(1,y1,y2-1,p[i].k);
// cout << abs(len-tree[1].len) << " " << tree[1].cut << endl;
ans += abs(len-tree[1].len);
ans += tree[1].cut*2*(p[i+1].x-p[i].x);
}
ans += p[cnt].y2 - p[cnt].y1;
cout << ans << endl;
return 0;
}
窗口的星星
这题比较考思维.如果要去移动这个窗口的话是不现实的(我也不知道怎么移能保证答案正确). 可以转换一下问题.
把每个星星能被框柱的范围先求出来. 可以发现的是,问题变成了在这一个个转化过后的小矩形里面找到一个点被覆盖的权值最大. 这些小矩形的表示也是有讲究的.因为边界的星星是不算进去的.所以我们可以把整个框架向右下角移动0.5个格子.这样的话矩形就变成了(x1-0.5,y1-0.5),(x1+W-0.5,y1+H-0.5). 因为都是整点.所以可以等效的看成(x1,y1),(x1+W-1,y1+H-1).
这样我们就转化成了上面两题差不多的问题. 一个星星能用两个四元组(x,y1,y2,w)表示. 如果是入边w就是正的,出边w就是负的. 每扫到一条边就相当于在[y1,y2]区间上加w. 这里和上面不太一样的是不是y1到y2-1.因为这里表示的是点而不是线段.可以画图理解一下. 而且还有一个细节. 我们前面说了,矩形是(x1,y1),(x1+W-1,y1+H-1). 入边是没问题的.但是出边其实是到x1+W的时候这条边要消除掉.所以最终我们存的是(x1+W,y1+H-1). lyd的书上关于这个讲的不是很清楚.我就瞎补充一点.
结合上面区间修改的知识.代码就出来了
还有最后一个小细节…那就是范围是2^31.而我们要+W.所以会爆int.还是用long long吧.
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#define L 2*i
#define R 2*i+1
#include <bits/stdc++.h>
using namespace std;
const int N = 2e4+10;
const int mod = 9901;
inline int read(){
int x = 0,f=1;char ch = getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
struct Star{
LL x,y,h;
}s[N];
struct Point{
LL x,y1,y2,w;
bool operator<(const Point & a)const{
if(x == a.x) return w < a.w;
return x < a.x;
}
}p[N*2];
struct Tree{
int l,r;
LL v,lz;
}tree[N*8];
vector<LL> all;
int n;
LL W,H;
int getpos(LL x){
return lower_bound(ALL(all),x)-all.begin();
}
void push_up(int i){
tree[i].v = max(tree[L].v,tree[R].v);
}
void push_down(int i){
int l = tree[i].lz;
if(l){
tree[L].lz += l;
tree[R].lz += l;
tree[L].v += l;
tree[R].v += l;
tree[i].lz = 0;
}
}
void build(int i,int l,int r){
tree[i] = {l,r,0,0};
if(l >= r) return;
int mid = l+r>>1;
build(L,l,mid);
build(R,mid+1,r);
}
void change(int i,int l,int r,LL v){
if(l <= tree[i].l && tree[i].r <= r){
tree[i].lz += v;
tree[i].v += v;
return;
}
push_down(i);
int mid = tree[i].l + tree[i].r >> 1;
if(r <= mid) change(L,l,r,v);
else if(l > mid) change(R,l,r,v);
else{
change(L,l,mid,v);
change(R,mid+1,r,v);
}
push_up(i);
}
int main(){
while(cin >> n >> W >> H){
fir(i,1,n) cin >> s[i].x >> s[i].y >> s[i].h;
LL ans = 0;
int cnt = 0;
all.clear();
all.pb(-1e9);
fir(i,1,n){
int x = s[i].x,y = s[i].y;
LL h = s[i].h;
p[++cnt] = {x,y,y+H-1,h};
p[++cnt] = {x+W,y,y+H-1,-h};
all.pb(y);
all.pb(y+H-1);
}
sort(p+1,p+1+cnt);
sort(ALL(all));
all.erase(unique(ALL(all)),all.end());
build(1,1,(int)all.size()-1);
fir(i,1,cnt){
int y1 = getpos(p[i].y1),y2 = getpos(p[i].y2);
change(1,y1,y2,p[i].w);
ans = max(ans,tree[1].v);
}
cout << ans << endl;
}
return 0;
}
4.线段树应用部分
我们先引入一个问题.
捕鱼达人
lis的O(nlogn)解法大家应该都不陌生.现在这题是带权的lis.lis的二分查找法已经不适用了.需要寻求新的解法.
我们来观察我们需要的是什么.对于当前的第i条鱼 它有权值和颜值两个属性.每次以i为结尾的权值最大的不下降子序列显然是接到可以选择的序列里权值最大的一段后面.即颜值比它小的里面权值最大的一段序列. 这个O(n)扫一遍是可以得到的.但是效率太低了.我们可以用线段树来加速这个过程.
对颜值建树(如果颜值范围过大的话可以先离散化).储存区间最大权值.我是习惯用1做跟节点,所以给每个颜值+1,这显然对答案没有影响.每次在1-当前颜值的区间里找到最大的权值.然后用这个来更新当前颜值的点.这样做就ok了.
代码
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#define L 2*i
#define R 2*i+1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4+10;
const int mod = 9901;
LL gcd(LL a,LL b){return b == 0 ? a : gcd(b,a%b);}
struct Tree{
int l,r,v;
}tree[N*8];
int a[N],v[N];
void build(int i,int l,int r){
tree[i] = {l,r,0};
if(l>=r) return;
int mid = l+r>>1;
build(L,l,mid);
build(R,mid+1,r);
}
void add(int i,int p,int v){
if(tree[i].l == tree[i].r){
tree[i].v = max(tree[i].v,v);
return;
}
int mid = tree[i].l + tree[i].r >> 1;
if(p <= mid) add(L,p,v);
else add(R,p,v);
tree[i].v = max(tree[L].v,tree[R].v);
}
int query(int i,int l,int r){
if(tree[i].l >= l && tree[i].r <= r) return tree[i].v;
int mid = tree[i].l + tree[i].r >> 1;
if(r <= mid) return query(L,l,r);
if(l > mid) return query(R,l,r);
return max(query(L,l,mid),query(R,mid+1,r));
}
int main(){
int t,tot=0;
cin >> t;
while(t--){
int n;
printf("Case #%d: ",++tot);
cin >> n;
int ans = -1e9;
vi va,sa;
fir(i,1,n){
cin >> a[i];
v[i] = a[i]/10000;
a[i] %= 10000;
ans = max(v[i],ans);
if(v[i] <= 0) continue;
va.pb(v[i]);
sa.pb(a[i]+1);
}
if(ans <= 0){
cout << ans << endl;
continue;
}
build(1,1,1e4+5);
fir(i,0,(int)sa.size()-1){
int cur = query(1,1,sa[i]);
add(1,sa[i],cur+va[i]);
ans = max(ans,cur+va[i]);
}
build(1,1,1e4+5);
afir(i,(int)sa.size()-1,0){
int cur = query(1,1,sa[i]);
add(1,sa[i],cur+va[i]);
ans = max(ans,cur+va[i]);
}
cout << ans << endl;
}
return 0;
}
另外一个问题
Description
我们遇到什么题目也不要怕,微笑着AC它!!
给你一个长度为n的数组,元素的值范围[-1e9,+1e9],请求出满足区间和>=k的最长的连续子区间,输出其长度。如果不存在满足区间和>=k的区间,输出 0 .
Input
第一行 两个整数 n,k ,1<=n<=100000, 0<=k<=1e9
第二行 n个整数 ai, ai∈[-1e9,+1e9]
Output
一个整数 ,表示区间和>=k的最长的连续子区间的长度
如果不存在满足区间和>=k的区间,输出 0 .
这是当初校赛的一道题目,留坑到了现在才解决了它(主要还是一次比赛里面又遇到了这种题型才决定回来填坑.).
先考虑暴力的做法.枚举区间长度.O(n^2)无法接受.但是答案不具有单调性.所以无法从答案优化解决的过程.就只能考虑优化这个扫描的过程.
由连续区间我们很容易能想到前缀和. 那么问题就变成了 我们在i这个位置能否找到一个前面的j点,使得sum[i]-sum[j-1] >= k 再移项就变成了 能否找到一个位置j 使得 sum[i]-k>=sum[j-1]. 而且还要求最靠前面. 这个问题就变得明确了起来,和前面一题类似,我们可以用线段树来维护i前面的最靠前并且小于sum[i]-k的点. 学校的oj就不给了.可以用这个程序对拍一下.
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#define L 2*i
#define R 2*i+1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int mod = 9901;
inline int read(){
int x = 0,f=1;char ch = getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
struct Tree{
int l,r,v;
}tree[N*4];
int n;
LL a[N],k;
set<LL> st;
vector<LL> all;
int allsize;
int getpos(LL x){
return lower_bound(ALL(all),x)-all.begin();
}
int get_pos(LL x){
return upper_bound(ALL(all),x)-all.begin();
}
void build(int i,int l,int r){
tree[i] = {l,r,1e9};
if(l >= r) return;
int mid = l+r>>1;
build(L,l,mid);
build(R,mid+1,r);
}
void add(int i,int p,int v){
if(tree[i].l == tree[i].r){
tree[i].v = v;
return;
}
int mid = tree[i].l + tree[i].r >> 1;
if(p <= mid) add(L,p,v);
else add(R,p,v);
tree[i].v = min(tree[L].v,tree[R].v);
}
int query(int i,int l,int r){
if(tree[i].l >= l && tree[i].r <= r) return tree[i].v;
int mid = tree[i].l + tree[i].r >> 1;
if(r <= mid) return query(L,l,r);
if(l > mid) return query(R,l,r);
return min(query(L,l,mid),query(R,mid+1,r));
}
int main(){
cin >> n >> k;
all.pb(-1e17);
all.pb(0);
fir(i,1,n) {
cin >> a[i];
a[i]+=a[i-1];
all.pb(a[i]);
}
sort(ALL(all));
all.erase(unique(ALL(all)),all.end());
allsize = (int)all.size()-1;
build(1,1,allsize);
add(1,getpos(0),0);
st.insert(0);
LL ans = 0,minx = 0;
fir(i,1,n){
if(a[i]-k < minx){
minx = min(a[i],minx);
if(!st.count(a[i])) add(1,getpos(a[i]),i);
st.insert(a[i]);
continue;
}
ans = max(ans,1LL*i-query(1,1,get_pos(a[i]-k)-1));
if(!st.count(a[i])) add(1,getpos(a[i]),i);
st.insert(a[i]);
minx = min(minx,a[i]);
}
cout << ans;
return 0;
}
线段树能处理的区间信息比起树状数组要强大很多.虽然代码量大了一点.但可以不会树状数组,但是线段树还是需要掌握好的.