今天主要学习了线段树,树状数组,ST表,差分,分块和树剖(好吧,这个已经没听懂了)
1.线段树
线段树涉及许多应用和思想,以下是今天所学
线段树主要用于处理一段连续区间的插入,查找,统计,查询等操作。
复杂度:设区间长度是n,所有操作的复杂度是logn级别。
性质:
线段树是平衡的2叉树,最大深度logn(n为线段树所表示区间的长度)
树中的每一个节点代表对应一个区间(叶子节点是一个点……)
每个节点(所代表的区间)完全包含它的所有子孙节点 对于任意两个节点(所代表的区间):要么完全包含,要么互不相交。
1)单点修改,区间查询
先来道裸题
有个长度为n的序列a[],m(m<=100000)次询问, ·
有两种操作: ·
1.询问: 询问区间L到R的数的和 ·
2.修改: 将序列中下标为p的的元素的值加v ·
要求输出每一次询问的结果…… (n<=100000,m<=100000)
直接上模板代码(解释见注解)
struct seg{
int l,r,sum;
}tree[2*n];
void build(int p,int l,int r) //建树,从头结点(1,l,r)开始
{
tree[p].l=l;
tree[p].r=r;
tree[p].sum=0;
if(l==r) return;
int mid=(l+r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
}
void add(int p,int x,int y) //单点修改,将x出的点加y,可将读入视为修改,从(1,i,y)开始
{
if(tree[p].l==tree[p].r)
{
tree[p].sum+=y;
return;
}
int mid=(tree[p].l+tree[p].r)>>1;
if(x<=mid) add(p<<1,x,y);
else add(p<<1|1,x,y);
tree[p].sum=tree[p<<1].sum+tree[p<<1|1].sum;
}
int ask(int p,int l,int r) //询问区间(l,r)元素的和
{
if(l==tree[p].l&& r==tree[p].r) return tree[p].sum;
int mid=(tree[p].l+tree[p].r)>>1;
if(r<=mid) return ask(p<<1,l,r);
else if (l>mid) return ask(p<<1|1,l,r);
else return ask(p<<1,l,mid)+ask(p<<1|1,mid+1,r);
}
2)区间修改,区间求和,lazy思想
老样子,来道裸题
有个长度为n的序列a[],m次询问
请实现两种操作:
1. 询问[L,R]的区间和
2.修改:将[L,R]的元素全部加x n<=100000,m<=100000
先来介绍lazy思想:
记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率。
在此通俗的解释Lazy的意思,比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果刚好执行到一个子节点,它的节点标记为rt, 这时我们可以一步更新此时rt节点的sum[rt]的值,sum[rt] += c* (r - l + 1),更这个节点的Lazy标记。注意关键的时刻来了,如果此时按照常规的线段树的update操作,这时候还应该更新rt子节点的sum[]值,而Lazy思想恰恰是暂时不更新rt子节点的sum[]值,到此就return,直到下次需要用到rt子节点的值的时候才去更新,这样避免许多可能无用的操作,从而节省时间。
#define L o<<1
#define R o<<1|1
#define LL long long
#define MAX 100000
struct node
{
int l,r;
LL sum;
LL lazy;
int length; //延迟标记,区间长度
}Tree[MAX<<2];
void push_up(int o)
{
Tree[o].sum = Tree[L].sum + Tree[R].sum;
}
void build(int o,int l,int r)
{
Tree[o].l = l;
Tree[o].r = r;
Tree[o].lazy = 0;
Tree[o].length = r - l + 1;
if (l == r)
{
scanf ("%lld",&Tree[o].sum);
return;
}
int mid = (l + r) >> 1;
build(L,l,mid);
build(R,mid+1,r);
push_up(o);
}
void push_down(int o)
{
if (Tree[o].lazy)
{
Tree[L].sum = Tree[L].sum + Tree[o].lazy * Tree[L].length;
Tree[R].sum = Tree[R].sum + Tree[o].lazy * Tree[R].length;
Tree[L].lazy += Tree[o].lazy;
Tree[R].lazy += Tree[o].lazy;
Tree[o].lazy = 0;
}
}
void add(int o,int l,int r,int lazy)
{
if (Tree[o].l == l && Tree[o].r == r)
{
Tree[o].sum += Tree[o].length * lazy;
Tree[o].lazy += lazy;
return;
}
push_down(o);
int mid = (Tree[o].l + Tree[o].r) >> 1;
if (mid >= r)
add(L,l,r,lazy);
else if (l > mid)
add(R,l,r,lazy);
else
{
add(L,l,mid,lazy);
add(R,mid+1,r,lazy);
}
push_up(o);
}
LL ask(int o,int l,int r)
{
if (Tree[o].l == l && Tree[o].r == r)
return Tree[o].sum;
push_down(o);
int mid = (Tree[o].l + Tree[o].r) >> 1;
if (mid >= r)
return ask(L,l,r);
else if (l > mid)
return ask(R,l,r);
else
{
return (ask(L,l,mid) + ask(R,mid+1,r));
}
}
对于其他的单点/区间修改,区间最值其实主要对push_down和其他小细节修改即可
3)合并思想
来道裸题
单点修改,查询区间最大连续子段和 (n<=100000,m<=100000)
分析:
定义lm为区间的左连续区间的最大和,rm为区间的右连续区间的最大和
mx为区间的最大子段和,sum为区间和
对于一个区间[L,R] mx[L,R]=max(max(mx[L,mid],mx[mid+1]),rm[L,mid]+lm[mid+1,R]])
那么只需维护这三个量即可。
lm[L,R]=max(lm[L,mid],sum[L,mid]+lm[mid+1,R])
rm[L,R]=max(rm[mid+1,R],rm[L,mid]+sum[mid+1,R])
单点修改很容易解决了 查询时,需要返回三元组(mx,lm,rm),即可维护答案。
2.树状数组
主要解决动态区间,前缀和的问题,即可以实现单点修改,询问前缀和
利用树状数组+差分,也可以解决单点修改询问区间和,区间加法询问单点值,甚至区间加区间求和也可以做。详细应用见传送门
常数小,代码短,二维也很容易实现
还有一些其他问题也可以通过树状数组来解决,比如区间。所以一般用线段树就够了,树状数组能做的,线段树一定能做,很少有出题人卡这个(所以还是有的),但是又短又小又快。。。能用为啥不用
树状数组可以很容易的解决最长上升子序列,还有一些高维偏序问题一般会套用树状数组,应用还有很多。
int bit[1000],n;
void add(int x,int val) //a[i]+=val 单点修改
{
for(; x < n ; x += x&-x ) bit[i] += val;
}
int ask(int x) //sum[1~x]
{
int sum = 0;
for(; x > 0; x -= x&-x) sum += bit[x];
return sum;
}
3.ST表
主要解决静态区间,最值问题,即没有修改,只有询问区间最值
容易推广到二维
倍增的思想很重要!
线段树预处理是o(nlogn),单次查询o(logn),空间o(n)
ST表预处理是o(nlogn),单次查询o(1),空间o(nlogn)
int log[maxn],f[17][maxn];
int ask(int x,int y)
{
int k = log[y-x+1];
return max(f[k][x],f[k][y-(1<<k)+1]);
}
void init()
{
for(int i = 2; i <= n; ++i) log[i] = log[i>>1] + 1;
for(int i = 1; i <= n; ++i) f[0][i] = a[i];
for(int j = 1; j <= log[n]; ++j)
for(int i = 1; i+(1<<j-1) <= n; ++i)
f[j][i] = max(f[j-1][i],f[j-1][i+(1<<j-1)]);
}
4.分块(上课听到这里已经......只能搬PPT了)
int n,b,num l[maxn*maxn],r[maxn*maxn],belong[maxn];
void init()
{
b=sqrt(n);
num = n/b;
if(n%b) ++num;
for(int i = 1;i <= n; ++i) belong[i] = (i-1)/b+1;
for(int i = 1;i <= num; ++i)
l[i] = (i-1)*b+1,r[i] = i*b;
r[num] = n;
//+块内维护的各种东西
}
5.后续知识点(现在先了解吧)
线段树相关:可持久化线段树,线段树套bulabula,和其他各种东西结合,zkw线段树
其他常用数据结构:trie,树剖,splay,Hash表,左偏树,莫队,LCT,K-D tree,Treap,可持久化xxx...
c++的pb_ds库:封装了很多数据结构,比如:平衡树,哈希表,字典树,堆...