前言
启发式算法是基于人类的经验和直观感觉,对一些算法的优化。—— 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;
}