2023.7.25 学习笔记

线段树(zr的最爱

基本长相:

  • 给你序列a,求序列a中的某种东西

  • 不一定完全的二叉树

  • 所有非子节点都有两个儿子

存储

  • 每一个节点的编号都对应一个序列的区间

  • 按照一个类似完全二叉树的形式存储在数组中

  • 对于下标为x的节点,其左儿子下标为$2x$,右儿子下标为$2x+1$,其父亲的下标为$\lfloor{\frac{x}{2}} \rfloor$

  • l,r记录当前节点对应区间的位置,其他变量对应区间信息

  • 对于叶子结点: val_i = num_i

  • 对于非叶子节点: val_i = val_{ls(i)} + val_{rs(i)}

基本代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 100010;
int val[4 * N], tag[4 * N];
int rs(int x)
{
	return x << 1;
}
int ls(int x)
{
  	return x << 1 | 1;
}

void push_up(int x)
{
  	val[x] = val[ls(x)] + val[rs(x)];
}

void work(int x, int l, int ,int k)
{
	tag[x] += k;
	val[x] += k * (r - l + 1);
}
void push_down(int x, int l, int r)
{
	int mid = (l + r) >> 1;
  	work(ls(x), l, mid, tag[x]);
  	work(rs(x), mid + 1, r, tag[x]);
	//传递tag
  	tag[x]=0;
	//清空tag
}
void build(int x,int l,int r)
{
	if(l == r)
	{
		val[x] = a[l];
		return;
   	}
 	int mid = (l + r) >> 1;
  	build(ls[x], l, mid);
  	build(rs[x], mid+1, r);
  	push_up(x);
}

int check(int needl, int needr, int l, int r, int x)//区间查询
{//所要查询的左端点      右端点 当前左端点 右端点 当前的节点
	int answer = 0;//答案
  	if(needl <= l && needr>=r)
		return val[x];//被包含,返回对应区间的sum
	int mid = (l + r) >> 1;
  	push_down(x, l, r);
	if(needl <= mid)
		answer += check(needl, needr, l, mid + 1, ls(x));
  	if(needr > mid)
		answer += check(needl, needr, mid + 1, r, rs(x));
	//二分递归
	return answer;
}
int update(int needl, int needr, int l, int r, int x)//区间修改
{//所要查询的左端点      右端点 当前左端点 右端点 当前的节点
  	if(needl <= l && needr>=r)
	{//被包含
		tag[x] += k;
		val[x] += k*(r - l + 1);
	}	
	push_down(x, l, r);
	int mid = (l + r) >> 1;
	if(needl <= mid)
		update(needl, needr, l, mid + 1, ls(x));
  	if(needr > mid)
		update(needl, needr, mid + 1, r, rs(x));
	//二分递归
	push_up(x);
}
signed main()
{
	……………………………………………………………………………………
}

大体思想

  • 不断分治向下找包含在自己区间内的节点并相加(下属)

  • 区间修改的重点: 懒惰标记,即记录一个标记$tag$,表示当前区间被修改的多少,当要进入子树时再下放,只在父亲上打标记

功能

  • 完成所有满足结合律的操作

  • 区间加(乘,异或,$min$,$max$),区间修改,区间求和,区间查询

复杂度

  • 单次操作基本为O(logn),push_down、push_up为O(1)

  • 所需空间一般为$4n$


例题

eg1: 线段树 2

思路:

  • 若区间乘$k$且完全包含时,则将加法标记,乘法标记,区间和都乘$k$

  • push_up:$val_x$ = val_{ls(x)} + val_{rs(x)}

  • push_down:先下传乘法标记,再下传加法标记乘区间长度(否则乘法标记会被乘两遍),然后记得清空标记

  • 下传乘法标记时:直接下传

  • 下传加法标记时:先乘后加

eg2:策略游戏

思路:

  • 1.当后手区间内有正有负时,先手取绝对值最小的正/负数,后手取绝对值最大的正/负数

  • 2.当后手区间内只有正数/只有负数,先手从最大/最小的正/负数里取,后手从最大值/最小值里取

  • 维护每个区间的$max$,$min$,区间有无负数,区间有无正数

  • push_up: minn = min(minn[ls(x)],minn[rs(x)]) , maxx=……,have_neg=have_neg[ls(x)] |have_neg[rs(x)] , have_pos=……

eg3:小白逛公园/Can you answer these queries I/Can you answer these queries III

思路

  • O(n)算法:贪心求最大子段和(但是不能修改)

  • 分治做法:将原数列分成等长的两段,递归下去求最大子段和,则原数列最大子段和可能是$\begin{cases} 左半最大子段和 \ 右半最大子段和 \ 跨过分界点的子段和 \end{cases}$

  • sum[x] = sum[ls(x)] + sum[rs(x)]

  • maxsum[x] = max(maxsum[ls(x)], maxsum[rs(x)], rsum[ls(x)] + lsum[rs(x)])

  • lsum[x] = max(lsum[ls(x)], sum[ls(x)]+lsum[rs(x)])

  • 而区间最长子段和即为$maxsum$

eg4:求区间绝对值之和

思路:

  • 开一个变量$sum$记录区间和,$cnt$记录区间内负数个数,$minval$记录区间内最大负数,用$minval$来判断有无负数变成正数,当有负数变成正数时,递归维护区间内负数个数

  • 加$k$时,若无正负变化,则区间绝对值和 += (正数个数-负数个数) * $k$,若有正负变化,递归更新新的$minval$以及$cnt$

  • 复杂度O(nlogn+mlogn)

eg5:贴海报/铺地板(二维染色)

思路:

  • 若该位置在之后被染色,那么最后必然不是此时的颜色

  • 因此倒序染色,可转化题意为区间染色+区间查询染色情况


扫描线 (求矩形面积并)

  • 当扫描到某地时,小矩形的面积即为线段长度 * 走过的路程,不断更新$L$和$D$,并将面积加上$L * D$

  • 可理解为将加入线段对应区间 + 1,退出线段对应区间 - 1,并用离散化离散值域

小组成员:jjc,spy,spy,gtz,mtx,…………