值域树状数组求 k−th
这里以第 k 小为例。
首先不难发现一定会有 k−1 个数比第 k 小的数小,那么只需要找到最大的满足 [−inf,pos] 中数的个数等于 k−1 的 pos,答案即为 pos+1。
考虑倍增求出 pos,显然由于树状数组维护的区间的长度是 2 的次幂,所以可以从大往小枚举 2 的次幂 2p 并尝试加上这段区间。
具体实现
inline int kth(int k) // 这里假设值域 [1,n]
{
k--;
int pos=0,sum=0;
for(int j=20;j>=0;j--) // 需要保证 2^j>=n
{
if(pos+(1<<j)<=n&&sum+tr[pos+(1<<j)]<=k)
{
sum+=tr[pos+(1<<j)];
pos+=1<<j;
}
}
if(sum!=k-1)
{
return -1; // 没有
}
return pos+1>n?-1:pos+1; // pos+1>n 也是没有的情况
}
值域倍增分块
经常用于解决把所有 ai>x 的 ai 减去 x 之类的东西。
例题:P7447 [Ynoi2007] rgxsxrs
题解
把值域分成 [2k,2k+1) 这样的 log 个块,那么每次修改就找到 x 所在的块 [2y,2y+1):
- 对于所有满足 z>y 的 [2z,2z+1):这些块将会集体减掉 x,那么维护每一块的最小值,暴力让该跌落的值跌落到更低的块;
- 对于所有满足 z<y 的 [2z,2z+1):这些块没有任何改变;
- 对于 [2y,2y+1):这一块中所有满足 ai>x 的 ai 都会跌落到更低的块,那么维护块内最大值暴力让该跌落的值跌落;
由于每个数最多只会跌落 logV 次,所以时间复杂度为 O(mlogVlogn+nlogV)。
更多例题:
减半报警器
用于解决类似这样的问题:
维护一个数据结构,刚开始给了一些范围,每个范围有对应的权值。每次把包含一个点的范围的权值减去 x,维护每个范围的权值最早被减完的时刻。
例题:Codeforces GYM 102452I
有 n 个国家,每个国家有权值 wi 和若干个(≤3)观测点。有 q 次操作,每次会将所有能观测到 x 的国家的 wi 减去 y。你要求出每个国家的权值第一次被减完是在第几次操作。
题解
观测点个数为 1 的情况是最简单的,直接把 wi 挂到观测点上即可;
观测点个数为 2 比较复杂,设这两个观测点为 x 和 y,把 ⌈2wi⌉ 挂到 x 上,⌈2wi⌉ 挂到 y 上。这样两边都没减完则 wi 一定没被减完,有一边被减完则判一下 wi 是否被减完,若没减完则重新平均分配。注意到每次 wi 至少减半,所以最多重新分配 logV 次,均摊时间复杂度 O(nlogV);
观测点个数 3 的情况和观测点个数为 2 的情况一样,只需要把 wi 平均分成三份,注意到每次重新分配会让 wi 变成原来的 32,那么均摊时间复杂度为 O(nlog1.5V)。
每次操作的时间复杂度为 O(logn),那么总时间复杂度即为 O(qlogn+nlog1.5V)。
注意挂的一定是向上取整的值,否则会出现 (1,0,0) 这种尴尬情况。
例题 2:Codeforces GYM 104065B
你要邀请 n 个人来参加会议,一个人同意参加当且仅当已经有至少 ki 个编号在 [li,ri] 内的人来参加,问最多能有多少个人参加。
也就是说你要确定一个邀请人的顺序。
题解
转换后的题意相当于给定了 m 个区间,第 i 个区间的权值为 ki,你需要动态维护这样的过程:
- 找到 ki≤0 的某个区间 i;
- 把它删掉,答案加一,同时将所有包含 i 的区间的 ki 都减去 1;
考虑所有区间都包含某个位置 p 的特殊情况,那么每个区间都可以被分成 [li,p] 和 [p+1,ri] 两段。可以把 ki 平均分到这两段上,每一次修改操作相当于找到左边的一个后缀或者右边的一个前缀减掉 1。
这样若一个区间的某一边被减完了则重新分配剩下的 ki,每次至少减半,均摊时间复杂度为 O(nlogV)。
用线段树动态维护这个东西,每次区间减,暴力找到需要重构的区间编号即可。
若所有区间没有包含共同的位置,那么可以把区间分成三类:
- 包含 ⌊2n⌋ 的;
- 在 ⌊2n⌋ 左边的;
- 在 ⌊2n⌋ 右边的;
这是一个类似线段树的结构,那么分治下去,每次修改都不断往 x 的方向走,改一下包含当前分治中心 mid 的区间。
一次修改会分治 O(logn) 次,每次分治的时间复杂度为 O(logn),那么总的时间复杂度即为 O(nlog2n+nlogV)。
更多例题:
支配
x 满足的条件包含 y 满足的条件,并且 x 的值比 y 的优,那么 y 就没有存在的必要了。
——《单调队列》
关于距离的支配
序列上
CF765F Souvenirs / CF1793F Rebrending
题解
CF 自己抄自己,两道题一模一样。
先假设只有 i<j 且 ai<aj 的 (i,j) 有贡献,另一种取值再跑一次即可。
注意到若 (i1,j1) 和 (i2,j2)(i1<j2,i2<j2)若满足 ai1−aj1≤ai2−aj2 且 i1≥i2,j1≤j2 则(i2,j2) 被 (i1,j1) 完全包含了,即 (i1,j1) 支配了 (i2,j2),所以只用统计 (i1,j1) 的贡献。
固定 i,考虑找到所有有用的 j。设 Si={j∣(i,j) 未被支配},考虑增量求解 Si,设 p=max{j∣j∈Si},那么 k(k>p)想要加入 Si 就必须满足:
- ak−ai<ap−ai⇒ak<ap;
- ak−ai<∣ap−ak∣⇒ak−ai<ap−ak⇒ak<2ap+ai;
所以 ∣Si∣ 是 logV 级别的,那么未被支配的 (i,j) 也就只有 nlogV 个。
接下来就变成二维数点问题了,扫描线即可,时间复杂度 O((q+nlogV)logn)。
树上
P9058 [Ynoi2004] rpmtdq
题解
考虑点分治,对于来自分治中心 rt 不同两个子树中的两个点 x,y,显然有 dis(x,y)=dis(x,rt)+dis(y,rt)。
那么不妨设 au=dis(u,rt),显然点对 (i,j)(i<j)未被支配当且仅当对于所有 i<k<j 都有 ai+aj<ai+ak 且 ai+aj<ak+aj。稍加化简可得 ai<ak,aj<ak,max(ai,aj)<ak,那么只有 ai 的前驱后继会和 i 构成支配,所以每层分治每个点只会组成 O(1) 个支配对,那么总支配对个数是 O(nlogn) 的。
接下来就变成二维数点问题了,扫描线即可,时间复杂度 O((q+nlogV)logn)。
一些特殊的支配
Mex 支配
对于一个长度为 n 的序列 a,设 bl,r=mexl≤i≤r{ai},有性质:
-
bl,r≥bl+1,r,证明显然;
-
b∗,r→b∗,r+1 相当于把所有 bi,r=ar+1 的 bi,r 都修改为大于 ar+1 的数,由第一个性质,需要被修改的 i 一定是在一个区间内,所以根据颜色段均摊的经典结论,所有 r 造成的总修改数是 O(n) 级别的;
-
满足不存在 [l′,r′]⊂[l,r],bl′,r′=bl,r 的区间 [l,r] 个数最多有 2n 个。
证明
对于每个满足条件的 [l,r],显然 al=ar,不妨假定 al>ar(另一种情况对称)。
现在来证明对于每个 l,都只有一个 r 满足 r>l,al>ar 且 [l,r] 满足条件。
考虑反证,设存在 l<r1<r2 满足 al>ar1,al>ar2 且 [l,r1] 与 [l,r2] 均满足条件。显然由于 [l,r1] 满足条件,一定有 bl,r1>al。由于 al>ar2,所以有 bl,r1>ar2,那么显然 [l,r2−1]⊂[l,r2] 且 bl,r2=bl,r2−1,所以 [l,r2] 不满足条件,矛盾。
那么对于每个位置 i,它作为 al 和 ar 中最小值时最多有一个合法区间,作为最大值时也最多有一个合法区间,所以合法区间个数最多有 2n 个。
Q.E.D.
更多例题
三维计数技巧
对于满足某些条件的有序三元组 (i,j,k) 且固定 k 和 j 后合法的 i 在一段区间内时,可以考虑枚举 k 同时维护每个 j 对应的 i 的合法区间 [lj,rj]。
例题:Nasty Donchik
题意:给定序列 a1,a2,…,an,求有多少有序三元组 (i,j,k) 满足 i≤j<k 且 a[i,j] 中出现的数集以及 a[j+1,k] 中出现的数放入两个不可重集后两个集合相等。
题解
固定 k 和 j 后显然 i 是在一个区间内的,考虑维护 lp 和 rp 表示 j=p 时 i 的上界和下界减一,则固定 k 后的答案即为 j=1∑k−1max(rj−lj,0)。那么设 Li=max{j∣j<i,aj=ai},Ri=min{j∣j>i,aj=ai},则有 lj=max{i∣i≤j,Ri>k},rj=min{Li∣j<i≤k}。
考虑 k→k+1 后对 lj 和 rj 的影响,注意到 lj 单调递增且 rj 单调递增,而 k→k+1 对 lj 的影响是删去了 Ri=k+1 的元素,体现为后缀取 min 即区间赋值;对 rj 的影响则是加入了 Lk+1,也体现为后缀取 min 即区间赋值。注意到由于两个东西都是单调递增的,所以可以用线段树来维护。
时间复杂度 O(nlogn)。
更多例题:
区间修改区间历史和线段树
考虑给线段树上每个点维护一个一次函数 f(x)=kx+b,f(x) 为 x 时的历史和。
单点修改区间查询是好做的,找到要修改的点,设在时刻 y 结束后要修改为 k′,那么把那个点的一次函数修改为 f(x)=k′x+y(k−k′)+b 即可。
区间修改区间查询分为两种情况:
- 区间加区间查询:很好做,给 k 和 b 打 tag 即可;
- 区间赋值区间查询:
注意到此时区间内 k 不同则无法一起修改,那么维护区间 k 最小值 mnk 和最大值 mxk。对于一个需要被修改的区间,若 mnk=mxk 则递归左右儿子修改,否则直接打 tag 修改。
这样做时间复杂度均摊是 O(n+qlogn) 的,其中 n 为序列长度,q 为操作数。因为刚开始最多有 n 个区间需要额外往下递归,每次修改最多会让 logn 个区间在以后的修改中需要额外往下递归,所以总共需要额外往下递归的区间是 O(n+qlogn) 的;
例题:
set 维护连续段
颜色段均摊若用线段树维护则会把一个连续段拆成 log 个,总连续段个数变为 O(nlogn)。
而用 set 维护则总连续段个数是 O(n) 的,并且很好写。
代码如下:
struct seg
{
int l,r,x;
inline bool operator<(const seg &b)const{return l<b.l;}
};
typedef set<seg>::iterator iter;
set<seg> st;
inline iter split(int p)
{
iter x=st.lower_bound((seg){p,0,0});
if(x!=st.end()&&x->l==p) return x;
x--;
seg vx=*x;
st.erase(x);
st.insert((seg){vx.l,p-1,vx.x});
return st.insert((seg){p,vx.r,vx.x}).first;
}
inline void ins(int lb,int rb,int x)
{
split(lb);
iter l,r=prev(split(rb+1));
l=st.lower_bound((seg){lb,0,0});
iter pr=l,ed=next(r);
while(pr!=ed)
{
// a_{[pr->l,pr->r]} = x
st.erase(pr++);
}
st.insert((seg){lb,rb,p});
}
摩尔投票
可以用来求集合中出现次数可能严格大于 ⌊kr−l+1⌋ 的 k−1 个数。
为什么是可能呢,因为它的原理如下:
每次选 k 个互不相同的数,将它们从集合中删去,剩下的至多 k−1 种数就是答案。
这是显然的,因为答案一定不会被删去(出现次数比其它数出现次数总和的 k−11 还多),而答案至多只有 k−1 个。
这个东西满足可加性,维护两个集合最后剩下的 k−1 个数即可。
那么可以做区间修改区间查询众数了。
没见过想不到。
例题:P3765 总统选举
NOI 考过:P8496 [NOI2022] 众数
模拟赛考过:【2024NOIP模拟赛73】数数
函数复合扫描线
给定一个函数的序列 fi,每次给定一个输入 x 和一个区间 [l,r],查询 fr(fr−1(fr−2(…fl(x)…))),即对 x 依次应用区间内函数后的结果。
这种题有一个套路就是离线,然后从左往右扫描函数序列,动态维护一个集合 S:
- 遇到查询左端点的时候将 x 加入 S,并保存好 x 在 S 中的指针;
- 扫过一个 i 就将 S 中元素集体应用 fi;
- 遇到查询右端点就拿出指针查询 x 的最终值;
”保存指针“:由于 S 一般要用某些数据结构维护,而 x 会不断改变,所以需要维护 x 在对应数据结构中的指针(例如平衡树的节点编号)。
例题: