背景
华为挑战赛第二场ICPC赛制训练赛的D题。
比赛的时候没做出来,但是有一个接近正解的方向了。
下来学习了一下,发现这道题拓宽了我在数据结构题方面的思路,有必要总结一下。
题目描述
给定一维数轴上的 n n n 条线段,每条线段形如 [ l u , r u ] [l_u,r_u] [lu,ru] ,线段长度是 r u − l u r_u-l_u ru−lu 。
n n n 条线段分别用 I 1 , I 2 . . . I n I_1,I_2 ... I_n I1,I2...In 表示。
定义函数 f ( L , R ) f(L,R) f(L,R) ,表示下标在 [ L , R ] [L,R] [L,R] 的所有线段(也就是线段 I L , I L + 1 . . . I R − 1 , I R I_L,I_{L+1}...I_{R-1},I_R IL,IL+1...IR−1,IR )覆盖的总长度(重叠部分的长度只计算一次)。
接下来有 m m m 组询问,每次一个区间 [ A , B ] [A,B] [A,B] ,询问 [ A , B ] [A,B] [A,B] 的所有子区间 [ L , R ] [L,R] [L,R] 的 f ( L , R ) f(L,R) f(L,R) 值之和除以 [ A , B ] [A,B] [A,B] 子区间个数的结果,答案对 998244353 998244353 998244353 取模,也就是求 ∑ A ≤ L ≤ R ≤ B f ( L , R ) ( A − B + 1 ) ( A − B + 2 ) 2 m o d 998244353 \frac{\sum_{A\leq L\leq R\leq B}f(L,R)}{\frac{(A-B+1)(A-B+2)}{2}} \bmod 998244353 2(A−B+1)(A−B+2)∑A≤L≤R≤Bf(L,R)mod998244353 。
允许离线,这恰恰是解题的关键之一。
题目分析与做法:
这种询问里面区间套子区间的情况对我来说还是比较陌生的,但我印象里之前打ECfinal的时候也有一道类似的题,因此不得不重视起来。
对于这样的问题,一般情况下,我们可以考虑两种方向:
- 线段树方向:能够把一个不断变化的序列多次完整的加在一个累加序列上的线段树。
- 树状数组方向:能够把查询转化为基于某个数列的高维求和,然后用树状数组维护。
入手:
如何解决一个区间的 f ( L , R ) f(L,R) f(L,R) 值?解决了这个问题,再去谈如何解决一个区间所有子区间的答案之和。
我们考虑这样一个过程:从第 1 1 1 个到第 n n n 个,逐个逐个的把线段 I u I_u Iu 涂在数轴上,每次涂的时候新涂的线段会覆盖之前已经涂过的线段。在这个过程中,我们动态维护每条线段暴露在最表层的总长度,具体的,当前第 i i i 条线段暴露在最表层的总长度用 c i c_i ci 表示。那么,区间 [ L , R ] [L,R] [L,R] 的 f ( L , R ) f(L,R) f(L,R) 值就是在线段 I R I_R IR 刚刚涂上之后, ∑ i = L R c i \sum_{i=L}^{R} c_i ∑i=LRci 的值。
那这个 c i c_i ci 怎么维护呢?其实就是每次涂上一条线段的时候需要找到每段其他的线段被这个线段覆盖的长度,这个可以用势能分析线段树解决,在区间赋值的时候递归直到节点是一个完整颜色区间,做处理就可以了,复杂度 O ( n log n ) O(n\log n) O(nlogn) (tag的处理可自行分析一下)。
当然,为了方便对 c i c_i ci 做求和值,还需要用另外的数据结构对它做维护,因此修改 c i c_i ci 需要看作 O ( log n ) O(\log n) O(logn) 的,这样实际上总的处理是 O ( n log 2 n ) O(n\log ^2n) O(nlog2n) 的。
至此,一个区间的 f ( L , R ) f(L,R) f(L,R) 值已经解决了。
接下来分别介绍两种做法。
线段树:
考虑一下,如何在 R R R 已经确定的情况下求解 ∑ i = L R f ( i , R ) \sum_{i=L}^Rf(i,R) ∑i=LRf(i,R) :
我们观察在这个值中,每个 c i c_i ci 对答案贡献了 ( i − L + 1 ) (i-L+1) (i−L+1) 次,因此这个值也等于 ∑ i = L R c i ( i − L + 1 ) \sum_{i=L}^Rc_i (i-L+1) ∑i=LRci(i−L+1) ,如果我们用两个数据结构维护分别维护 i × c i i\times c_i i×ci 和 c i c_i ci 的区间求和,以及单点修改,我们就能够在 O ( log n ) O(\log n) O(logn) 的时间复杂度查询出 ∑ i = L R c i \sum_{i=L}^Rc_i ∑i=LRci 和 ∑ i = L R i × c i \sum_{i=L}^Ri\times c_i ∑i=LRi×ci ,然后把前者乘上常数 ( 1 − L ) (1-L) (1−L) 再求和就求出来这个值了。
接下来考虑求 [ L , R ] [L,R] [L,R] 内所有子区间的值,也就是 ∑ i = L R ∑ j = i R f ( i , j ) \sum_{i=L}^R\sum_{j=i}^Rf(i,j) ∑i=LR∑j=iRf(i,j):
这个查询也可以看作对一系列右端点确定的询问的求和,可以写作:
∑
j
=
L
R
∑
i
=
L
j
f
(
i
,
j
)
\sum_{j=L}^R\sum_{i=L}^jf(i,j)
j=L∑Ri=L∑jf(i,j)
结合我们上面的转化,这个值也就可以写作:
∑
j
=
L
R
∑
i
=
L
j
c
j
i
(
i
−
L
+
1
)
\sum_{j=L}^R\sum_{i=L}^j c_{ji}(i-L+1)
j=L∑Ri=L∑jcji(i−L+1)
这里为什么用
c
j
i
c_{ji}
cji 表示呢?因为每当我们处理完一个新的右端点,
c
i
c_i
ci 序列会发生变化,所以用
c
j
i
c_{ji}
cji 表示第
j
j
j 个线段刚刚加入后的
c
i
c_i
ci 序列。值的注意的一点,每次
c
i
c_i
ci 序列经过了大概
O
(
log
n
)
O(\log n)
O(logn) 次的单点修改得到了下一个版本的
c
i
c_i
ci 序列。我们为了能够在当前
R
R
R 版本下求出之前每个版本的求和信息的和,我们可以维护一个能够实现序列累加的线段树。
也就是说,维护 c i c_i ci 的线段树需要维护两个域:第一个是 c i c_i ci 域,随着 R R R 的增长,每次会进行单点修改生成新的 c i c_i ci 序列。第二个是 ∑ j = 1 R c j i \sum_{j=1}^R c_{ji} ∑j=1Rcji 域,线段树的第 i i i 个位置存的是当前所有版本的 c i c_i ci 的累加。每次为了维护这个累加域,都需要执行一次区间操作,把第一个域当前的序列对应加一次在第二个域上。这个可以用 lazytag 的方式, O ( log n ) O(\log n) O(logn)实现。
这样我们在第二个域上进行区间求和,就可以 O ( log n ) O(\log n) O(logn) 查出 ∑ i = L R ∑ j = 1 R c j i \sum_{i=L}^R\sum_{j=1}^Rc_{ji} ∑i=LR∑j=1Rcji 的值,注意一点,因为对于任意的 j < i , c j i = 0 j<i,c_{ji}=0 j<i,cji=0 ,所以 ∑ i = L R ∑ j = 1 R c j i = ∑ i = L R ∑ j = i R c j i \sum_{i=L}^R\sum_{j=1}^Rc_{ji} = \sum_{i=L}^R\sum_{j=i}^Rc_{ji} ∑i=LR∑j=1Rcji=∑i=LR∑j=iRcji。
同理,第二棵线段树也维护两个域,就能让我们 O ( log n ) O(\log n) O(logn) 查出 ∑ i = L R ∑ j = 1 R i × c j i \sum_{i=L}^R\sum_{j=1}^R i\times c_{ji} ∑i=LR∑j=1Ri×cji 的值。
查出这两个值,在乘上系数,加一下,就是我们要求的查询值了。
也就是:
(
1
−
L
)
(
∑
i
=
L
R
∑
j
=
i
R
c
j
i
)
+
(
∑
i
=
L
R
∑
j
=
i
R
i
×
c
j
i
)
=
∑
i
=
L
R
∑
j
=
i
R
c
j
i
(
i
−
L
+
1
)
=
∑
j
=
L
R
∑
i
=
L
j
c
j
i
(
i
−
L
+
1
)
(1-L)(\sum_{i=L}^R\sum_{j=i}^R c_{ji}) + (\sum_{i=L}^R\sum_{j=i}^R i\times c_{ji}) =\sum_{i=L}^R\sum_{j=i}^R c_{ji}(i-L+1) \\ =\sum_{j=L}^R\sum_{i=L}^j c_{ji}(i-L+1)
(1−L)(i=L∑Rj=i∑Rcji)+(i=L∑Rj=i∑Ri×cji)=i=L∑Rj=i∑Rcji(i−L+1)=j=L∑Ri=L∑jcji(i−L+1)
所以这道题解决完毕。
因为有下面的树状数组做法,所以我没有写上述做法的代码,但是据说 riteme 比赛的时候是这么写就过了的。
树状数组:
树状数组高维求和:
有一个著名的双树状数组解决区间求和区间查询问题的例子:
相信大家都比较熟悉:维护一个数据结构支持对序列 a i a_i ai 进行区间加上同一个值,并且支持查询区间和。
考虑差分数组
b
i
=
a
i
−
a
i
−
1
b_i = a_i -a_{i-1}
bi=ai−ai−1,我们看一下一次区间求和用数组
b
i
b_i
bi 表示是什么样子的:
∵
a
i
=
∑
j
=
1
i
b
j
∑
i
=
L
R
a
i
=
∑
i
=
L
R
∑
j
=
1
i
b
j
=
(
R
−
L
+
1
)
∑
j
=
1
L
−
1
b
j
+
∑
j
=
L
R
b
j
(
R
−
j
+
1
)
=
(
R
−
L
+
1
)
(
∑
j
=
1
L
−
1
b
j
)
+
(
R
+
1
)
(
∑
j
=
L
R
b
j
)
−
(
∑
j
=
L
R
j
×
b
j
)
\because a_i = \sum_{j=1}^i b_j \\ \sum_{i=L}^Ra_i=\sum_{i=L}^R\sum_{j=1}^i b_j=(R-L+1)\sum_{j=1}^{L-1}b_j \ \ +\ \ \sum_{j=L}^R b_j(R-j+1) \\ =(R-L+1)(\sum_{j=1}^{L-1}b_j)+(R+1)(\sum_{j=L}^R b_j)-(\sum_{j=L}^R j\times b_j)
∵ai=j=1∑ibji=L∑Rai=i=L∑Rj=1∑ibj=(R−L+1)j=1∑L−1bj + j=L∑Rbj(R−j+1)=(R−L+1)(j=1∑L−1bj)+(R+1)(j=L∑Rbj)−(j=L∑Rj×bj)
这样我们把原来的区间求和问题转化成了差分序列的高维度求和问题,我们用两个树状数组,每次区间修改的时候分别单点维护
b
i
b_i
bi 序列和
i
×
b
i
i\times b_i
i×bi 序列,询问的时候直接区间求和查出来,再做一点运算就可以求出答案了。
那么这道题也可以采用这种思想。
在讨论上一种做法的时候,我们用到了 c j i c_{ji} cji 序列,其中 c j i c_{ji} cji 表示的是刚刚添加完毕第 j j j 个线段的时候,此时第 i i i 条线段暴露在表面的总长度。而我们知道,这个 c j i c_{ji} cji 是在 c ( j − 1 ) i c_{(j-1)i} c(j−1)i 的基础上加上变化量得到的。
那我们不妨设 d j i = c j i − c ( j − 1 ) i d_{ji} = c_{ji}-c_{(j-1)i} dji=cji−c(j−1)i,然后考虑重写我们的查询答案。
同样我们可以知道 j < i j<i j<i 时 d j i = 0 d_{ji}=0 dji=0,这有助于我们化简。
注意,我们的查询一定是刚刚处理到第
R
R
R 条线段,然后给出一个
L
L
L 常数,然后去查
∑
i
=
L
R
∑
j
=
i
R
f
(
i
,
j
)
\sum_{i=L}^R\sum_{j=i}^Rf(i,j)
∑i=LR∑j=iRf(i,j) 的值。
∑
i
=
L
R
∑
j
=
i
R
f
(
i
,
j
)
=
∑
j
=
L
R
∑
i
=
L
j
c
j
i
(
i
−
L
+
1
)
=
∑
j
=
L
R
∑
i
=
L
j
∑
k
=
1
j
d
k
i
(
i
−
L
+
1
)
\sum_{i=L}^R\sum_{j=i}^Rf(i,j) =\sum_{j=L}^R\sum_{i=L}^j c_{ji}(i-L+1)=\sum_{j=L}^R\sum_{i=L}^j \sum_{k=1}^j d_{ki}(i-L+1)
i=L∑Rj=i∑Rf(i,j)=j=L∑Ri=L∑jcji(i−L+1)=j=L∑Ri=L∑jk=1∑jdki(i−L+1)
先利用
k
<
i
k<i
k<i 时
d
k
i
=
0
d_{ki}=0
dki=0 的条件改写一下得到:
∑
j
=
L
R
∑
i
=
L
j
∑
k
=
i
j
d
k
i
(
i
−
L
+
1
)
\sum_{j=L}^R\sum_{i=L}^j \sum_{k=i}^j d_{ki}(i-L+1)
j=L∑Ri=L∑jk=i∑jdki(i−L+1)
然后我们把
j
j
j 换到最里面去:
=
∑
i
=
L
R
∑
j
=
i
R
∑
k
=
i
j
d
k
i
(
i
−
L
+
1
)
=
∑
i
=
L
R
∑
k
=
i
R
∑
j
=
k
R
d
k
i
(
i
−
L
+
1
)
=\sum_{i=L}^R\sum_{j=i}^R \sum_{k=i}^j d_{ki}(i-L+1)=\sum_{i=L}^R\sum_{k=i}^R \sum_{j=k}^R d_{ki}(i-L+1)
=i=L∑Rj=i∑Rk=i∑jdki(i−L+1)=i=L∑Rk=i∑Rj=k∑Rdki(i−L+1)
发现
j
j
j 的求和直接就是一个常数了,于是可以写作:
∑
i
=
L
R
∑
k
=
i
R
d
k
i
(
i
−
L
+
1
)
(
R
−
k
+
1
)
\sum_{i=L}^R\sum_{k=i}^R d_{ki}(i-L+1)(R-k+1)
i=L∑Rk=i∑Rdki(i−L+1)(R−k+1)
我们发现这里面常数是
L
,
R
L,R
L,R 变量是
i
,
k
i,k
i,k 于是根据变量的多项式分项得到:
∑
i
=
L
R
∑
k
=
i
R
(
1
−
L
)
(
R
+
1
)
d
k
i
+
(
R
+
1
)
i
⋅
d
k
i
+
(
L
−
1
)
k
⋅
d
k
i
−
i
k
⋅
d
k
i
\sum_{i=L}^R \sum_{k=i}^R (1-L)(R+1)d_{ki}+(R+1)i\cdot d_{ki}+(L-1)k\cdot d_{ki} - ik\cdot d_{ki}
i=L∑Rk=i∑R(1−L)(R+1)dki+(R+1)i⋅dki+(L−1)k⋅dki−ik⋅dki
再利用
k
<
i
k<i
k<i 时
d
k
i
=
0
d_{ki}=0
dki=0 的条件改写一下得到:
∑
i
=
L
R
∑
k
=
1
R
(
1
−
L
)
(
R
+
1
)
d
k
i
+
(
R
+
1
)
i
⋅
d
k
i
+
(
L
−
1
)
k
⋅
d
k
i
−
i
k
⋅
d
k
i
\sum_{i=L}^R \sum_{k=1}^R (1-L)(R+1)d_{ki}+(R+1)i\cdot d_{ki}+(L-1)k\cdot d_{ki} - ik\cdot d_{ki}
i=L∑Rk=1∑R(1−L)(R+1)dki+(R+1)i⋅dki+(L−1)k⋅dki−ik⋅dki
现在 我们令
s
i
=
∑
k
=
1
R
d
k
i
t
i
=
∑
k
=
1
R
k
⋅
d
k
i
S
i
=
i
⋅
∑
k
=
1
R
d
k
i
=
i
⋅
s
i
T
i
=
i
⋅
∑
k
=
1
R
k
⋅
d
k
i
=
i
⋅
t
i
s_i = \sum_{k=1}^R d_{ki}\\ t_i = \sum_{k=1}^R k\cdot d_{ki} \\ S_i= i\cdot \sum_{k=1}^R d_{ki} = i\cdot s_i\\ T_i= i\cdot \sum_{k=1}^R k\cdot d_{ki} =i\cdot t_i\\
si=k=1∑Rdkiti=k=1∑Rk⋅dkiSi=i⋅k=1∑Rdki=i⋅siTi=i⋅k=1∑Rk⋅dki=i⋅ti
那么所求答案值就可以写作:
(
∑
i
=
L
R
(
1
−
L
)
(
R
+
1
)
s
i
)
+
(
∑
i
=
L
R
(
R
+
1
)
S
i
)
+
(
∑
i
=
L
R
(
L
−
1
)
t
i
)
−
(
∑
i
=
L
R
T
i
)
=
(
1
−
L
)
(
R
+
1
)
(
∑
i
=
L
R
s
i
)
+
(
R
+
1
)
(
∑
i
=
L
R
S
i
)
+
(
L
−
1
)
(
∑
i
=
L
R
t
i
)
−
(
∑
i
=
L
R
T
i
)
(\sum_{i=L}^R (1-L)(R+1)s_i)+(\sum_{i=L}^R(R+1)S_i)+(\sum_{i=L}^R(L-1)t_i) - (\sum_{i=L}^RT_i)\\ = (1-L)(R+1)(\sum_{i=L}^R s_i)+(R+1)(\sum_{i=L}^RS_i)+(L-1)(\sum_{i=L}^Rt_i) - (\sum_{i=L}^RT_i)\\
(i=L∑R(1−L)(R+1)si)+(i=L∑R(R+1)Si)+(i=L∑R(L−1)ti)−(i=L∑RTi)=(1−L)(R+1)(i=L∑Rsi)+(R+1)(i=L∑RSi)+(L−1)(i=L∑Rti)−(i=L∑RTi)
也就是上面四个序列的区间求和再乘一乘常数在相加即可。
所以我们用四个树状数组维护上面四个序列。
但修改时怎么修改呢?我们可以看到每个序列的单个值都是一个从 1 1 1 加到当前添加的第 R R R 条线段的 R R R 的求和。所以当 R R R 增加时,我们对于四个序列所有个元素都要加上一个和 d R i d_{Ri} dRi 有关的值。但是,我们已经分析过了, d R i d_{Ri} dRi 其实就是第 R R R 条线段添加时对第 i i i 条线段暴露长度的影响值,纵观整个过程, d r i , ( 1 ≤ r ≤ n ) d_{ri},(1\leq r \leq n) dri,(1≤r≤n) 中不为 0 0 0 的情形是不会超过 O ( n log n ) O(n\log n) O(nlogn) 个的,我们在最开始提到的势能分析线段树中,在修改某个完整节点的 tag 的时候,就可以维护与 d R i d_{Ri} dRi 相关的值了。而且每次只会对一个 i i i 进行修改,所以是单点修改。
因此用四个单点修改区间求和的树状数组就可以解决这道题了。
参考代码(树状数组做法)
#include <map>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define MAXN 200100
#define MOD 998244353
#define LL long long
using namespace std;
int n,m;
int stk[MAXN*2],cnt;
int b[MAXN*2];
map<int,int> h;
struct Itv{
int pl,pr;
int l,r;
int id;
int ans;
}a[MAXN],q[MAXN];
bool cmp(Itv x,Itv y )
{
return x.r<y.r;
}
bool cmp_id(Itv x,Itv y)
{
return x.id<y.id;
}
inline void Mod(int &x)
{
if(x>=MOD)x-=MOD;
if(x<=-MOD)x+=MOD;
}
int qpow(int x,int y)
{
x%=MOD;
int ret=1;
while(y)
{
if(y&1)
ret=1ll*ret*x%MOD;
x=1ll*x*x%MOD;
y>>=1;
}
return ret;
}
struct BIT{
int q[MAXN];
BIT(){memset(q,0,sizeof(q));}
void add(int loc,int v)
{
for(int i=loc;i<MAXN;i+=i&-i)
{
q[i]+=v;
Mod(q[i]);
}
}
int sum(int x)
{
int ret=0;
for(int i=x;i;i-=i&-i)
{
ret+=q[i];Mod(ret);
}
return ret;
}
}s,t,isi,iti;
void Add(int loc,int v,int r)
{
s.add(loc,v);
t.add(loc,1ll*r*v%MOD);
isi.add(loc,1ll*loc*v%MOD);
iti.add(loc,1ll*r*v%MOD*loc%MOD);
}
int Sum(int l,int r)
{
return ((1ll*(isi.sum(r)-isi.sum(l-1))*(r+1) - (iti.sum(r)-iti.sum(l-1)) + 1ll*(1-l)*(r+1)%MOD*(s.sum(r)-s.sum(l-1)) + 1ll*(l-1)*(t.sum(r)-t.sum(l-1)))%MOD+MOD)%MOD;
}
struct Segment_Tree_A{
struct node{
int l,r,tag;
int sum;
node ()
{
l=r=tag=0;
}
}tr[MAXN*2*4];
int num;
Segment_Tree_A(){num=1;}
void pushup(int x)
{
tr[x].tag=(tr[tr[x].l].tag!=tr[tr[x].r].tag)?-1:tr[tr[x].l].tag;
}
void Build(int x,int L,int R)
{
tr[x].tag=0;
if(L==R)
{
tr[x].tag=0;
tr[x].sum=b[L];
return;
}
int mid=(L+R)/2;
tr[x].l=++num;
Build(tr[x].l,L,mid);
tr[x].r=++num;
Build(tr[x].r,mid+1,R);
pushup(x);
tr[x].sum=tr[tr[x].l].sum+tr[tr[x].r].sum;
Mod(tr[x].sum);
}
void pushdown(int x)
{
if(tr[x].tag!=-1)
{
tr[tr[x].l].tag=tr[tr[x].r].tag=tr[x].tag;
}
}
void Modify(int x,int L,int R,int al,int ar,int v)
{
if(R<al||ar<L)return;
if(al<=L&&R<=ar&&tr[x].tag!=-1)
{
if(tr[x].tag)Add(tr[x].tag,-tr[x].sum,v);
tr[x].tag=v;
return;
}
pushdown(x);
int mid=(L+R)/2;
Modify(tr[x].l,L,mid,al,ar,v);
Modify(tr[x].r,mid+1,R,al,ar,v);
pushup(x);
}
}segA;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&a[i].pl,&a[i].pr);
stk[++cnt]=a[i].pl;
stk[++cnt]=a[i].pr;
}
sort(stk+1,stk+cnt+1);
int UPD=0;
for(int i=1;i<=cnt;i++)
{
if(i==1 || stk[i]!=stk[i-1])
{
h[stk[i]]=++UPD;
b[UPD-1]=stk[i]-stk[i-1];
}
}
for(int i=1;i<=n;i++)
{
a[i].l=h[a[i].pl];
a[i].r=h[a[i].pr]-1;
}
segA.Build(1,1,UPD);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&q[i].l,&q[i].r);
q[i].id=i;
}
sort(q+1,q+m+1,cmp);
//cerr<<"done"<<endl;
for(int i=1,j=1;i<=n && j<=m;i++)
{
segA.Modify(1,1,UPD,a[i].l,a[i].r,i);
//cerr<<"mdone"<<endl;
Add(i,(a[i].pr-a[i].pl)%MOD,i);
//cerr<<"adddone"<<endl;
while(j<=m && q[j].r == i)
{
//cerr<<"done"<<endl;
q[j].ans=Sum(q[j].l,q[j].r);
j++;
}
}
sort(q+1,q+m+1,cmp_id);
for(int i=1;i<=m;i++)
{
printf("%d\n",(int)(q[i].ans*2ll*qpow(q[i].r-q[i].l+1,MOD-2)%MOD*qpow(q[i].r-q[i].l+2,MOD-2)%MOD));
}
return 0;
}
致谢:
感谢 riteme 大爷的指导。
这篇 @范艺杰 的博客 让我学会了这道题。