再读《挑战程序设计竞赛》——出类拔萃(3)

线段树

这篇文章就是线段树模板和各类用法(懒标记,扫描线,可持久化,树套树)的大杂烩。个人不用树状数组所以不谈。
朴素的线段树模板
宏定义:

#define mid ((l+r)>>1)
#define Lson root<<1,l,mid
#define Rson root<<1|1,mid+1,r 

建立线段树

void build(int root,int l,int r)//当前节点编号,左区间,右区间
{
    if (l==r)//递归到了叶子节点
    {
        sum[root]=a[i];//叶子节点的内容因我们
        //维护的内容而异
        return ;
    }
    build(Lson);
    build(Rson);
    pushup(root);
}

pushup操作将我们计算出来的左子树和右子树的结论更新到根节点中。更新操作也是因我们要维护的区间内容而异。
这里的代码维护了一个区间和。

void pushup(int root)
{
    sum[root]=sum[root<<1]+sum[root<<1 | 1];//左右儿子节点和
}

单点修改操作

void change(int root,int l,int r,int x,int v)//x点权值修改成v
{
    if (l==r)//目标位置
    {
        sum[root]=v;//修改操作
        return ;
    }
    if (x<=mid)//左儿子上
        change(Lson,x,v);
    else//右儿子身上
        change(Rson,x,v);
    pushup(root);
}

区间查询操作:

int Query_sum(int root,int l,int r,int ql,int qr)//当前节点是rt,当前区间[l,r],目标区间[ql,qr]
{
    int ans=0;
    if (ql<=l && r<=qr) //目标区间包括了当前区间[l,r]
        return sum[root];//直接返回当前区间
    if (ql<=mid) //目标区间有部分在当前区间的左边
        ans+=Query_sum(Lson,ql,qr);
    if (qr>mid)//目标区间有部分在当前区间的右边
        ans+=Query_sum(Rson,ql,qr);
    Push_up(root);
    return ans;
}

区间修改与线段树懒标记

懒标记的实质:推迟信息更新
这边以最小值的区间修改为例
只要是update和query操作,都需要加入pushdown
在update中的pushdown并不能做到底,找到update的所有区间就停止了,因此query中还需要pushdown一次保证每个查询区间覆盖的值都加上了懒标记的效果。
如果一道题目包含加减乘除等各种操作,pushdown的时候还要记录是哪种操作延迟进行。

struct TreeNode
 {
     int val;
     int mark;//延迟标记
 }Tree[1e5+10];//定义线段树
 
 
 void build(int root,int l, int r)
 {
     Tree[root].mark = 0;//----设置标延迟记域
     if(l == r)//叶子节点
     segTree[root].val = arr[istart];
     else
     {
         build(LSon);//递归构造左子树
         build(RSon);//递归构造右子树
         //根据左右子树根节点的值,更新当前根节点的值
         Tree[root].val = min(Tree[root<<1|1].val, segTree[root<<1].val);
     }
 }
 
 
 void pushDown(int root)
 {
     if(Tree[root].mark != 0)
     {
         //设置左右孩子节点的标志域,因为孩子节点可能被多次延迟标记又没有向下传递
         //所以是 “+=”
         Tree[root<<1|1].mark += Tree[root].mark;
         Tree[root<<1].mark += Tree[root].mark;
         //根据标志域设置孩子节点的值。因为我们是求区间最小值,因此当区间内每个元
         //素加上一个值时,区间的最小值也加上这个值
         Tree[root<<1|1].val += Tree[root].mark;
         Tree[root<<1].val += Tree[root].mark;
         //传递后,当前节点标记域清空
         Tree[root].mark = 0;
     }
 }
 
 int query(int root, int l, int r, int ql, int qr)
 {
     //查询区间和当前节点区间没有交集
     if(ql > l || qr > r)
         return 0x3f3f3f3f3f3f3f;//因为求的是最小值,如果没有交集自然不用计入
        //最小值,于是用inf代替
     //当前节点区间包含在查询区间内
     if(ql <= l && qr >= r)
         return Tree[root].val;
     //分别从左右子树查询,返回两者查询结果的较小值
     pushDown(root); //延迟标志域向下传递
        //这个时候就是我们要查询返回值的时候了,必须pushdown更新
     return min(query(LSon, ql, qr),
                query(RSon, ql, qr));
 
 }
 
 void update(int root, int l, int r, int ul, int ur, int addVal)
 {
        //分多少类和query是一样的
     //更新区间和当前节点区间没有交集
     if(ul > l || ur > r)
         return ;
     //当前节点区间包含在更新区间内
     if(ul <= l && ur >= r)
     {
         Tree[root].mark += addVal;
         Tree[root].val += addVal;
         return ;
     }
     pushDown(root); //延迟标记向下传递
     //更新左右孩子节点
     update(LSon, ul, ur, addVal);
     update(RSon, ul, ur, addVal);
     //根据左右子树的值回溯更新当前节点的值
     Tree[root].val = min(Tree[root<<1|1].val, Tree[root<<1].val);
}

解题中重要的问题

  1. 怎么判断能否用线段树?只要通过左右区间的属性能得到整个区间的属性就行。(操作为:修改+(不一定是区间,整体1到n也可以的)查询)
  2. 线段树应该存储哪些属性?结果不可直接求出的话,求要求的结果需要用到什么属性就存什么。

主席树

补充习题部分

Crane

  1. 向量是可以叠加的
  2. 维护当前区间从头指向尾的向量v。一开始初始化为每一段的长度即可。
  3. 还要维护左子树到右子树转了多少角度w
    通过v和w就可以用左右子树的向量解出整个区间的向量了,最后查询1到n。
    在这里插入图片描述
    pushup的时候先把左子树的向量旋转y角再和右子树的向量相加。
    向量的旋转公式:
    在这里插入图片描述

冒泡排序的最大次数
这是个求逆序数的问题。逆序数还可以用归并排序计算。
对于每个j,要求求出i<j且a[j]>a[i]的i的个数。

for(int j=0;j<n;j++)
{
	ans+=j-query(1,a[j]);//j-a[j]之前的数的个数
	add(a[j],1);//j正式变为“之前的数”
}

Acwing1275 最大数
m个操作,说明最多有m个数。建立有m个数的线段树,由于求最大值,没有值的地方初始化为0即可。
如果是A操作,只要把n+1位置改为添加的数。

懒标记+扫描线 Acwing 247 亚特兰蒂斯

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值