Preface
本题是2018CCPC吉林站的H题。吉林站是我参加的第一场正式比赛,当时我们写的最后一题就是这道题。我在赛场上想出了正确的大致解法,却因为细节没有想全,没有在赛场中A掉这道题。也是从这次比赛开始,我所在的队伍在拿银的时候,几乎每次离金牌就差一题,而且这一题的算法已经被我们想到,只是没有写完。每场比赛打完,我的感受大多是“要是再写出一题就好了”。
时光飞逝,如今我都快退役了。我还有两场正式比赛要打,而其中一场比赛将在这个周末举办。我想在比赛前临阵磨枪,找一些代码量比较大的题来提升写代码的能力。我想起了这道当时认为比较繁琐的题目。
没想到WA了一次后,这道题就被我十分轻松的A掉了。思路清晰,大脑能量充足的情况下,这道题对我来说竟然算不上一道难题,甚至谈不上一道有代码量的题。我都不知道该怎样评价自己的水平了。该写出题的时候写不出题,训练的时候又看得过去。
不管怎样,我要给这道对我来说意义重大的题写一篇题解。
Description
数轴上有$n$个字符串,每个字符串初始为空。现在要进行$m$次以下操作:
- 操作1:把一个$[l, r]$区间里所有字符串前面加上一位十进制数字,后面加上一位十进制数字。比如”44”这个字符串“加上”5后,就变成了”5445”.
- 操作2:求$[l, r]$区间里所有字符串代表的数字之和。
样例组数$1\leq t\leq5$,长度$1 \leq n \leq 10^5$,操作次数$1 \leq m \leq 10^5$
Sample Input & Output
1 | 2 |
1 | Case 1: |
Solution
看到关于区间的操作、求和,第一反应肯定是线段树。
问题的关键在于,在一个数字前面和最后加了一位,这个操作的实质是什么。
假设当前某个数字是$x$,有$d$位,在它的前后放了一个$y$,那么这步操作的结果是$x \leftarrow y * 10^{d+2} + x \times10 + y$。可以发现,后面的乘法和加法都十分好维护。用两个lazy变量来标记区间该乘和该加的值。每次乘的时候还要改变加的标记。问题的关键是,前面加的那一堆东西和当前数无关,而是和当前数的位数有关。
既然操作和数位数有关,那么维护每个数的位数一定是一个可行的做法。单由于操作是对区间进行操作,一个区间里不同的数有不同的位数,这个东西哪怕可以用vector存下,合并的时间复杂度都会爆炸。有没有一种办法能够合并区间所有数的位数这个信息呢?
观察操作的式子我们可以发现,我们做的所有运算都是加法和乘法。我们不关心区间里每一个数有几位,而是要让这些数的最高位乘上一个操作数$y$再乘100,最后再加到一起。这样想的话,我们可以把每个数的最高位加起来,以获取这个区间所有数的最高位信息。比如一个区间的数是$11,2222,4444$,那么最高数位之和就是$10 + 1000 + 1000 = 2010$。那么对这个区间信息操作时,不再是$x \leftarrow y 10^{d+2} + x \times10 + y$,而是$x \leftarrow y 2010^{d+2} + x \times10 + y$。如果我们能维护一个最高数位和信息,我们就能计算出当前区间的数字和,也就是操作2询问的内容了。
现在问题就变成了如何维护区间的最高数位和及相关lazy变量。为了计算出新的$x$,我们要用$y$来乘最高数位和。由于区间操作要在线段树上下放,而父区间和子区间$y$乘最高数位和的结果是不同的。因此,我们只能每次下放$y$这个操作数。我们需要一个新的lazy变量来储存我们下放的$y$。
光存了$y$不够,因为子区间可能有没有计算过的操作数。比如子区间进行了操作数3的操作,子区间的数字应当是$3x3$,但子区间没有被访问,标记还没有计算过。现在又对整个区间进行了操作数4的操作,子区间的数字现在应当是$43x34$。此时子区间依然没有被访问。也就是说,之前lazy变量是3,经过操作后lazy变量变成了43。
每次进行操作,lazy变量前面都会加一个数。往前面加数这个操作可不好做。但是,我们可以记录这个lazy变量的最大位数。比如lazy变量现在是43,那么最大位数就是2。下次进行操作数5的操作时,$5 \times 10^2 + 43 = 543$,就能正确计算出新的lazy变量了。这个lazy变量的最大位数也是一个新的lazy变量。
所以,所有的变量我们都可以用线段树来维护,这道题就写完了。
Code
1 | struct node |
我一般是不放代码的,但我觉得这段代码写的非常有条理,所以就打算展示一下。
线段树操作的精髓在于node
的操作。不管是怎样的区间操作、查询,所有modify
和query
都可以调同一个格式的函数,只要修改node
中函数的实现细节就可以了。
len
变量不是一个动态的变量,我是在建树的时候把len
算了一下。
markMult
和preMove
似乎是相关的两个变量,但多用一个变量也不会MLE会TLE,用两个变量能够让写代码时思路更加清晰。
Review
写多了线段树的区间操作后,你就能对线段树有更深的认识。线段树本质是维护一些满足幺半群的运算,也就是一些不可逆,互相叠加的运算,比如加法、乘法、求最值。
在对区间进行操作的时候,要想清楚两件事。一个是lazy标记怎么对固有变量产生影响的,一个是操作怎么对lazy标记产生影响的。如果只是简单的加法和乘法,这两个步骤实在是过于显然,以至于问题一变复杂,我们就搞不清应该如何维护lazy变量了。只要认识了区间操作的本质,任何单纯线段树区间操作都是很简单的题。