上期传送门:线段树专题一:基本应用
线段树专题二:进阶应用
一 最大子段和的扩展问题
0 回顾
要想研究扩展问题,肯定得先回顾一遍基础问题。
小白逛公园(洛谷P4513)
特点:一是更新的规则,二是query模拟合并的操作,需要借助结构体。我们就把这两部分回顾一下。
wyx update(wyx x,wyx y){
wyx z;
z.lrs = x.lrs + y.lrs;
z.ls = max(z.lrs,max(x.ls,max(x.lrs,x.lrs + y.ls)));
z.rs = max(z.lrs,max(y.rs,max(y.lrs,y.lrs + x.rs)));
z.mx = max(x.mx,max(y.mx,max(x.rs + y.ls,max(z.ls,max(z.rs,z.lrs)))));
return z;
}
wyx query(int k,int l,int r,int x,int y){
// printf("%d %d %lld?\n",l,r,mx[k]);
if(x <= l && r <= y) return tre[k];
if(y <= mid) return query(k << 1,l,mid,x,y);
if(x > mid) return query(k << 1 | 1,mid + 1,r,x,y);
if(x <= mid && y > mid) return update(query(k << 1,l,mid,x,y),query(k << 1 | 1,mid + 1,r,x,y));
}
从思想上,最大子段和的根本难点其实是细化了左右儿子向上更新父亲的过程。 如果我们写的是类似于最大子段和的问题(如接下来要讨论的类最大子段和问题),也要从这方面考虑。
1 最大子段和问题
T1 KIN(洛谷P3582)(难度4)
题意概括:给定一个序列,选择一段,其中重复的全部无价值,求最大子段和。
题意非常简单粗暴,而且就是最大子段和,所以根本不变。
但是这题序列长度可以达到1e6,如果我们每次查询都得查一次重,根本不必分析复杂度就知道过不了。所以这题的查重必须得预处理出来。
怎么预处理?假设我们选定1~x的一段序列,加入一个新数,那么这时候与之重复的和这个一起选,总价值就变成0。那么思路就来了:我们可以把这两个当中一个价值变成相反数,这样这俩一起选的时候就抵消了。
问题来了,替换哪个呢?这里主要的问题是,可能会选到负价值而没选对应的正价值,这时候显然更新有误。如果改前面的,由于在加新的数之前,前面的就已经处理过,且一定比修改后大,所以无影响,因此选前面的。这也提醒我们,每次加入一个新数,都保证之前的序列已经被修改入树,所以结果要多次取最大值。
现在又加进来一个,这时候假设又多重复了一遍,为了使这三次一起选为0,应该改出一个0。
问题又来了,这个0放哪儿?根据刚才的经验,关键是要考虑已更新的能否抵消修改的影响。所以新来的应该是正价值,剩下两个一个负价值一个0。假设我们更新的序列包括了后两个数,如果倒数第二个为0,这时候同时选两个产生了价值,显然错误,所以倒数第二个应该为负价值。
再多的我们就不必说了。总而言之,由于我们不断在尾部插入新的数,要从后往前依次保证多个抵消,应该使倒数第一个为正价值,倒数第二个为负价值,剩下全为0。尽管我们花了一个n来一个一个插入,也明显比找到之后去重好。
这样我们的线段树只需要包括更新和修改功能就可以。刚才所述的功能,一个O(n)预处理就够了。
总之这题难在思路,代码并不难写。关键在于理解线段树的更新过程,以及从线段树的查改角度去思考问题(像去重就是完全不基于线段树思想的一个功能,实现出来肯定是不够优的)。
完整代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define mid (l + r >> 1)
int a[1000001],b[1000001],pre[1000001],mark[1000001],judge[1000001];
struct wyx{
long long ls,rs,lrs,mx;
}tre[4000001];
struct yjx{
wyx update(wyx x,wyx y){
wyx z;
z.lrs = x.lrs + y.lrs;
z.ls = max(z.lrs,max(x.ls,max(x.lrs,x.lrs + y.ls)));
z.rs = max(z.lrs,max(y.rs,max(y.lrs,y.lrs + x.rs)));
z.mx = max(x.mx,max(y.mx,max(x.rs + y.ls,max(z.ls,max(z.rs,z.lrs)))));
return z;
}
void modify(int k,int l,int r,int x,int c){
if(l == r){
tre[k].ls = tre[k].rs = tre[k].lrs = tre[k].mx = (long long)c;
return;
}
if(x <= mid) modify(k << 1,l,mid,x,c);
else modify(k << 1 | 1,mid + 1,r,x,c);
tre[k] = update(tre[k << 1],tre[k << 1 | 1]);
}
}STr;
int main(){
//freopen("Segtree.in","r",stdin);
//freopen("Segtree.out","w",stdout);
int i,m,n,w,x,y,z;
long long res = 0;
scanf("%d %d",&n,&m);
for(i = 1;i <= n;i++) scanf("%d",&a[i]);
for(i = 1;i <= m;i++) scanf("%d",&b[i]);
for(i = n;i >= 1;i--){
if(!judge[a[i]]){
mark[a[i]] = i;
++judge[a[i]];
}
else{
if(judge[a[i]] == 1){
pre[mark[a[i]]] = i;
mark[a[i]] = i;
++judge[a[i]];
}
else{
pre[mark[a[i]]] = i;
mark[a[i]] = i;
}
}
}
//for(i = 1;i <= n;i++) printf("%d ",pre[i]);
//printf("\n");
for(i = 1;i <= n;i++){
STr.modify(1,1,n,i,b[a[i]]);
if(pre[i]) STr.modify(1,1,n,pre[i],-b[a[i]]);
if(pre[pre[i]]) STr.modify(1,1,n,pre[pre[i]],0);
res = max(res,tre[1].mx);
}
printf("%lld\n",res);
return 0;
}
这里我的预处理虽然是O(n),但是比较麻烦,应该有更好的处理方法。
2 类最大子段和问题
T2 Snow Boots G(洛谷P4269)(难度3.5)
题意概括:恕我无能,概括不了。
设能走的为0,不能走的为1(一会儿会说明这样做的原因),为了能走过去,这个序列里面不能存在一段长度大于x的1。显然我们要更新和维护的就是这个最长的1。
首先,这题并不是最大字段和问题。01111和10111这两个总的最大子段和显然都是4,但是前者最长的连续1是4个而后者是3个。这就要求我们改变最大子段和的处理方式。
还是从左右儿子开始研究。ls仍然记从左开始最长的1,rs仍然记从右开始最大的1,别的都完全不变。合并的时候,很显然,父亲的左端最长至少为左儿子的左端,而如果左儿子全为1,还可以加上右儿子的左端最长。右端同理。最大值合并的时候,也是取儿子和自己的mx,以及中间的部分(这个倒是基本和最大子段和一样)。这种问题理论上可以叫最长子串问题,更新比最大子段和还能简单一点点。
之所以要把不能走的设为1,就是因为这样可以使得最长子串的权值代表长度。如果把不能走的设为0,就会变得麻烦一点。
wyx update(wyx x,wyx y){
wyx z;
z.lr = x.lr + y.lr;
z.ls = x.ls,z.rs = y.rs;
if(x.lr == x.mx) z.ls += y.ls;
if(y.lr == y.mx) z.rs += x.rs;
z.mx = max(x.mx,max(y.mx,x.rs + y.ls));
return z;
}
剩下的线段树部分与最大子段和无异。
接着研究一下序列的处理。很显然,如果对每一双靴子都处理一遍,就是O(n2),不够好。很容易想到,把靴子排个序,让可以经过深度小的靠前,这样前面可以通过的格子后面也都能通过。但是如果要实现这一功能,我们还是每次都要遍历一边序列,其实根本没有体现出排序的意义。
这就出现一种巧妙的方法:把地砖和靴子混合排序,使得深度相同时靴子靠后。如果找到地砖,把对应位置变成0,这样一来就使得每一个地砖只会被查找并修改一次,问题就解决了。这个思路个人感觉还是相当有实战价值的。
完整代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define mid (l + r >> 1)
int a[100001],res[100001];
struct wyx{
long long ls,rs,mx,lr;
}tre[400001];
struct zwy{
long long len,dep,id;
}p[200001];
bool cmp(zwy x,zwy y){
if(x.dep != y.dep) return x.dep < y.dep;
else return x.len < y.len;
}
struct yjx{
wyx update(wyx x,wyx y){
wyx z;
z.lr = x.lr + y.lr;
z.ls = x.ls,z.rs = y.rs;
if(x.lr == x.mx) z.ls += y.ls;
if(y.lr == y.mx) z.rs += x.rs;
z.mx = max(x.mx,max(y.mx,x.rs + y.ls));
return z;
}
void build(int k,int l,int r){
if(l == r){
tre[k].ls = tre[k].rs = tre[k].mx = tre[k].lr = 1;
return;
}
build(k << 1,l,mid);
build(k << 1 | 1,mid + 1,r);
tre[k] = update(tre[k << 1],tre[k << 1 | 1]);
}
void modify(int k,int l,int r,int x,int c){
if(l == r){
tre[k].ls = tre[k].rs = tre[k].mx = c;
return;
}
if(x <= mid) modify(k << 1,l,mid,x,c);
else modify(k << 1 | 1,mid + 1,r,x,c);
tre[k] = update(tre[k << 1],tre[k << 1 | 1]);
}
}STr;
int main(){
int i,m,n,w,x,y,z,cnt;
scanf("%d %d",&n,&m);
for(i = 1;i <= n;i++){
scanf("%d",&p[i].dep);
p[i].len = 0;
p[i].id = i;
}
for(i = n + 1;i <= n + m;i++){
scanf("%d %d",&p[i].dep,&p[i].len);
p[i].id = i;
}
STr.build(1,1,n);
sort(p + 1,p + n + m + 1,cmp);
for(i = 1;i <= m + n;i++){
if(!p[i].len) STr.modify(1,1,n,p[i].id,0);
else{
cnt = p[i].id - n;
res[cnt] = tre[1].mx < p[i].len;
}
}
for(i = 1;i <= m;i++) printf("%d\n",res[i]);
return 0;
}
T3 括号匹配(难度3)
题意描述:给定长度为n的括号序列和m次询问,规定合法序列如下:
(1)空序列合法;
(2)若S合法,(S)合法;
(3)若S1、S2合法,S1S2合法。
每次询问,询问“ [l,r]区间能选出多少个括号使得选出的括号组成的序列在[l,r]之间能形成最长的括号序列。(这描述属实迷惑行为)
这题首先要看懂输出,“选出”就说明我们其实可以跳着选。比如序列"())()",最长的合法括号只有2个,但是如果可以选的话,那么最长就是4个,即"()()"。不过很显然我们不能打乱顺序,不然这题就没意义了。所以这题也可以说是“最大子段和”问题。
考虑一下怎么维护。仍然从左右儿子去考虑,在合并的时候,一些原来左右多余的括号就会组合起来,而根据上面的规定,新的合法的组合一定来源于左儿子中多余的左括号和右儿子中多余的右括号的组合(不用担心它们之间是什么,因为我们可以跳着选,也就是可以把这之间的非法部分跳过)。因此我们更新的思路就来了:线段树中维护合法括号对数,多余左、右括号个数三个信息。更新的时候,父亲的合法括号数等于两个儿子的合法数加新的组合数,而左、右多余括号数则是两个儿子多余括号数的和减去新的组合数(也就是已匹配掉的),即:
zwy up(zwy x, zwy y) {
int temp;
zwy z;
temp = min(x.ls, y.rs);
z.c = x.c + y.c + temp;
z.ls = x.ls + y.ls - temp;
z.rs = x.rs + y.rs - temp;
return z;
}
剩下的部分与最大子段和问题完全无异。
鉴于这题洛谷没有,还是附上完整代码:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define mid (l + r >> 1)
char s[400001];
struct zwy {
long long c, ls, rs;
} tre[1600001];
struct yjx {
zwy up(zwy x, zwy y) {
int temp;
zwy z;
temp = min(x.ls, y.rs);
z.c = x.c + y.c + temp;
z.ls = x.ls + y.ls - temp;
z.rs = x.rs + y.rs - temp;
return z;
}
void build(int k, int l, int r) {
if (l == r) {
if (s[l] == '(')
tre[k].ls += 1;
else
tre[k].rs += 1;
return;
}
build(k << 1, l, mid);
build(k << 1 | 1, mid + 1, r);
tre[k] = up(tre[k << 1], tre[k << 1 | 1]);
}
zwy query(int k, int l, int r, int x, int y) {
long long ret = 0;
if (x <= l && r <= y)
return tre[k];
if (y <= mid)
return query(k << 1, l, mid, x, y);
else {
if (x > mid)
return query(k << 1 | 1, mid + 1, r, x, y);
else
return up(query(k << 1, l, mid, x, y), query(k << 1 | 1, mid + 1, r, x, y));
}
}
} STr;
int main() {
int m, n, i, j, x, y;
long long w;
scanf("%d %d", &n, &m);
scanf("%s", s + 1);
STr.build(1, 1, n);
for (i = 1; i <= m; i++) {
scanf("%d %d", &x, &y);
printf("%lld\n", STr.query(1, 1, n, x, y).c << 1);
}
return 0;
}
总结一下,最大子段和和它的衍生问题(基于基本线段树)的关键主要是两个方面:一是如何更新,这要从左右儿子的特点思考;二是如何处理序列,我们要尽可能使处理出来的序列便于线段树操作,利用线段树的思想去解决问题。
二 基础线段树的灵活运用
所谓基础线段树的灵活运用,归根结底还是在基本的线段树基础上进行,只不过要更灵活的利用线段树,通过优化过程/线段树以外的部分完成任务。
T4 和或异或(难度3)
题意描述:给定一个长度为2的n次方的序列,先对相邻两个数做或操作,所得序列缩短一半,再对相邻两个数做异或操作,以此类推,直到只剩下一个数。
每一次还会把一个数改成另一个数,然后输出上面操作的结果。
这题显然要在树上实现这个合并的过程,因为这个过程实在很像线段树的效果,两个数操作得一个数,就相当于儿子更新成父亲,而更新的规则就是或以及异或。这样一来,我们可以多开一个数组,记录更新规则应该是或还是异或,让父亲的操作从儿子那里传递过来。第一次操作是或,所以记或操作为1,异或操作为0,则有:
op[k] = op[k << 1] ^ 1;
这样一来就解决了。
这题洛谷好像也没,所以还是附上完整代码:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define mid (l + r >> 1)
long long a[1 << 19 + 1], tre[1 << 19 + 1];
bool op[1 << 19 + 1];
struct yjx {
void up(int k) {
op[k] = op[k << 1] ^ 1;//实现传递效果
if (op[k])
tre[k] = tre[k << 1] | tre[k << 1 | 1];
else
tre[k] = tre[k << 1] ^ tre[k << 1 | 1];
}
void build(int k, int l, int r) {
if (l == r) {
tre[k] = a[l];
return;
}
build(k << 1, l, mid);
build(k << 1 | 1, mid + 1, r);
up(k);
}
void modify(int k, int l, int r, int x, long long c) {
if (l == r) {
tre[k] = c;
return;
}
if (x <= mid)
modify(k << 1, l, mid, x, c);
else
modify(k << 1 | 1, mid + 1, r, x, c);
up(k);
}
} STr;
int main() {
int m, n, i, j, x, y;
long long w;
scanf("%d %d", &n, &m);
for (i = 1; i <= (1 << n); i++) scanf("%lld", &a[i]);
STr.build(1, 1, (1 << n));
for (i = 1; i <= m; i++) {
scanf("%d %lld", &x, &w);
STr.modify(1, 1, (1 << n), x, w);
printf("%lld\n", tre[1]);
}
return 0;
}
T5 排序 (洛谷P2824)(难度4.5)
题意概括:给定序列,对序列中的一段作几次升序排序或降序排序,输出经过操作后第x位的数。
题意非常明了,但是这个操作仔细想想就会发现很难实现。如果在树上sort,显然违反了我们总结的“以线段树为基础思想”的理论,而且时间绝对不够用。
那到底怎么做才能使得功能贴近线段树,还能变快?
其实这题是学长当例题讲的,说实话以下的思路是我的话在考场上打死我都想不出来(捂脸)。不得不说确实是一道很有意思的题。
排序操作类似于区间修改操作,最后的输出显然是单点询问操作。从排序的角度考虑,如果我们要区间修改,就不能把原来的数排完了一个一个塞回去。这样的排序在修改里面是nlogn,整个修改是mlog2n,n次是nmlog2n,我们想要的是mlog2n的复杂度。一般的区间修改的复杂度只有logn,所以如果我们只用最一般的区间修改,可以在线段树之外进行nlogn的操作。
这时就产生一个巧妙的想法:既然单个单个的数很难区间修改,可以把这些具体的数抽象为0和1,这样把一段排序,就等价于把从左/右开始的一段序列填充为1,这个长度就是这段区间的和,很方便。为了支持这种功能,总要给0和1一个区别的规则。所以用二分,每次把大于等于mid的变为1,小于的变为0。如果最终x位置上的数是0,说明mid太大,r=mid;另一种情况同理。最后得到的结果就是应该出现在x位置的数,总复杂度正好mlog2n。
思路出来之后,剩下的就好办了:线段树只需维护区间和,实现建树、懒标记传递、修改和更新的基本功能。
这里还要注意一个细节,由于有的时候会把一段修改为0,会出现把懒标记人为赋值为0的情况,但是按我们一般的写法会被打回,无法实现;不打回,本来已经清空的懒标记又会死而复生。所以这里人为把修改为0的懒标记换成-1(其实换成2更好),以避免这种问题。所以要注意:需要人为把懒标记赋值为0,转换懒标记的值。
完整代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define mid (l + r >> 1)
int a[1000001],l_[1000001],r_[1000001],z[1000001];
struct yjx{
int tre[4000001],laz[4000001];
void build(int k,int l,int r,int x){
if(l == r){
tre[k] = a[l] >= x;
laz[k] = 0;
return;
}
build(k << 1,l,mid,x);
build(k << 1 | 1,mid + 1,r,x);
tre[k] = tre[k << 1] + tre[k << 1 | 1];
laz[k] = 0;
//printf("%d %d %d\n",l,r,tre[k]);
}
void push(int k,int l,int r){
if(!laz[k]) return;
laz[k << 1] = laz[k << 1 | 1] = laz[k];
if(laz[k] == -1) tre[k << 1] = tre[k << 1 | 1] = 0;
else tre[k << 1] = mid - l + 1,tre[k << 1 | 1] = r - mid;
laz[k] = 0;
}
void modify(int k,int l,int r,int x,int y,int c){
if(x > r || y < l) return;
if(x <= l && r <= y){
laz[k] = c;
if(!c) laz[k] = -1;
tre[k] = (r - l + 1) * c;
return;
}
push(k,l,r);
if(x <= mid) modify(k << 1,l,mid,x,y,c);
if(y > mid) modify(k << 1 | 1,mid + 1,r,x,y,c);
tre[k] = tre[k << 1] + tre[k << 1 | 1];
}
int query(int k,int l,int r,int x,int y){
if(x <= l && r <= y){
//printf("%d %d %d??\n",l,r,tre[k]);
return tre[k];
}
int ret = 0;
push(k,l,r);
if(x <= mid) ret += query(k << 1,l,mid,x,y);
if(y > mid) ret += query(k << 1 | 1,mid + 1,r,x,y);
//printf("%d %d %d??\n",l,r,ret);
return ret;
}
}STr;
int t_sort(int n,int m,int p){
int i,l = 1,r = n,temp,w;
while(l < r){
w = (l + r + 1) >> 1;
//printf("%d %d %d?\n",l,r,w);
STr.build(1,1,n,w);
for(i = 1;i <= m;i++){
//printf("alpha\n");
//if(l_[i] > r_[i]) continue;
temp = STr.query(1,1,n,l_[i],r_[i]);
//printf("temp=%d\n",temp);
if(!z[i]){
STr.modify(1,1,n,r_[i] - temp + 1,r_[i],1);
STr.modify(1,1,n,l_[i],r_[i] - temp,0);
}
else{
STr.modify(1,1,n,l_[i],l_[i] + temp - 1,1);
STr.modify(1,1,n,l_[i] + temp,r_[i],0);
}
}
if(!STr.query(1,1,n,p,p)) r = w - 1;
else l = w;
//printf("%d %d %d!\n\n",l,r,w);
}
return l;
}
int main(){
int n,m,p,i;
scanf("%d %d",&n,&m);
for(i = 1;i <= n;i++) scanf("%d",&a[i]);
for(i = 1;i <= m;i++) scanf("%d %d %d",&z[i],&l_[i],&r_[i]);
scanf("%d",&p);
printf("%d\n",t_sort(n,m,p));
return 0;
}
虽然框架基本补齐了,其实还没有更完。从这一篇来讲,还有一道子串问题出了点玄学bug而没法拿上来,还有线段树上二分没有学;从长远的来讲,进阶线段树还有非常多内容要学,什么动态开点,主席树,树链剖分之类,这我都还没学。大概到暑假还会再更新吧…总之Thank you for reading!
本文深入探讨线段树的应用技巧,包括最大子段和及其扩展问题,以及基础线段树在不同场景下的灵活运用。通过具体实例讲解如何优化算法,并提供完整的代码实现。
904

被折叠的 条评论
为什么被折叠?



