一类线段树合并与分裂相关总结

一类线段树合并与分裂相关总结

这几天写了一些线段树合并与分裂相关的题目,来此总结一下。

什么叫线段树合并呢?事实上我个人认为应该叫动态开点权值线段树合并,顾名思义,就是将若干颗值域相同的线段树的信息合并到一颗线段树上。一般合并的信息都是当前值域值出现的个数等

下面来分析一下线段树合并的复杂度,两颗线段树合并的复杂度取决于最小的那颗树的重合点数,因为合并等同于删点,线段树合并通过动态开点保证了一次合并的复杂度是 O ( l o g n ) O(logn) O(logn)的,所以对于 m m m次在值域上的操作,复杂度可以近似看成 O ( m l o g n ) O(mlogn) O(mlogn),当然得保证动态开点下每次操作只开 l o g n logn logn个点,一般情况下,复杂度上界不超过最终总点数

下面我们先来看下怎么合并

int merge(int p,int q,int l,int r){//把q信息合并到p上
    if(!p||!q)return p+q;
    if(l==r){
        //合并操作 return p;
	}
    int mid=l+r>>1;
    ls=merge(ls,lc[q],l,mid);
    re=merge(rs,rc[q],mid+1,r);
    pushUp(p);
    return p;
}

下面代码可以看成线段树合并的板子,但是其实这东西千变万化,不用板子也没事,可以直接void merge通过引用更新,也可以每个结点直接做不用pushUp

上面这种写法是把一颗的信息直接附加到另外一颗上,这样会破坏原来其中一颗的结构,如果有必要的话你也可以重新开一颗,

另外我们可以发现,被合并的那颗其实仍然占用空间,动态开点的时候可以回收空间也可以不回收,后面会再细讲。

线段树合并是一种时空都常数很大的做法,所以下面的时空复杂度分析我将带上常数

下面看题

1.cf600E

题目:给定一棵树,每个结点染颜色,求出现最多的颜色的颜色值之和

思路:

可以用dsu on tree来做(但我不会),直接上线段树合并,每个点建一颗权值线段树,然后dfs从子树一路暴力线段树合并就好了,线段树维护出现的最大值以及最大值之和,pushUp的时候讨论一下就好了

时间复杂度 O ( 2 n l o g n ) O(2nlogn) O(2nlogn) 空间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

//子树各众数值之和
#include<bits/stdc++.h>
#define pb push_back
#define ls lc[p]
#define rs rc[p]
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
#define N maxn*36
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
int n,a[maxn],s,e,mx;
ll ans[maxn];
vector<int>G[maxn];
struct SegmentTree{
    int rt[maxn],lc[N],rc[N],tot,cnt[N];//cnt区间权值线段树中最大值
    ll sum[N];//各个众数之和
    void pushUp(int p){
        cnt[p]=max(cnt[ls],cnt[rs]);
        if(cnt[ls]==cnt[rs])sum[p]=sum[ls]+sum[rs];
        else sum[p]=cnt[ls]>cnt[rs]?sum[ls]:sum[rs];
    }
    void update(int &p,int l,int r,int x,int val){
        if(!p)p=++tot;
        if(l==r){
            cnt[p]+=val;sum[p]=l;return;
        }
        int mid=l+r>>1;
        if(x<=mid)update(lson,x,val);
        else update(rson,x,val);
        pushUp(p);
    }
    int merge(int p,int q,int l,int r){
        if(!p||!q)return p+q;
        if(l==r){
            cnt[p]+=cnt[q];
            sum[p]=l;return p;
        }
        int mid=l+r>>1;
        ls=merge(ls,lc[q],l,mid);
        rs=merge(rs,rc[q],mid+1,r);
        pushUp(p);
        return p;
    }
}tr;
void dfs(int x,int fa){
    for(auto&v:G[x]){
        if(v==fa)continue;
        dfs(v,x);
        tr.rt[x]=tr.merge(tr.rt[x],tr.rt[v],1,mx);   
    }
    tr.update(tr.rt[x],1,mx,a[x],1);
    ans[x]=tr.sum[tr.rt[x]];
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;++i)cin>>a[i],mx=max(a[i],mx);
    for(int i=1;i<n;++i){
        cin>>s>>e;
        G[s].pb(e);
        G[e].pb(s);
    }
    dfs(1,-1);
    for(int i=1;i<=n;++i)
        cout<<ans[i]<<" ";
    return 0;
}

2.bzoj 3307 雨天的尾巴

题意:N个点,形成一个树状结构。有M次发放,每次选择两个点x,y
对于x到y的路径上(含x,y)每个点发一袋Z类型的物品。完成
所有发放后,每个点存放最多的是哪种物品。

思路:

线段树合并的经典应用,m次操作,每次选一条树链加上同样的值,问每个点出现的最多的值是哪个。

首先树上点差分,对应x++,y++,lca(x,y)–,fat(x,y)–,当前点的权值信息等于其所有子树的权值信息的累加,所以就是每个点开一颗权值线段树,维护最大次数以及答案,pushUp的时候分类讨论一下就好了

时间复杂度 O ( 2 n l o g n ) O(2nlogn) O(2nlogn) 空间复杂度$O(4nlogn) $

#include<bits/stdc++.h>
#define N maxn*50
#define ls lc[p]
#define rs rc[p]
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
#define pb push_back
using namespace std;
const int maxn=1e5+5;
vector<int>G[maxn];
int n,m,a,b,lg[maxn],f[maxn][18],d[maxn],L[maxn],R[maxn],val[maxn],mx=0,fat[maxn];
int ans1[maxn];
struct SegmentTree{
    int rt[maxn],tot,lc[N],rc[N],num[N],ans[N];
    void pushUp(int p){
        num[p]=max(num[ls],num[rs]);
        if(num[ls]>=num[rs])ans[p]=ans[ls];
        else ans[p]=ans[rs];
    }
    void update(int&p,int l,int r,int x,int val){
        if(!p)p=++tot;
        if(l==r){
            num[p]+=val;ans[p]=l;return;
        }
        int mid=l+r>>1;
        if(x<=mid)update(lson,x,val);
        else update(rson,x,val);
        pushUp(p);
    }
    int merge(int p,int q,int l,int r){
        if(!p||!q)return p+q;
        if(l==r){
            num[p]+=num[q];
            ans[p]=l;return p;
        }
        int mid=l+r>>1;
        ls=merge(ls,lc[q],l,mid);
        rs=merge(rs,rc[q],mid+1,r);
        pushUp(p);
        return p;
    }
}tr;
void dfs(int x,int fa){
    d[x]=d[fa]+1;
    f[x][0]=fa;
    for(int i=1;i<=lg[d[x]];++i)
        f[x][i]=f[f[x][i-1]][i-1];
    for(auto&v:G[x]){
        if(v==fa)continue;
        dfs(v,x);
        fat[v]=x;
    }
}
int lca(int x,int y){
    if(d[x]<d[y])swap(x,y);
    while(d[x]>d[y])
        x=f[x][lg[d[x]-d[y]]-1];
    if(x==y)return x;
    for(int i=lg[d[y]]-1;i>=0;--i)
        if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
    return f[x][0];
}
void dfs1(int x,int fa){
    for(auto&v:G[x]){
        if(v==fa)continue;
        dfs1(v,x);
        tr.rt[x]=tr.merge(tr.rt[x],tr.rt[v],1,mx);
    }
    if(!tr.num[tr.rt[x]])ans1[x]=0;
    else ans1[x]=tr.ans[tr.rt[x]];
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;++i)lg[i]=lg[i-1]+(1<<lg[i-1]==i);
    for(int i=1;i<n;++i){
        cin>>a>>b;
        G[a].pb(b);
        G[b].pb(a);
    }
    dfs(1,0);
    for(int i=1;i<=m;++i){
        cin>>L[i]>>R[i]>>val[i];
        mx=max(val[i],mx);
    }
    for(int i=1;i<=m;++i){
        tr.update(tr.rt[L[i]],1,mx,val[i],1);
        tr.update(tr.rt[R[i]],1,mx,val[i],1);
        int k=lca(L[i],R[i]);
        tr.update(tr.rt[k],1,mx,val[i],-1);
        tr.update(tr.rt[fat[k]],1,mx,val[i],-1);//dfs父亲设为0,不然越界到-1了
    }
    dfs1(1,0);
    for(int i=1;i<=n;++i)
        cout<<ans1[i]<<"\n";
    return 0;
}
3.bzoj 4756 Promotion Counting

题意:n只奶牛构成了一个树形的公司,每个奶牛有一个能力值pi,1号奶牛为树根。问对于每个奶牛来说,它的子树中有几个能力值比它大的。

思路:

遇到与值域有关,且与子树合并信息有关的考虑每个点建树直接暴力线段树合并。这题是板子中的板子了,直接统计权值出现个数,dfs从子树合并,到当前点直接查询大于当前值的个数即可,需要注意的是查询的时候要保证范围合法

时间复杂 O ( 2 n l o g n ) O(2nlogn) O(2nlogn) 空间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

#include<bits/stdc++.h>
#define pb push_back
#define ls lc[p]
#define rs rc[p]
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
#define N maxn*50
using namespace std;
const int maxn=1e5+5;
int n,a[maxn],x,ans[maxn],mx;
vector<int>G[maxn];
struct SegmentTree{
    int rt[maxn],tot,num[N],lc[N],rc[N];
    void pushUp(int p){
        num[p]=num[ls]+num[rs];
    }
    void update(int&p,int l,int r,int x){
        if(!p)p=++tot;
        if(l==r){
            num[p]++;return;
        }
        int mid=l+r>>1;
        if(x<=mid)update(lson,x);
        else update(rson,x);
        pushUp(p);
    }
    int query(int&p,int l,int r,int L,int R){
        if(R<L)return 0;
        if(L<=l&&r<=R)return num[p];
        int mid=l+r>>1;
        int ans=0;
        if(L<=mid)ans+=query(lson,L,R);
        if(R>mid)ans+=query(rson,L,R);
        return ans;
    }
    int merge(int p,int q,int l,int r){
        if(!p||!q)return p+q;
        if(l==r){
            num[p]+=num[q];return p;
        }
        int mid=l+r>>1;
        ls=merge(ls,lc[q],l,mid);
        rs=merge(rs,rc[q],mid+1,r);
        pushUp(p);
        return p;
    }
}tr;
void dfs(int x,int fa){
    for(auto&v:G[x]){
        if(v==fa)continue;
        dfs(v,x);
        tr.rt[x]=tr.merge(tr.rt[x],tr.rt[v],1,mx);
    }
    tr.update(tr.rt[x],1,mx,a[x]);
    ans[x]=tr.query(tr.rt[x],1,mx,a[x]+1,mx);
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;++i)cin>>a[i],mx=max(mx,a[i]);
    for(int i=2;i<=n;++i){
        cin>>x;
        G[x].pb(i);
        G[i].pb(x);
    }
    dfs(1,0);
    for(int i=1;i<=n;++i)cout<<ans[i]<<"\n";    
    return 0;
}
4.bzoj 4719 天天爱跑步

题意:

树上每个点有对应的值,每次给你一个起点为0,公差为1的等差数列链,问与每个点权值相同的有多少。

思路:

一道非常毒瘤的题,思路挺难的。

我们把从x->y的路径分为从【x->lca(x,y) 】 (lca(x,y),y】一条上行路径,一条下行路径。

1.上行路径满足的点i是

d e p [ x ] − d e p [ i ] = w [ i ] dep[x]-dep[i]=w[i] dep[x]dep[i]=w[i],所以就是 d e p [ x ] = d e p [ i ] + w [ i ] dep[x]=dep[i]+w[i] dep[x]=dep[i]+w[i]

2.对于下行链,要考虑走的时间,所以直接用两点间距离算,(其实可以看成是翻转了上行链),下行链满足的点i满足

d e p [ x ] + d e p [ i ] − 2 d e p [ l c a ( x , y ) ] = w [ i ] dep[x]+dep[i]-2dep[lca(x,y)]=w[i] dep[x]+dep[i]2dep[lca(x,y)]=w[i]

d e p [ x ] − 2 d e p [ l c a ( x , y ) ] = w [ i ] − d e p [ i ] dep[x]-2dep[lca(x,y)]=w[i]-dep[i] dep[x]2dep[lca(x,y)]=w[i]dep[i]

联想到线段树合并的经典应用,一条链加同样的值,维护整条链或者说子树上值域的状况,转为树上差分直接上线段树合并就行了,单点查询是否该点是否可以就行了,这里需要注意的是,由于差分标记合法的情况不一样,所以对每个点我们都开两颗线段树,分别维护上行和下行链,另外上行的合法查询路径在【0,2n】,而下行在【-n,n】

时间复杂度 O ( 2 n l o g n ) O(2nlogn) O(2nlogn) 空间复杂度 O ( 2 n l o g n ∗ 2 ) O(2nlogn*2) O(2nlogn2)

这题有点怕卡常,用了快读

#include<bits/stdc++.h>
#define N maxn*2*18*2
#define pb push_back
#define ls lc[p]
#define rs rc[p]
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
using namespace std;
const int maxn=3e5+5;
inline int read(){
    char c = getchar();int x = 0,s = 1;
    while(c < '0' || c > '9') {if(c == '-') s = -1;c = getchar();}//是符号
    while(c >= '0' && c <= '9') {x = x*10 + c -'0';c = getchar();}//是数字
    return x*s;
}
inline void in(int&x){
    x=read();
}
int n,m,w[maxn],a,b,f[maxn][21],d[maxn],lg[maxn],fat[maxn],ans[maxn];
int head[maxn],ver[maxn<<1],next1[maxn<<1],cnt;
void add(int x,int y){
	ver[++cnt]=y,next1[cnt]=head[x];head[x]=cnt;
} 
struct SegmentTree{
    int rt1[maxn],rt2[maxn],tot,sum[N],lc[N],rc[N];
    void pushUp(int p){
        sum[p]=sum[ls]+sum[rs];
    }
    void update(int&p,int l,int r,int x,int val){
        if(!p)p=++tot;
        if(l==r){
            sum[p]+=val;return;
        }
        int mid=l+r>>1;
        if(x<=mid)update(lson,x,val);
        else update(rson,x,val);
        pushUp(p);
    }
    int query(int p,int l,int r,int x){
        if(!p)return 0;
        if(l==r)return sum[p];
        int mid=l+r>>1;
        if(x<=mid)return query(lson,x);
        else return query(rson,x);
    }
    int merge(int p,int q,int l,int r){
        if(!p||!q)return p+q;
        if(l==r){
            sum[p]+=sum[q];return p;
        }
        int mid=l+r>>1;
        ls=merge(ls,lc[q],l,mid);
        rs=merge(rs,rc[q],mid+1,r);
        pushUp(p);
        return p;
    }
}tr;
void dfs1(int x,int fa){
    d[x]=d[fa]+1;
    f[x][0]=fa;
    for(int i=1;i<=lg[d[x]];++i)
        f[x][i]=f[f[x][i-1]][i-1];
    for(int i=head[x];i;i=next1[i]){
        int v=ver[i];
        if(v==fa)continue;
        dfs1(v,x);
        fat[v]=x;
    }
}
int lca(int x,int y){
    if(d[x]<d[y])swap(x,y);
    while(d[x]>d[y])
        x=f[x][lg[d[x]-d[y]]-1];
    if(x==y)return x;
    for(int i=lg[d[y]]-1;i>=0;--i)
        if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
    return f[x][0];
}
void dfs2(int x,int fa){
    for(int i=head[x];i;i=next1[i]){
    	int v=ver[i];
        if(v==fa)continue;
        dfs2(v,x);
        tr.rt1[x]=tr.merge(tr.rt1[x],tr.rt1[v],-n,n);
        tr.rt2[x]=tr.merge(tr.rt2[x],tr.rt2[v],-n,n);
    }
    ans[x]=tr.query(tr.rt1[x],0,2*n,d[x]+w[x])+tr.query(tr.rt2[x],-n,n,w[x]-d[x]);

}
int main(){
    in(n);in(m);
    for(int i=1;i<n;++i){
        in(a);in(b);
        add(a,b);
        add(b,a);
    }
    for(int i=1;i<=n;++i)in(w[i]);
    for(int i=1;i<=n;++i)lg[i]=lg[i-1]+(1<<lg[i-1]==i);
    dfs1(1,0);
    for(int i=1;i<=m;++i){
        in(a);in(b);
        int LCA=lca(a,b);
        tr.update(tr.rt1[a],0,2*n,d[a],1);//查找的位置会到2*n
        tr.update(tr.rt1[fat[LCA]],0,2*n,d[a],-1);
        tr.update(tr.rt2[b],-n,n,d[a]-2*d[LCA],1);
        tr.update(tr.rt2[LCA],-n,n,d[a]-2*d[LCA],-1);
    }
    dfs2(1,0);
    for(int i=1;i<=n;++i)printf("%d ",ans[i]);
    return 0;
}
5.bzoj 2733 永无乡

题意:给你n座岛,每个岛都有1到n的排列权值,2个操作,x和y连通,查询与x连通的的第k重要的岛是哪个

思路:

并查集+线段树合并板子题,并查集的根的编号i就是当前集合线段树的根的归属。

合并的时候,记住根是rt[find(x)],就这样维护就好了

为了卡常按秩合并和快读也上了

时间复杂度 O ( q l o g n + 4 n ) O(qlogn+4n) O(qlogn+4n) 4是阿克曼函数的复杂度

空间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

#include<bits/stdc++.h>
#define N maxn*36
#define ls lc[p]
#define rs rc[p]
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
using namespace std;
const int maxn=1e5+5;
inline int read(){
    char c = getchar();int x = 0,s = 1;
    while(c < '0' || c > '9') {if(c == '-') s = -1;c = getchar();}//是符号
    while(c >= '0' && c <= '9') {x = x*10 + c -'0';c = getchar();}//是数字
    return x*s;
}
inline void in(int&x){
    x=read();
}
int n,m,a,b,Q,id[maxn];
char s[2];
struct SegmentTree{
    int rt[maxn],lc[N],rc[N],num[N],tot;
    void pushUp(int p){
        num[p]=num[ls]+num[rs];
    }
    void update(int&p,int l,int r,int x,int val){
        if(!p)p=++tot;
        if(l==r){
            num[p]+=val;return;
        }
        int mid=l+r>>1;
        if(x<=mid)update(lson,x,val);
        else update(rson,x,val);
        pushUp(p);
    }
    int merge(int p,int q,int l,int r){
        if(!p||!q)return p+q;
        if(l==r){
            num[p]+=num[q];return p;
        }
        int mid=l+r>>1;
        ls=merge(ls,lc[q],l,mid);
        rs=merge(rs,rc[q],mid+1,r);
        pushUp(p);
        return p;
    }
    int query(int p,int l,int r,int k){
        if(l==r)return l;
        int mid=l+r>>1;
        if(num[ls]>=k)return query(lson,k);
        else return query(rson,k-num[ls]);
    }
}tr;
struct DS{
    int fa[maxn],rank[maxn];
    void init(){
        for(int i=1;i<=n;++i)fa[i]=i,rank[i]=0;
    }
    int find(int x){
        if(x==fa[x])return x;
        return fa[x]=find(fa[x]);
    }
    void merge(int x,int y){
        int fx=find(x),fy=find(y);
        if(fx==fy)return;
        if(rank[fx]<rank[fy]){
            fa[fx]=fy;
            tr.rt[fy]=tr.merge(tr.rt[fy],tr.rt[fx],1,n);
        }else{
            fa[fy]=fx;
            tr.rt[fx]=tr.merge(tr.rt[fx],tr.rt[fy],1,n);
            if(rank[fy]==rank[fx])
                rank[fx]++;
        }
    }
}ds;
int main(){
    in(n);in(m);
    for(int i=1;i<=n;++i){
        in(a);id[a]=i;
        tr.update(tr.rt[i],1,n,a,1);
    }
    ds.init();
    for(int i=1;i<=m;++i){
        in(a);in(b);
        ds.merge(a,b);
    }
    in(Q);
    for(int i=1;i<=Q;++i){
        scanf("%s",s);
        in(a);in(b);
        if(s[0]=='Q'){
            int xx=ds.find(a);
            if(tr.num[tr.rt[xx]]<b){
                puts("-1");
            }else  
                cout<<id[tr.query(tr.rt[xx],1,n,b)]<<"\n";
        }else
            ds.merge(a,b);
        
    }
    return 0;
}

6.bzoj 2212 ROT-Tree Rotations

题意:给定一颗有 n个叶节点的二叉树。每个叶节点都有一个权值 p,所有叶节点的权值构成了一个 1到n的排列
对于这棵二叉树的任何一个结点,保证其要么是叶节点,要么左右两个孩子都存在。
现在你可以任选一些节点,交换这些节点的左右子树。
在最终的树上,按照先序遍历遍历整棵树并依次写下遇到的叶结点的权值构成一个长度为 n 的排列,你需要最小化这个排列的逆序对数。

思路:

非常明显的线段树合并的套路,因为当前交换与否不会影响上层结点交换与否,只要每次都对交换与不交换取min,同时从子树自底向上合并线段树维护值域即可,这里学到了用权值线段树合并左右子树维护逆序对的方法,对于同一个值域,假如不交换,显然是 s u m [ r c [ p ] ] ∗ s u m [ l c [ q ] ] sum[rc[p]]*sum[lc[q]] sum[rc[p]]sum[lc[q]],交换的话就是 s u m [ l c [ p ] ] ∗ s u m [ r c [ q ] ] sum[lc[p]] * sum[rc[q]] sum[lc[p]]sum[rc[q]],注意LL

另外这题输入太恶心了,dfs的时候同时返回树上每个点的根结点

时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn) 空间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

#include<bits/stdc++.h>
#define N maxn*36
#define ls lc[p]
#define rs rc[p]
using namespace std;
typedef long long ll;
const int maxn=2e5+5;
inline int read(){
    char c = getchar();int x = 0,s = 1;
    while(c < '0' || c > '9') {if(c == '-') s = -1;c = getchar();}
    while(c >= '0' && c <= '9') {x = x*10 + c -'0';c = getchar();}
    return x*s;
}
inline void in(int&x){
    x=read();
}
int n,val;
ll ans1,ans2,ans;
struct SegmentTree{
    int tot,lc[N],rc[N],sum[N];
    void pushUp(int p){
        sum[p]=sum[ls]+sum[rs];
    }
    int update(int l,int r,int x){
        int pos=++tot;
        sum[pos]++;
        if(l==r)return pos;
        int mid=l+r>>1;
        if(x<=mid)
            lc[pos]=update(l,mid,x);
        else rc[pos]=update(mid+1,r,x);
        return pos;
    }
    int merge(int p,int q,int l,int r){
        if(!p||!q)return p+q;
        if(l==r){sum[p]+=sum[q];return p;}
        ans1+=1ll*sum[rs]*sum[lc[q]];
        ans2+=1ll*sum[ls]*sum[rc[q]];
        int mid=l+r>>1;
        ls=merge(ls,lc[q],l,mid);
        rs=merge(rs,rc[q],mid+1,r);
        pushUp(p);
        return p;
    }
}tr;
int dfs(){
    int rt;in(val);
    if(!val){
        int lc=dfs(),rc=dfs();
        ans1=0;ans2=0;
        rt=tr.merge(lc,rc,1,n);
        ans+=min(ans1,ans2);
    }else
        rt=tr.update(1,n,val);
    return rt;
}
int main(){
    in(n);
    dfs();
    cout<<ans<<"\n";
    return 0;
}
7.cf 490F

题意:

求树上最长子序列的长度

思路:

1. O ( n 2 l o g n ) O(n^2logn) O(n2logn)

所有根都dfs,然后用二分的dp做就行了,记得回溯

2. O ( 2 n l o g n ) O(2nlogn) O(2nlogn)的线段树合并优化dp(常数大x) 空间 O ( 2 n l o g n ) O(2nlogn) O(2nlogn)

显然,第一个复杂度的瓶颈在于需要所有根都 d f s dfs dfs,所以我们需要想办法对于任意的路径都能维护,我们发现只要用线段树同时维护以 a [ i ] a[i] a[i]为结尾的最大 L I S LIS LIS和最大 L D S LDS LDS,就可以从子树往上回溯直接一次做完

从子树开始扩大,维护到当前点的时候,线段树上维护的是以当前子树中的点结尾的 m a x ( L I S ) max(LIS) max(LIS) m a x ( L D S ) max(LDS) max(LDS)

到当前点的 d p dp dp过程其实类似树形dp转移的过程,当前结点和当前到的子结点,一个是包含当前结点的 l i s lis lis l d s lds lds,一个是不包含,前者等于通过v的 n o w l i s + 之 前 的 m a x l i s + 1 nowlis+之前的maxlis+1 nowlis+maxlis+1和通过 v 的 n o w l d s + 之 前 的 m a x l d s + 1 v的nowlds+之前的maxlds+1 vnowlds+maxlds+1 后者直接在merge的时候查找就行了,有点类似逆序对

转移是通过 l i s [ l c [ p ] ] + l d s [ r c [ q ] ] lis[lc[p]]+lds[rc[q]] lis[lc[p]]+lds[rc[q]] l d s [ r c [ p ] ] + l i s lds[rc[p]]+lis lds[rc[p]]+lis[lc[q]]

这里我写复杂了,不如直接开两个根算了,不用传bool还好写

#include<bits/stdc++.h>
#define N maxn*30
#define ls lc[p]
#define rs rc[p]
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
using namespace std;
const int maxn=6005;
const int INF=0x3f3f3f3f;
int a[maxn],len,ans,cnt,head[maxn],ver[maxn<<1],next1[maxn<<1],n,val[maxn],mxlis,mxlds,m;
inline int read(){
    char c = getchar();int x = 0,s = 1;
    while(c < '0' || c > '9') {if(c == '-') s = -1;c = getchar();}
    while(c >= '0' && c <= '9') {x = x*10 + c -'0';c = getchar();}
    return x*s;
}
inline void in(int&x){
    x=read();
}
void add(int x,int y){
    ver[++cnt]=y,next1[cnt]=head[x],head[x]=cnt;
}
struct SegmentTree{
    int tot,rt[maxn],lc[N],rc[N],lis[N],lds[N];
    int query(int p,int l,int r,int L,int R,bool f){
        if(L>r||R<l)return 0;
        if(L<=l&&r<=R)return f?lis[p]:lds[p];
        int mid=l+r>>1;
        int ans=0;
        if(L<=mid)ans=max(ans,query(lson,L,R,f));
        if(R>mid)ans=max(ans,query(rson,L,R,f));
        return ans;
    }
    int merge(int p,int q,int l,int r){//不包含当前点lis
        if(!p||!q)return p+q;
        lis[p]=max(lis[p],lis[q]);lds[p]=max(lds[p],lds[q]);
        if(l==r)return p;
        ans=max(ans,max(lis[ls]+lds[rc[q]],lds[rs]+lis[lc[q]]));
        int mid=l+r>>1;
        ls=merge(ls,lc[q],l,mid);
        rs=merge(rs,rc[q],mid+1,r);
        return p;
    }
    void pushUp(int p,bool f){
        if(f)lis[p]=max(lis[ls],lis[rs]);
        else lds[p]=max(lds[ls],lds[rs]);
    }
    void update(int&p,int l,int r,int x,int val,bool f){
        if(!p)p=++tot;
        if(l==r){
            if(f)lis[p]=max(lis[p],val);
            else lds[p]=max(lds[p],val);
            return;
        }
        int mid=l+r>>1;
        if(x<=mid)update(lson,x,val,f);
        else update(rson,x,val,f);
        pushUp(p,f);
    }
}tr;
void dfs(int x,int fa){
    int mxlis=0,mxlds=0;
    for(int i=head[x];i;i=next1[i]){
        int v=ver[i];
        if(v==fa)continue;
        dfs(v,x);
        int nowlis=tr.query(tr.rt[v],1,m,1,a[x]-1,1);
        int nowlds=tr.query(tr.rt[v],1,m,a[x]+1,m,0);
        ans=max(ans,max(nowlis+mxlds,nowlds+mxlis)+1);//包含当前点的lis
        mxlis=max(mxlis,nowlis);mxlds=max(mxlds,nowlds);
        tr.rt[x]=tr.merge(tr.rt[x],tr.rt[v],1,m);
    }
    tr.update(tr.rt[x],1,m,a[x],mxlis+1,1);
    tr.update(tr.rt[x],1,m,a[x],mxlds+1,0);
}
int main(){
    in(n);
    int x,y;
    for(int i=1;i<=n;++i)in(a[i]),val[i]=a[i];
    for(int i=1;i<n;++i){
        in(x);in(y);
        add(x,y);
        add(y,x);
    }
    sort(val+1,val+1+n);
    m=unique(val+1,val+1+n)-(val+1);
    for(int i=1;i<=n;++i){
        a[i]=lower_bound(val+1,val+1+m,a[i])-val;
    }
    dfs(1,0);
    cout<<ans<<"\n";
    return 0;
}

3. O ( n l o g 2 ) O(nlog^2) O(nlog2)的dsu on tree(常数小) 待补

4. O ( n l o g n ) O(nlogn) O(nlogn)的线段树做法 某次gym遇到,队长切了,还能求方案数,不用线段树合并,回去自己补一下 待补

8.bzoj 3545 Peaks

题意:

n个点,点权为 h i h_i hi, q q q次询问,输出从点v开始只经过困难值小于等于x的路径所能到达的山峰中第k高的山峰,如果无解输出-1。

思路:

这题可以离线,bzoj 3551与gym 101194都是强制在线,得用Kruskal重构树,这两题有时间再补

这题按x离线排序,就是板子了,记得离散化

时间复杂度 O ( 5 n l o g n + q l o g n ) O(5nlogn+qlogn) O(5nlogn+qlogn) 空间复杂度 O ( n l o g n ) O(nlogn) O(nlogn),最多建点只有 n l o g n nlogn nlogn

#include<bits/stdc++.h> 
#define N maxn*35
#define ls lc[p]
#define rs rc[p]
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
using namespace std;
const int maxn=1e5+5;
const int maxm=5e5+5;
int n,m,q,h[maxn],cnt,id[maxn],a,b,c,ans[maxm],val[maxn];
inline int read(){
    char c = getchar();int x = 0,s = 1;
    while(c < '0' || c > '9') {if(c == '-') s = -1;c = getchar();}
    while(c >= '0' && c <= '9') {x = x*10 + c -'0';c = getchar();}
    return x*s;
}
inline void in(int&x){
    x=read();
}
struct Edge{
    int u,v,c;
    bool operator<(const Edge&a){
        return c<a.c;
    }
}edge[maxm];
struct Q{
    int v,x,k,id;
    bool operator<(const Q&a){
        return x<a.x;
    }
}qs[maxm];
struct SegmentTree{
    int rt[maxn],lc[N],rc[N],tot,num[N];
    void pushUp(int p){
        num[p]=num[ls]+num[rs];
    }
    void update(int&p,int l,int r,int x){
        if(!p)p=++tot;
        if(l==r){
            num[p]++;return;
        }
        int mid=l+r>>1;
        if(x<=mid)update(lson,x);
        else update(rson,x);
        pushUp(p);
    }
    int merge(int p,int q,int l,int r){
        if(!p||!q)return p+q;
        if(l==r){
            num[p]+=num[q];return p;
        }
        int mid=l+r>>1;
        ls=merge(ls,lc[q],l,mid);
        rs=merge(rs,rc[q],mid+1,r);
        pushUp(p);
        return p;
    }
    int query(int p,int l,int r,int k){
        if(l==r)return l;
        int mid=l+r>>1;
        if(num[rs]>=k)return query(rson,k);
        else return query(lson,k-num[rs]);
    }
}tr;
struct DS{
    int fa[maxn],rank[maxn];
    void init(){
        for(int i=1;i<=n;++i)fa[i]=i,rank[i]=0;
    }
    int find(int x){
        if(x==fa[x])return x;
        return fa[x]=find(fa[x]);
    }
    void merge(int x,int y){
        int fx=find(x),fy=find(y);
        if(fx==fy)return;
        if(rank[fx]<rank[fy]){
            fa[fx]=fy;
            tr.rt[fy]=tr.merge(tr.rt[fy],tr.rt[fx],1,cnt); 
        }else{
            fa[fy]=fx;
            tr.rt[fx]=tr.merge(tr.rt[fx],tr.rt[fy],1,cnt);
            if(rank[fx]==rank[fy])
                rank[fx]++;
        }
    }
}ds;
int main(){
    in(n);in(m);in(q);
    for(int i=1;i<=n;++i)in(h[i]),val[i]=h[i];
    sort(val+1,val+1+n);
    cnt=unique(val+1,val+1+n)-(val+1);
    ds.init();
    for(int i=1;i<=n;++i){
        h[i]=lower_bound(val+1,val+1+cnt,h[i])-val;
        tr.update(tr.rt[i],1,cnt,h[i]);
    }
    for(int i=1;i<=m;++i){
        in(a);in(b);in(c);
        edge[i]={a,b,c};
    }
    sort(edge+1,edge+1+m);
    for(int i=1;i<=q;++i){
        in(a);in(b);in(c);
        qs[i]={a,b,c,i};
    }
    sort(qs+1,qs+1+q);
    int now=1;
    for(int i=1;i<=q;++i){
        while(edge[now].c<=qs[i].x&&now<=m){
            if(edge[now].u==edge[now].v)continue;
            ds.merge(edge[now].u,edge[now].v);
            now++;
        }
        int id=ds.find(qs[i].v);
        if(tr.num[tr.rt[id]]<qs[i].k)ans[qs[i].id]=-1;
        else ans[qs[i].id]=val[tr.query(tr.rt[id],1,cnt,qs[i].k)];
    }
    for(int i=1;i<=q;++i)
        cout<<ans[i]<<"\n";
    return 0;
}
线段树分裂

线段树分裂实质上是线段树合并的逆过程,大家觉得很难,其实就是把一颗拆成2颗,对应需要的部分就拆,不需要的就直接接点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6Ji7Ntgd-1613089071668)(C:\Users\98753\AppData\Roaming\Typora\typora-user-images\image-20210211230635928.png)]

类似上图的过程,绿色是跟着新建的点,一旦区间包含了,就把当前点直接赋值,相当于直接接过去,然后把原来的给断掉(赋0)

我们可以发现,实际上这个的时间复杂度最坏是 O ( 2 l o g n ) O(2logn) O(2logn),相当于区间查询的复杂度,还蛮优的,所以一次分裂每次最多最多也只是多 O ( 2 l o g n ) O(2logn) O(2logn)个点

注意在分裂和合并都存在的时候,我们在合并的必须回收结点,因为如果像以前那样合并完另外一个不管的话,在各种分裂时候,会可能出现结点重复占用等问题,下面那道排序我就调了13h才发现这个问题

下面这题模板题之所以不用,是因为被合并之后的保证不出现第二次!!!

洛谷P 5494 模板题
//线段树分裂 分裂其实可看作区间查询 每次最多增加O(2logn)结点
//维护多个可重集 合并带内存回收
//0.p集合中值从x到y的放入放入新集合
//1.把t合并到p中,清空t(t以后不出现) 这里用了内存回收 2.p中加入x个q
//3.查询p中x到y数量     4.查询p中第k小
#include<bits/stdc++.h>
#define ls lc[p]
#define rs rc[p]
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
using namespace std;
typedef long long ll;
#define N maxn*32
const int maxn=2e5+5;
int n,m,op,cntset=1,a[maxn];
struct SementTree{
    int rt[maxn*3],lc[N],rc[N],tot,rub[N],cnt=0;
    ll sum[N];
    int New(){//内存回收  分裂合并同时有的时候必须使用
        if(cnt)return rub[cnt--];
        return ++tot;
    }
    void del(int&p){
        ls=rs=sum[p]=0;
        rub[++cnt]=p;
        p=0;
    }
    void pushUp(int p){
        sum[p]=sum[ls]+sum[rs];
    }
    void build(int&p,int l,int r){
        if(!p)p=New();
        if(l==r){
            sum[p]=a[l];return;
        }
        int mid=l+r>>1;
        build(lson);
        build(rson);
        pushUp(p);
    }
    void update(int&p,int l,int r,int x,int val){
        if(!p)p=New();
        if(l==r){
            sum[p]+=val;return;
        }
        int mid=l+r>>1;
        if(x<=mid)update(lson,x,val);
        else update(rson,x,val);
        pushUp(p);
    }
    int merge(int &p,int &q,int l,int r){
        if(!p||!q)return p+q;
        if(l==r){
            sum[p]+=sum[q];
            del(q);
            return p;
        }
        int mid=l+r>>1;
        ls=merge(ls,lc[q],l,mid);
        rs=merge(rs,rc[q],mid+1,r);
        del(q);
        pushUp(p);
        return p;
    }
    void split(int&p,int&q,int l,int r,int L,int R){
        if(L>r||R<l)return;
        if(!p)return;
        if(L<=l&&r<=R){
            q=p;
            p=0;return;//直接断边接上
        }
        if(!q)q=New();
        int mid=l+r>>1;
        if(L<=mid)split(ls,lc[q],l,mid,L,R);
        if(R>mid)split(rs,rc[q],mid+1,r,L,R);
        pushUp(p);//两颗更新
        pushUp(q);
    }
    ll query(int p,int l,int r,int L,int R){
        if(!p)return 0;
        if(L<=l&&r<=R)return sum[p];
        int mid=l+r>>1;
        ll ans=0;
        if(L<=mid)ans+=query(lson,L,R);
        if(R>mid)ans+=query(rson,L,R);
        return ans;
    }
    int kth(int p,int l,int r,int k){
        if(l==r)return l;
        int mid=l+r>>1;
        if(sum[ls]>=k)return kth(lson,k);
        else return kth(rson,k-sum[ls]);
    }
}tr;
int main(){
    int x,y,z;
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;++i)cin>>a[i];
    tr.build(tr.rt[1],1,n);
    for(int i=1;i<=m;++i){
        cin>>op;
        if(!op){
            cin>>x>>y>>z;
            tr.split(tr.rt[x],tr.rt[++cntset],1,n,y,z);
        }else if(op==1){
            cin>>x>>y;
            tr.rt[x]=tr.merge(tr.rt[x],tr.rt[y],1,n);
        }else if(op==2){
            cin>>x>>y>>z;
            tr.update(tr.rt[x],1,n,z,y);
        }else if(op==3){
            cin>>x>>y>>z;
            cout<<tr.query(tr.rt[x],1,n,y,z)<<"\n";
        }else if(op==4){
            cin>>x>>y;
            if(tr.sum[tr.rt[x]]<y)cout<<-1<<"\n";
            else cout<<tr.kth(tr.rt[x],1,n,y)<<"\n";
        }
    }
    return 0;
}
1.bzoj 4552

题意:给1到n的排列,进行m次局部排序,问某个点的值

两种方法

一. O ( n l o g n 2 ) O(nlogn^2) O(nlogn2)二分+线段树,

二分答案,小于的赋值为0,大于的为1,满足单调性,代码有时间再补

二. O ( n l o g n ) O(nlogn) O(nlogn)的线段树合并与分裂

每个有序区间用一颗动态开点线段树维护,用set维护区间的左端点,split函数表示将x区间分成左x右y的两个区间,Split(x),相当于在x这个位置加个左端点分裂开,分裂完后再合并,具体保留前k大还是前k小,用op标记维护,细节非常多,还是看代码吧,个人觉得当成板子题就行了x

我们发现每次最多只会分裂多出2个区间

时间复杂度和空间复杂度都是 O ( n l o g n + 2 m l o g n ) O(nlogn+2mlogn) O(nlogn+2mlogn)

注意这题合并必须删掉原来的点,不删就得一直++tot,所以还是回收空间吧

//区间排序后求某点
#include<bits/stdc++.h>
#define ls lc[p]
#define rs rc[p]
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
#define N maxn*54
using namespace std;
typedef set<int>::iterator IT;
const int maxn=1e5+5;
int n,m,a,L,R,pos;
int op,Op[maxn];
set<int>s;//存储有序区间的左端点(根结点)方便分裂与合并
struct SegmentTree{
    int rt[maxn],lc[N],rc[N],sum[N],rub[N],tot,cnt=0;
     int New(){//内存回收 衡量垃圾桶与点数越界的内存差
        if(cnt)return rub[cnt--];
        return ++tot;
    }
    void del(int&p){
        ls=rs=sum[p]=0;
        rub[++cnt]=p;
        p=0;
    }
    int merge(int p,int&q,int l,int r){
        if(!p||!q)return p+q;
        sum[p]+=sum[q];
        if(l==r){del(q);return p;}
        int mid=l+r>>1;
        ls=merge(ls,lc[q],l,mid);
        rs=merge(rs,rc[q],mid+1,r);
        del(q);
        return p;
    }
    void pushUp(int p){
        sum[p]=sum[ls]+sum[rs];
    }
    void update(int&p,int l,int r,int x){
        if(!p)p=New();
        if(l==r){
            sum[p]++;return;
        }
        int mid=l+r>>1;
        if(x<=mid)update(lson,x);
        else update(rson,x);
        pushUp(p);
    }
    int query(int p,int l,int r){
        if(l==r)return l;
        int mid=l+r>>1;
        if(ls)return query(lson);
        else return query(rson);
    }
    void split(int p,int&q,int k,int op,int l,int r){//把p分裂成左p右q的两个区间 给p留指定的k个
        if(!p)return;//有重复元素的时候叶子也可以是k 防空结点
        if(sum[p]==k)return;
        if(!q)q=New();
        sum[q]=sum[p]-k;sum[p]=k;
        if(op){//1降序找前k大
            if(sum[rs]>=k){
                split(rs,rc[q],k,op);
                lc[q]=lc[p];lc[p]=0;//左边直接断给q
            }else
                split(ls,lc[q],k-sum[rs],op);
        }else{//0升序找前k小
            if(sum[ls]>=k){
                split(ls,lc[q],k,op);
                rc[q]=rc[p];rc[p]=0;
            }else
                split(rs,rc[q],k-sum[ls],op);
        }
    }
}tr;
IT Split(int x){
    auto v=s.lower_bound(x);
    if(*v==x)return v;
    --v;
    Op[x]=Op[*v];//拆分的时候保证前k大还是前k小选择正确
    tr.split(tr.rt[*v],tr.rt[x],x-*v,Op[x]);
    return s.insert(x).first;//插入分裂的区间的新的左端点
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    s.insert(n+1);
    for(int i=1;i<=n;++i){
        cin>>a;
        tr.update(tr.rt[i],1,n,a);
        s.insert(i);
    }
    for(int i=1;i<=m;++i){
        cin>>op>>L>>R;
        auto st=Split(L);
		auto ed=Split(R+1);
        for(auto it=++st;it!=ed;++it){
			tr.rt[L]=tr.merge(tr.rt[L],tr.rt[*it],1,n);
		}
        Op[L]=op;s.erase(st,ed);//最多2个
    }
    cin>>pos;
    Split(pos);Split(pos+1);//分裂成单点 求整个区间每个i分裂一次就好了
    cout<<tr.query(tr.rt[pos],1,n)<<"\n";
    return 0;
}
2.cf 490F

题意:对26个字母的序列进行区间排序,输出最后的字母

思路:

一、 O ( 26 n l o g n ) O(26nlogn) O(26nlogn)

二、$O(nlogn) $

和上题一样,唯一要注意的是由于叶子结点的sum不止一个,所以当有重复的时候,必须在空结点的时候直接返回,否则会出错,比方法一大概常数小了十倍?

//区间排序后输出某点值

#include<bits/stdc++.h>
#define ls lc[p]
#define rs rc[p]
#define lson lc[p],l,mid
#define rson rc[p],mid+1,r
#define N maxn*40
using namespace std;
typedef set<int>::iterator IT;
const int maxn=1e5+5;
int n,m,L,R,pos;
int op,Op[maxn],val[100];
char a[maxn],mp[150];
set<int>s;//存储有序区间的左端点(根结点)方便分裂与合并
struct SegmentTree{
    int rt[maxn],lc[N],rc[N],sum[N],rub[N],tot,cnt=0;
     int New(){//内存回收 衡量垃圾桶与点数越界的内存差
        if(cnt)return rub[cnt--];
        return ++tot;
    }
    void del(int&p){
        ls=rs=sum[p]=0;
        rub[++cnt]=p;
        p=0;
    }
    int merge(int p,int&q,int l,int r){
        if(!p||!q)return p+q;
        sum[p]+=sum[q];
        if(l==r){del(q);return p;}
        int mid=l+r>>1;
        ls=merge(ls,lc[q],l,mid);
        rs=merge(rs,rc[q],mid+1,r);
        del(q);
        return p;
    }
    void pushUp(int p){
        sum[p]=sum[ls]+sum[rs];
    }
    void update(int&p,int l,int r,int x){
        if(!p)p=New();
        if(l==r){
            sum[p]++;return;
        }
        int mid=l+r>>1;
        if(x<=mid)update(lson,x);
        else update(rson,x);
        pushUp(p);
    }
    int query(int p,int l,int r){
    	if(!p)return 0;
        if(l==r)return l;
        int mid=l+r>>1;
        if(ls)return query(lson);
        else return query(rson);
    }
    void split(int p,int&q,int k,int op){//把p分裂成左p右q的两个区间 给p留指定的k个
 		if(!p)return;//有重复值,必须加
        if(sum[p]==k)return;
        if(!q)q=New();
        sum[q]=sum[p]-k;sum[p]=k;
        if(!op){//1降序找前k大
            if(sum[rs]>=k){
                split(rs,rc[q],k,op);
                lc[q]=lc[p];lc[p]=0;//左边直接断给q
            }else
                split(ls,lc[q],k-sum[rs],op);
        }else{//0升序找前k小
            if(sum[ls]>=k){
                split(ls,lc[q],k,op);
                rc[q]=rc[p];rc[p]=0;
            }else
                split(rs,rc[q],k-sum[ls],op);
        }
    }
}tr;
IT Split(int x){
    auto v=s.lower_bound(x);
    if(*v==x)return v;
    --v;
    Op[x]=Op[*v];//拆分的时候保证前k大还是前k小选择正确
    tr.split(tr.rt[*v],tr.rt[x],x-*v,Op[x]);
    return s.insert(x).first;//插入分裂的区间的新的左端点
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    cin>>(a+1);
    s.insert(n+1);
    for(int i=97;i<=97+26-1;++i)val[i-'a']=i-'a'+1;
	for(int i=97;i<=97+26-1;++i)mp[i]=i;	
    for(int i=1;i<=n;++i){
        tr.update(tr.rt[i],1,26,val[a[i]-'a']);
        s.insert(i);
    }
    for(int i=1;i<=m;++i){
        cin>>L>>R>>op;
        auto st=Split(L);
		auto ed=Split(R+1);
        for(auto it=++st;it!=ed;++it){
			tr.rt[L]=tr.merge(tr.rt[L],tr.rt[*it],1,26);
		}
        Op[L]=op;s.erase(st,ed);
    }
    for(int i=1;i<=n;++i){
		Split(i); 
	}
	for(int i=1;i<=n;++i) 
		cout<<mp[(tr.query(tr.rt[i],1,26)+96)];
    return 0;
}

总结:

线段树合并常用于子树信息向上合并过程值域多重集维护dp优化

比较常用的套路是每个点建一颗权值线段树再合并,或者每个多重集建再合并

线段树分裂常用于值域多重集分裂区间排序问题

下面提出一些要注意的点

1.线段树合并常数较大,需要严格计算,但一般不超过最终的总点数,看情况

2.动态开点空间允许情况下再开两倍,实在不行就空间回收,编译器的空间不是静态的,而是动态用了多少算多少,空间回收可以省大概

2倍空间

3.线段树分裂与合并一起用的时候必须写空间回收

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值