暑期集训第二周
第二周全部是数据结构。
8.8 树套树
这些题都很毒瘤,比卡时间更恶心的是卡空间,对我这种人傻常数大的选手很不友好。
很久以前写的,原题过了,加强版被卡常了,待补。外层树状数组在下标维上开,内层动态开点线段树在值域维上开。
将对三角形的操作转化到矩阵上。方法在暑期集训第一周中介绍过了。
以上是简单题
同样外层树状数组在下标维上开,内层动态开点线段树在值域维上开。运用颜色段均摊的思想,我们可以把一个等差数列 \(a[l,\dots,r]\) 看作 \(l\) 这个位置上有这个数列的全部数,而 \(a[l+1,\dots,r]\) 是空的。这样我们很方便的完成添加等差数列的操作:只用修改位置 \(l\) 就可以。我们还可以将一个等差数列分裂成两段,即变成 \(l\) 中存有 \(a[l,\dots,mid-1]\),\(mid\) 存有 \(a[mid,r]\)。通过分裂操作,就可以完整的处理修改操作:若要将 \([l,r]\) 修改为等差数列,先通过分裂操作,将原本横跨 \(l,r\) 点的等差数列劈成两端。接着把所有在 \([l,r]\) 内的等差删掉,最后把新的数列放上来就可以。逆序对数量统计在这个过程中逐步算就可以。参考题解。
内层值域线段树的标记永久化:如果修改是区间,而询问是单点的,可以使用标记永久化。在这道题使用标记永久化可以不用 pushdown,减少新增节点数量,节约空间。最开始没有标记永久化被卡了。
前面概率的部分想清楚后维护树套树相对简单一点。又用到标记永久化。只要想清楚操作的先后顺序不影响答案就能理解这样做的正确性。不用应该也可以,不过没写过,不清楚是否能过。
P1903 【模板】带修莫队 / [国家集训队] 数颜色 / 维护队列
与 P1972 [SDOI2009] HH 的项链 相比多了修改操作,这使得我们不能离线下来然后扫描线做。换成树套树就可以简单做到在线的查询。修改操作的话拿 set 维护一下 \(pre\),想清楚 \(pre\) 改变了哪些就可以。
在集训之前就遇到了这道好题。题目的查询很特别,问的是在此以前有哪些时刻是联通的,我们可以通过贡献提前计算解决。如果两个站点在 \(t\) 时刻联通了,就将它们间的权值加上 \(q-t\),表示接下来他们都将联通。同理如果 \(t\) 时刻断了,就减去 \(q-t\)。在 \(i\) 时刻查询时,如果还是联通状态,记得还要减去 \(q-i\)。因为这里的“权值”指的是从头到尾一共亮了多久,而查询只是从头到 \(i\) 的。
将连续一段亮着的灯称为一个连通块,改变一个路灯的状态相当于改变左右两个连通块间所有点对的状态,这在平面上是一个矩形,是很好维护的。
8.9 扫描线技巧和数据结构优化
扫描线
P8512 [Ynoi Easy Round 2021] TEST_152
由于是覆盖,所以我们只关心一个点最后一次的值,这告诉我们要把询问挂到 \(r\) 上然后做扫描线。如果一个点最后一次操作是 \(i\) 给的,称这个点属于操作 \(i\)。那么当我们处理了 \([1,R]\) 所有操作后,查询 \([L,R]\) 相当于问属于 \([L,R]\) 的所有点的点权和。ODT 和树状数组维护属于 \(i\) 的点的点权和就可以。
P11536 [NOISG 2023 Finals] Curtains
把询问挂到 \(R\) 上,扫描线,逐渐加入 \(r_i\le R\) 的幕布。记 \(f_i\) 表示如果要覆盖 \(i\),幕布的左端点最大能是多少。如果 \(\min\limits_{i=s_j}^{e_j} f_i \ge s_j\) 就可以做到(实际上只可能取等。加入幕布直接将 \([l,r]\) 的 \(f\) 与 \(l\) 取 \(\max\) 就可以。
mex 相关
前两道题是之后 8.11 可持久化线段树中的题目。但明显合理的顺序是先做 8.11 的两道。
这道题告诉我们怎么在线求 mex。值域上建立可持久化线段树,查询的时候拿两颗线段树差分一下,线段树上二分就可以。
CF1436E Complicated Computations
一个区间的 \(\mathrm{mex} = a\) 要求它不含有 \(a\),除此之外,它包含的数字越多越好。所以,存在子区间的 \(\mathrm{mex} = a\),相当于把原数组看作被 \(a\) 划分成的若干段,这些段中有一个区间的 \(\mathrm{mex} = a\)。
完全是上一题的加强版。一个很重要的结论:极短的 \(\mathrm{mex}\) 至多有 \(2n\) 个。证明在下面可略过。
设 \([l,r]\) 是一个极短 \(\mathrm{mex}\)。
在 \([l+1,r]\) 中 \(0,\dots,a_l-1\) 全部出现,而 \(a_l\) 没有出现。故 \(\operatorname{mex}(a_{l+1},\dots,a_r)=a_l\)
- 若存在 \(j\in[l+1,r]\) 使 \(a_j=a_l\),则删去位置 \(l\) 后区间内出现的数值集合不变,\(\mathrm{mex}\) 不变,与极短矛盾。
- 若存在 \(v<a_l\) 不在 \([l+1,r]\) 中出现,由于 \(v\ne a_l\),\(v\) 也不在 \([l,r]\) 中出现,于是 \(M\le v<a_l\),即 \(a_l\) 不参与 \(\mathrm{mex}\) 的判定,删去 \(a_l\) 不改变 \(\mathrm{mex}\),矛盾。
对称地,\(\operatorname{mex}(a_l,\dots,a_{r-1})=a_r\)
显然 \(a_l\ne a_r\),且不妨设 \(a_l>a_r\)。结合 \(\operatorname{mex}(a_l,\dots,a_{r-1})=a_r\) 知
固定 \(l\),令 \(f(x)=\operatorname{mex}(a_{l+1},\dots,a_x)\ (x\ge l+1)\)。
若 \([l,r]\) 极短且 \(a_l>a_r\),由前两步知 \(f(r-1)=a_r<a_l,f(r)=a_l\ge a_l\)
当 \(r=l+1\) 时 \(f(l)=\operatorname{mex}(\varnothing)=0=a_r<a_l\),同样成立。
由于 \(f\) 单调不降,“使 \(f(x)\ge a_l\) 的 \(x\)” 唯一确定。所以每个左端点 \(l\) 至多对应一个满足 \(a_l>a_r\) 的极短 mex 区间,这类区间至多 \(n\) 个。
把同样的论证从右往左做可得:每个右端点 \(r\) 至多对应一个满足 \(a_l<a_r\) 的极短 mex 区间,这类区间也至多 \(n\) 个。
又由第二步极短 mex 区间必有 \(a_l\ne a_r\),两类已经覆盖全部情形,得证。
还有通过“序列上的区间在平面上的点对刻画”的证明,但是要画图所以算了。
回到这道题上,我们可以预处理出所有的极小 \(\operatorname{mex}\) 区间。先处理出 \(\operatorname{mex}\le 2\) 的区间,然后从 \(2\) 开始扩展,找到其前后第一个等于 \(\operatorname{mex}\) 的数,计算扩展后的区间的 \(\operatorname{mex}\),把新的区间塞到对应 \(\operatorname{mex}\) 的 vector 里。如是扩展 \(\mathrm{mex}=1,\dots,i-1\) 的区间后,对于已经求出来的 \(\operatorname{mex} = i\) 的区间,排序一遍将有包含关系的区间去掉就可以。
然后对于每一个询问 \([L,R]\),我们需要求最小的 \(x\),使得 \([L,R]\) 内部没有 \(\operatorname{mex}=x\) 的极小区间,扫描线加上权值线段树二分就做完了。
具体的:我们把所有极小 \(\operatorname{mex}\) 区间挂到其 \(R\) 上,从小到大对 \(R\) 扫描线,权值线段树维护每个 \(\mathrm{mex}\) 的最大的 \(L\),对于询问线段树二分即可。
非常难受的是在线求 \(\mathrm{mex}\) 要一个可持久化线段树,最后扫描线的时候也要一个权值线段树。按我的写法如果开两棵树的话是会 MLE 的,所以只能共用一个。
类似的,由上述方法求出极短的 \(\mathrm{mex}\) 区间后,对于每个区间 \([l,r]\) ,设它左边最靠近它的值为 \(\mathrm{mex}\) 的数的下标为 \(L\),类似的右边为 \(R\)。那么它最短的长度是 \(r-l+1\),最长能扩展的是 \(R-L-1\)。
不太好说清楚呢,看代码可能好懂一点。
set<int> s;
vector<int> add[maxn + 5] , del[maxn + 5];
fo(i , 0 , n){
for(auto x : f[i]){ // mex 为 i 的所有区间存在 vector f[i] 中
int pre = *(--lower_bound(num[i].begin() , num[i].end() , x.first));
int nxt = *(upper_bound(num[i].begin() , num[i].end() , x.second));
pre++ , nxt--;
add[x.second - x.first + 1].push_back(i);
del[nxt - pre + 1].push_back(i);
}
}
fo(i , 0 , n) s.insert(i); // s 是补集
fo(i , 1 , n){
for(int mex : add[i]){
if(cnt[mex] == 0) s.erase(mex);
cnt[mex]++;
}
cout << (*s.begin()) << " ";
for(int mex : del[i]){
cnt[mex]--;
if(cnt[mex] == 0) s.insert(mex);
}
}
序列上的区间在平面上的点对刻画
NOIP 2025 T4,当初在考场上写了一个暴力就跑了。将区间在二维平面上表示,很直观,但是依然还没有补完。
8.10 线段树高级应用和线段树二分
单侧递归
最基础经典的题。pushup 是 \(O(\log n)\) 的,单次修改要做 \(O(\log n)\) 次 pushup,总复杂度 \(O(\log^2 n)\)。
很复杂的一道题……
首先我们会换维。注意到查询的实际上是单点的信息,于是扫描序列轴,用数据结构维护时间轴。然后,然后我写不动了,看这篇题解吧,我觉得写的很好。不过他的代码太不清真了,这里给出我的代码,自认为比较清晰。
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<ctime>
#include<vector>
#define mp make_pair
#define chkmax(a , b) a = max(a , b)
#define chkmin(a , b) a = min(a , b)
#define fo(i , x , y) for(int i = (x) ; i <= (y) ; i++)
#define go(i , x , y) for(int i = (x) ; i >= (y) ; i--)
#define local
#ifdef local
#define debug(x) cerr << #x" : " << x << ", "
#define debugn(x) cerr << #x" : " << x << "\n"
#define debugarr(a , n) cerr << #a" :\n"; fo(i , 1 , n) cerr << a[i] << " "; cerr << "\n"
#define debugs(s) cerr << s
#else
#define debug(x) "n-buna bless me."
#define debugn(x) "Nayutan bless me."
#define debugarr(a , n) "wowaka bless me."
#define debugs(s) "Deco*27 bless me."
#endif
using namespace std;
const int maxn = 100000;
const int maxpoint = maxn * 2;
long long val[maxn + 5];
struct node{
long long cntl , cntr , sum;
friend node operator + (const node a , const node b){
return {a.cntl + b.cntl , a.cntr + b.cntr , a.sum + b.sum};
}
friend node operator - (const node a , const node b){
return {a.cntl - b.cntl , a.cntr - b.cntr , a.sum - b.sum};
}
};
struct Segment_Tree{
private:
node t[maxpoint + 5];
int tot , ls[maxpoint + 5] , rs[maxpoint + 5];
node cal(long long del , int l , int r , int k){
if(l == r){
if(t[k].cntr <= del) return {t[k].cntl - t[k].cntr , 0 , 0};
else return {t[k].cntl - del , t[k].cntr - del , (t[k].cntr - del) * val[l]};
}
int mid = (l + r) / 2;
if(t[rs[k]].cntr <= del){
long long newdel = del + t[rs[k]].cntl - t[rs[k]].cntr;
return cal(newdel , l , mid , ls[k]) + (node){newdel - del , 0 , 0};
}
else{
return t[k] - t[rs[k]] + cal(del , mid + 1 , r , rs[k]);
}
}
public:
int build(int l , int r){
int k = ++tot;
if(l == r) return k;
int mid = (l + r) / 2;
ls[k] = build(l , mid);
rs[k] = build(mid + 1 , r);
return k;
}
void update(int x , int op , long long num , int l , int r , int k){
if(l == r){
if(op == 1){ // update (
t[k].cntr += num , t[k].sum += val[l] * num;
}
else{ // update )
t[k].cntl += num;
}
return;
}
int mid = (l + r) / 2;
if(x <= mid) update(x , op , num , l , mid , ls[k]);
else update(x , op , num , mid + 1 , r , rs[k]);
t[k] = cal(t[rs[k]].cntl , l , mid , ls[k]) + t[rs[k]];
}
node query(int R , long long del , int l , int r , int k){
if(r <= R){
node ans = cal(del , l , r , k);
ans.cntl += del;
return ans;
}
int mid = (l + r) / 2;
if(mid + 1 <= R){
node ansr = query(R , del , mid + 1 , r , rs[k]);
node ansl = query(R , ansr.cntl , l , mid , ls[k]);
ansr.cntl = 0;
return ansl + ansr;
}
else return query(R , del , l , mid , ls[k]);
}
}tr;
int n , m;
struct Opera{
int op , t;
long long cnt;
int f;
};
vector<Opera> q[maxn + 5];
long long res[maxn + 5];
int main(){
ios :: sync_with_stdio(0) , cin.tie(0) , cout.tie(0);
// freopen(".in" , "r" , stdin);
// freopen(".out" , "w" , stdout);
#ifdef local
clock_t starttime = clock();
#endif
cin >> n >> m;
fo(i , 1 , m){
res[i] = -1;
int op , l , r;
long long cnt;
cin >> op;
if(op == 1){
cin >> l >> r >> cnt >> val[i];
q[l].push_back((Opera){1 , i , cnt , 0});
q[r + 1].push_back((Opera){1 , i , -cnt , 0});
}
else if(op == 2){
cin >> l >> r >> cnt;
q[l].push_back((Opera){2 , i , cnt , 0});
q[r + 1].push_back((Opera){2 , i , -cnt , 0});
}
else{
res[i] = 0;
long long L , R;
cin >> l >> L >> R;
q[l].push_back({3 , i , R , 1});
q[l].push_back({3 , i , L - 1 , -1});
}
}
tr.build(1 , m);
fo(i , 1 , n){
for(Opera x : q[i]){
if(x.op == 1){
tr.update(x.t , 1 , x.cnt , 1 , m , 1);
}
else if(x.op == 2){
tr.update(x.t , 2 , x.cnt , 1 , m , 1);
}
else{
node ans = tr.query(x.t , 0 , 1 , m , 1);
long long del = max(0ll , ans.cntr - x.cnt);
ans = tr.query(x.t , del , 1 , m , 1);
res[x.t] += x.f * ans.sum;
}
}
}
fo(i , 1 , m)
if(res[i] != -1) cout << res[i] << "\n";
#ifdef local
clock_t endtime = clock();
double elapsed = (double)(endtime - starttime) / CLOCKS_PER_SEC;
debugn(elapsed);
#endif
}
李超线段树
标记永久化的思想。
典题。启发式合并加李超线段树是 \(O(n \log^2 n)\) 的,李超线段树直接合并是 \(O(n \log n)\)。注意到直接在数据结构上合并可以比启发式合并少一个 \(\log\)。另一篇文章省选级的串串入门中的 LOJ6198 也有类似的复杂度分析。
典题。点分治加李超线段树,不知道多久以前就做过了。
线段树上二分
哈希真的想不到,想题的时候甚至想到了 \(O(\frac{n^2}{w})\) bitset 做法。想到哈希之后的可持久化线段树和二分就是水到渠成啦。学到了 mt19937_64 可以生成 unsigned long long 范围内的数。
势能分析
阉割版的吉司机线段树。做法是记录最小值 \(min1\) 与严格次小值 \(min2\)。对于一次取 \(\mathrm{chkmin(x)}\) 操作,如果 \(x \le min1\),不用管。如果 \(min1 < x < min2\),只修改 \(min1\) 的标记。否则暴力修改。
我们通过势能分析,来证明复杂度是 \(O((n+q)\log n)\) 的。定义一个节点的势能是不同元素的多少。上述操作保证了每次暴力递归都会使势能减小 \(1\),即把 \(min1\) 与 \(min2\) 变为相同。总势能是 \(O(n \log n)\) 的,故总复杂度也是这个。
HDU 这个 OJ 一直很神秘,通过 vjudge 交了很多次都是 TLE。一直以为是复杂度假了,然而把关了同步流的 cin cout 换成 scanf printf 就过了。
接下来时简单应用。
定义势能是极差即可
定义势能是极差即可*2
8.11 可持久化线段树与线段树合并
可持久化线段树
可持久化线段树用来在线求 \(\mathrm{mex}\)。8.9 介绍过了。
此题还有随机化做法。
以上是简单题。
在值域上建立线段树,在序列维上可持久化。
首先 \(+x\) 只是整体平移了一段,在查询的时候处理一下就可以。按位考虑即可,差分查询就可以。复杂度是两只 \(\log\) 的。
典题,之前做过了。一个广为人知的 trick 是二分答案 \(mid\),然后 \(<mid\) 的赋成 \(-1\),\(\ge mid\) 的赋成 \(1\)。一个区间的中位数 \(\ge mid\) 等价于 \(1/-1\) 序列上的区间和是 \(\ge 0\)。
而且这道题启发了在序列维上建立线段树,在值域维上可持久化的方法。取决于我们更关心一个值出现的位置还是大小。
和上题几乎一样。唯一不同的是单点修改变为了区间修改,似乎这样子可持久化线段树就不能下放懒标记了,只能标记永久化。
妙妙题没有想出来。更重要的是想清楚一个确定集合的神秘数怎么求。从小到大考虑这个集合中的数,假设已经可以表示出 \([1,x]\),新加入的数是 \(a_i\)。如果 \(a_i > x + 1\),那么 \(x+1\) 肯定表示不出来,剩下的就不用看了。否则能表示的区间就变为 \([1,x+a_i]\)。
考虑优化这个过程,假设目前能表示出来 \([1,x]\) 中的所有数,设 \(\le x+1\) 的数的和是 \(res\),若 \(res\ge x+1\),那么令 \(x\gets res\)。否则答案就是 \(x+1\)。
用可持久化线段树维护就可以。每次查询时 \(x\) 增长是指数的,所以只会增长 \(\log\) 次,算上线段树查询,总复杂度是两只 \(\log\) 的。
线段树合并
P3521 [POI 2011] ROT-Tree Rotations
典题。两颗二叉树合为一个时,其内部节点顺序是不影响的当下的贡献的。用值域线段树的合并逐步模拟这个合并二叉树的过程即可,每次贪心的选使代价最少的方案。
线段树合并以维护 dp 的好题。64pts 的朴素 dp 是很好像的。然后在深度维上开线段树维护就可以。这样做的原因是我们注意到总共只有 \(m\) 个限制,相对于 dp 的定义 \(dp_{u,dep}\) 的 \(O(n^2)\) 个点来说是很稀疏的,也就是势能很小。所以使用线段树合并。
线段树分裂
没有补完。原以为暑假后会有时间,现在看来是补不完了。
模版
颜色段均摊。颜色段的分裂与合并用线段树分裂和合并就可以。注意不可以使用平衡树,因为没法做到 \(O(\log n)\) 合并两个树。直接所有数乘起来显然会溢出,但是可以对每个数取 \(\lg\),然后区间加。不过没写过啊,不知道这样的精度够不够。
8.12 数据结构进阶
有很多很难的东西……自己也不太能够清晰的讲述出来。
KTT
学习文章是这个,很全面很清晰了。
一个可以警示后人的是,如果第二个 hack 创了,截距相同的直线应该选斜率更大的。
也就是前一题的再推广。
首先将单点加,链求和转化成子树加,单点查询。也就是 dfn 序上区间加,查区间最大值,KTT 维护即可。
还有一个问题是有绝对值的话,有可能就不是区间加正数了。小 trick 是 \(\left | a \right | = \max(a,-a)\)。开两颗线段树,将绝对值去掉就可以了。现在还没来得及写,待补。
其实今天原本的名字是“专题:可持久化数据结构进阶”,但是我没看出来有什么和可持久化有关的东西。就当作 DS 大杂烩吧www剩下的都是一些杂题。
很厉害的技巧是倍增分块。首先按题面给的贪心顺序给物品排序。当 \(c\) 落到 \([2^k,2^{k+1}-1]\) 中时,此时要么选足够多个 \(w<2^k\) 的物品,掉到下一层。或者选了若干个 \(w<2^k\) 的物品后碰到了 \(w\in [2^k,2^{k+1}-1]\) 的物品,选了掉到下一层。为每层开一个物品维的线段树,维护要选多少个 \(w<2^k\) 的物品才能掉到下一层,和通过选 \(w\in [2^k,2^{k+1}-1]\) 的物品掉到下一层至少 \(c\) 要多少。
将边权从小到大排序,二分答案,那么一组能选的边是一个区间,双指针搞一下,每个边有加入和删除的时间。线段树分治搞一下,复杂度有二分+线段树分治+并查集共两只 \(\log\) 和一个 \(\alpha\),不过这是可以过的。应该是今天最简单的一道题。
如果记忆力好的话,你会立刻反应过来这就是 P4587 [FJOI2016] 神秘数 的带修加强版本。在那道题使用了主席树来解决,然而这题待修改所以只能用树套树维护。这也就是为什么树套树又叫做动态主席树。
下面这三道 Ynoi 第一期做过了。做这个可以尽情享受卡常的乐趣。
距离为一的点是它自己、它父亲、它重儿子、它轻儿子。拿一个重链剖分和树状数组维护每个点的值。查询的时候它自己、它父亲、它重儿子都可以直接查出来。每个点再开一个平衡树维护轻儿子的权值。注意到一条链上只有 \(O(\log n)\) 个轻儿子,直接暴力该就可以。复杂度是重链剖分+树状数组 \(O(n\log^2 n)\) 的。
然而这样是过不去的。老师和题解会告诉你注意到查询是 \(O(\log n)\) 而修改是 \(O(\log^2n)\) 的。所以考虑平衡它们的复杂度。考虑记录每个点记录 \(p\) 个重儿子,那么修改是 \(O(\log_p n \log n)\) 的,查询是 \(O(p\log n)\) 的。平衡一下取 \(p=\frac{\log n}{\log\log n}\),得到总复杂度 \(O(n\frac{\log^2n}{\log\log n})\)。实际上本人十分辛苦的将上述过程实现后,把 \(p\) 的取值从 \(8\) 逐步往下调,结果发现 \(p\) 值越小越快……最后发现取一个重儿子就是最快的,当然同时也是过不了的,因为上述过程的常数很大,反而不如只取一个重儿子。
那怎么办呢?我现场学了一下常数极小的有旋 Treap,代替了原来无旋的 FHQ-Treap,直接过了……
8.13
考试。
先把大样例下下来,再浏览一遍题面。这个好习惯以后要带到 noip 与省选赛场上。发现 T3 是典题,但是忍住先不写,已经因为倒序开题崩盘过一次了。看 T1,数据范围很奇怪,随便写了写就过了。暴力枚举一维,另一维树状数组维护区间加,区间求和。\(m=50000,n=1000,O(mn\log n)\),过了。赛后发现二维树状数组维护矩形加,矩形和,可以做到 \(O(m\log^2 n)\),思想和一维是差不多的。
去写典的 T3。卡了一会大样例,发现题面错了,改完过了。xyd模拟赛的质量堪忧啊。去想最后的 T2,上厕所时想出了不带修改时的 \(O(n\log^2n)\) 的做法,带修的话,加线段树分治那不是三只 \(\log\) 了?没有前途所以写完了不带修改的特殊性质就跑了。\(100+55+100=255\)。T2 把二分线段树改为线段树二分就消掉一只 \(\log\),加上线段树分治就是 \(O(n\log^2n)\) 的,可以通过。这个没想到该罚。
T2 是 Gym103627A Points

浙公网安备 33010602011771号