洛谷 GSS1 - Can you answer these queries I

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++aj1+ajlijr}

∣ a i ∣ ≤ 15007 |a_i|\le 15007 ai15007 1 ≤ n ≤ 5 × 1 0 4 1\le n\le 5\times 10^4 1n5×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

题目分析

这些信息容易按照区间进行划分和合并,所以可以使用线段树解决,而这道题需要求一个区间内的最长子序列之和。

对于一个区间,它的最长子序列之和有几种情况

包含最左端
包含最右端
不含任意端点
全序列
如果我们要合并两个区间的话,也有两种情况:

  1. 左区间(的最长子序列的)右端点和右区间(的最长子序列的)左端点挨着:
    中间较粗的线是边界,左边是左区间,右边是右区间
    这种情况,合并后的最长子序列和一定是原先两个区间的最长子序列和之和
  2. 左区间(的最长子序列的)右端点和右区间(的最长子序列的)左端点不挨着:
    一个颜色代表一种情况
    这种情况,合并就不能直接相加两端区间的最长子序列和之和
    这种时候又有三种情况:
    • 只取左区间的数(直接取左区间的最长子序列和
    • 只取右区间的数(直接取右区间的最长子序列和
    • 两边都会取

那么我们可以发现,只储存最长子序列和是无法满足我们合并区间的需求的,所以我们还需要新加一些变量储存其他的数据。

首先,我们需要求第二种合并情况的第三种情况。我们可以看做左区间取包含右端点的最长子序列和加上右区间包含左端点的最长子序列和,所以我们还需要两个变量存这个区间的包含右端点的最长子序列和包含左端点的最长子序列和

其次,我们还需要能求包含右端点的最长子序列和包含左端点的最长子序列和
那么要区间合并的话,以计算包含左端点的最长子序列和为例,又有两种情况:

  1. 只取左区间的数(直接取左区间的包含左端点的最长子序列和
  2. 两个区间都要取,这样的话,左区间的所有数都要算上,再算上右区间的包含左端点的最长子序列和

所以,我们还需要一个储存区间总和的变量,而求区间总和是可以只用区间总和就能得出的变量,所以我们不需要再加新的变量了。

剩下的题解,将用:

  • sum代表区间总和
  • lm代表包含左端点的最长子序列和
  • rm代表包含左端点的最长子序列和
  • data代表最长子序列和

接下来,我们先推导pushup()函数:

  1. 首先是求sum
    tree[p].sum=tree[p<<1].sum+tree[p<<1|1].sum
  2. 然后是求lmrm:
    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)
  3. 最后是求data
    tree[p].data=max(tree[p<<1].data,max(tree[p<<1|1].data,tree[p<<1].rm+tree[p<<1|1].lm))

然后只用写好普通线段树的建树、查找等函数就好了,用法没有大区别。

这里再详细说说一些区别:

  1. 查找函数定义成定义tree[]的结构体更好,可以直接返回找到的点,而查找递归时,要分情况讨论,左右是否有儿子
  2. 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. 我们可以不需要建树,因为总共区间的个数是定值,就是[1,n],那么我们可以事先建一个区间为[1,n]的线段树,因为总区间是固定的,所以我们每个点的区间也是固定的,那么我们就不需要在结构体里定义lr来记录这个点的区间范围,这样我们只用定义一个结构体的数组,就等于完成了空树的建造,也就是啥都不用做。
    而输入就可以直接使用修改点的函数 (其实就是偷懒)
  2. 线段树的查找是先序遍历,所以我们找到完全被包含的区间的顺序一定是从左到右的,即我们其实是把要找的区间划分为了若干个区间,然后再合并在一起,而找到这些子区间的顺序就是从左到右的。
    那么我们可以定义一个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;
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值