启发式合并

启发式合并

n n n个集合进行合并,最后合并为1个集合

暴力合并

假设一次合并的时间复杂度为 O ( o p ) O(op) O(op)

合并过程中的复杂度为 O ( 1 + 2 + 3 ⋯ + n ) = O ( n 2 ) O(1 + 2 + 3 \dots+n) = O(n^2) O(1+2+3+n)=O(n2)

总的时间复杂度为 O ( o p n 2 ) O(opn^2) O(opn2)

需要优化!

如果每一次将小的集合合并到大的集合里面

复杂度为: O ( o p × n l o g n ) O(op \times nlogn) O(op×nlogn)

观察每一个元素对最终计算量的贡献

它取决于每一次所在集合合并的次数

假设最小的集合,集合内的元素个数为 x x x

合并一次之后,两个集合合并之后元素个数为 2 ∗ x 2*x 2x

AcWing 2154. 梦幻布丁 - AcWing

题意:

n个布丁,有m个颜色

操作:

op1:要把一个颜色的布丁全部变为另一种颜色
op2:询问连续的颜色段个数

每一次将 x x x的颜色变成 y y y,就代表 x x x y y y的珠子合并成同一类

计算一个珠子的贡献,如果它变化前和左边不一样,则贡献加一,如果和右边不一样,则贡献为2。

合并之后,贡献减2。

存储时候,需要将每种颜色对应珠子的编号进行存储。

每一次合并的时候,把小的集合链合并到大的集合链,并且维护颜色之间的包含关系

#include 
#include 
#include 
#include 

using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;
int n, m, ans;
int color[N], h[M], ne[N], e[N], id;
int fa[M], sz[M];

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

void merge(int &a, int &b)
{
    if(a == b) return;
    if(sz[a] > sz[b]) swap(a, b);
    for(int i = h[a]; i != -1; i = ne[i])
    {
        int j = e[i];
        ans -= (color[j - 1] == b) + (color[j + 1] == b);
    }
    for(int i = h[a]; i != -1; i = ne[i])
    {
        int j = e[i];
        color[j] = b;
        if(ne[i] == -1)
        {
            ne[i] = h[b];
            h[b] = h[a];
            break;
        }
    }
    sz[b] += sz[a];
    sz[a] = 0;
    h[a] = -1;
}

int main()
{
    memset(h, -1, sizeof(h));
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++)
    {
        scanf("%d", &color[i]);
        add(color[i], i);
    }
    ans = 1;
    for(int i = 2; i <= n; i++)
    {
        if(color[i] != color[i - 1]) ans++;
    }
    
    for(int i = 1; i < M; i++) fa[i] = i;
    
    while(m --)
    {
        int op;
        scanf("%d", &op);
        if(op == 1)
        {
            int x, y;
            scanf("%d%d", &x, &y);
            merge(fa[x], fa[y]);
        }
        else
        {
            printf("%d\n", ans);
        }
    }
    
    return 0;
}

3189. Lomsat gelral - AcWing题库(树上启发式合并)

Problem
树的节点有颜色,一种颜色占领了一个子树,当且仅当没有其他颜色在这个子树中出现得比它多。求占领每个子树的所有颜色之和 CF600E

solution
算法思路

这道题我们可以遍历整棵树,并用一个 c n t cnt cnt数组记录每种颜色出现几次
但是每做完一棵子树就需要清空 c n t cnt cnt,以免对其兄弟造成影响。
而这样做它的祖先时就要把它重新搜一遍,浪费时间
但是我们发现,对于每个节点x,最后一棵子树是不用清空的,因为做完那棵子树后可以把其结果直接加入x的答案中。
选哪棵子树呢?当然是所含节点最多的一棵咯,我们称之为“重儿子”

考虑暴力怎么写:遍历每个节点—>把子树中的所有颜色暴力统计出来更新答案—>消除该节点的贡献—继续递归这肯定是 O ( n 2 ) O(n^2) O(n2)的。

d s u o n t r e e dsu on tree dsuontree巧妙的利用了轻重链剖分的性质,把复杂度降到了 O ( n l o g n ) O(nlogn) O(nlogn)。你不知道啥叫轻重链剖分?一句话:对于树上的一个点,与其相连的边中,连向的节点子树大小最大的边叫做重边,其他的边叫轻边

算法证明

设根到该节点有x条轻边,该节点的大小为y,根据轻重边的定义,轻边所连向的点的大小不会成为该节点总大小的一半。这样每经过一条轻边,y的上限就会/2,因此 y < n / 2 x yy<n/2x,是log的。即一个节点到根的路径上轻边个数不会超过 l o g n logn logn条。所以最后的复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

算法实现,对于节点i:

遍历每一个节点
递归解决所有的轻儿子,同时消除递归产生的影响
递归重儿子,不消除递归的影响
统计所有轻儿子对答案的影响
更新该节点的答案
删除所有轻儿子对答案的影响

#include 
#include 
#include 
#include 

using namespace std;
typedef long long ll;
const int N = 1e5 + 50, M = N * 2;
int n;
int h[N], ne[M], e[M], id;
int son[N], mx, cnt[N], sz[N], color[N];
ll ans[N], sum;

void addedge(int u, int v)
{
    e[id] = v, ne[id] = h[u], h[u] = id++;
}

int dfs_son(int u, int father)
{
    sz[u] = 1;
    for(int i = h[u]; i != -1; 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];
}

void update(int u, int father, int sign, int pson)
{
    // 计算每个结点的sum
    int c = color[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 != -1; i = ne[i])
    {
        int j = e[i];
        if(j == father || j == pson) continue; // 更新的时候避开轻儿子
        update(j, u, sign, pson);
    }
}

void dfs(int u, int father, int op) // 重儿子op为1, 轻儿子op为2
{
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if(j == father || j == son[u]) continue; // 避开重儿子
        dfs(j, u, 0);  
    }
    
    if(son[u]) dfs(son[u], u, 1);
    update(u, father, 1, son[u]); // 重儿子不需要清空
    
    ans[u] = sum;
    
    if(!op) 
    {
        update(u, father, -1, 0); // 轻儿子需要清空
        sum = mx = 0; 
    }
}

int main()
{
    memset(h ,-1, sizeof(h));
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", &color[i]);
    for(int i = 1; i < n; i++)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        addedge(a, b), addedge(b, a);
    }
    
    dfs_son(1, -1);
    dfs(1, -1, 1);
    
    for(int i = 1; i <= n; i++) printf("%lld ", ans[i]);
    
    return 0;
}

你可能感兴趣的:(算法,启发式算法)