GSS1 - Can you answer these queries I
题面翻译
给定长度为 n n n 的序列 a 1 , a 2 , ⋯ , a n a_1, a_2,\cdots,a_n a1,a2,⋯,an。现在有 m m m 次询问操作,每次给定 l i , r i l_i,r_i li,ri,查询 [ l i , r i ] [l_i,r_i] [li,ri] 区间内的最大子权和。
区间 [ l , r ] [l,r] [l,r] 的最大子权和被定义为 max { a i + a i + 1 + ⋯ + a j − 1 + a j ∣ l ≤ i ≤ j ≤ r } \max\{a_i+a_{i+1}+\cdots+a_{j-1}+a_j\mid l\le i\le j\le r\} max{ai+ai+1+⋯+aj−1+aj∣l≤i≤j≤r}。
∣ a i ∣ ≤ 15007 |a_i|\le 15007 ∣ai∣≤15007, 1 ≤ n ≤ 5 × 1 0 4 1\le n\le 5\times 10^4 1≤n≤5×104。
题目描述
You are given a sequence A[1], A[2], …, A[N] . ( |A[i]| ≤ 15007 , 1 ≤ N ≤ 50000 ). A query is defined as follows:
Query(x,y) = Max { a[i]+a[i+1]+…+a[j] ; x ≤ i ≤ j ≤ y }.
Given M queries, your program must output the results of these queries.
输入格式
- The first line of the input file contains the integer N.
- In the second line, N numbers follow.
- The third line contains the integer M.
- M lines follow, where line i contains 2 numbers xi and yi.
输出格式
Your program should output the results of the M queries, one query per line.
样例 #1
样例输入 #1
3
-1 2 3
1
1 2
样例输出 #1
2
题目分析
这些信息容易按照区间进行划分和合并,所以可以使用线段树解决,而这道题需要求一个区间内的最长子序列之和。
对于一个区间,它的最长子序列之和有几种情况
如果我们要合并两个区间的话,也有两种情况:
- 左区间(的最长子序列的)右端点和右区间(的最长子序列的)左端点挨着:
这种情况,合并后的最长子序列和
一定是原先两个区间的最长子序列和
之和 - 左区间(的最长子序列的)右端点和右区间(的最长子序列的)左端点不挨着:
这种情况,合并就不能直接相加两端区间的最长子序列和
之和
这种时候又有三种情况:- 只取左区间的数(直接取左区间的
最长子序列和
) - 只取右区间的数(直接取右区间的
最长子序列和
) - 两边都会取
- 只取左区间的数(直接取左区间的
那么我们可以发现,只储存最长子序列和
是无法满足我们合并区间的需求的,所以我们还需要新加一些变量储存其他的数据。
首先,我们需要求第二种合并情况的第三种情况。我们可以看做左区间取包含右端点的最长子序列和加上右区间包含左端点的最长子序列和,所以我们还需要两个变量存这个区间的包含右端点的最长子序列和
、包含左端点的最长子序列和
其次,我们还需要能求包含右端点的最长子序列和
、包含左端点的最长子序列和
那么要区间合并的话,以计算包含左端点的最长子序列和
为例,又有两种情况:
- 只取左区间的数(直接取左区间的
包含左端点的最长子序列和
) - 两个区间都要取,这样的话,左区间的所有数都要算上,再算上右区间的
包含左端点的最长子序列和
)
所以,我们还需要一个储存区间总和
的变量,而求区间总和
是可以只用区间总和
就能得出的变量,所以我们不需要再加新的变量了。
剩下的题解,将用:
sum
代表区间总和
lm
代表包含左端点的最长子序列和
rm
代表包含左端点的最长子序列和
data
代表最长子序列和
接下来,我们先推导pushup()函数:
- 首先是求
sum
:
tree[p].sum=tree[p<<1].sum+tree[p<<1|1].sum
- 然后是求
lm
和rm
:
tree[p].lm=max(tree[p<<1].lm,tree[p<<1].sum+tree[p<<1|1].lm)
tree[p].rm=max(tree[p<<1|1].rm,tree[p<<1].rm+tree[p<<1|1].sum)
- 最后是求
data
:
tree[p].data=max(tree[p<<1].data,max(tree[p<<1|1].data,tree[p<<1].rm+tree[p<<1|1].lm))
然后只用写好普通线段树的建树、查找等函数就好了,用法没有大区别。
这里再详细说说一些区别:
- 查找函数定义成定义tree[]的结构体更好,可以直接返回找到的点,而查找递归时,要分情况讨论,左右是否有儿子
- pushup()也可以把传进来的参数定义为结构体,处理数据也就更加方便
下面是代码(输入有一点不一样,后面还会讲一个小优化):
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 500010;
int n, m, w[N];
struct Node{
int l, r, lmax, rmax, tmax, sum;
} tr[4 * N];
void pushup(Node &u, Node &l, Node &r) //由子区间更新父区间
{
u.sum = l.sum + r.sum; //区间总和
u.lmax = max(l.lmax, l.sum + r.lmax); //前缀和 max(左子区间前缀和,左子区间总和+右子区间前缀和)
u.rmax = max(r.rmax, r.sum + l.rmax); //后缀和 max(右子区间后缀和,右子区间总和+左子区间后缀和)
u.tmax = max(max(l.tmax, r.tmax), l.rmax + r.lmax); //区间最大值 max(左子区间前缀和,右子区间后缀和,(左子区间后缀和+右子区间前缀和))
}
void pushup(int u)
{
pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
}
void build(int u, int l, int r) //建树
{
tr[u].l = l, tr[u].r = r;
if ( l == r )
tr[u].lmax = w[l], tr[u].rmax = w[l], tr[u].sum = w[l], tr[u].tmax = w[l];
else
{
int mid = l + r >> 1;
build(u << 1, l, mid); //递归建左树
build(u << 1 | 1, mid + 1, r); //递归建右树
pushup(u); //更新信息
}
}
void modify(int u, int x, int v) //修改
{
if ( tr[u].l == x && tr[u].r == x )
tr[u].lmax = v, tr[u].rmax = v, tr[u].tmax = v, tr[u].sum = v;
else
{
int mid = tr[u].r + tr[u].l >> 1;
if ( mid >= x ) modify(u << 1, x, v); //往左递归(画图好理解)
else modify(u << 1 | 1, x, v); //往右递归
pushup(u); //修改后更新信息
}
}
Node query(int u, int l, int r) //查询,由于存在跨左右子区间的情况,返回结构体类型,再用pushup计算结果
{
if ( tr[u].l >= l && tr[u].r <= r ) return tr[u]; //如果当前区间在查询区间内
else
{
int mid = tr[u].l + tr[u].r >> 1;
if ( mid >= r ) return query(u << 1, l, r);
else if ( mid < l ) return query(u << 1 | 1, l, r);
else //有跨子区间的情况,处理左右子区间,再用pushup求结果
{
Node left = query(u << 1, l ,r);
Node right = query(u << 1 | 1, l, r);
Node res; //res看作left和right的父区间
pushup(res, left, right);
return res;
}
}
}
int main()
{
cin >> n >> m;
for ( int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
build(1, 1, n);
while ( m -- )
{
int k, x, y;
scanf("%d%d%d", &k, &x, &y);
if ( k == 1 )
{
if ( x > y ) swap(x, y);
printf("%d\n", query(1, x, y).tmax);
}
else modify(1, x, y);
}
return 0;
}
优化
- 我们可以不需要建树,因为总共区间的个数是定值,就是
[1,n]
,那么我们可以事先建一个区间为[1,n]
的线段树,因为总区间是固定的,所以我们每个点的区间也是固定的,那么我们就不需要在结构体里定义l
和r
来记录这个点的区间范围,这样我们只用定义一个结构体的数组,就等于完成了空树的建造,也就是啥都不用做。
而输入就可以直接使用修改点的函数(其实就是偷懒) - 线段树的查找是先序遍历,所以我们找到完全被包含的区间的顺序一定是从左到右的,即我们其实是把要找的区间划分为了若干个区间,然后再合并在一起,而找到这些子区间的顺序就是从左到右的。
那么我们可以定义一个ans
结构体,用于表示目前找到的所有的子区间的合并后的区间,这样我们在查找的时候,找到后可以直接合并ans
和这个区间,而ans
一定在这个区间的左侧。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,x,y,z;
struct node
{
int sum,lm,rm,data;
}tree[200005],ans;
/***计算父节点也可以理解为区间合并***/
void Pushup(node& a,node b,node c)//使用左儿子b和有儿子c更新父节点a的值 或者 将位于左侧的区间b和位于右侧的区间c合并到区间a
{
a.sum=b.sum+c.sum;
a.lm=max(b.lm,b.sum+c.lm);
a.rm=max(c.rm,c.sum+b.rm);
a.data=max(b.data,max(c.data,b.rm+c.lm));
}
/***更改一个点的值***/
void change(int p,int z,int x,int l,int r)//当前点p,修改目标z,修改数值x,点p的区间范围[l,r]
{
if(l==r){tree[p].sum=tree[p].lm=tree[p].rm=tree[p].data=x;return ;}//如果是叶节点,全部改为x
int mid=l+r>>1;//找到中点
if(z<=mid) change(p<<1,z,x,l,mid);//如果在左边
else change(p<<1|1,z,x,mid+1,r);//如果在右边
Pushup(tree[p],tree[p<<1],tree[p<<1|1]);//求该点
}
/***查询一个区间内的最短子序列和***/
void ask(int p,int l,int r,int ql,int qr)//当前点p,点p的区间范围[l,r],查询区间范围[ql,qr]
{
/***
线段树查找是先序遍历,所以查找到的完全覆盖区间一定是从左到右
ans代表目前找到的所有完全覆盖区间的合并结果
所以新找到的区间一定在ans区间的右侧,所以可以直接合并,不需要讨论
***/
if(ql<=l&&r<=qr){Pushup(ans,ans,tree[p]);return ;}//如果完全覆盖,直接合并
int mid=l+r>>1;//求中点
if(mid>=ql) ask(p<<1,l,mid,ql,qr);//如果包含左边
if(mid<qr) ask(p<<1|1,mid+1,r,ql,qr);//如果包含右边
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)//不建树,看作刚开始有一颗全是0的树,然后进行改点
{
cin>>x;
change(1,i,x,1,n);//从父节点1开始,改第i个点,改为1,父节点1的区间范围[1,n]
}
cin>>m;
while(m--)
{
cin>>x>>y>>z;
if(x)//求区间最长子序列和
{
ans.data=ans.sum=ans.lm=ans.rm=-0x3f3f3f3f;//初始化ans
ask(1,1,n,y,z);//从父节点1开始,父节点1的区间范围[1,n],查找区间范围[y,z]
cout<<ans.data<<endl;
}
else change(1,y,z,1,n);//从父节点1开始,修改位置y,修改数值z,父节点1的区间范围[1,n]
}
return 0;
}