并查集,分治,拓扑排序

并查集

1:加边,查询两点什么时候连通

考虑重构树,将连通变成查阅路径上的权值最大值

动态加边可能需要LCT去维护

2:加边,询问第\(i\)时刻的\(x\)所在联通块大小

继续考虑重构树

实质上就是求边权第一次大于\(i\)的位置

此时的点的size就是所求答案

3:维护单点染黑,查询区间第一个白

\(f_i\)\(i\)后面的第一个零

单点染色之后

\(f_x = x + 1\)

之后把\(f\)当做并查集的数组

进行查询

4区间染色无颜色点

线段树可以直接递归做

因为每个点最多只会被染色一次

所以时间复杂度\(nlogn\)

并查集我们考虑和上一个差不多的做法

和上面一样

因为每个点最多被染色一次

所以时间复杂度均摊\(O(n)\)

5 区间开根区间求和

每次找区间第一个非\(1\)个位置

暴力开根

因为开根次数不会太多

所以和上面一样用并查集维护

暴力开根之后维护一个树状数组查询区间和

6求\(\max{a_i * b_j *\min_{k = i}^jc_k}\)

单调栈处理一下每个\(c_i\)单调延伸的位置

区间\(RMQ\)即可

考虑如何用并查集去做

考虑分治做法,每次考虑跨\(mid\)的答案贡献

而并查集实质就是分治倒过来的过程

我们将\(i\)边权定义为\(min(c_i,c_{i + 1})\)

将边权排序

每个并查集维护\(a,b\)的最大值

每次合并就是将\(i,i + 1\)所在联通块合并

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

每次加边操作实质上就是将这两个点路径上的所有点缩成一个

并查集维护的实质上是缩完点后当前联通块的最高点

每次暴力跳都跳一个缩完后的点,就可以保证时间复杂度。

时间复杂度貌似是\(O(n)\)

相同区间(scoi2016萌萌哒)

给定一个序列\(a\),\(a_i\in[0,k]\)给定\(m\)个限制,形如\(a[l1…r1]=a[l2…r2]\)

求最终可能的序列个数

直接暴力并查集合并的时间复杂度是\(O(nmlogn)\)

我们考虑利用\(st\)表的方式

将每次操作的区间分成\(log\)个小区间

将小区间之间一一对应

最后将区间关系拆分成长度为\(1\)的区间就可以得到了

#include<cstdio>
#include<cstring>
#include<iostream>
#include<vector>
#include<algorithm>
#include<set>
#include<map>
#define LL long long
using namespace std;
const int N = 1e5 + 3;
const LL mod = 1e9 + 7; 
int fa[N * 21];
inline int getf(int x){
    return fa[x] == x ? x : fa[x] = getf(fa[x]);    
}
inline LL quick(LL a,LL b){
    LL res = 1;
    while(b){
        if(b & 1) res = res * a % mod;
        b >>= 1;
        a = a * a % mod;    
    }
    return res;
}
inline int read(){
    int v = 0,c = 1;char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') c = -1;
        ch = getchar(); 
    }
    while(isdigit(ch)){
        v = v * 10 + ch - 48;
        ch = getchar(); 
    }
    return v * c;
}
inline void add(int x,int y){
    //printf("%d %d\n",x,y);
    x = getf(x),y = getf(y);
    fa[y] = x;  
}
int n,m;
int main(){
    n = read(),m = read();
    for(int i = 1;i <= n * 20;++i) fa[i] = i;
    for(int i = 1;i <= m;++i){
        int l1 = read(),r1 = read(),l2 = read(),r2 = read();
        int len = r1 - l1 + 1;
        int now1 = l1,now2 = l2;
        for(int i = 19;i >= 0;--i)
            if(len & (1 << i)) {
                add(i * n + now1,i * n + now2); 
            //  printf("%d %d\n",i * n + now1,i * n + now2);
                now1 += (1 << i);
                now2 += (1 << i);
            }   
    }
    for(int i = 19;i >= 1;--i){
        for(int j = 1;j <= n;++j){
            int x = getf(i * n + j);
            if(x != i * n + j){
                add(i * n + j - n,x - n);
                add(i * n + j - n + (1 << (i - 1)),x - n + (1 << (i - 1)));
            }   
        }
    }
    LL sum = 0;
    for(int i = 1;i <= n;++i) if(getf(i) == i) sum++;//cout << sum << endl;
    printf("%lld\n",9 * quick(10ll,sum - 1) % mod);
    return 0;   
}

CF468B

我们发现,对于

\(x\),如果没有\(a- x\)\(b -x\)

那么无解

否则,如果没有\(a-x\)那么只能在\(b\)

如果没有\(b - x\)那么只能在\(a\)

如果都存在,那么我们只能 得到\(x\),\(a -x\),\(b-x\)必须在一起

注意,在合并过程中只要出现和之前的判断不一样的解,就是无解

同时,如果到了最后这个元素没有被强制分配,那么它在那个集合都是可以的

#include<cstdio>
#include<cstring>
#include<iostream>
#include<vector>
#include<algorithm>
#include<set>
#include<map>
using namespace std;
const int N = 2e5 + 3;
map <int,int> m;
inline int read(){
    int v = 0,c = 1;char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') c = -1;
        ch = getchar(); 
    }
    while(isdigit(ch)){
        v = v * 10 + ch - 48;
        ch = getchar(); 
    }
    return v * c;
}
int n,a,b;
int fa[N];
int x[N];
inline int getf(int x){
    return fa[x] == x ? x : fa[x] = getf(fa[x]);    
}
int main(){
    n = read(),a = read(),b = read();
    for(int i = 1;i <= n;++i) x[i] = read(),m[x[i]] = i;
    for(int i = 1;i <= n + 2;++i) fa[i] = i;
    for(int i = 1;i <= n;++i){
        if(m[a - x[i]] == 0 && m[b - x[i]] == 0){
            printf("NO\n");
            return 0;
        }
        if(m[a - x[i]] == 0){
            int g = getf(i);
            int gg = getf(m[b - x[i]]);
            if(g == getf(n + 1) || gg == getf(n + 1)){
                printf("NO\n");
                return 0;   
            }
            fa[gg] = fa[g] = getf(n + 2);   
        }
        else if(m[b - x[i]] == 0){
            int g = getf(i);
            int gg = getf(m[a - x[i]]);
            if(g == getf(n + 2) || gg == getf(n + 2)){
                printf("NO\n");
                return 0;   
            }
            fa[gg] = fa[g] = getf(n + 1);
        }
        else {
            int g = getf(i);int now = 0;
            int xx = getf(m[a - x[i]]);
            int xxx = getf(m[b - x[i]]);
            int n1 = getf(n + 1),n2 = getf(n + 2);
            if(g == n1 || xx == n1 || xxx == n1) now++;
            if(xxx == n2|| g == n2 || xx == n2) now++;
            if(now == 2){
                printf("NO\n");
                return 0;   
            }
            fa[g] = xxx;
            fa[xx] = xxx;   
        }
    }
/// cout << "GG" << endl;
//  for(int i = 1;i <= n;++i){
//      int x = getf(i);
//      if(x != getf(n + 1) && x != getf(n + 2)){
//          printf("NO\n");
//          return 0;
//      }
//  }
    printf("YES\n");
    for(int i = 1;i <= n;++i){
        int x = getf(i);
        if(x == getf(n + 1)) printf("0 ");
        else printf("1 ");  
    }
    return 0;   
}

BZOJ2054

我们发现,我们将所有的操作倒过来进行,问题就变成了上面并查集的问题4

直接跑那个算法就好了

但是注意

在预处理的时候\(fa\)数组拿不准一定要多多处理几位

这道题如果不处理\(fa_{n + 1}\)就会出现\(fa_{n+1} = 0\)的情况而导致每次跳到\(n + 1\)时跳到\(0\)而死循环

#include<cstdio>
#include<cstring>
#include<iostream>
#include<vector>
#include<algorithm>
#include<set>
#include<map>
#define LL long long
using namespace std;
const int N = 1e6 + 3;
int fa[N];
int c[N];
inline int read(){
    int v = 0,c = 1;char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') c = -1;
        ch = getchar(); 
    }
    while(isdigit(ch)){
        v = v * 10 + ch - 48;
        ch = getchar(); 
    }
    return v * c;
}
inline int getf(int x){
    return x == fa[x] ? x : fa[x] = getf(fa[x]);    
}
int n,m,p,q;
int main(){
    n = read(),m = read(),p = read(),q = read();
    for(int i = 1;i <= n + 1;++i) fa[i] = i;
    for(int i = m;i >= 1;--i){
    //  cout << i << endl;
        int l = (1ll * i * p + q) % n + 1;
        int r = (1ll * i * q + p) % n + 1;
        if(l > r) swap(l,r);
    //  printf("l:%d r:%d\n",l,r);
        int x = getf(l);
        while(x <= r){
            c[x] = i;
            int t = getf(x + 1);
            fa[x] = t;
            x = t;
        }
    }
    for(int i = 1;i <= n;++i) printf("%d\n",c[i]);
    return 0;   
}

NOI程序自动分析(强制在线版)

每个并查集要维护一个vector,表示当前集合内的不等价关系

每次合并对vector进行启发式合并

在合并之前,要暴力枚举小的vector,判断能否进行合并

也就是看不等关系是否出现在另一个集合

时间复杂度\(mlog(n)\)

BZOJ3712

克鲁斯卡尔重构树

我们对于每一次操作都新加一个节点当做他们的整合节点,这样的话我们对于每一次合并,肯定是在他们的克鲁斯卡尔重构树的LCA处进行的

也就是说,我们要在克鲁斯卡尔重构树的新增节点上维护一下点对在这个点的合并关系

每次利用剩余的进行合并就好了

#include<cstdio>
#include<cstring>
#include<cctype>
#include<iostream>
#include<vector>
#include<algorithm>
#define LL long long
#define pii pair<int,int>
#define mk make_pair
#define fi first
#define se second
using namespace std;
const int N = 5e5 + 3;
LL ans;
int n,m,k,tot,root;
int g[N],head[N];
int deep[N];
int st[21][N];
int fa[N];
struct opt{
    int from;
    int to; 
}a[N];
vector <pii> G[N];
struct edge{
    int to;
    int nxt;    
}e[N << 1];
vector <pii> s;
inline void add(int x,int y){
    e[++tot].to = y;
    e[tot].nxt = head[x];
    head[x] = tot;
}
inline int read(){
    int v = 0,c = 1;char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') c = -1;
        ch = getchar(); 
    }
    while(isdigit(ch)){
        v = v * 10 + ch - 48;
        ch = getchar(); 
    }
    return v * c;
}
inline int getf(int x){
    return fa[x] == x ? x : fa[x] = getf(fa[x]);    
}
inline int dfs(int x,int f,int dep){
    st[0][x] = f;
    deep[x] = dep;
    for(int i = head[x];i;i = e[i].nxt){
        int y = e[i].to;
        dfs(y,x,dep + 1);
    }
}
inline int lca(int x,int y){
    if(deep[x] < deep[y]) swap(x,y);
    for(int i = 20;i >= 0;--i) if(deep[st[i][x]] >= deep[y]) x = st[i][x];
    if(x == y) return x;
    for(int i = 20;i >= 0;--i)
        if(st[i][x] != st[i][y]) x = st[i][x],y = st[i][y];
    return st[0][x];
}
int main(){
    n = read(),m = read(),k = read();
    for(int i = 1;i <= n;++i) g[i] = read();
    for(int i = 1;i <= n * 2;++i) fa[i] = i;
    for(int i = 1;i <= m;++i){
        a[i].from = read();
        a[i].to = read();   
        int x = getf(a[i].from),y = getf(n + i);        
        add(y,x);fa[x] = y;
        x = getf(a[i].to);
        add(y,x);fa[x] = y;
    }
    for(int i = 1;i <= n + m;++i){
        if(!deep[i])
        dfs(getf(i),0,1);   
    }
    for(int i = 1;i <= 20;++i){
        for(int j = 1;j <= n + m;++j)
            st[i][j] = st[i - 1][st[i - 1][j]]; 
    }
    for(int i = 1;i <= k;++i){
        int x = read(),y = read();
        int L = lca(x,y);
        G[L].push_back(mk(x,y));
    }
    for(int i = n + 1;i <= n + m;++i){
    //  printf("i:%d\n",i);
        for(int j = 0;j < (int)G[i].size();++j){
    //      printf("%d %d\n",G[i][j].fi,G[i][j].se);
            LL x = min(g[G[i][j].fi],g[G[i][j].se]);
            ans += 1ll * x * 2;
            g[G[i][j].fi] -= x;
            g[G[i][j].se] -= x;
        }
    }
    cout << ans << endl;
    return 0;   
}

NOI2018归程

离线肯定很好做

我们将查询按照硬度排序

不断将满足条件的边加进去,然后查阅\(1\)号点所在并查集中\(dis_x\)的最小值

这是离线是我们可以考虑的做法

我们对于在线做法,考虑克鲁斯卡尔重构树,其本质就是并查集

分治

多项式乘法

求多项式\(f(x)* g(x)\)的值

当然,这可以直接\(FFT\)

我们考虑从分治的思想去了解

\(f(x)= A(x) * x^{\frac{n}{2}}+B(x)\)

\(g(x) =C(x)*x^{\frac{n}{2}}+D(x)\)

\(f(x)*g(x) = A(x)C(x) x^n + (B(x)C(x)+A(x)D(x))x^{\frac{n}{2}} + B(x)D(x)\)

ZJOI2016

考虑每次跨过\(mid\)的答案,我们就是