启发式合并

前言

        启发式算法是基于人类的经验和直观感觉,对一些算法的优化。—— OI Wiki

        启发式合并是一种看似暴力,实则暗含优化的算法。最经典的应用就是大家耳熟能详的并查集了。

        初学者推荐去看这两篇文章:

        树上启发式合并 - OI Wiki (oi-wiki.org)

        启发式合并 看似暴力实则很快的算法 - 知乎 (zhihu.com)

        启发式合并的核心思想就是在合并的时候,将较小的集合合并到较大的集合上去。这样可以保证每次合并时,当前集合的大小一定至少翻了一倍,那么每个元素最多就会被合并 log(n) 次,在时间上做到了高效率。

例题

Luogu P3201 [HNOI2009] 梦幻布丁

        题目链接:P3201 [HNOI2009] 梦幻布丁 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

        题意:

        给一行 n 个布丁,m 次操作,每次将某种颜色的布丁全部变成另一种颜色,每次操作后询问当前有多少段颜色。

        数据范围:1 <= n,m <= 10^5

        思路:

        思路是极其暴力的,即依题意模拟把该变的颜色变成另一种颜色。但是这样当然会超时,我们想到了启发式合并,在合并两种不同颜色的时候,我们始终把 size 较小的那种颜色的布丁合并到 size 较大的那种颜色的布丁的集合里去,只不过我们合并的时候需要多一步映射。因为题目要求是将 x 变成 y,那假如 y 的 size 比 x 大,我们合并的时候就要把 y 合并到 x 上(也就是将 y 变成 x),这样的话原本的 y 就消失了,但是原本应该消失的 x 现在就代表着 y 这种颜色,因此我们需要将 y 映射到 x 身上,即用 x 的肉身将 y 的精神传承下去。

        这样一来我们可以在 O(n log n) 的时间复杂度内解决问题,于是我们也成功签到了 2009湖南省选。

#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;

#define N 1000005

vector<int> v[N];

int n,m,op,ans,a[N],now[N];

void merge(int x,int y)
{
    for (auto i : v[x])
        ans -= (a[i - 1] == y) + (a[i + 1] == y),v[y].push_back(i);
    for (auto i : v[x]) a[i] = y;
    v[x].clear();
    return;
}

int main()
{
    scanf("%d%d",&n,&m),ans = a[0] = a[n + 1] = 0;
    for (int i = 1;i <= n;++ i)
    {
        scanf("%d",&a[i]);
        now[a[i]] = a[i];
        v[a[i]].push_back(i);
        ans += (a[i] != a[i - 1]);
    }
    while (m --)
    {
        scanf("%d",&op);
        if(op == 2) printf("%d\n",ans);
        else
        {
            int x,y,t;
            scanf("%d%d",&x,&y);
            if(x == y) continue;
            if(v[now[x]].size() > v[now[y]].size())
                t = now[x],now[x] = now[y],now[y] = t;
            merge(now[x],now[y]),now[x] = 0;
        }
    }
    return 0;
}

Luogu U41492 树上数颜色

        题目链接:U41492 树上数颜色 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

        树上启发式合并,用到了 “根节点到树上任意节点的轻边数不超过 log(n) 条” 的重要性质。

        OI Wiki 讲得很好,这里就不多赘述,附上本人代码。

#include<cstdio>
#include<cstring>
using namespace std;

#define N 100005

int n,m,cnt,now,a[N],st[N],siz[N],son[N],tot[N],dfn[N],ans[N],id[N];

struct Edge
{
    int next,to;
}ed[N << 1];


void add(int u,int v)
{
    ed[++ cnt].next = st[u];
    ed[cnt].to = v;
    st[u] = cnt;
    return;
}

void cal(int x)
{
    if(!tot[a[x]]) ++ now;
    ++ tot[a[x]];
    return;
}

void del(int x)
{
    if(tot[a[x]] == 1) -- now;
    -- tot[a[x]];
    return;
}

void pdfs(int x,int fa)
{
    siz[x] = 1,dfn[x] = ++ now,id[now] = x;
    for (int i = st[x]; ~i ;i = ed[i].next)
    {
        int rec = ed[i].to;
        if(rec == fa) continue;
        pdfs(rec,x);
        siz[x] += siz[rec];
        if(!son[x] || siz[rec] > siz[son[x]]) son[x] = rec;
    }
    return;
}

void dfs(int x,int fa,int bz)
{
    for (int i = st[x]; ~i ;i = ed[i].next)
    {
        int rec = ed[i].to;
        if(rec != fa && rec != son[x]) dfs(rec,x,0);
    }
    if(son[x]) dfs(son[x],x,1);
    for (int i = st[x]; ~i ;i = ed[i].next)
    {
        int rec = ed[i].to;
        if(rec == fa || rec == son[x]) continue;
        for (int j = dfn[rec];j <= dfn[rec] + siz[rec] - 1;++ j)
            cal(id[j]);
    }
    cal(id[dfn[x]]);
    ans[x] = now;
    if(!bz)
    {
        for (int i = dfn[x];i <= dfn[x] + siz[x] - 1;++ i)
            del(id[i]);
    }
    return;
}

int main()
{
    memset(st,-1,sizeof st),cnt = now = 0;
    scanf("%d",&n);
    for (int i = 1,u,v;i < n;++ i)
        scanf("%d%d",&u,&v),add(u,v),add(v,u);
    for (int i = 1;i <= n;++ i) scanf("%d",&a[i]);
    pdfs(1,0);
    now = 0;
    dfs(1,0,0);
    scanf("%d",&m);
    for (int i = 1,u;i <= m;++ i)
        scanf("%d",&u),printf("%d\n",ans[u]);
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值