树状数组与线段树经典问题的python实现


title: 树状数组与线段树经典问题的python实现
date: 2020-03-26 22:13:26
categories: 算法
tags: [python, 树状数组与线段树]

树状数组

作用:单点修改,区间求和

时间复杂度:修改和查询的复杂度都是O(logN)

要点:


l o w b i t ( x ) = x & ( − x ) = 2 k lowbit(x)=x\&(-x) =2^{k} lowbit(x)=x&(x)=2k
k:x在二进制位下面末尾连续0的个数

原理:

利用的负数的存储特性,负数是以补码存储的,对于整数运算 x&(-x)有

1. 当x为0时,即 0 & 0,结果为0;
   2. 当x为奇数时,最后一个比特位为1,取反加1没有进位,故x和-x除最后一位外**前面的位正好相反,且最后一位均为1**。结果为1。5&(-5)=(101)&(011)=1
   3. 当x为偶数时,取反时,末尾连续k个0均为变成1,加1时,往前进一位,正好是2^k次方。

则定C[x]=sum(x-lowbit(x),x](sum(l,r]表示数组a区间(l,r]的区间和)

区间求和有C[x]定义很明了。

对于单点增加:在图上树的过程,假设增加a[i],我们只需要修改其父亲和祖宗节点,例如增加a[5],我们需要修改C[5],C[6],C[8],C[16],可以证明x的父亲节点有且仅有一个为x+lowbit(x)

动态求连续区间和

给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b][a,b] 的连续和。

输入格式

第一行包含两个整数n 和 m,分别表示数的个数和操作次数。

第二行包含 n 个整数,表示完整数列。

接下来 m 行,每行包含三个整数 k,k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。

数列从 11 开始计数。

输出格式

输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。

数据范围

1≤n≤100000
1≤m≤100000,
1≤a≤b≤n

输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8
输出样例:
11
30
35

挑战模式


n,m=map(int,input().split())

a=[0 for  i in range(0,n+1)]
tr=[0 for i in range(0,n+1)]

def lowbit(x):
    return x&(-x)

def query(x):
    res=0
    while x:
        res+=tr[x]
        x-=lowbit(x)
    return res

def add(x,val):
    while x<=n:
        tr[x]+=val
        x+=lowbit(x)
    
a=list(map(int,input().split()))

for i in range(0,n):
    add(i+1,a[i])
for t in range(0,m):
    k,l,r=map(int,input().split())
    if k==0:
        print(query(r)-query(l-1))
    else:
        add(l,r)
        

线段树

本质是二叉树,分治思想。

u的左儿子u<<1(2u)和右儿子u<<1|1(2u+1)

数列区间最大值

输入一串数字,给你 M 个询问,每次询问就给你两个数字 X,Y,要求你说出 X到 Y 这段区间内的最大数。

输入格式

第一行两个整数 N,M 表示数字的个数和要询问的次数;

接下来一行为 N 个数;

接下来 MM 行,每行都有两个整数 X,Y。

输出格式

输出共 M行,每行输出一个数。

数据范围

1≤N≤105,
1≤M≤106,
1≤X≤Y≤N,
数列中的数字均不超过2^31−1

输入样例:
10 2
3 2 4 5 6 8 1 2 9 7
1 4
3 8
输出样例:
5
8

class Node:
    def __init__(self,l=0,r=0,maxx=0):
        self.l=l
        self.r=r
        self.maxx=maxx
n,m=map(int,input().split())
tr=[Node() for i in range(0,100005*4+100)]

def pushup(u):
    tr[u].maxx=max(tr[u<<1].maxx, tr[u<<1|1].maxx)
    
def build(u,l,r):
    if l == r:
        tr[u] = Node(l, r, w[r-1])
    else:
        tr[u]=Node(l,r)
        mid=(l+r) >> 1 
        build(u << 1, l, mid)
        build(u << 1 | 1, mid+1, r)
        pushup(u)

def query_max(u, l, r):
    if tr[u].l >= l and tr[u].r <= r:
        return tr[u].maxx
    mid=(tr[u].l + tr[u].r) >> 1
    maxx=-1e18
    if l <= mid : maxx=max(query_max(u << 1, l, r), maxx)
    if r > mid : maxx=max(query_max(u << 1 | 1, l, r), maxx)
    return maxx



w=list(map(int,input().split()))
build(1, 1, n)

for i in range(0,m):
    x,y=map(int,input().split())
    print(query_max(1, x, y))

算法有兴趣的可以来看看 在自然数,且所有的数不大于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、付费专栏及课程。

余额充值