A

加边,维护连通性

使用路径压缩,时间复杂度

使用按秩合并,时间复杂度

两个都用,时间复杂度

B

加边,撤消上次加边,维护连通性

使用按秩合并,记录上次加的边对应的树边。这样在撤消的时候是的。

时间复杂度

可撤消并查集:

struct dsu{
    int f[M],g[M];
    void init(int n){ FOR(i,0,n)f[i]=i,g[i]=1; }
    dsu(){}
    dsu(int n){init(n);}
    typedef pair<pair<int*,int>,int> ppi;//f[],val,time
    stack<ppi> s;
    int current_time(){ return s.empty()?0:s.top().se; }
    int get(int u){ return f[u]==u?u:get(f[u]); }
    bool find(int u,int v){ return get(u)==get(v); }

    void merge(int u,int v){
        int cur=current_time()+1;
        u=get(u),v=get(v);
        if(u==v)return;
        if(g[u]==g[v]){
            s.push(mk(mk(f+u,f[u]),cur)), f[u]=v;
            s.push(mk(mk(g+v,g[v]),cur)), g[v]++;
        } else {
            if(g[u]>g[v])swap(u,v);
            s.push(mk(mk(f+u,f[u]),cur)), f[u]=v;
        }
    }
    void undo(){
        int cur=current_time();
        while(current_time()==cur){
            ppi u=s.top();s.pop();
            *u.fi.fi = u.fi.se;
        }
    }
    void undo(int untiltime){
        while(current_time()>untiltime)undo();
    }
};

C

支持加边,询问

表示最早在什么时候连通。

维护每个连通块的大小,加边的时候统计一下即可。

时间复杂度

D

支持加边,求两个点在什么时候连通。

路径权值为它被添加的时间,显然可以建树,倍增求路径最小值。时间复杂度

E

支持加边,询问第个点在第时刻所在连通块的大小。

显然可以离线,按时刻排序后可以顺着做一遍。

还有一个在线的思路,就是每一条边的权值为它的出现时间,那么建一个 kruskal 重构树(就是在加边的时候新建虚点)。然后每个子树记录它非虚点的个数。显然,祖先路径上边的权值显然是从下到上递增的。这样问题就转化为求祖先路径上第一个权值大于的边对应的结点,倍增搞一下就行。

F

给一个数组,一开始全是,每次两种操作:

  1. 输出中第一个的位置。

建立一个并查集,指向它右边第一个的位置。初始时都指向自己。赋值相当于

G

给一个数组, 一开始全是, 每次操作会将中还是的全部变成
求最后的数组⻓什么样。

仍然是并查集指向下一个的位置,直接暴力改就行,均摊并查集复杂度。

H

一个序列,支持区间开根,区间求和。

并查集指向右边第一个大于的数,然后暴力改就行。

均摊复杂度

I

给定,求的最大值。

按照从大到小排序,每次加入一个点,连接两边相邻点所在连通块。显然可以每个连通块维护的最大值,然后在合并的时候更新答案。有一些细节需要考虑。

复杂度

J

给定一棵树,每次加边或者询问两个点之间是否有至少两条边不相交的路径。

即询问两个点是否在一个环上。手玩一下可以发现,每添加一条边相当于覆盖一条路径,当两个覆盖有重叠部分时它们就“连通”了,两个路径上的点都可以互达。于是就可以并查集,每次指向祖先路径上还没被覆盖的点。每个点记一个深度,然后就可以愉快并查集了。