启发式合并

  • Post author:
  • Post category:其他




启发式合并



1.算法分析



1.1 一般启发式合并

启发式合并指的是,对于两个集合x和y合并的操作,每次把元素个数更少的集合合并到元素个数更大的集合内,即:设x为元素更少的集合,那么就把x合并到y内。

可以证明,启发式合并的时间复杂度为:



O ( n l o g n ) O(nlog_n)






O


(


n


l


o



g










n


















)





,因为对于每个元素,他所处的集合被合并k次,那么这个元素就被合并k次,但是每次合并都会使得集合的大小乘以2,那么一个集合最多被合并



l o g 2 n log_2n






l


o



g










2


















n





次,所有一个元素最多被合并



l o g 2 n log_2n






l


o



g










2


















n





次,因此n个元素被合并的复杂度为



O ( n l o g 2 n ) O(nlog_2n)






O


(


n


l


o



g










2


















n


)






1.2 树上启发式合并

强大的解决对于子树的询问问题。


第一种就是先走轻边,再走重边,重边的不清空数组回来,这样修改次数就是 logn 次的。


第二种就是看set map的size,把size小的并到大的上。



1.2.1 先走轻边的启发式合并距离

树上启发式合并(dsu on tree)对于某些树上离线问题可以速度大于等于大部分算法且更易于理解和实现的算法。

考虑下面的问题:

给出一棵树,每个节点有颜色,询问一些子树的颜色数量(颜色可重复)。

对于每一个节点,我们可以dfs向下搜索来计数每个子树内出现次数最多的颜色是哪个,然后dfs完就需要再次dfs来清空数组,这样是



O ( n 2 ) O(n^2)






O


(



n










2









)





。启发式合并是指,我们每次将dfs划分为对重儿子dfs和轻儿子dfs,那么对于父节点u的答案就是两种dfs的和。

我们每次再做dfs清空的时候不需要清空u节点的所有子树,只需要清空所有的轻子树,这样的复杂度为



O ( n l o g n ) O(nlogn)






O


(


n


l


o


g


n


)





。复杂度证明:对于每个点,其所处的子树被清空一次就会有一次操作,对于一个点来说,最坏的情况就是这个点一直处于轻子树,那就会一直被清空,而轻边最多有



O ( l o g n ) O(logn)






O


(


l


o


g


n


)





条,那么一个点最多被清空



O ( l o g n ) O(log_n)






O


(


l


o



g










n


















)





次,则所有点最多被清空



O ( n l o g n ) O(nlog_n)






O


(


n


l


o



g










n


















)





次。



1.2.2 set类型

dfs时将子树的set不断合并到根的set上,每次合并的时候都看下哪个size更大,把小的合并到大的上面



2.模板


树上启发式合并

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 100010, M = N * 2;

int n;
int h[N], e[M], ne[M], idx;
int color[N], cnt[N], sz[N], son[N];  // son[u]记录u点的重儿子
LL ans[N], sum;
int mx;

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++; }

// 把每个点的重儿子找到
int dfs_son(int u, int father) {
   
    sz[u] = 1;
    for (int i = h[u]; ~i; i = ne[i]) {
   
        int j = e[i];
        if (j == father) continue;
        sz[u] += dfs_son(j, u);
        if (sz[j] > sz[son[u]]) son[u] = j;
    }
    return sz[u];
}

// 计数u为根的子树的主要颜色,但是不计数pson(如果pson != 0说明不计算重儿子,如果=0说明全部都要计算)
// sign表示当前节点对于cnt数组的贡献,sign=1表示当前节点要加到cnt数组,=-1说明要情况当前节点的贡献
void update(int u, int father, int sign, int pson) {
   
    int c = color[u];  // u节点颜色
    cnt[c] += sign;  // 加上当前节点贡献
    if (cnt[c] > mx)  // 如果大于最大的颜色数目
        mx = cnt[c], sum = c;  // 更新最大的颜色数目,更新最大的颜色权重和
    else if (cnt[c] == mx)  // 如果和最大的颜色数目相等,只要更新最大的颜色权值和
        sum += c;

    for (int i = h[u]; ~i; i = ne[i]) {
     // 遍历子树,计算子树的贡献,不计算pson的贡献
        int j = e[i];
        if (j == father || j == pson) continue;
        update(j, u, sign, pson);  // 计数所有轻儿子
    }
}

// 计算u点的主要颜色的和,keep表示当前节点的子树是否留下来,keep=0删,keep=1留
void dfs(int u, int father, int keep) {
   
    for (int i = h[u]; ~i; i = ne[i]) {
   
        int j = e[i];
        if (j == father || j == son[u]) continue;  // 如果是重儿子,不计算
        dfs(j, u, 0);  // 优先遍历轻儿子,然后清理cnt数组,打上keep=0的标记
    }

    if (son[u]) dfs(son[u], u, 1);  // 搜索重儿子,它的cnt数组不清空,打上keep=1的标记
    update(u, father, 1, son[u]);  // 将cnt数组加上所有除了重儿子之外的贡献(即轻儿子)

    ans[u] = sum;  // 更新u点的出现次数最多的颜色的权值和

    if (!keep) update(u, father, -1, 0), sum = mx = 0;  // 如果当前点的keep标记为0,那么表示要删除当前点的贡献,即cnt数组
}

int main() {
   
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &color[i]);
    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i++) {
   
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);
    }

    dfs_son(1, -1);  // 先把每个点的重儿子找到
    dfs(1, -1, 1);  // 计算主要颜色

    for (int i = 1; i <= n; i++) printf("%lld ", ans[i]);
    return 0;
}



3.典型例题



3.1 一般启发式合并


acwing2154. 梦幻布丁


题意:

给定n个数字,每个数字有一个权值代表其颜色



a i a_i







a










i





















,现在有m次询问:

询问1:询问当前有多少个颜色段?(比如1 2 2 1就是3个颜色段)

询问2:将x颜色全部修改为颜色y。




0 < n , m < = 1 e + 1 , 0 < A i , x , y < 1 0 6 0<n,m<=1e+1,0<A_i,x,y<10^6






0




<








n


,




m




<






=








1


e




+








1


,




0




<









A










i


















,




x


,




y




<








1



0










6












题解:

启发式合并。给每个颜色维护一个链表,链表上是当前颜色所有点的下标。对于询问1,我只需要判断当前点的颜色是否和前一个点的颜色相同,不同的话段数就会加一,因此我每次修改颜色时再暴力枚举当前颜色的链表,逐个判断,暴力维护。对于询问2,将颜色x修改为y等价于我将颜色y修改为x,然后将颜色y的指针修改为指向x。因此,我可以暴力枚举集合size更小的y(启发式合并思想),然后暴力维护段数,最后去修改指针即可。这样操作的复杂度为



O ( n l o g n ) O(nlogn)






O


(


n


l


o


g


n


)





代码:

#include <bits/stdc++.h>

using namespace std;

const int N = 100010, M = 1000010;

int n, m;
int h[M], e[N], ne[N], idx;
int color[N], sz[M], p[M];
int ans;

void add(int a, int b) {
   
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
    sz[a]++;
}

void merge(int& x, int& y) {
     // 合并颜色x和颜色y的链表
    if (x == y) return;
    if (sz[x] > sz[y]) swap(x, y);  //让x为更小的那个链表
    for (int i = h[x]; ~i; i = ne[i]) {
     // 枚举x颜色所在的链表
        int j = e[i];
        ans -= (color[j - 1] == y) + (color[j + 1] == y);  // 如果更新了和前一个或者后一个相同,那么段的数目就会减少
    }
    for (int i = h[x]; ~i; i = ne[i]) {
     // 修改链表的结构,使得数目小的接到大的后面
        int j = e[i];
        color[j] = y;  // 修改当前点的颜色
        if (ne[i] == -1) {
     // 如果指到链表的尾部了
            ne[i] = h[y], h[y] = h[x];
            break;
        }
    }
    h[x] = -1;  // 清空颜色x的链表
    sz[y] += sz[x], sz[x] = 0;  // 修改颜色y的链表大小
}

int main() {
   
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i++) {
   
        scanf("%d", &color[i]);
        if (color[i] != color[i - 1]) ans++;  // 当前颜色和前一个颜色不同时,段的数目加一
        add(color[i], i);  // 每一个颜色维护一个链表
    }

    for (int i = 0; i < M; i++) p[i] = i;  // 记录一下每个点连接的颜色点

    while (m--) {
   
        int op;
        scanf("%d", &op);
        if (op == 2)
            printf("%d\n", ans);
        else {
   
            int x, y;
            scanf("%d%d", &x, &y);
            merge(p



版权声明:本文为m0_49959202原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。