P7402 [COCI 2020/2021 #5] Sjeckanje 解题报告
P7402 [COCI 2020/2021 #5] Sjeckanje 解题报告
〇、写在前面
这篇解题报告旨在用最直观的方式,帮助你理解这道题从思索到解决的全过程。我们将从问题的核心难点出发,一步步将它转化为一个我们熟悉且可以解决的模型。
一、题目想让我们做什么?
首先,我们快速回顾一下题目要求:
- 有一个整数数组
a
。 - 要对它进行
q
次修改,每次都是给一个区间[l, r]
里的所有数加上x
。 - 每次修改后,都要回答一个问题:把当前的数组
a
切分成若干个连续的小段,每一段的价值是(这段的最大值 - 这段的最小值),我们希望所有小段的价值之和最大。求这个最大值。
举个例子: 数组是 a = [4, 3, 3, 4]
。
一种切分方法是 [4, 3], [3, 4]
。
- 第一段
[4, 3]
的价值是max(4, 3) - min(4, 3) = 4 - 3 = 1
。 - 第二段
[3, 4]
的价值是max(3, 4) - min(3, 4) = 4 - 3 = 1
。 - 总价值是
1 + 1 = 2
。
我们需要找到一种切分方法,让这个总价值最大。
二、第一步:简化问题——引入“差分”
题面中的“区间加法”是一个非常经典的信号。处理这类问题,一个强大的工具是差分数组。
我们定义一个差分数组 d
,其中 d[i] = a[i] - a[i-1]
(我们让 d[1] = a[1]
,不过实际上 d[2]
到 d[n]
才是我们关心的)。
为什么要用差分?
因为对原数组 a
的区间 [l, r]
加上 x
,在差分数组 d
上只会影响两个点:
d[l]
会增加x
(因为a[l]
增加了x
,而a[l-1]
不变)。d[r+1]
会减少x
(因为a[r]
增加了x
,而a[r+1]
不变,所以a[r+1]-a[r]
减少了x
)。
这样,复杂的“区间修改”就变成了简单的“单点修改”,问题处理起来就方便多了。
三、第二步:找到价值的来源——差分与价值的关系
现在,我们的操作简化了,但目标(最大化所有分段的价值之和)和差分数组有什么关系呢?
我们来看一段单调的区间,比如 [2, 5, 8, 10]
。
它的价值是 10 - 2 = 8
。
我们看看它的差分值(只看相邻的):5-2=3
, 8-5=3
, 10-8=2
。这些差分值都是正数。
它们的和是 3 + 3 + 2 = 8
。
这和区间的价值相等!
再看一个单调递减的区间 [10, 5, 2, 1]
。
它的价值是 10 - 1 = 9
。
它的差分值是:5-10=-5
, 2-5=-3
, 1-2=-1
。这些都是负数。
它们绝对值的和是 |-5| + |-3| + |-1| = 5 + 3 + 1 = 9
。
这也和区间的价值相等!
核心发现:
对于一个单调的区间 [l, r]
,它的价值 max - min
就等于区间内所有相邻元素之差的绝对值之和,也就是 sum(|d[i]|)
(i 从 l+1
到 r
)。
那如果区间不单调呢?比如 [2, 8, 5]
。
- 它的价值是
8 - 2 = 6
。 - 差分是
d_1 = 8-2 = 6
,d_2 = 5-8 = -3
。 |d_1| + |d_2| = 6 + 3 = 9
。6 < 9
。
如果我们把它切分成单调的两段[2, 8]
和[5]
,总价值是(8-2) + (5-5) = 6
。
如果我们把它切分成[2]
和[8, 5]
, 总价值是(2-2) + (8-5) = 3
。
这启发我们:为了让总价值最大,我们应该尽可能地把数组切分成一个个单调的小段。因为在单调小段内,价值可以完全由差分数组的绝对值累加得到。如果一个段不单调,它的价值会小于其内部差分绝对值的和,造成了“浪费”。
结论: 问题的目标,可以转化为最大化我们选择的 |d[i]|
之和。我们应该在差分值符号变化的地方(比如从正数变为负数)进行切分,这样每个小段内部的差分值符号都相同(或为0),保证了每个小段都是单调的。
所以,最终的总价值,似乎就是把所有差分值 d[i]
(从 i=2 到 n) 的绝对值加起来?
等等,这也不完全对。比如 [2,0,2,1]
,差分是 [-2, 2, -1]
。|-2|+|2|+|-1| = 5
。但最优切分是 [2,0,2], [1]
,价值是 (2-0) + (1-1) = 2
。
问题出在哪里?当我们因为 d[i]
和 d[i+1]
符号不同而被迫切分时,我们损失了什么?
这个“损失”发生在 a[i]
这个点上。比如 a[i-1], a[i], a[i+1]
,如果 d[i]>0
而 d[i+1]<0
,说明 a[i]
是一个局部最高点。我们无法同时“上坡”又“下坡”。这意味着 d[i]
和 d[i+1]
不能被算在同一个单调段的价值里。我们必须在它们之间做出选择。
这引出了我们的最终模型:一个在差分数组上的动态规划。
四、第三步:最终兵器——线段树动态规划
我们面对的是:
- 对差分数组
d
的单点修改。 - 每次修改后,查询一个全局的最优解。
“单点修改,区间(全局也是一种区间)查询” -> 线段树!
线段树的每个节点,要维护其代表的差分区间 d[L...R]
的信息。我们需要什么信息,才能从子节点合并出父节点的信息呢?
这就是动态规划(DP)的部分了。对于一个区间 d[L...R]
,它的最优解可能受到区间外的影响。具体来说,d[L]
是否需要和左边的 d[L-1]
连接成一个单调段?d[R]
是否需要和右边的 d[R+1]
连接?
这提示我们,每个线段树节点 p
需要存储 4 个值:
sgt[p][左端点状态][右端点状态]
- 左/右端点状态:
0
表示不与外部连接(即此处为切分点),1
表示需要与外部连接。 - 值:在该状态下,这个区间能产生的最大价值。
如何合并(Updata
函数)?
假设我们要合并左孩子 lc
(代表 d[L...mid]
)和右孩子 rc
(代表 d[mid+1...R]
)来得到父节点 p
的信息。关键点在于 d[mid]
和 d[mid+1]
的关系。
-
和谐情况:
d[mid] * d[mid+1] >= 0
它们的符号相同(或有0,0和任何符号都“和谐”)。这意味着从a[mid-1]
到a[mid]
再到a[mid+1]
的趋势是连贯的(一直是上坡或一直是下坡)。
在这种情况下,把它们连接起来一定比不连接更优。因为连接起来可以把|d[mid]|
和|d[mid+1]|
的贡献都算上,而不连接则可能损失其中一个。
所以,父区间的状态[i][j]
就直接由子区间必须连接的状态[i][1]
和[1][j]
相加得到:
sgt[p][i][j] = sgt[lc][i][1] + sgt[rc][1][j]
-
冲突情况:
d[mid] * d[mid+1] < 0
它们的符号相反。这意味着在a[mid]
这里趋势发生了逆转(比如上坡后接着下坡)。d[mid]
和d[mid+1]
无法被包含在同一个单调段里。
我们必须在这里“切一刀”。但这一刀有两种切法:- 保留
d[mid]
的贡献,放弃d[mid+1]
(在连接处)。对应策略是:左半段要连接到mid
,右半段从mid+1
重新开始。价值为sgt[lc][i][1] + sgt[rc][0][j]
。 - 放弃
d[mid]
的贡献,保留d[mid+1]
。对应策略是:左半段在mid
处断开,右半段从mid+1
连接过来。价值为sgt[lc][i][0] + sgt[rc][1][j]
。
我们取这两种策略中更好的那个:
sgt[p][i][j] = max(sgt[lc][i][1] + sgt[rc][0][j], sgt[lc][i][0] + sgt[rc][1][j])
- 保留
最终答案
每次修改完 d
数组上的两个点后,整个数组 a
的最大划分价值就是线段树根节点 root
的 sgt[root][1][1]
。
为什么是 [1][1]
?因为对于整个 d[2...n]
区间,它的两端没有“外部”了,我们默认它们是连接的(即不作为切分点),这样能包含 d[2]
和 d[n]
的贡献,肯定比不包含更优。
五、代码实现细节
- 数据范围:
n, q
很大,价值可能超过int
范围,记得开long long
。 - 差分数组:我们关心的是
d[2]
到d[n]
,所以线段树建在[2, n]
这个区间上。 - 建树:可以在一开始
O(N)
建树,也可以像题解代码一样,在读入时,每次读入一个a[i]
就计算d[i]
,然后在线段树上做一次单点修改。后者代码更简洁,虽然理论上慢一点,但对于本题足够了。 - 修改:
l, r, x
的修改,变成d[l] += x
和d[r+1] -= x
。- 注意边界:如果
l=1
,d[l]
不在我们[2,n]
的差分数组里,不用管。如果r=n
,d[r+1]
超出范围了,也不用管。 - 每次修改
d[k]
后,在线段树上更新位置k
即可。
至此,我们就把一个复杂的问题,通过差分转化、价值分析、线段树DP,一步步地解决了。希望这份报告能帮助你彻底理解这道题的精髓!