线段树及相关
大概是acm/oi选手和计算机相关学生的分界 不会线段树可以说acm/oi就还没入门(虽然最近才开始认真看...) 前置技能是递归和基础树的知识
区间最值
即给一数组 多次询问区间的最(大,小)值
直接模拟 单次查询复杂度O(n)而用dp和二进制的思想 可以做到O(nlogn)的空间复杂度 O(nlogn)时间的预处理 O(1)的查询
数组Dp[n][log2n] dp[i][j]的含义是 从第i个数起连续2^j个数的最值(即闭区间[i,i+2^j-1])
显然dp[i][0]=a[i],考虑每个区间都可以均分为2个区间 [I,i+2^j-1]可分为:
[i,i+2^(j-1)-1]和[i+2^(j-1),i+2^j-1]
所以有转移方程 dp[i][j]=min/max(dp[i][j-1],dp[i+1<<(j-1)][j-1])
查询也很神奇 对于区间[l,r]看做2个长度相等的区间合并 一个起点为l 另一个终点为r 即[l,l+2^i-1]和[r-2^i+1,r] 这两个区间可以有交集 但不能超出边界 显然必然存在这样的i值 可以用对数O(1)的求出i值
它叫做st算法 高效好写 缺点是不支持数据的更改
经常用到 应作为模板 计蒜客初赛时用它过了中等题 擦边进入复赛
Poj 3264 裸rmq问题 数据范围有误 数组开到80k以上才能过
线段树
细写一下入门环节 花了很久整理优化代码 对比结果 才明白点
从最简单的栗子开始
有1—n个瓶子 开始每个瓶子中都可以有一些糖 之后有2种操作:
把其中1个瓶子中拿走或增加x个糖
或 询问第x到y个瓶子中一共有多少糖
直接用数组模拟的话 操作1复杂度O(1) 操作2复杂度O(n)k次操作的最坏复杂度O(kn) 太高了
或者用前缀数组?这样操作2复杂度是O(1) 但操作1复杂度是O(n) 最坏复杂度不变
所以要找一个折中的方法 让2种操作的复杂度都尽量小
于是就有了线段树 以二叉树和区间的思想 让每次操作的复杂度都是O(logn)
从最基本的定义开始 想象一颗二叉树 每个节点描述一个区间(以下所有区间为闭区间)
对于任意节点(l,r)且l!=r 它的左儿子是(l,(l+r)/2) 右儿子是((l+r)/2+1,r)
从这一句可以推导出:
每个节点都被它的子节点均分为2部分 不存在只有一个儿子的节点 左节点大于等于右节点
显然这样的树除去最后一层是满二叉树(但不一定是完全二叉树!) 且其所有叶子节点描述的区间是一个点(即l==r)
数据范围是n 则有n个叶子节点 且根节点是整个区间(1,n)总节点数不超过叶子节点数的2倍(虽然不是完全二叉树但符合这个性质) 所以它的空间复杂度是O(n)(但实际考虑它不是完全二叉树 通常空间取n的3-4倍)
还有很关键的一点 近似满二叉树可以用数组模拟 令数组下标为1的节点是根节点 则下标为x的节点的左右子节点是2*x和2*x+1(稍微数学推导一下就好)
分析它的修改复杂度 对单点修改 只需要从根开始向下一路修改到叶子结点 复杂度是树的深度O(logn)
对于询问 任意区间都可以由尽量大的区间节点恰好构成 就像2进制一样 其复杂度O(logn)
所以 仅靠红色的那句定义 我萌就推出线段树的基本性质及用法
显然这是可行的方案 只剩下代码实现环节(为了逼格全部使用位运算 x<<1即2*x x>>1即x/2 x<<1|1即2*x+1)
首先 知道数据范围后 用数组模拟二叉树 要初始化数组 数组保存该节点的区间及该节点的状态(就是区间内糖果的总和) 函数3个参数分别是当前区间左右值和当前节点对应的数组位置 开始时运行bulid(1,n,1) 即根节点表示最大区间[1,n] 对应数组下标为1 之后就是递推到全树的过程 复杂度为O(n)
void build(int l, int r, int k)
{
T[k].l = l, T[k].r = r, T[k].n = 0;
int mid = (l + r) >> 1;
if (l != r)
build(l, mid, k << 1), build(mid + 1, r, k << 1 | 1);
}
修改单点函数 即从根节点一路下到该叶子结点 经过的所有节点的状态值+n
函数3个值 分别是修改值 要修改的元素(不是它的数组下标!) 当前的节点对应的数组下标 从insert(n,d,1)开始
void insert(int n, int d, int k)
{
T[k].n += n;
int mid = (T[k].r + T[k].l) >> 1;
if (T[k].r != T[k].l)
if (d <= mid) insert(n, d, k << 1);
else insert(n, d, k << 1 | 1);
}
查询函数 即拼凑区间的过程
如果需要的区间和当前点对应的区间正好相同 则返回当前节点的值(废话..)
如果需要的区间全部在当前节点区间的左或右儿子上 则在它的左或右儿子上继续查找该区间
否则(即需要的区间在它的左右儿子上各一部分)把需要的区间以mid为分界拆为2部分 每部分在当前节点的左右儿子继续查找
三个参数是 需要的区间的左右值 当前节点的下标 开始时search(l,r,1)
int search(int l, int r, int k)
{
if (T[k].l == l&&T[k].r == r) return T[k].n;
int mid = (T[k].l + T[k].r) >> 1;
if (r <= mid) return search(l, r, k << 1);
else if (l > mid) return search(l, r, k << 1 | 1);
else return search(l, mid, k << 1) + search(mid + 1, r, k << 1 | 1);
}
一点时间优化 对于一开始就给出所有点初始值的题目 初始化线段树时显然不用为0 可以把数都存下来然后初始化的时候直接赋值 更巧妙的方法是 递推时是从左儿子开始 所以经过叶子结点的顺序必然是1到n的顺序 在判断叶子结点的时候读取数据直接赋值 可以节省O(nlogn)的时间 在询问次数远小于n时效果明显
}
Hdu 1166 单点更新
Hdu 1754 把上一题的加换成了取最大值 稍改一下就好
区间更新
在保持红色的关键定义不变的情况下 单改1点的复杂度为O(logn) 所以遍历区间修改的复杂度是O(nlogn) 即使统计每个节点的区间内被修改点的的次数乘以修改量 还是有O(n)个节点需要修改 复杂度最低为O(n)
保持线段树的情况下降低最坏复杂度不太现实 但还是有方法的 我萌用到了日常使用的lazy思想
区间和问题lazy思想类似于:知道了控计院每个班加2人 这时先不把每个班的信息修改 只修改总控计院的信息 查询到自动化一共多少人时 再把控计院的修改细分到每个专业(询问到专业 就细分到专业而不细分到班级)
大概就是这个意思 给每个节点增加一个状态 表示子节点需要变化的量 把需要更新的区间像search函数一样 分割成几个尽量大的节点代表的区间的并 再给这些节点做上标记 表示这些节点的子节点需要更新xx值 但并不执行更新 直到有涉及这些节点的询问 再实施更新(更新区间时 如果修改区间与当前区间有交集但不包含 则先要实施该节点的更新 再搜索其左右节点 否则会标记混乱 wa)
每次询问的区间最多由O(logn)个区间并成 每个区间最多更新lazy标记O(logn)次 所以最坏复杂度大概是常数小的O(lognlogn)?
树状数组(巧妙用二分和位运算的数据结构)可以实现复杂度O(logn) 且时间常数小代码短 但其可解决的问题是线段树的子集 也很少有题目卡线段树的时间常数 有兴趣可以了解下
区间加减值(poj3468)一种模块化,逻辑清晰,相对高效的几个关键函数的写法:
void up(int i)//通过子节点更新当前节点的值 递归更新子节点之后用
{
t[i].n = t[i << 1].n + t[i << 1 | 1].n;
}
void fx(int i, LL c)
{
t[i].n += (t[i].r - t[i].l + 1)*c;
t[i].ad += c;
}
void down(int i)//将当前点的lazy标记传到下一点
{
fx(i << 1, t[i].ad), fx(i << 1 | 1, t[i].ad);
t[i].ad = 0;
}
void update(int l, int r, int c, int i)
{
if (l == t[i].l&&r == t[i].r)
{
fx(i, c);
return;
}
if (t[i].ad) down(i);
int mid = (t[i].l + t[i].r) >> 1;
if (r <= mid) update(l, r, c, i << 1);
else if (l > mid) update(l, r, c, i << 1 | 1);
else update(l, mid, c, i << 1), update(mid + 1, r, c, i << 1 | 1);
up(i);
}
Poj 3468 区间更新(用树状数组的话 1000ms 500+B可过)
Hdu 1698 同上 修改区间的方式有点变化 改下更新函数就好
Poj 1823 线段树求区间最长线段 状态是:经过左右端点的最长线段和及区间内的最长线段 这3个 可以推导出up函数 不用sea函数 很友好的题
Poj 3667 带查询的区间最长线段 和上一题名字都一模一样..加上sea函数就好(ps 这就是操作系统内存分配的首次适应算法...)
染色时的lazy思想
类似于:知道了控计院都是男生 给全院做上标记 问计算2个班的性别情况 发现计算2个班属于控计院 就回答 这2班全是男生(而不细分控计院的lazy标记)
代码几乎一样 search函数更简单 代码就不附了
染色时 查询与修改区间的大概复杂度都是O(logn)体现了线段树的精髓
Zoj 1610 染色问题 线段树lazy思想的应用
Poj 2892 据说是线段树 可用stl模拟可秒之 模拟出了最短代码hhh
Hdu 3974 线段树染色+dfs+链表存边 有点复杂的综合题目
Hdu 4614 墙裂推荐 基础染色线段树+二分 比较简单的综合题目
复杂的线段树
区间修改时 关键是pushup和pushdown操作 前者是通过子节点更新当前节点(例如t[x]=t[x<<1]+t[x<<1|1])后者是把当前节点的lazy标记推向子节点
简单线段树的这些操作只有1-2句 不必单独写出函数 但复杂线段树中这两个操作最难写 pushdown时从优先级最低的开始(保证优先级高的覆盖优先级低的) 逐个检查lazy标记并根据含义更新子节点(通常每个含义写一个函数 因为update时也会用到) up函数根据子节点更新当前节点 根据线段树的节点参数推导函数 详见hdu4553
这题的关键函数的模块化的写法:
struct { int l, r, n1l, n1r, n1m, n2l, n2r, n2m, ad1, ad2, ad3; }t[100005 << 2];
void godness(int i)
{
t[i].n1l = t[i].n1r = t[i].n1m = t[i].n2l = t[i].n2r = t[i].n2m = 0;
t[i].ad1 = t[i].ad3 = 0, t[i].ad2 = 1;
}
void stu(int i)
{//stu和godness互斥 以保证后来的清除先来的 区间里不可能2个都存在
t[i].n1l = t[i].n1r = t[i].n1m = t[i].n2l = t[i].n2r = t[i].n2m = lo(i);
t[i].ad1 = t[i].ad2 = 0, t[i].ad3 = 1;
}
void si(int i)
{//这里不可更新ad3!! ad2肯定是0更新不更新都一样
t[i].n1l = t[i].n1r = t[i].n1m = 0, t[i].ad1 = 1, t[i].ad2 = 0;
}
void down(int i)
{//必须是312的顺序
if (t[i].ad3)
stu(i << 1), stu(i << 1 | 1), t[i].ad3 = 0;
if (t[i].ad1)
si(i << 1), si(i << 1 | 1), t[i].ad1 = 0;
if (t[i].ad2)
godness(i << 1), godness(i << 1 | 1), t[i].ad2 = 0;
}
void up(int i)
{
t[i].n1m = max(t[i << 1].n1m, max(t[i << 1 | 1].n1m, t[i << 1].n1r + t[i << 1 | 1].n1l));
t[i].n1l = t[i << 1].n1l + (t[i << 1].n1l == lo(i << 1) ? t[i << 1 | 1].n1l : 0);
t[i].n1r = t[i << 1 | 1].n1r + (t[i << 1 | 1].n1r == lo(i << 1 | 1) ? t[i << 1].n1r : 0);
t[i].n2m = max(t[i << 1].n2m, max(t[i << 1 | 1].n2m, t[i << 1].n2r + t[i << 1 | 1].n2l));
t[i].n2l = t[i << 1].n2l + (t[i << 1].n2l == lo(i << 1) ? t[i << 1 | 1].n2l : 0);
t[i].n2r = t[i << 1 | 1].n2r + (t[i << 1 | 1].n2r == lo(i << 1 | 1) ? t[i << 1].n2r : 0);
}
void build(int l, int r, int i)
{
t[i].l = l, t[i].r = r;
t[i].n1l = t[i].n1r = t[i].n1m = t[i].n2r = t[i].n2l = t[i].n2m = r - l + 1;
t[i].ad1 = t[i].ad2 = t[i].ad3 = 0;
if (l == r) return;
int mid = (l + r) >> 1;
build(l, mid, i << 1), build(mid + 1, r, i << 1 | 1);
}
void update(int l, int r, int k, int i)
{
if (l <= t[i].l&&r >= t[i].r)
{
if (k == 1) si(i);
if (k == 2) godness(i);
if (k == 3) stu(i);
return;
}
down(i);
int mid = (t[i].l + t[i].r) >> 1;
if (r <= mid) update(l, r, k, i << 1);
else if (l > mid) update(l, r, k, i << 1 | 1);
else update(l, mid, k, i << 1), update(mid + 1, r, k, i << 1 | 1);
up(i);
}
int sea1(int n, int i)
{
if (t[i].l == t[i].r) return t[i].l;
down(i);
if (n <= t[i << 1].n1m) sea1(n, i << 1);
else if (t[i << 1].n1r + t[i << 1 | 1].n1l >= n)
return t[i << 1].r - t[i << 1].n1r + 1;
else sea1(n, i << 1 | 1);
}
int sea2(int n, int i)
{//和sea1只有查询元素下标不同
if (t[i].l == t[i].r) return t[i].l;
down(i);
if (n <= t[i << 1].n2m) sea2(n, i << 1);
else if (t[i << 1].n2r + t[i << 1 | 1].n2l >= n)
return t[i << 1].r - t[i << 1].n2r + 1;
else sea2(n, i << 1 | 1);
}
Hdu 4578 线段树神题..几k的巨大代码量...但知识只涉及简单数学和逻辑推导 主要是线段树参数太多...想清楚关系慢慢写还是可能ac的 虽然mod10007但是不用__i64d很容易wa
Hdu 4553 比较复杂的区间最长线段树 算上lr总共11个参数... seach函数需要考虑一下 3个标记的优先级及相互关系注意一下 wa了好多次(相当于带优先级的首次适应内存分配算法)
线段树扫描线
一些和几何有关的题目 只要知道某些参数就可以线性递推出答案 但参数的计算涉及区间修改 这时就用到了线段树
例如求矩形面积交
简单的说 把每个矩形竖线所在的直线称作扫描线 从左向右遍历扫描线 记录每个扫描线以左的矩形左边线的长度和(扫描到右边线就删除对应的左边线) 把每个长度和乘以扫描线到下一线的间距 就是总面积
但每次求扫描线以左的长度和涉及区间修改 这时就用到了线段树 这里线段树没有lazy思想 查询只涉及根节点 不需要sea函数 但参数更新却不太容易 通常需要其他构造参数辅助
显然扫描线需要离散化并去重 推荐用unique()函数去重 它返回的参数正好建树时需要
难点主要在于扫描线思想和构造正确的参数和up函数递推关系
Hdu 1542 求面积并
Hdu 1255 求面积交(和上一个差不多 多一个参数)
Poj 1177图形并的周长(参数有点难想..)
Hdu 3642 求体积3次交 有点变态 算法容易想 和面积交差不多 但写起来很复杂..
Poj 3264 裸rmq问题 数组开到80k以上才能过
Hdu 1166 单点更新
Hdu 1754 把上一题的加换成了取最大值
Poj 3468 区间更新
Hdu 1698 另一种区间更新
Poj 1823区间最长线段简化
Poj 3667 区间最长线段(操作系统内存分配 首次适应算法)
Zoj 1610 染色问题 线段树lazy思想的应用
Poj 2892 据说是线段树 可用stl模拟
Hdu 4614 墙裂推荐 染色线段树+二分 比较简单的综合题目
Hdu 3974 线段树染色+dfs+链表存边 有点复杂的综合题目
Hdu 4578 复杂线段树 巨大代码量...
Hdu 4553 复杂的区间最长线段树
Hdu 1542 面积交
Hdu 1255 面积并
Poj 1177 矩形并的周长
Hdu 3642 体积3次交
线段树是acm入门中比较难的专题..即使最简单的模板题 逻辑上无误的写法都有很多种 附的代码是功能模块化,高效率,每个语句都是充要条件(比如r==t[i].r换成r>=t[i].r也对 但只可能有r==t[i].r的情况出现 就写成前一种),代码也不太长的写法 和之前的可读情况下尽量短的风格不同,这些代码是逻辑尽量清晰 不一定是某题最好算法或尽量短的代码
除了模板题更是几乎每题都有新变化新知识..都可以单独写一份题解 但懒得写了..