插入-标记-回收算法

插入-标记-回收算法

引例:

你有一个长度为 \(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\sqrt{n}\log^2n) \]

调整块长后可以做到:

\[O(n\sqrt{n}\log n) \]

完全可过 。

注意到插入操作和查询操作都是在某个时刻集中做,可以考虑优化。

先说查询,原先查询是每个点要向上走然后回溯时下传标记。

但由于现在每个点都要查,那直接遍历整个数就行了,一次遍历 \(O(n)\) 。

再说建树,由于要把 \([0,maxv]\) 内的所有数都插入进树,且插入的数的值是递增的,那么考虑下面的建树方法:

用一个 \(stack\) 维护当前树上的右链(显然只可能插入在右链上),由于右链上的优先值递减,所以插入的数的位置唯一。从右链底部往上扫,找到合法的插入位置。

设当前点为 \(x\) ,插入位置上方为 \(u\) ,下方为 \(v\) ,则 \(u\) 的右儿子为 \(x\) ,\(x\) 的左儿子为 \(v\) 。

可以做到 \(O(n\sqrt{n}+n\log^2 n)\) ,真完全可过。

posted @ 2026-10-14 19:12  wlhao  阅读(7)  评论(0)    收藏  举报