线段树
故事的开始是leetcode的一道Mid难度题1109. Corporate Flight Bookings
一开始做这道题我就直接暴力遍历和加法,结果当然直接超时(qwq蒟蒻的哭泣)。
于是看了一下午线段树,结果这道题也不用自己构建树,因为不涉及区间查询。
直接线性思考(差分数组),直接从头到位输出就可以了。
class Solution {
public:
vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
const int N = 20000 + 5;
vector<int>t;
int arr[N];//存储首位末尾订票信息
memset(arr, 0, sizeof(arr));
for (int i = 0; i < bookings.size(); i++)//更新首位信息
{
arr[bookings[i][0]] += bookings[i][2];
arr[bookings[i][1]+1] -= bookings[i][2];
}
int ans = 0;
for (int i = 1; i <= n; i++)//取出更新
{
ans += arr[i];
t.push_back(ans);
}
return t;
}
};
那么问题来了,如果要区间查找该怎么办?如果按照上面的算法,先将ans存储下来,每次重新构造以后都要从头查询到尾,时间开销应该是相当大的,所以这个时候就不得不去搭建一棵线段树。
先来看看线段树的结构:
每个节点内是存的区间,下面的序号就是该节点在树中的序号。
下面是线段树节点的结构体定义:
struct node
{
int l, r, v, f;//左,右区间,区间内存储的值,懒标记(区间操作的时候非常有用)
}tree[4*n];//要构造至少4*n的空间,为什么?待考虑。。。
线段树的核心是二分。
它有以下两点重要的性质:
- 左孩子范围为[l,mid],右孩子存储的范围为[mid+1,r]
- 如果一个节点序号为\(k\),那么左孩子节点序号为\(2*k\),右孩子节点序号为\(2*k+1\)
那首先来构建一棵线段树:
void build(int l,int r,int k)//递归,从叶子一直向上给节点赋值|||l为需要构造的左区间,r为需要构造的右区间,k为节点序号,从root开始
{
tree[k].l = l;//左区间赋值
tree[k].r = r;//右区间赋值
if (l == r)//当到达叶子节点的时候给叶子赋值,即给每一个具体的点赋值如(1,1)
{
scanf("%d", &tree[k].v);
return;
}
int mid = (l + r) / 2;//找中点
build(l, mid, k * 2);//构造左孩子
build(mid + 1, r, k * 2 + 1);//构造右孩子
tree[k].v = tree[k * 2].v + tree[k * 2 + 1].v;//父节点值的更新
}
接下来引入懒标记的概念,懒标记是为了高效的区间操作而设置的,他将要修改的值先全部囤积在大区间里,等到要下一次向下修改或者查找的时候再将值更新给接下来的子节点。这样就避免了修改一个值,要遍历很多节点的情况,大大减少了时间复杂度。
下面是懒标记向下调整的代码:
void down(int k)//懒标记的向下调整,注意下面四条语句都是+=!!!
{
tree[2 * k].f += tree[k].f;//懒标记传给左孩子
tree[2 * k+1].f += tree[k].f;//懒标记传给右孩子
tree[2 * k].v += (tree[2 * k].r - tree[2 * k].l + 1)*tree[k].f;//用懒标记给左孩子赋值 这里要乘父节点的懒标记!!!!因为自己的懒标记可能是父节点多次下传的,乘自己的懒标记可能会导致重复计算!!!!所以每次的value只用刚传下来的懒标记更新就可以了!!!
tree[2 * k + 1].v += (tree[2 * k + 1].r - tree[2 * k + 1].l + 1)*tree[k].f;//懒标记给右孩子赋值
tree[k].f = 0;//既然懒标记已经下传,父节点懒标记清零
}
单点查询
void one_search(int k, int t,int &x)//k为节点序号,从root开始,t为需要查找的点,x为返回值
{
if (tree[k].l == tree[k].r)//左右区间相等就找到点
{
x = tree[k].v;
return;
}
if (tree[k].f)//懒标记下传
down(k);
int mid = (tree[k].l + tree[k].r) / 2;//二分查找
if (t <= mid)
one_search(k * 2, t, x);
else
one_search(k * 2 + 1, t, x);
}
单点修改
void one_change(int k,int x,int v)//k为节点序号,从root开始,x为要改变的点,v是改变的大小
{
if (tree[k].l == tree[k].r)//找到目标
{
tree[k].v += v;
return;
}
if(tree[k].f)//懒标记下传
down(k);
int m = (tree[k].l + tree[k].r) / 2;//二分
if (x <= m)
one_change(k * 2, x, v);
else
one_change(k * 2 + 1, x, v);
tree[k].v = tree[k * 2].v + tree[k * 2 + 1].v;//合并状态,修改了就要去合并状态!!
}
区间查询(求和)
void interval_search(int k, int a, int b)//k为节点序号,从root开始,[a,b]为查询区间
{
if (tree[k].l >= a && tree[k].r <= b)//如果要查询的区间包含现在的区间那么就执行求和
{
ans += tree[k].v;//ans为全局变量,统计总和
return;
}
if(tree[k].f)//如果当前有懒标记,那么懒标记的向下传递
down(k);
int mid = (tree[k].l + tree[k].r) / 2;
if(a<=mid)//这儿的二分是将区间不停分成一小块一小块,这样才能统计到全部的区间,所以和单点的二分不同
interval_search(k * 2, a, b);
if(b>mid)//这里不能>=!!!!
interval_search(k * 2 + 1, a, b);
}
区间修改
void interval_change(int k, int a, int b, int v)//区间修改 k为节点序号 a,b分别为要修改的左右区间 v为要增加的值
{
if (tree[k].l >= a && tree[k].r <= b)
{
tree[k].v += (tree[k].r - tree[k].l + 1)*v;
tree[k].f += v;//!!给懒标记加上增加的值,能够向下传递!!!
return;
}
if(tree[k].f)//如果当前有懒标记,那么懒标记的向下传递
down(k);
int mid = (tree[k].l + tree[k].r) / 2;
if (a <= mid)//思想和区间查询一样
interval_change(2 * k, a, b, v);
if (b > mid)
interval_change(2 * k + 1, a, b, v);
tree[k].v = tree[k * 2].v + tree[2 * k + 1].v;//不要忘记合并状态
}
对于数据量很大的题目,树的定义和变量直接用long long类型!(血的教训qwq)
来一道例题:P2894 [USACO08FEB]酒店Hotel
代码加注释:
#include<bits/stdc++.h>
using namespace std;
/*这颗树参数和一般的线段树不同,里面没有存左右区间,比较特别*/
struct node
{
long long sum;//区间最大连续空房数
long long len;//总长
long long lmax, rmax;//从左和从右开始空房间数量
int lazy;//懒标记
}tree[300020];//不知道这儿为什么开20w左右会不够用,以后尽量开大点吧
void build(int k, int a, int b);
void down(int k);
void renew(int k);
void interval_change(int k, int l, int r, int L, int R, int x);
long long interval_search(int k, int l, int r, int x);
int main()
{
int n, m;
cin >> n >> m;
build(1, 1, n);
while(m--)
{
int i;
cin >> i;
if (i == 1)
{
int x;
cin >> x;
if (tree[1].sum >= x)
{
int left = interval_search(1, 1, n, x);
cout << left << endl;
interval_change(1, 1, n, left, left + x - 1, 1);
}
else
cout << 0 << endl;
}
else
{
int x, y;
cin >> x >> y;
interval_change(1, 1, n, x, x + y - 1, 2);
}
}
return 0;
}
void build(int k, int a, int b)//将线段树构造起来
{
tree[k].len = tree[k].lmax = tree[k].rmax = tree[k].sum = b - a + 1;
tree[k].lazy = 0;
if (a == b)
return;
int mid = (a + b) / 2;
build(k * 2, a, mid);
build(k * 2 + 1, mid + 1, b);
}
void down(int k)//懒标记下传,这儿的懒标记只有0,1,2三种状态,0代表空房状态,1代表在区间里的房间即将被用,2表示该区间房间要被退。这儿的懒标记就要改sum,lmax,rmax三个状态了
{
if (tree[k].lazy == 0)
return;
if (tree[k].lazy == 1)//开房向下更新置0
{
tree[2 * k].lazy = tree[2 * k + 1].lazy = 1;
tree[2 * k].sum = tree[2 * k].lmax = tree[2 * k].rmax = 0;
tree[2 * k + 1].sum = tree[2 * k + 1].lmax = tree[2 * k + 1].rmax = 0;
}
else//退房向下更新还原
{
tree[2 * k].lazy = tree[2 * k + 1].lazy = 2;
tree[2 * k].sum = tree[2 * k].lmax = tree[2 * k].rmax = tree[2 * k].len;
tree[2 * k + 1].sum = tree[2 * k + 1].lmax = tree[2 * k + 1].rmax = tree[2 * k + 1].len;
}
tree[k].lazy = 0;
}
void renew(int k)//更新状态,因为合并状态不像传统的线段树,这儿要合并三个状态,所以单独开函数
{
if (tree[2 * k].sum == tree[2 * k].len)//左区间全部为空房
tree[k].lmax = tree[k * 2].len + tree[2 * k + 1].lmax;
else
tree[k].lmax = tree[k * 2].lmax;
if (tree[2 * k+1].sum == tree[2 * k+1].len)//右区间全部为空房
tree[k].rmax = tree[k * 2+1].len + tree[2 * k].rmax;
else
tree[k].rmax = tree[k * 2+1].rmax;
tree[k].sum = max(max(tree[2 * k].sum, tree[2 * k + 1].sum), tree[2 * k].rmax + tree[2 * k + 1].lmax);
//三种情况,全左,全右,跨越左右
}
void interval_change(int k,int l,int r,int L,int R,int x)//区间修改,通过l,r找区间,因为线段树自己没有区间的变量储存
{
down(k);//一开始就要懒标记下传,因为不像传统线段树,可以叠加,这个只能一次一次更新
if (l >= L && r <= R)
{
if (x == 1)
tree[k].sum = tree[k].lmax = tree[k].rmax = 0;
else
tree[k].sum = tree[k].lmax = tree[k].rmax = tree[k].len;
tree[k].lazy = x;
return;
}
int mid = (l + r) / 2;
if (L <= mid)
interval_change(2 * k, l, mid, L, R, x);
if (R > mid)
interval_change(2 * k + 1, mid + 1, r, L, R, x);
renew(k);//更新状态
}
long long interval_search(int k,int l,int r,int x)
{
down(k);
if (l == r)//如果找点,就把左区间返回
return l;
int mid = (l + r) / 2;
/*三个状态,这样就左区间从小到大返回了*/
if (tree[2 * k].sum >= x)//全在左区间
return interval_search(2 * k, l, mid, x);
else if (tree[2 * k].rmax + tree[2 * k + 1].lmax >= x)//左右区间都有
return mid - tree[2 * k].rmax + 1;
else//右区间
return interval_search(2 * k + 1, mid + 1, r, x);
}