当前位置: 首页 > news >正文

023数据结构之线段树——算法备赛

线段树

线段树(Segment Tree)可以说是竞赛出题人最喜欢考核的数据结构了。线段树是历程碑式的知识点,熟练掌握线段树,标志着脱离初级学习阶段,走向中高级学习阶段——《算法竞赛》。

基本应用场景

下面举例基本应用场景:

  1. 区间最值问题

有长度为 n 的数组 a ,需多次进行一下操作

  • 求最值,对于原数组,给定i,j≤n,求区间[i,j]内的最值
  • 修改元素,对于原数组a,给定 kx,把第 k个元素 a[k]修改为x

如果用普通数组存储,上述两个操作,单次求最值的复杂度为O(n),修改元素的复杂度为O(1)。如果有m修改元素+求最值操作,总复杂度就是O(mn)。如果 m 和 n 比较大,那这个复杂度在竞赛中是不可接受的。

  1. 区间和问题

给出一个长度为 n 的数组 a ,先更改某些数的值,然后询问给定 i,j≤n,求数组[i,j]区间和。更改和询问操作总数为m,总的复杂度就是O(mn)


对于上述两类问题,线段树都可以在O(mlogn)的时间复杂度内解决。

线段树和树状数组都是解决区间问题的数据结构。他们各有优点。

  1. 逻辑结构。线段树基于二叉树,数据结构直观,清晰易懂。另外,由于二叉树灵活丰富,能用于更多场景,比树状数组适应面广。
  2. 代码长度。线段树的维护需要维护二叉树,而树状数组只需处理一个Tree数组,所以线段树的代码更长。

线段树概念

基本定义

概括的说,线段树可以理解为 分治+二叉树结构+Lazy-Tag技术(针对区间修改问题的Lazy-Tag技术,后面高级部分再介绍)。

线段树是分治法二叉树的结合。它本质是一棵二叉树,树上的节点是==【线段】==(或者理解为区间),线段是根据分治法得到的。

图示为包含10个元素的线段树
在这里插入图片描述
它的基本特征如下:

  1. 用分治法自顶而下建立,每次分治,左右子树各一半
  2. 每个节点都表示一个线段区间,非叶子节点都包含多个元素,叶子节点只有一个元素。
  3. 除最后一层不一定满外,其他层都是满的

考查每个线段[L,R]

  1. L=R,说明这个节点只有一个元素,是一个叶子节点。
  2. L<R,说明这个节点代表多个元素,它有两个孩子,左孩子 [L,M] ,右孩子 [M+1,R],其中M=(L+R)/2

节点所代表的值,可以是区间和,最值或其他灵活定义的值。

线段树的核心理念是大区间的解可以从小区间的解合并而来

元素数量与节点数量的关系

了解了线段树的定义与结构,我们来探讨一下【原数组元素数量】与【线段树节点数量】的关系(这对后面封装数据结构有帮助)。

以上述10个元素的原数组为例,它对应的线段树节点数量有19个。

具体地,原数组大小为n,其对应的线段树的节点数量size是多少呢?

首先线段树的叶子节点数是n,因为线段树除最后一层不一定满外,其他层都是满的,所以最后一层【满节点的层数】为 ⌊log2n⌋ (即n的二进制宽度bit_width(n),记为s),树的深度为 ⌈log2n⌉ (即为bit_width(n-1)+1)。
剩余未满的一层节点数为2*(n-(1<<s-1)),总的节点数就是(1<<s-1) + 2*(n-(1<<s-1))

在后续编码中,为了快速编写代码,常使用静态数组来实现一棵【满二叉树】,所以需要的空间最少为1<<(bit_width(n-1)+1)也可写成2<<bit_width(n-1),粗放一点,可以开n<<2的空间 。

定义数据结构

竞赛中一般使用静态数组来实现满二叉树

//定义根节点为tree[1],tree[0]不用。
//第一种方法:定义二叉树结构体
struct{int L,R,data;
}tree[N<<2];  //分配静态数组,开4倍空间
//第二种方法:直接用数组表示二叉树,竞赛时常用。
int tree[N<<2];   //二叉树空间开4倍,即元素数量的4倍
int ls(int p){return p<<1;}  //p号节点的左孩子,下标为p*2
int rs(int p){return p<<1|1;}  //p号节点的右孩子,下标为p*2+1;  或写成 (p<<1)+1

构造线段树(以求区间和为例)

void push_up(int p){tree[p]=tree[ls(p)]+tree[rs(p)];//tree[p]=max(tree[ls(p)],tree[rs(p)])  求最大值
}
void build(int p,int pl,int pr){  //节点p指向区间[pl,pr]if(pl==pr){tree[p]=a[pl];return;}  //递归出口,找到底层叶子节点,存值。int mid=(pl+pr)>>1;  //分治,折半build(ls(p),pl,mid);  //递归左孩子build(rs(p),mid+1,pr);  //递归右孩子push_up(p);  //回溯 从下往上传递区间和。
}  //调用  build(1,1,n) 完成初始化

区间查询(以查询区间和为例)

//递归查询
int query(int L,int R,int p,int pl,int pr){if(L<=p1&&pr<=R) return tree[p];  //完全覆盖,直接返回int mid=(pl,pr)>>1;  //mid为p节点的左孩子的右边界  mid+1为p节点右孩子的左边界//res定义为全局变量if(L<=mid) res+=query(L,R,ls(p),p,mid);  //L与左子节点重叠。if(R>mid) res+=query(L,R,rs(p),mid+1,pr);  //R与右子节点重叠。return res;
}  //调用 query(L,R,1,1,n)  (L,R)为要查询的区间。

类模版封装(以区间最大值为例)

class SegmentTree{  //以区间最大值为例vector<int>mx;void maintain(int i){mx[i]=max(mx[i<<1],mx[(i<<1)+1]);}void build(const vector<int>d,int i,int l,int r){if(l==r){mx[i]=d[l];return;}int mid=(l+r)/2;build(d,i<<1,l,mid);build(d,(i<<1)+1,mid+1,r);maintain(i);}
public:SegmentTree(const vector<int>d){int n=d.size();mx.resize(2<<bit_width((unsigned)n-1));  //确定数组大小,节省空间build(d,1,0,n-1);}int findMax(int l,int r,int i,int L,int R){  //寻找区间[l,r]内最大值if(l<=L&&r>=R) return mx[i];int mid=(L+R)/2;int ls=INT_MIN,rs=INT_MAX;if(mid>=l) ls=findMax(l,r,i<<1,L,mid); //与左子树表示区间有重叠if(mid<r) rs=findMax(l,r,(i<<1)+1,mid+1,R);  //与右子树表示区间有重叠return max(ls,rs);}  //调用方式findMax(l,r,1,0,n-1); int findFirstMax(int i,int l,int r,int x){  //寻找左数第一个大于等于x的数if(mx[i]<x) return -1;  //区间内没有大于等于x的数if(l==r) return l;  //寻找到叶子节点int mid=(l+r)/2;int ans=findFirstMax(i<<1,l,mid,x);  if(ans==-1) ans=findFirstMax((i<<1)+1,mid+1,r,x);  //左子树找不到再找右子树return ans;}
}

应用

龙骑士军团

问题描述
在这里插入图片描述

原题链接

思路解析

本题需要在[a,b]区间中选择一点 i[c,d]区间中选择一点 j,使区间[i,j]的区间和最大.

首先用计算以每个元素为右边界的前缀和sum。

进行q次查询,每次查询[c,d]区间的最大sum值和[a-1,b-1]区间的最小sum值。因为c>d所以两区间无交集,结果即为两者之差。

每次需要进行q次查询,考虑使用线段树数据结构,可在O(qlogn)的时间复杂度内完成。暴力法O(qn)将超时。

代码

#include<algorithm>
#include<vector>
#include<iostream>
#include<climits>
#define ll long long
using namespace std;
int ls(int i) { return i << 1; }
int rs(int i) { return i << 1 | 1; }
struct node {ll Max, Min;node(ll a, ll b) { Max = a; Min = b; }node() { Max = 0, Min = 0; }
};
node tr = node(LONG_MIN, LONG_MAX);
vector<node>tree;
vector<ll>sum;
void init(int p, int l, int r) {if (l == r) { tree[p].Max = sum[l]; tree[p].Min = sum[l]; return; };int mid = (l + r) >> 1;init(ls(p), l, mid);init(rs(p), mid + 1, r);tree[p].Max = max(tree[ls(p)].Max, tree[rs(p)].Max);tree[p].Min = min(tree[ls(p)].Min, tree[rs(p)].Min);
}
node query(int L, int R, int p, int l, int r) {if (L <= l && R >= r) return tree[p];int mid = (l + r) >> 1;if (L <= mid) {  //目标区间与左子节点有重叠node st = query(L, R, ls(p), l, mid);tr.Max = max(tr.Max, st.Max);tr.Min = min(tr.Min, st.Min);}if (R > mid) {  //目标区间与右子节点有重叠node st = query(L, R, rs(p), mid + 1, r);tr.Max = max(tr.Max, st.Max);tr.Min = min(tr.Min, st.Min);}return tr;
}
int main()
{// 请在此输入您的代码ios::sync_with_stdio(0);cin.tie(0);int n, q; cin >> n >> q;sum = vector<ll>(n + 1);tree = vector<node>(4 * n + 1);tree[0] = node();for (int i = 0; i < n; i++) {int arr;cin >> arr;sum[i + 1] = sum[i] + arr;}init(1, 1, n);while (q--) {int a, b, c, d;cin >> a >> b >> c >> d;ll min1, max2;//查询[a-1,b-1]中的最小值时,下标可能小于1,因此分情况讨论if (b == 1) min1 = 0;else if (a == 1)  min1 = min(0ll, query(1, b - 1, 1, 1, n).Min);else min1 = query(a - 1, b - 1, 1, 1, n).Min;tr = node(LONG_MIN, LONG_MAX);max2 = query(c, d, 1, 1, n).Max;tr = node(LONG_MIN, LONG_MAX);ll tar = max2 - min1;cout << tar;if (q > 0) cout << endl;}return 0;
}

水果成篮|||

问题描述

给你两个长度为 n 的整数数组,fruitsbaskets,其中 fruits[i] 表示第 i 种水果的 数量baskets[j] 表示第 j 个篮子的 容量

你需要对 fruits 数组从左到右按照以下规则放置水果:

  • 每种水果必须放入第一个 容量大于等于 该水果数量的 最左侧可用篮子 中。
  • 每个篮子只能装 一种 水果。
  • 如果一种水果 无法放入 任何篮子,它将保持 未放置

返回所有可能分配完成后,剩余未放置的水果种类的数量。

原题链接

思路分析

枚举每个fruits中的元素,查找baskets中第一个大于等于fruits[i]且没被用过的篮子,如果直接从左往右暴力搜索,那总的时间复杂度将是O(n*m),问题规模大一点将超时。对于枚举的fruits[i],是否可以对baskets进行二分查找?若baskets是有序的,那当然可以,但现在它是无序的且不能对它排序。

线段树可以帮助对无序数组进行二分查找,用一个线段树维护baskets的区间最大值。

对于 x=fruits[i],在线段树上二分查找第一个 ≥x 的数。

  • 如果整个区间的最大值都小于 x,那么没有这样的数,直接返回 true,表示查找失败。
  • 如果能递归到叶子,返回false,表示查找成功。
  • 先递归左子树,如果左子树没找到,再递归右子树 (这样保证查找到的数在最左边)。
  • 如果没有找到这样的数,把答案加一。

否则,把对应的位置改成 −1,然后向上更新修改,表示不能放水果。

代码

class SegmentTree{vector<int>mx;void getmx(int f){mx[f]=max(mx[f<<1],mx[(f<<1)+1]);}void build(vector<int>&arr,int f,int l,int r){if(l==r){mx[f]=arr[l];return;}int m=(l+r)/2;build(arr,f<<1,l,m);build(arr,(f<<1)+1,m+1,r);getmx(f);    }
public:SegmentTree(vector<int>&arr){int n=arr.size();mx.resize(2<<bit_width((unsigned)(n-1)));build(arr,1,0,n-1);}bool findAndUpdate(int f,int l,int r,int x){if(mx[f]<x){return true;  //未找到,返回true}if(l==r){mx[f]=-1;  //找到具体元素,不能再用,标记为最小值-1return false;}int m=(l+r)/2;bool flat=findAndUpdate(f<<1,l,m,x);  //先找左子树if(flat){  //左子树找不到再找右子树flat=findAndUpdate((f<<1)+1,m+1,r,x);}getmx(f);  //向上更新区间最大值return flat;}
};
class Solution {
public:int numOfUnplacedFruits(vector<int>& fruits, vector<int>& baskets) {int n=baskets.size(),ans=0;SegmentTree tree(baskets);for(int i:fruits){if(tree.findAndUpdate(1,0,n-1,i)){ans++;  //为找到符合要求的篮子}}return ans;}
};
http://www.dtcms.com/a/558301.html

相关文章:

  • 做化工回收的 做那个网站广东新闻发布会
  • 《信息系统项目管理师》2024 年上第 2 批次案例分析题及解析
  • 华为OD机试双机位A卷 - 插队 (C++ Python JAVA JS GO)
  • 裕华区建设局网站九天智能建站软件
  • 牛客101:递归/回溯
  • flash网站开源全国网站建设哪家专业
  • 网站整体克隆包含后台安卓app软件制作工具
  • 【Linux lesson3】进程概念
  • XPath语法及Python的lxml包学习
  • 网站管理系统源码怎么做网站icp备案
  • 生活视角下Prompt 提示词思考
  • 网站建设空间空间有几种类型成都电商网站
  • ROS2系列 (17) : Python服务通信实例实例——Server端
  • Windows 11 回退至windows 10
  • Pandas--数据读取与写入
  • 东莞seo网站优化方式毕业设计网站建设英文文献
  • 网站开发验收流程图网站建设渠道合作
  • 网站建设项目设计的图片做阿里巴巴怎么进公司网站
  • (N_158)基于微信小程序学生社团管理系统
  • html5经管网站模板企业oa管理系统
  • 【Kubernets】Kubernetes 资源类型大全:使用场景与配置示例
  • 成都哪里做网站如何做像京东淘宝那样的网站
  • 有没有建筑学做区位分析的网站淘宝联盟合作网站api
  • 《守正传艺:谷晟阳奇门遁甲教学的真实实践路径》
  • 网上家教网站开发网站首页原型图咋做
  • 数据结构==优先级队列与堆==
  • ⸢ 拾壹 ⸥⤳ 威胁感知与响应应用的实践案例
  • 在哪个网站上做实验仪器比较好深圳网站建设卓企
  • 基于n8n实现数据库多表数据同步
  • 网站服务器租广州各区最新动态