线段树-代码实现细节与技巧

写在最前

写代码,就是不断地去模仿他人代码,总结出其中要点,再加上自己的体会。
线段树我一开始也是三段式:

if (r<=mid) query(x+x, l, r);
else if (l>mid) query(x+x+1, l, r);
else query(x+x, l, mid), query(x+x+1, mid+1, r);

冗长且复杂,遇到多操作多询问的问题必定只能复制,开始打错了一点更正都要很久的时间。
其实虽然算法是固定的,但是打法就是多种多样的了。一开始只能跟着书上的标程来模仿,自然是很臃肿的。后来跟学长沟通交流之后,模仿了他的写法,再做了自己的一些修改。
从K-D树网上的模板我学到了指针的打法;LCT的模板也是网上的找的。善于模仿他人的代码并加为己用,是一种很好的学习码农题的一种方式。

线段树的两种实现方法

写线段树一般有两种方法:自顶向下和自底向上。
一般会使用自顶向下来编写:优点-易于理解,方便调试,打标记很方便。
自底向上这种方法参见zkw的统计的力量。优点:常数小,代码短。缺点:不易调试,打标记麻烦。(一般在简单的线段树题目中会很方便,但是原理并不好记,所以我放弃了这种方法)。

自顶向下我的编写是:

void update(int x, int l, int r)
{
    if(opl<=l && r<=opr)
    {
        operate(x);
        return;
    }
    int mid = (l+r) >> 1;
    if(opl<=mid) update(x+x, l, mid);
    if(opr>mid) update(x+x+1, mid+1, r);
}

其中主程序是这样写的:

opl = L, opr = R, update(1, 1, n);

例如我们想要操作的区间是(L,R),那就定义两个全局变量opl,opr,记录操作区间。
当(l,r)区间完全被包含就代表我可以进行操作了;否则就判断左右两个区间是否有被包含,递归操作。(比起写三个if要方便很多。)

多操作

而面对很多题目中,有各种的区间操作(区间加,区间乘),和区间查询(区间和,区间MIN),我们要写超多段类似的东西。虽然可以复制,但很容易写错。
运用全局变量,我们可以只写一个过程,来完成各种操作:
用opx来记录当前操作是什么:例如,如果有四种操作(两修改两查询),可以用1,2,3,4来分别代表它们:1代表1类修改,2代表2类修改,3代表1类查询,4代表2类询问。
同时用全局变量opans来记录当前的答案,来避免返回值造成的程序混乱(修改操作是没有返回值的,而询问操作是有返回值的,通过记录ans来规避询问操作有返回值这个问题,使得修改操作与询问操作能用一个程序搞定)。

void update(int x, int l, int r)
{
    if(opl<=l && r<=opr)
    {
        if(opx==1) modify1(x);
        else if (opx==2) modify2(x);
        else if (opx==3) query1(x);
        else query2(x);
        return;
    }
    int mid = (l+r) >> 1;
    if(opl<=mid) update(x+x, l, mid);
    if(opr>mid) update(x+x+1, mid+1, r);
}

区间操作、懒标记

对于区间操作,无论是线段树还是平衡树都避免不了这个问题。其中懒标记更是处理的重点。
我一般编三个小过程:

void pup(int x) //合并x的两个子区间中的信息(put-up)
void modify(int x, int y) //对x的区间加上y的修改
void pdw(int x) //将x区间的信息下传(put-down)

那我的程序可以变成这样:

void update(int x, int l, int r)
{
    if(opl<=l && r<=opr)
    {
        operate(x);
        return;
    }
    int mid = (l+r) >> 1;
    pdw(x);
    if(opl<=mid) update(x+x, l, mid);
    if(opr>mid) update(x+x+1, mid+1, r);
    if(opx==修改操作) pup(x);
}

Codeforces266E - More Queries to Array

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long LL;

const int N = 1e5 + 5;
const int T = 6;
const int MD = 1e9 + 7;

int n, m, a[N], tag[N<<2];
int opl, opr, op;

int c[T][T];
int tax[T][N], sum[T][N];
int pt[T];

struct rc
{
    int a[T];
    int siz;
    rc(){memset(a, 0, sizeof a); siz=0;}
}f[N<<2];

rc merge(rc p, rc q) // 合并的过程
{
    if(!p.siz) return q;
    rc r; r.siz = p.siz + q.siz; int s = p.siz;
    for(int i=0; i<T; i++) r.a[i] = p.a[i];
    for(int i=0; i<T; i++) for(int j=0; j<=i; j++) (r.a[i] += ((LL(c[i][j]) * tax[i-j][s]) % MD * q.a[j]) % MD) %= MD;
    return r;
}

void put(int x, int p) // 修改的过程
{
    int s = f[x].siz;
    for(int i=0; i<T; i++) f[x].a[i] = (LL(sum[i][s]) * p) % MD;
    tag[x] = p;
}

void lay(int x) // 下传标记,会用到修改的过程
{
    if(tag[x]>=0) put(x+x, tag[x]), put(x+x+1, tag[x]), tag[x]=-1;
}

rc query(int x, int l, int r) // 我这里把查询和修改分开来写,其实是可以合并起来的:使用全局变量opx
{
    if(opl<=l && r<=opr) return f[x];
    int md = (l+r) >> 1; rc ret; lay(x);
    if(opl<=md) ret = merge(ret, query(x+x, l, md));
    if(opr>md) ret = merge(ret, query(x+x+1, md+1, r));
    return ret;
}

void update(int x, int l, int r)
{
    if(opl<=l && r<=opr) {put(x, op); return;}
    int md = (l+r) >> 1; lay(x);
    if(opl<=md) update(x+x, l, md);
    if(opr>md) update(x+x+1, md+1, r);
    f[x] = merge(f[x+x], f[x+x+1]);
}

void bud(int x, int l, int r) // 建立线段树的操作
{
    if(l==r) {f[x].siz=1, put(x, a[l]); return;}
    int md = (l+r) >> 1; tag[x] = -1;
    bud(x+x, l, md), bud(x+x+1, md+1, r);
    f[x] = merge(f[x+x], f[x+x+1]);
}

int rd()
{
    int c;
    while(c=getchar(), c<=' ');
    return c;
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i=1; i<=n; i++) scanf("%d", &a[i]);

    for(int i=1; i<=n; i++) tax[0][i] = 1;
    for(int i=1; i<T; i++) for(int j=1; j<=n; j++) tax[i][j] = (LL(tax[i-1][j]) * j) % MD;
    for(int i=0; i<T; i++) for(int j=1; j<=n; j++) sum[i][j] = (sum[i][j-1] + tax[i][j]) % MD;
    for(int i=0; i<T; i++)
    {
        c[i][0] = c[i][i] = 1;
        for(int j=1; j<i; j++) c[i][j] = (c[i-1][j] + c[i-1][j-1]) % MD;
    }

    bud(1, 1, n);

    for(int i=1; i<=m; i++)
    {
        if(rd()=='?')
        {
            scanf("%d%d%d", &opl, &opr, &op);
            rc ret = query(1, 1, n);
            printf("%d\n", ret.a[op]);
        }
        else
        {
            scanf("%d%d%d", &opl, &opr, &op);
            update(1, 1, n);
        }
    }

    return 0;
}

可能以后还会补上更典型的例子-

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对算法有兴趣的可以来看看 在自然数,且所有的数不大于30000的范围内讨论一个问题:现在已知n条线段,把端点依次输入告诉你,然后有m个询问,每个询问输入一个点,要求这个点在多少条线段上出现过; 最基本的解法当然就是读一个点,就把所有线段比一下,看看在不在线段中; 每次询问都要把n条线段查一次,那么m次询问,就要运算m*n次,复杂度就是O(m*n) 这道题m和n都是30000,那么计算量达到了10^9;而计算机1秒的计算量大约是10^8的数量级,所以这种方法无论怎么优化都是超时 因为n条线段是固定的,所以某种程度上说每次都把n条线段查一遍有大量的重复和浪费; 线段树就是可以解决这类问题的数据结构 举例说明:已知线段[2,5] [4,6] [0,7];求点2,4,7分别出现了多少次 在[0,7]区间上建立一棵满二叉树:(为了和已知线段区别,用【】表示线段树中的线段) 【0,7】 / \ 【0,3】 【4,7】 / \ / \ 【0,1】 【2,3】 【4,5】 【6,7】 / \ / \ / \ / \ 【0,0】 【1,1】 【2,2】 【3,3】 【4,4】 【5,5】 【6,6】 【7,7】 每个节点用结构体: struct line { int left,right; // 左端点、右端点 int n; // 记录这条线段出现了多少次,默认为0 }a[16]; 和堆类似,满二叉树的性质决定a[i]的左儿子是a[2*i]、右儿子是a[2*i+1]; 然后对于已知的线段依次进行插入操作: 从树根开始调用递归函数insert // 要插入的线段的左端点和右端点、以及当前线段树中的某条线段 void insert(int s,int t,int step)

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值