插入-标记-回收算法
插入-标记-回收算法
引例:
你有一个长度为 \(n\) 的序列,其中第 \(i\) 个元素的值为 \(a_i\)。
现在有 \(q\) 组询问,每组询问给定 \(x, l, r\),表示你刚开始有一个数字 \(x\),然后你从序列的第 \(l\) 个位置走到第 \(r\) 个位置,每经过一个位置就执行 \(x = max(x, a_i − x)\) 。你需要回答每组询问最后 \(x\) 的值。
做法:
直接做不好做,我们先把询问离线下来,然后按照位置依次扫描,同时维护一个集合。
当扫描到 \(i\) 时,我们把所有左端点为 \(i\) 的询问对应的 \(x\) 加入这个集合中。
然后按照第 \(i\) 个位置的规则快速更新集合中的数。
然后再对于所有右端点为 \(i\) 的询问,取出之前在左端点加入集合中的 \(x\) 。
也就是说,对于一个询问,其对应的 \(x\) 会在 \(l\) 被插入,中间被不断更新,然后在 \(r\) 被回收。
这就是插入-标记-回收算法的名称由来。
维护:
上面的例子只是简述了这个算法的思想,然而每一步具体怎么快速更新集合中的数才是真正的难点。
还是考虑引例:
第 \(i\) 步时,我们可以大致将数分成两类:
-
\(x\ge a_i-x:\) 此时数的值不用变。
-
\(x< a_i-x:\) 此时会变成 \(-x+a_i\) 。
那么我们需要一个数据结构,支持:
-
值域区间取反。
-
值域区间加。
-
插入。(这里其实不用删除, 到了该回收的时候直接回收就行了,不用硬性删去。 )
发现值域线段树做不了,但可能可以用平衡树做。
无旋Treap:
这里说明无旋 \(Treap\) 的做法:
还是要维护基本的内容:
struct Node{
int key;//键值
ll val;//优先级,用rand()随机
int cnt;//键值的个数
int lson,rson;//左右儿子
}node[N];
还是打懒标记,考虑打一个取反标记和加法标记。即:
struct Node{
int key;ll val;
int cnt;
int tag1,tag2; //取反、加法
int lson,rson;
}node[N];
Merge:
你会发现正常的无旋 \(Treap\) 的 \(merge\) 在此不适用。
原因是正常的 \(merge\) 要求两颗树的值域不交,这样才能保证单次操作 \(\log n\) 的时间复杂度。
考虑稍微改一下 \(merge\) 函数:
设当前要合并的两个根为 \(x,y\) ,不妨设 \(node[x].val>node[y].val\) (否则可以交换一下 \(x,y\) )。
那么根据 \(Treap\) 的定义,\(y\) 应该挂在 \(x\) 的下面。
那么我们可以根据 \(x\) 的权值将 \(y\) 分裂成三部分:
-
\(key< node[x].key\) 的部分,记为① , 根为 \(rt_1\)。
-
\(key=node[x].key\) 的部分, 记为② , 根为 \(rt_2\)。
-
\(key>node[x].key\) 的部分, 记为③,根为 \(rt_3\)。
由于②部分的 \(key=node[x].key\) ,所以直接将 \(node[rt2].cnt\) 累加到 \(node[x].cnt\) 上就行了。
对于①:
递归执行 \(merge(node[x].lson,rt_1)\)
对于③:
递归执行 \(merge(node[x].rson,rt_3)\)
时间复杂度分析:不会啊,结论是均摊 \(O(n\log^2 n)\) 。
注意,无旋 \(Treap\) 必须保证键值是严格不同的,如果有多个相同值,可以考虑向上文一样压缩到 \(cnt\) 里面或者存一个二元组 \((key,id)\) 。
修改:
由于打标记后,标记范围内 \(key\) 的相对大小取反,但可以通过值域区间翻转来变换回去(交换左右子树),所以可以打标记。
修改要注意打标记的顺序,具体的:
void mark(int x,int a,int b){
if (!x) return;
node[x].key=node[x].key*a+b;
node[x].tag1*=a;
node[x].tag2=node[x].tag2*a+b;//先应用取反,在应用加法
if (a==-1) swap(node[x].lson,node[x].rson);//key的相对大小关系取反,需要交换子树来保持二叉搜索树性质
}
void push_down(int x){
if (!x) return;
if (node[x].tag1==1&&node[x].tag2==0) return;
int ls=node[x].lson,rs=node[x].rson;
mark(ls,node[x].tag1,node[x].tag2);//下传标记
mark(rs,node[x].tag1,node[x].tag2);
node[x].tag1=1;node[x].tag2=0;//还原
}
取出:
取出是一个问题,因为我们不知道最开始插入的值现在在哪一个节点(可能中途被合并了)。
那么其实用一个并查集维护每个值现在在哪一个节点就行了,当发生合并时,其实就是执行了一次 \(merge\) 操作。
然而这样还是有问题,因为此时上方节点可能还没有下传懒标记,那么我们对每个结点单独多计一个 \(fa\) ,并在 \(merge/split\) 时更新维护。然后查询时不断跳父亲到根节点,然后回溯时下传懒标记即可。
完整Code:
struct DSU{
int f[N];
void init(){
for (int i=1;i<N;i++) f[i]=i;
}
int getf(int x){
if (f[x]==x) return x;
return f[x]=getf(f[x]);
}
void merge(int a,int b){
int fa=getf(a),fb=getf(b);
f[fa]=fb;
}
}dsu;
struct Treap{
struct A{
ll key,val;
int cnt;
int lson,rson;
ll tag1,tag2;
int fa;
}node[N];
int cnt_node=0;
int RT=0;
int new_node(ll key){
cnt_node++;
node[cnt_node]={key,rng(),1,0,0,1,0,0};
return cnt_node;
}
void mark(int rt,ll a,ll b){
if (!rt) return;
node[rt].key=a*node[rt].key+b;
node[rt].tag1*=a;
node[rt].tag2=node[rt].tag2*a+b;
if (a==-1) swap(node[rt].lson,node[rt].rson);
}
void push_down(int rt){
if (!rt) return;
if (node[rt].tag1==1&&node[rt].tag2==0) return;
mark(node[rt].lson,node[rt].tag1,node[rt].tag2);
mark(node[rt].rson,node[rt].tag1,node[rt].tag2);
node[rt].tag1=1;
node[rt].tag2=0;
}
void connect(int l,int r,int flag){
if (r) node[r].fa=l;
node[l].fa=0;
if (!flag) node[l].lson=r;
else node[l].rson=r;
}
pair<int,int> split(int rt,ll key){
if (!rt) return {0,0};
push_down(rt);
int ls=node[rt].lson,rs=node[rt].rson;
if (key<node[rt].key){
pair<int,int> res=split(ls,key);
int lrt=res.first;
connect(rt,res.second,0);
node[lrt].fa=0;
return {lrt,rt};
}
else{
pair<int,int> res=split(rs,key);
int rrt=res.second;
connect(rt,res.first,1);
node[rrt].fa=0;
return {rt,rrt};
}
}
int merge(int rt1,int rt2){
if (!rt1) return rt2;
if (!rt2) return rt1;
if (node[rt1].val<node[rt2].val) swap(rt1,rt2);
push_down(rt1);
pair<int,int> res=split(rt2,node[rt1].key-1);
int p1=res.first;
pair<int,int> res2=split(res.second,node[rt1].key);
int p2=res2.first,p3=res2.second;
if (p2){
node[rt1].cnt+=node[p2].cnt;
dsu.merge(p2,rt1);
}
if (p1) connect(rt1,merge(node[rt1].lson,p1),0);
if (p3) connect(rt1,merge(node[rt1].rson,p3),1);
node[rt1].fa=0;
return rt1;
}
void insert(ll key,int id){
int rt=new_node(key);
pos[id]=rt;
RT=merge(RT,rt);
node[RT].fa=0;
}
void push_down2(int rt){
if (!rt) return;
push_down2(node[rt].fa);
push_down(rt);
}
ll query(int rt){
push_down2(rt);
return node[rt].key;
}
};
另一道题:
给定一个长为 \(n\) 的序列 \(a\),每个位置是一个变换 \(x = |x - a_i|\),每次查询给出一个区间 \([l, r]\) 和一个值 \(v\),依次令 \(i\) 从 \(l\) 到 \(r\),访问每个元素 \(a_i\),将 \(v\) 变为 \(|v - a_i|\),求结束后的 \(v\) 的值。
\(1 \le n, m, a_i, v \le 10^5\),\(1 \le l, r \le n\)。强制在线 。
强制在线:
强制在线了怎么办呢?注意到值域很小,考虑分块。
设值域上限为 \(maxv\) 。
对于每个值 \([0,maxv]\) 都用上述平衡树处理出经过某个块会变成什么。
正常建树,正常查询会变成:
调整块长后可以做到:
完全可过 。
注意到插入操作和查询操作都是在某个时刻集中做,可以考虑优化。
先说查询,原先查询是每个点要向上走然后回溯时下传标记。
但由于现在每个点都要查,那直接遍历整个数就行了,一次遍历 \(O(n)\) 。
再说建树,由于要把 \([0,maxv]\) 内的所有数都插入进树,且插入的数的值是递增的,那么考虑下面的建树方法:
用一个 \(stack\) 维护当前树上的右链(显然只可能插入在右链上),由于右链上的优先值递减,所以插入的数的位置唯一。从右链底部往上扫,找到合法的插入位置。
设当前点为 \(x\) ,插入位置上方为 \(u\) ,下方为 \(v\) ,则 \(u\) 的右儿子为 \(x\) ,\(x\) 的左儿子为 \(v\) 。
可以做到 \(O(n\sqrt{n}+n\log^2 n)\) ,真完全可过。

浙公网安备 33010602011771号