从程设作业到线段树

目录

梦开始的地方:

线段树有什么用:

线段树是什么:

那么线段树代码怎么实现呢:

建树:

单点修改操作:

区间查询操作 :

最后关于线段树的几点说明:

时间复杂度:

如何维护更多的信息:

可以维护哪些信息:

 关于线段树的更多拓展:


梦开始的地方:

 一道very easy的程设题

  • 我的提交论一个人是怎么偷懒的: 
#include<stdio.h>


int main()
{
    int maxn = -10000,minn = 10000;
    int x;
    while(scanf("%d",&x)!=EOF)
    {
        maxn = maxn > x ? maxn : x;
        minn = minn < x ? minn : x;
    }
    printf("%d,%d",minn,maxn);
}
  • 可能的正解(c++)
#include<bits/stdc++.h>

typedef long long ll;
typedef unsigned long long  ull;

const int N = 1e5+10,M = 2 * N,INF = 0x3f3f3f3f,mod = 1e9+7;

int w[N];

int max(int l,int r)
{
    if(l==r)
    {
        return w[r];
    }
    else
    {
        int mid = l + r >> 1;
        return std::max(max(l,mid),max(mid+1,r));
    }
}

int min(int l,int r)
{
    if(l==r)
    {
        return w[r];
    }
    else
    {
        int mid = l + r >> 1;
        return std::min(min(l,mid),min(mid+1,r));
    }
}

int main()
{
//    std::ios::sync_with_stdio(false);
//    std::cin.tie(nullptr);
//    std::cout.tie(nullptr);
    int n = 0;
    while(std::cin>>w[++n]);
    std::cout<<min(1,n)<<','<<max(1,n);
    return 0;
}
  • 关于我突发奇想的代码(线段树):
#include<bits/stdc++.h>

typedef long long ll;
typedef unsigned long long  ull;

const int N = 1e5+10,M = 2 * N,INF = 0x3f3f3f3f,mod = 1e9+7;
struct Node{
    int l,r;
    int max,min;
}tr[N*4];

int w[N];

void pushup(int u)
{
    tr[u].max = std::max(tr[u<<1].max,tr[u<<1|1].max);
    tr[u].min = std::min(tr[u<<1].min,tr[u<<1|1].min);
}

void build(int u,int l,int r)
{
    if(l==r)
    {
        tr[u] = {l,r,w[r],w[r]};
    }
    else
    {
        tr[u] = {l,r};
        int mid = l + r >> 1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}

int main()
{
//    std::ios::sync_with_stdio(false);
//    std::cin.tie(nullptr);
//    std::cout.tie(nullptr);
    int n = 0;
    while(std::cin>>w[++n]);
    build(1,1,n);
    std::cout<<tr[1].min<<','<<tr[1].max;
    return 0;
}
  •  本题与线段树之间的联系:

我们可以发现,程设中寻找最大最小值的时间复杂度都是O(nlogn)

但是这题显然有一个地球人都知道的O(n)做法

但是假如我们把递归的过程中每个区间的最大最小值存起来,那么我们惊奇地发现,这个过程就是线段树的初始化,那么具体线段树是什么呢,且听我娓娓道来。

线段树有什么用:

可以处理very多的区间问题,而且一次操作的时间都是O(logn)

对于这一道程设题,确实是大炮打苍蝇。

但是如果当这道题目的查询次数不是一次,而是几千次甚至几万次的时候,O(logn)的查询就显得相当重要了。

这里我先摆一道洛谷上的模板题:

树状数组1

虽然题目为树状数组,但是这题也可以用线段树来做。

(事实上线段树在功能上几乎可以代替树状数组)

因为洛谷中的线段树模板题需要懒标记,我们在这里不会介绍懒标记,所以这里采用洛谷的这一题作为模板。

线段树是什么:

线段树就是一种特殊的二叉树,不要想得太复杂

其中的每一个结点存储的信息是一个区间,故名线段树

那么线段树代码怎么实现呢:

 我们先看看线段树的一个图大概线段树博客都会有这样的图吧

 一般来说,一个节点可以用一个结构体表示。

struct Node{
    int l,r;
}tr[N * 4];

这样就准备好了

以本题为例,因为我们要求区间和,所以我们要在结构体中多维护一个区间和

struct Node{
    int l,r,sum;
}tr[N * 4];

特别声明:线段树所需开的节点个数应该是区间长度的4倍(不作证明)

下面的代码实现会使用几种位运算:

  1.  x<<1   x * 2
  2.  x>> 1  x / 2
  3.  x<<1|1  x * 2 + 1

建树:

首先:

因为是二叉树,所以我们把 1 号点 作为 根(root)节点

当父节点为 点 u 时,左右儿子分别为:

  1. 左儿子 = u * 2 ----> u << 1
  2. 右儿子 = u * 2 + 1 ----> u << 1 | 1
tr[u<<1],tr[u<<1|1]

先上代码:

pushup函数:用儿子节点的信息更新父亲节点的信息。

void pushup(int u)
{
    tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum;
}
void build(int u,int l,int r)
{
    if(l==r)
    {
        tr[u] = {l,r,w[r]};//w[r]是这个点的值
    }
    else
    {
        tr[u] = {l,r};
        int mid = l + r >> 1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);//递归建树
        pushup(u);//用儿子节点更新父节点
    }
}

最终的建树效果见上面的图。

单点修改操作:

如果更新了原区间中的某一个点,那么我们显然要把所有含有这个点的节点全部修改。

代码如下:

void modify(int u,int x,int v)
{
    if(tr[u].l==x&&tr[u].r==x)
    {
        tr[u].sum += v;
    }
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if(x <= mid)modify(u<<1,x,v);
        else modify(u<<1|1,x,v);
        pushup(u);
    }
}

 修改过程中,我们查看左儿子和右儿子,所更新的点在哪个儿子内,就修改哪个儿子。

要注意更新完后使用pushup函数更新父节点。

区间查询操作 :

这里我们先放代码,再进行解释:

int query(int u,int l,int r)
{
    if(tr[u].l >= l && tr[u].r <= r)
    {
        return tr[u].sum;
    }
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        int res = 0;
        if(l <= mid)res += query(u<<1,l,r);
        if(r > mid)res += query(u<<1|1,l,r);
        return res;
    }
}
  1. 如果询问区间包含了某个节点的区间,返回这个节点的和即可。
  2. 如果询问区间与左儿子相交,就去递归询问左儿子
  3. 如果询问区间与右儿子相交,就去递归询问右儿子

例子:询问[4,9]的区间和

至此,在本题中的基本操作都完成了,把上面这些核心操作合并,加上main函数,那么这道题的AC代码就完成了。

#include<bits/stdc++.h>

typedef long long ll;
typedef unsigned long long  ull;

const int N = 5e5+10,M = 2 * N,INF = 0x3f3f3f3f,mod = 1e9+7;
struct Node{
    int l,r,sum;
}tr[N * 4];

int w[N];

void pushup(int u)
{
    tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum;
}

void build(int u,int l,int r)
{
    if(l==r)
    {
        tr[u] = {l,r,w[r]};
    }
    else
    {
        tr[u] = {l,r};
        int mid = l + r >> 1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}

void modify(int u,int x,int v)
{
    if(tr[u].l==x&&tr[u].r==x)
    {
        tr[u].sum += v;
    }
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if(x <= mid)modify(u<<1,x,v);
        else modify(u<<1|1,x,v);
        pushup(u);
    }
}

int query(int u,int l,int r)
{
    if(tr[u].l >= l && tr[u].r <= r)
    {
        return tr[u].sum;
    }
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        int res = 0;
        if(l <= mid)res += query(u<<1,l,r);
        if(r > mid)res += query(u<<1|1,l,r);
        return res;
    }
}

int main()
{
    int n,m;
    std::cin>>n>>m;
    for(int i = 1 ; i <= n ; i++)std::cin>>w[i];
    build(1,1,n);
    while(m--)
    {
        int op,x,y;
        std::cin>>op>>x>>y;
        if(op==1)
        {
            modify(1,x,y);
        }
        else
        {
            std::cout<<query(1,x,y)<<'\n';
        }
    }
    return 0;
}

最后关于线段树的几点说明:

时间复杂度:

建树过程:O(nlogn)

修改操作:O(logn)

查询操作:O(logn)

总复杂度(操作数很大):O(nlogn)

如何维护更多的信息:

我们只需要在结构体中加上我们所需维护的信息,并在各个函数中稍作修改即可。

具体的例子可以看开头的代码(维护最大值和最小值)。

可以维护哪些信息:

我们可以发现,线段树的信息是需要由左右儿子(左右区间)推出的。

所以,我们所维护的信息必须要具有结合性,如:

  1. 区间和
  2. 最大值
  3. 最小值
  4. 异或和

pushup函数如下:

tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum;
tr[u].min = std::min(tr[u<<1].min + tr[u<<1|1].min);
tr[u].max = std::max(tr[u<<1].max + tr[u<<1|1].max);
tr[u].v = tr[u<<1].v ^ tr[u<<1|1].v;

 关于线段树的更多拓展:

  1. 区间修改
  2. 动态开点

挖坑行为,本博客中不涉及

区间修改:我们容易发现,如果我们像单点修改一样去区间修改,那么时间复杂度将不再是O(logn),所以我们会采用一个操作——懒标记

动态开点:在普通线段树中,区间长度一旦给定,就是无法修改的,也不能增加新的点,我们可以将线段树可持久化,也就是大名鼎鼎的动态开点线段树——主席树。

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值