LYCの线段树学习笔记

Posted on 2020-03-29 23:20  Lynkcat  阅读(176)  评论(0)    收藏  举报

一.线段树的原理及实现

1.原理

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。——百度百科

没错事实上它就是这么个东西。在这里插入图片描述
支持区间操作的一个数据结构,可以来维护一些能合并的值。

让我们来了解一下线段树是如何实现的吧!

2.实现

  • Pushup——合并
    Pushup指的是线段树的合并操作,我们知道线段树一个区间的值是由它的两个儿子的值合并而成的,接下来的操作都以区间和为例。
procedure push_up(k:longint);
begin
  tree[k].sum:=tree[k*2].sum+tree[k*2+1].sum;
end;
  • Build建树
    要做线段树,首先你得有个树。
procedure build(k,l,r:longint);
begin
  tree[k].l:=l;tree[k].r:=r;
  if l=r then tree[k].sum:=a[l];//单点就直接赋值并退出
  if l<>r then
  begin
    build(k*2,l,l+(r-l) div 2);//建左区间
    build(k*2+1,l+(r-l) div 2+1,r);//建右区间
    push_up(k);//合并
  end;
end;
  • Lazy标记及其Pushdown下传操作
    Lazy标记是区间修改能达到 \(\log\) 时间复杂度的必要操作。假设我当前这个区间被修改过,我就标记一下,表示子区间也需要修改,因为之后在走到子区间之前必定先走过父区间,所以可以顺带下传标记并修改。
procedure push_down(k:longint);
begin
  if tree[k].lz=0 then exit;
  inc(tree[k*2].lz,tree[k].lz);//下传标记
  inc(tree[k*2+1].lz,tree[k].lz);//下传标记
  inc(tree[k*2].sum,tree[k].lz*(tree[k*2].r-tree[k*2].l+1));//修改子区间
  inc(tree[k*2+1].sum,tree[k].lz*(tree[k*2+1].r-tree[k*2+1].l+1));//修改子区间
  tree[k].lz:=0;//清除标记以免重复下传
end;
  • Query查询
    Query是线段树的查询操作,也是线段树的重要组成部分之一。很显然如果我们需要一个 \(\log\) 的时间复杂度的话必须把查询的整个大区间分为几个小区间合并起来的值。
function cx(k,l,r:longint):int64;
var mid:longint;
begin
  if (tree[k].l=l)and(tree[k].r=r) then exit(tree[k].sum);//当这个区间被完全覆盖,直接返回总值
  cx:=0;
  mid:=tree[k].l+(tree[k].r-tree[k].l) div 2;//取此区间的中点
  push_down(k);//在走之前先记得下传标记
  if l<=mid then cx:=cx+cx(k*2,l,min(mid,r));//假设要查询的区间与左子区间有交集那么就往左边走
  if r>mid then cx:=cx+cx(k*2+1,max(mid+1,l),r);//假设要查询的区间与右子区间有交集那么就往右边走
end;
  • Updata修改
    接下来就是修改部分了,与查询类似。
procedure xg(k,l,r:longint);
var mid:longint;
begin
  if (tree[k].l=l)and(tree[k].r=r) then
  begin
    tree[k].sum:=tree[k].sum+z*(tree[k].r-tree[k].l+1);
    tree[k].lz:=tree[k].lz+z;//记录懒标记
    exit;
  end;
  push_down(k);
  mid:=tree[k].l+(tree[k].r-tree[k].l) div 2;
  if l<=mid then xg(k*2,l,min(mid,r));
  if r>mid then xg(k*2+1,max(mid+1,l),r);
  push_up(k);
end;

以上是线段树最基本的知识,掌握之后我们就继续深入学习这个好玩EX的东西 。

二、基础线段树题目讲解(个人认为的思维难度从易到难)

1.[JSOI2008]最大数

洛谷传送链接

我们容易观察到最大的 \(m\) 也只有 \(200000\) 。很容易想到建一棵长度为 \(200000\) 的线段树来进行操作,转化成了一道一道单点修改区间查询的模板题。(虽然ST表能轻松A

uses math;
type rec=record
     l,r,sum:longint;
     end;
var t,t1,x,i,m,d:longint;tree:array[0..1000000]of rec;ch,ch1:char;
procedure push_up(k:longint);
begin
  tree[k].sum:=max(tree[k*2].sum,tree[k*2+1].sum) mod d;
end;
procedure build(k,l,r:longint);
begin
  tree[k].l:=l;tree[k].r:=r;
  if l=r then tree[k].sum:=-maxlongint;
  if l<>r then
  begin
    build(k*2,l,l+(r-l) div 2);
    build(k*2+1,l+(r-l) div 2+1,r);
    push_up(k);
  end;
end;
procedure xg(k,l,r,k1:longint);
var mid:longint;
begin
  if (tree[k].l=l)and(tree[k].r=r) then
  begin
    tree[k].sum:=k1;
    exit;
  end;
  mid:=tree[k].l+(tree[k].r-tree[k].l) div 2;
  if l<=mid then xg(k*2,l,min(mid,r),k1);
  if r>mid then xg(k*2+1,max(mid+1,l),r,k1);
  push_up(k);
end;
function cx(k,l,r:longint):int64;
var mid:longint;
begin
  if (tree[k].l=l)and(tree[k].r=r) then exit(tree[k].sum);
  cx:=-maxlongint;
  mid:=tree[k].l+(tree[k].r-tree[k].l) div 2;
  if l<=mid then cx:=max(cx,cx(k*2,l,min(mid,r)));
  if r>mid then cx:=max(cx,cx(k*2+1,max(mid+1,l),r));
end;
begin
  readln(m,d);t:=0;t1:=0;
  build(1,1,200000);
  for i:=1 to m do
  begin
    readln(ch,ch1,x);
    if ch='A' then
    begin
      xg(1,t1+1,t1+1,(x+t) mod d);
      inc(t1);
    end else
    begin
      t:=cx(1,t1-x+1,t1); writeln(t);
    end;
  end;
end.//没啥好说,转换下模型就行

2.上帝造题的七分钟2 / 花神游历各国

洛谷传送链接

一道经典的题,学线段树必做。

也许你看到这题之后一脸懵逼,这开平方咋整?!

显然直接区间修改是不可行的,那么我们只能暴力单点修了。(大波的TLE即将袭来!!!

为了打败TLE这个恶魔,我们需要优化。容易发现 \(\sqrt 1=1\)

所以当我们这个点值为 \(1\) 时,再继续开方就没有意义了。

于是这道题也就这样轻松解决了!

uses math;
type tpp=record
     l,r:longint;
     lz:boolean;
     sum:int64;
     end;
var tree:array[0..2000000]of tpp;a:array[0..2000000]of int64;l,r,t,n,i,x,m,j:longint;
procedure pu(k:longint);
begin
  tree[k].sum:=tree[k*2].sum+tree[k*2+1].sum;
end;
procedure build(k,l,r:longint);
begin
  tree[k].l:=l;tree[k].r:=r;
  if tree[k].l=tree[k].r then
  begin
    tree[k].sum:=a[l];
  end
  else
  begin
    build(k*2,l,l+(r-l) div 2);
    build(k*2+1,l+(r-l) div 2+1,r);
    pu(k);
  end;
end;
procedure xg(k,l,r:longint);
begin
  if (tree[k].l=tree[k].r) then
  begin
    tree[k].sum:=trunc(sqrt(tree[k].sum));
    exit;
  end;
  if (l<=(tree[k].l+tree[k].r) div 2)and((tree[k*2].sum<>tree[k*2].r-tree[k*2].l+1)) then xg(k*2,l,min(r,(tree[k].l+tree[k].r) div 2));//在走的时候顺便判是否都为1
  if (r>(tree[k].l+tree[k].r) div 2)and((tree[k*2+1].sum<>tree[k*2+1].r-tree[k*2+1].l+1)) then xg(k*2+1,max((tree[k].l+tree[k].r) div 2+1,l),r);
  pu(k);
end;
function cx(k,l,r:longint):int64;
begin
  if (tree[k].l=l)and(tree[k].r=r) then
    exit(tree[k].sum);
  cx:=0;
  if l<=(tree[k].l+tree[k].r) div 2 then cx:=cx+cx(k*2,l,min(r,(tree[k].l+tree[k].r) div 2));
  if r>(tree[k].l+tree[k].r) div 2 then cx:=cx+cx(k*2+1,max((tree[k].l+tree[k].r) div 2+1,l),r);
end;
begin
  read(n);
  for i:=1 to n do read(a[i]);
  build(1,1,n);
  read(m);
  for i:=1 to m do
  begin
    read(x,l,r);
    if l>r then
    begin
      t:=l;
      l:=r;
      r:=t;
    end;
    if x=0 then
      xg(1,l,r)
    else
      writeln(cx(1,l,r));
  end;
end.

3.[USACO08FEB]Hotel G

洛谷传送链接

又是一道线段树经典好题!

这题的做法用线段树来进行区间合并,我们考虑维护这么几个值:左边连续空房个数,右边连续空房个数,与整个区间中最大的空房个数。

那么怎么合并呢?

对于一个区间,他的两个子区间的最大值进行比较,挑选大的作为整个区间中最大的空房个数。这样对吗?

显然我们还要考虑把两个区间合并起来后中间拼接起来的空房个数是否更优。

于是这题就做完了!(???

uses math;
type tpp=record
     l,r,lx,mx,rx,lz:longint;
     end;
var tree:array[0..300000]of tpp;n,m,t,i,d,x,k:longint;
procedure build(k,l,r:longint);
begin
  tree[k].l:=l;tree[k].r:=r;
  tree[k].lx:=r-l+1;tree[k].rx:=r-l+1;
  tree[k].mx:=r-l+1;tree[k].lz:=0;
  if l<>r then
  begin
    build(k*2,l,l+(r-l) div 2);
    build(k*2+1,l+(r-l) div 2+1,r);
  end;
end;
procedure push_up(k:longint);
begin
  tree[k].mx:=max(max(tree[k*2].mx,tree[k*2+1].mx),tree[k*2].rx+tree[k*2+1].lx);//三种情况取最大
  tree[k].lx:=tree[k*2].lx;//左左空=区间左空
  if tree[k*2].mx=(tree[k*2].r-tree[k*2].l+1) then inc(tree[k].lx,tree[k*2+1].lx);//如果左全空则左全空+右左空=区间左空
  tree[k].rx:=tree[k*2+1].rx;//同理
  if tree[k*2+1].mx=(tree[k*2+1].r-tree[k*2+1].l+1) then inc(tree[k].rx,tree[k*2].rx);
  tree[k].mx:=max(max(tree[k].lx,tree[k].rx),tree[k].mx);
end;
procedure push_down(k:longint);
begin
  if tree[k].lz=2 then exit;
  if tree[k].lz=1 then
  begin
    tree[k*2].lz:=1;
    tree[k*2].lx:=0;tree[k*2].rx:=0;tree[k*2].mx:=0;
    tree[k*2+1].lz:=1;
    tree[k*2+1].lx:=0;tree[k*2+1].rx:=0;tree[k*2+1].mx:=0;
  end else
  begin
    tree[k*2].lz:=0;
    tree[k*2].lx:=tree[k*2].r-tree[k*2].l+1;
    tree[k*2].rx:=tree[k*2].r-tree[k*2].l+1;
    tree[k*2].mx:=tree[k*2].r-tree[k*2].l+1;
    tree[k*2+1].lz:=0;
    tree[k*2+1].lx:=tree[k*2+1].r-tree[k*2+1].l+1;
    tree[k*2+1].rx:=tree[k*2+1].r-tree[k*2+1].l+1;
    tree[k*2+1].mx:=tree[k*2+1].r-tree[k*2+1].l+1;
  end;
  tree[k].lz:=2;
end;
function cx(k,x:longint):longint;
var t:longint;
begin
  if tree[k].mx<x then exit(0);
  push_down(k);
  if tree[k*2].mx>=x then exit(cx(k*2,x));
  if tree[k*2].rx+tree[k*2+1].lx>=x then exit(tree[k*2].r-tree[k*2].rx+1);
  if tree[k*2+1].mx>=x then exit(cx(k*2+1,x));
end;
procedure xg(k,l,r,o:longint);
var mid:longint;
begin
  if (tree[k].l=l)and(tree[k].r=r) then
  begin
    tree[k].lz:=o;
    tree[k].lx:=(tree[k].r-tree[k].l+1)*(1-o);
    tree[k].rx:=(tree[k].r-tree[k].l+1)*(1-o);
    tree[k].mx:=(tree[k].r-tree[k].l+1)*(1-o);
    exit;
  end;
  push_down(k);
  mid:=tree[k].l+(tree[k].r-tree[k].l) div 2;
  if l<=mid then
    xg(k*2,l,min(r,mid),o);
  if r>mid then
    xg(k*2+1,max(l,mid+1),r,o);
  push_up(k);
end;
begin
  read(n,m);
  build(1,1,n);
  for i:=1 to m do
  begin
    read(k,x);
    if k=1 then
    begin
      t:=cx(1,x);
      writeln(t);
      if t<>0 then xg(1,t,t+x-1,1);//查完别忘了修改
    end else
    begin
      read(d);
      xg(1,x,x+d-1,0);
    end;
  end;
end.

4.[HEOI2016/TJOI2016]排序

洛谷传送链接

这题有在线做法,但是作为菜鸡的我是肯定不会的!

所以我们来探讨下离线做法。我们先从简单的入手,如何给一串 \(01\) 序列排序?

显然我们记序列和为 \(x\) ,总长度为 \(s\) 的话,那么排序后显然 \(1\)\(s-x\) 的位置上都是 \(0\) ,其他地方都是 \(1\)

那么我们显然可以维护 \(x\) 来做这个事情。

现在我们考虑一种神奇的做法:首先二分答案。我们把原排列中大于等于 \(mid\) 的数都标记为 \(1\) ,小于 \(mid\) 的都标记为 \(0\) 。然后对于每个操作我们就将 \(01\) 序列排个序。最后如果查询的位置上仍是 \(1\) 的话就是可行的。

这样二分的单调性如何证明呢?首先,当不可行时显然我们要减小 \(mid\) 的值才行。那么可行的时候,我们就需要尽量增大 \(mid\) 来确定它的位置。

uses math;
type tpp=record
     l,r,sum,lz:longint;
     end;
var n,m,i,q,l1,r1,mid,ans:longint;tree:array[0..500000]of tpp;a,l,r,w:array[0..100000]of longint;
procedure push_down(k:longint);
begin
  tree[k*2].lz:=tree[k].lz;
  tree[k*2+1].lz:=tree[k].lz;
  tree[k*2].sum:=(tree[k*2].r-tree[k*2].l+1)*tree[k].lz;
  tree[k*2+1].sum:=(tree[k*2+1].r-tree[k*2+1].l+1)*tree[k].lz;
  tree[k].lz:=-1;
end;
procedure push_up(k:longint);
begin
  tree[k].sum:=tree[k*2].sum+tree[k*2+1].sum;
end;
procedure build(k,l,r,x:longint);
begin
  tree[k].l:=l;tree[k].r:=r;tree[k].lz:=-1;
  if l=r then
  begin
    if a[l]>=x then tree[k].sum:=1 else tree[k].sum:=0;
    exit;
  end;
  build(k*2,l,l+(r-l) div 2,x);
  build(k*2+1,l+(r-l) div 2+1,r,x);
  push_up(k);
end;
function cx(k,l,r:longint):longint;
var mid:longint;
begin
  if (tree[k].l=l)and(tree[k].r=r) then exit(tree[k].sum);
  if tree[k].lz<>-1 then push_down(k);
  mid:=tree[k].l+(tree[k].r-tree[k].l) div 2;
  cx:=0;
  if l<=mid then
    cx:=cx+cx(k*2,l,min(r,mid));
  if r>mid then
    cx:=cx+cx(k*2+1,max(l,mid+1),r);
end;
procedure xg(k,l,r,o:longint);
var mid:longint;
begin
  if (tree[k].l=l)and(tree[k].r=r) then
  begin
    tree[k].lz:=o;
    tree[k].sum:=o*(tree[k].r-tree[k].l+1);
    exit;
  end;
  if tree[k].lz<>-1 then push_down(k);
  mid:=tree[k].l+(tree[k].r-tree[k].l) div 2;
  if l<=mid then
    xg(k*2,l,min(r,mid),o);
  if r>mid then
    xg(k*2+1,max(l,mid+1),r,o);
  push_up(k);
end;
function check(k:longint):boolean;
var i,t:longint;
begin
  build(1,1,n,k);
  for i:=1 to m do
  begin
    t:=cx(1,l[i],r[i]);//查询区间内1的个数
    if w[i]=1 then//降序
    begin
      xg(1,l[i],l[i]+t-1,1);//修改1
      xg(1,l[i]+t,r[i],0);//修改0
    end else//正序
    begin
      xg(1,l[i],r[i]-t,0);
      xg(1,r[i]-t+1,r[i],1);
    end;
  end;
  if cx(1,q,q)=1 then
    exit(true) else exit(false);
end;
begin
  read(n,m);
  for i:=1 to n do read(a[i]);
  for i:=1 to m do read(w[i],l[i],r[i]);
  read(q);
  l1:=1;r1:=n;
  while l1<=r1 do
  begin
    mid:=l1+(r1-l1) div 2;
    if check(mid) then
    begin
      l1:=mid+1;
      ans:=mid;
    end else r1:=mid-1;
  end;
  writeln(ans);
end.

未完待续……