• 动态开点线段树学习笔记


    前言

    Dynamic open point segment tree

    用来复习模板,加强运用的熟练度和理解的程度。

    例题&模板

    P3919 【模板】可持久化线段树 1(可持久化数组)

    【题目传送门】
    相比于普通线段树,不同之处在于生成新的版本的时候不需要新建 \(n\) 个节点,只需新建 \(\log n\) 个节点,其余和之前相同的节点在 update 的时候直接继承过来即可。
    动态开点的过程在于每次操作产生的新的版本,初始需要建树 \(rt_0\)

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    #define mid ((l+r)>>1)
    #define FCC fclose(stdin),fclose(stdout)
    const int INF = 0x3f3f3f3f,N = 1e6+10;
    inline ll read()
    {
    	ll ret=0;char ch=' ',c=getchar();
    	while(!(c>='0'&&c<='9')) ch=c,c=getchar();
    	while(c>='0'&&c<='9') ret=(ret<<1)+(ret<<3)+c-'0',c=getchar();
    	return ch=='-'?-ret:ret;
    }
    int n,m,a[N],rt[N<<5],tot;
    struct Segtree
    {
    	int ls[N<<5],rs[N<<5],sum[N<<5];
    	void build(int &k,int l,int r)
    	{
    		if(!k) k=++tot;
    		if(l==r) return void(sum[k]=a[l]);
    		build(ls[k],l,mid);
    		build(rs[k],mid+1,r);
    	}
    	void update(int &k,int pre,int l,int r,int x,int v)
    	{
    		if(!k) k=++tot;
    		if(l==r) return void(sum[k]=v);
    		if(x<=mid) update(ls[k],ls[pre],l,mid,x,v),rs[k]=rs[pre];
    		else update(rs[k],rs[pre],mid+1,r,x,v),ls[k]=ls[pre];
    	}
    	int query(int k,int l,int r,int x)
    	{
    		if(l==r) return sum[k];
    		if(x<=mid) return query(ls[k],l,mid,x);
    		else return query(rs[k],mid+1,r,x);
    	}
    }str;
    void work()
    {
    	n=read(),m=read();
    	for(int i=1;i<=n;i++) a[i]=read();
    	str.build(rt[0],1,n);
    	for(int i=1;i<=m;i++) 
    	{
    		int pre=read(),op=read(),x=read();
    		if(op==1)
    		{
    			int v=read();
    			str.update(rt[i],rt[pre],1,n,x,v);
    		}
    		else printf("%d\n",str.query(rt[pre],1,n,x)),rt[i]=rt[pre];//别忘了生成新的版本 
    	}
    }
    int main()
    {
    	work();
    	return 0;
    }
    

    P3834 【模板】可持久化线段树 2(主席树)

    【题目传送门】
    比上一个模板有技术含量。
    这里是静态的,考虑建立 \(n\) 个权值线段树,第 \(i\) 个线段树表示前 \(i\) 个数字的值对应在区间 \([l,r]\) 内的个数。

    这是一个前缀和的形式,所以对于查询区间 \([l,r]\)(这里是下标区间)内第 \(k\) 小的数,就可以通过二分实现,每次对应区间上的数字个数是第 \(r\) 棵线段树与第 \(l-1\) 棵线段树的 \(sum\) 之差,根据这个二分即可。

    动态开点的过程在于每次加入第 \(i\) 个数字,初始不需要建树,因为初始就是全为 \(0\)(如果思路不清晰,建树也没问题,相当于先把点开好嘛)

    PS:一开始我写的时候还忘记了权值线段树一个位置对应多个元素的特点,还发了个丢人的讨论版:【点击查看】

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    #define mid ((l+r)>>1)
    #define FCC fclose(stdin),fclose(stdout)
    const int INF = 0x3f3f3f3f,N = 2e5+10;
    inline ll read()
    {
    	ll ret=0;char ch=' ',c=getchar();
    	while(!(c>='0'&&c<='9')) ch=c,c=getchar();
    	while(c>='0'&&c<='9') ret=(ret<<1)+(ret<<3)+c-'0',c=getchar();
    	return ch=='-'?-ret:ret;
    }
    int n,m,a[N],rt[N<<5],tot,p[N];
    struct Segtree
    {
    	int ls[N<<5],rs[N<<5],sum[N<<5];
    	void update(int &k,int pre,int l,int r,int x)
    	{
    		if(!k) k=++tot;
    		sum[k]=sum[pre]+1;
    		if(l==r) return;
    		if(x<=mid) update(ls[k],ls[pre],l,mid,x),rs[k]=rs[pre];
    		else update(rs[k],rs[pre],mid+1,r,x),ls[k]=ls[pre];
    	}
    	int query(int k,int pre,int l,int r,int x)
    	{
    		if(l==r) return l;
    		int sumL=sum[ls[k]]-sum[ls[pre]];
    		if(x<=sumL) return query(ls[k],ls[pre],l,mid,x);
    		else return query(rs[k],rs[pre],mid+1,r,x-sumL);
    	}
    }str;
    void work()
    {
    	n=read(),m=read();
    	for(int i=1;i<=n;i++) p[i]=a[i]=read();
    	sort(p+1,p+n+1);
    	int totp=unique(p+1,p+n+1)-p-1;//这里用totp,小心别和tot用混了 
    	for(int i=1;i<=n;i++)		
    	{
    		int x=lower_bound(p+1,p+totp+1,a[i])-p;
    		str.update(rt[i],rt[i-1],1,totp,x);
    	}
    	for(int i=1;i<=m;i++) 
    	{
    		int l=read(),r=read(),k=read();
    		printf("%d\n",p[str.query(rt[r],rt[l-1],1,totp,k)]);
    	}
    }
    int main()
    {
    	work();
    	return 0;
    }
    

    P3960 [NOIP2017 提高组] 列队

    原本看到是紫题,标签还有平衡树,都不想做了(雾)。后来 \(\mathtt{LastOrder\_}\) 说这道题很好,我就来试试。

    首先部分分也很不错,\(50\) 是离散化,\(70\) 是线段树二分。
    对于 \(70\) 的线段树二分,采取在队尾新加入 \(q\) 个空位的方法,很巧妙。

    对于满分,是对于每一行和最后一列开 \(n+1\) 个线段树维护区间内人的个数,也同样用类似的线段树二分找到对应排名的人,同时记录编号。具体实现上,可以用 vector 把超出队尾的元素记录(可以节省一点空间),查询的时候如果查到的 \(pos\)\(n\) 或者 \(m\) 大,就到对应的 vector 里找。

    动态开点的过程在于每次删除元素的时候,一路动态开点更新区间内离队的人数(如果记录在队里的元素还要 pushup,是另一种写法了)。相当于一开始队列是满的,离队人数都是 \(0\),这样即使初始有原图也不需要建树了。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    #define FCC fclose(stdin),fclose(stdout)
    #define mid ((l+r)>>1)
    #define int ll
    const int INF = 0x3f3f3f3f,N = 1e6+10;
    inline ll read()
    {
    	ll ret=0;char ch=' ',c=getchar();
    	while(!(c>='0'&&c<='9')) ch=c,c=getchar();
    	while(c>='0'&&c<='9') ret=(ret<<1)+(ret<<3)+c-'0',c=getchar();
    	return ch=='-'?-ret:ret;
    }
    int n,m,q,now,pos,nn;
    
    struct Segtree
    {
    	int sum[N<<2],tot,rt[N],ls[N<<2],rs[N<<2];
    	vector<ll> ve[N];
    	int find(int k,int l,int r,int v)
    	{
    		if(l==r) return l;
    		int sumL=mid-l+1-sum[ls[k]];
    		if(v<=sumL) return find(ls[k],l,mid,v);
    		else return find(rs[k],mid+1,r,v-sumL);//v-sumL,又忘记 
    	}
    	void update(int &k,int l,int r,int x)
    	{
    		if(!k) k=++tot;
    		sum[k]++;
    		if(l==r) return;
    		if(x<=mid) update(ls[k],l,mid,x);
    		else update(rs[k],mid+1,r,x);
    	}
    	ll modify1(ll x,ll v)
    	{
    		int pos=find(rt[n+1],1,nn,x);
    		ll ret=0ll;
    		update(rt[n+1],1,nn,pos);
    		if(pos<=n) ret=pos*m;
    		else ret=ve[n+1][pos-n-1];
    		if(v) ve[n+1].push_back(v);
    		else ve[n+1].push_back(ret);
    		return ret;
    	}
    	ll modify2(ll x,ll y)
    	{
    		int pos=find(rt[x],1,nn,y);
    		ll ret=0ll;
    		update(rt[x],1,nn,pos);
    		if(pos<m) ret=(x-1)*m+pos;
    		else ret=ve[x][pos-m];
    		ve[x].push_back(modify1(x,ret));
    		return ret;
    	}
    }str;
    signed main()
    {
    	n=read(),m=read(),q=read();
    	nn=max(n,m)+q;
    	for(int i=1;i<=q;i++)	
    	{
    		int x=read(),y=read();
    		if(y==m) printf("%lld\n",str.modify1(x,0));
    		else printf("%lld\n",str.modify2(x,y));	
    	}
    	return 0;
    }
    

    CF915E Physical Education Lessons

    【题目传送门】
    板子了属于是。
    之前的话可以用离散化做,但是维护原区间的 \(sum\) 感觉有点麻烦(实际上是扫描线思路)。

    动态开点的过程在于每次改哪个点就开哪个点,注意 pushdown 的时候也要新开。
    虽然是查询整个区间,相当于区间查改,需要懒标记。

    代码:(由于有点卡常把 add 函数删了)

    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    #define FCC fclose(stdin),fclose(stdout)
    #define mid ((l+r)>>1)
    const int INF = 0x3f3f3f3f,N = 5e5+10;
    inline ll read()
    {
    	ll ret=0;char ch=' ',c=getchar();
    	while(!(c>='0'&&c<='9')) ch=c,c=getchar();
    	while(c>='0'&&c<='9') ret=(ret<<1)+(ret<<3)+c-'0',c=getchar();
    	return ch=='-'?-ret:ret;
    }
    int n,q,rt;
    struct Segtree
    {
    	int sum[N<<5],ls[N<<5],rs[N<<5];
    	int tot,laz[N<<5];
    	inline void pushdown(int k,int l,int r)
    	{
    		if(~laz[k]) 
    		{
    		//add(ls[k],l,mid,laz[k]);
    		//add(rs[k],mid+1,r,laz[k]);
    		if(!ls[k]) ls[k]=++tot;
    		laz[ls[k]]=laz[k],sum[ls[k]]=(mid-l+1)*laz[k];
    		if(!rs[k]) rs[k]=++tot;
    		laz[rs[k]]=laz[k],sum[rs[k]]=(r-mid)*laz[k];
    		laz[k]=-1;
    		}
    	}
    	void modify(int &k,int l,int r,int x,int y,int v)
    	{
    		if(!k) k=++tot;		
    		if(x<=l&&r<=y) 
    		{
    			sum[k]=(r-l+1)*v;
    			laz[k]=v;
    			return;
    		}
    		pushdown(k,l,r);
    		if(x<=mid) modify(ls[k],l,mid,x,y,v);
    		if(y>mid)  modify(rs[k],mid+1,r,x,y,v);
    		sum[k]=sum[ls[k]]+sum[rs[k]];
    		
    	}
    }str;
    void work()
    {
    	n=read(),q=read();
    	memset(str.laz,-1,sizeof(str.laz));
    	str.modify(rt,1,n,1,n,0);
    	while(q--)
    	{
    		int l=read(),r=read(),op=read();
    		if(op==1) str.modify(rt,1,n,l,r,1);
    		else str.modify(rt,1,n,l,r,0);
    		printf("%d\n",n-str.sum[rt]);
    	}
    	return;
    }
    
    int main()
    {
    	work();
    	return 0;
    }
    

    CF600E Lomsat gelral(线段树合并)

    【题目传送门】
    原来用树上启发式合并写的,现在学着用线段树合并写。

    线段树合并一般在树上进行,合并的是动态开点线段树。
    在 DFS 的过程中,对于一对父子 \(u,v\),把所有的 \(v\) 节点线段树合并吗,得到 \(u\) 节点的信息。
    合并的时候采用递归,如果当前 \(u\)\(v\) 线段树对应的节点为空,就可以直接嫁接。否则递归下去。

    对于本题,记录区间占主导地位所需的重复颜色数量 \(mx\),和区间主导地位编号和 \(sum\)pushup 的时候记录即可。

    动态开点的过程在于对于每一个点为根的情况开一颗线段树,方便过程中合并,初始需要建树。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    #define FCC fclose(stdin),fclose(stdout)
    #define mid ((l+r)>>1)
    const int INF = 0x3f3f3f3f,N = 1e5+10;
    inline ll read()
    {
    	ll ret=0;char ch=' ',c=getchar();
    	while(!(c>='0'&&c<='9')) ch=c,c=getchar();
    	while(c>='0'&&c<='9') ret=(ret<<1)+(ret<<3)+c-'0',c=getchar();
    	return ch=='-'?-ret:ret;
    }
    int n,tot,rt[N];
    struct Segtree
    {
    	int ls[N<<5],rs[N<<5],mx[N<<5];
    	ll sum[N<<5];
    	inline void pushup(int k)
    	{
    		mx[k]=max(mx[ls[k]],mx[rs[k]]);
    		if(mx[ls[k]]>mx[rs[k]]) sum[k]=sum[ls[k]];
    		else if(mx[ls[k]]<mx[rs[k]]) sum[k]=sum[rs[k]];
    		else sum[k]=sum[ls[k]]+sum[rs[k]];
    	}
    	void build(int &k,int l,int r,int x)
    	{
    		if(!k) k=++tot;
    		if(l==r) return mx[k]++,sum[k]=l,void();
    		if(x<=mid) build(ls[k],l,mid,x);
    		else build(rs[k],mid+1,r,x);
    		pushup(k);
    	}
    	void merge(int &u,int v,int l,int r)
    	{
    		if(!u) return void(u=v);
    		if(!v) return;
    		if(l==r) return void(mx[u]+=mx[v]);
    		merge(ls[u],ls[v],l,mid);
    		merge(rs[u],rs[v],mid+1,r);
    		pushup(u);
    	}
    }str;
    ll ans[N];
    int head[N],ecnt=-1;
    void init_edge(){memset(head,-1,sizeof(head)),ecnt=-1;}
    struct edge
    {
    	int nxt,to;
    }a[N<<1];
    inline void add_edge(int x,int y)
    {
    	a[++ecnt]=(edge){head[x],y};
    	head[x]=ecnt;
    }
    void dfs(int u,int fa)
    {
    	for(int i=head[u];~i;i=a[i].nxt)
    	{
    		int v=a[i].to;
    		if(v==fa) continue;
    		dfs(v,u);
    		str.merge(rt[u],rt[v],1,n);
    	}
    	ans[u]=str.sum[rt[u]];
    }
    
    int main()
    {
    	init_edge();
    	n=read();
    	for(int i=1;i<=n;i++) str.build(rt[i],1,n,read());
    	for(int i=1;i<n;i++) 
    	{
    		int u=read(),v=read();
    		add_edge(u,v),add_edge(v,u);
    	}
    	dfs(1,0);
    	for(int i=1;i<=n;i++) printf("%lld ",ans[i]);
    	puts("");
    	return 0;
    }
    

    未完待续······

  • 相关阅读:
    Hook技术
    进程间的调试关系
    常见的2种断点方法
    CrackMe的简单破解
    PE文件结构
    DLL卸载
    DLL注入
    调用DLL的2种方式
    iOS密码输入框的实现
    UITableView.separatorInset
  • 原文地址:https://www.cnblogs.com/conprour/p/15555492.html
Copyright © 2020-2023  润新知