参考文章:

树上启发式合并

[dsu on tree]树上启发式合并总结

树上启发式合并の详解

启发式合并

启发式算法是什么呢?

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

举个例子,最常见的就是并查集的启发式合并了,代码是这样的:

void merge(int x, int y) {
int xx = find(x), yy = find(y);
if (size[xx] < size[yy]) swap(xx, yy);
fa[yy] = xx;
size[xx] += size[yy];
}

在这里,对于两个大小不一样的集合,我们将小的集合合并到大的集合中,而不是将大的集合合并到小的集合中。

为什么呢?这个集合的大小可以认为是集合的高度(在正常情况下),而我们将集合高度小的并到高度大的显然有助于我们找到父亲。

让高度小的树成为高度较大的树的子树,这个优化可以称为启发式合并算法。

[HNOI2009] 梦幻布丁

P3201 [HNOI2009] 梦幻布丁

题目描述

\(n\) 个布丁摆成一行,进行 \(m\) 次操作。每次将某个颜色的布丁全部变成另一种颜色的,然后再询问当前一共有多少段颜色。

例如,颜色分别为 \(1,2,2,1\) 的四个布丁一共有 \(3\) 段颜色.

输入格式

第一行是两个整数,分别表示布丁个数 \(n\) 和操作次数 \(m\)。

第二行有 \(n\) 个整数,第 \(i\) 个整数表示第 \(i\) 个布丁的颜色 \(a_i\)。

接下来 \(m\) 行,每行描述一次操作。每行首先有一个整数 \(op\) 表示操作类型:

  • 若 \(op = 1\),则后有两个整数 \(x, y\),表示将颜色 \(x\) 的布丁全部变成颜色 \(y\)。
  • 若 \(op = 2\),则表示一次询问。

输出格式

对于每次询问,输出一行一个整数表示答案。

样例 #1

样例输入 #1

4 3
1 2 2 1
2
1 2 1
2

样例输出 #1

3
1

提示

样例 1 解释

初始时布丁颜色依次为 \(1, 2, 2, 1\),三段颜色分别为 \([1, 1], [2, 3], [4, 4]\)。

一次操作后,布丁的颜色变为 \(1, 1, 1, 1\),只有 \([1, 4]\) 一段颜色。

数据规模与约定

对于全部的测试点,保证 \(1 \leq n, m \leq 10^5\),\(1 \leq a_i ,x, y \leq 10^6\)。

提示

请注意,不保证颜色的编号不大于 \(n\),也不保证 \(x \neq y\),\(m\) 不是颜色的编号上限。

思路

在处理颜色布丁集合合并的问题时,我们面临的是一系列颜色布丁集合,每个集合可以看作一个队列。我们需要频繁地合并两个集合,每次合并操作涉及到两个集合 \(x\) 和 \(y\)。如果采用暴力合并方法,每次合并的复杂度最坏为 \(O(n)\),其中 \(n\) 是所有集合元素的总和。

为了优化这一过程,我们引入了启发式合并的概念。启发式合并的核心思想是每次将较小的集合合并到较大的集合中,这样每次合并的复杂度为 \(O(|短的队列|)\)。虽然单次合并的复杂度看起来没有显著改善,但通过均摊分析,我们可以得到更好的整体性能。我们使用贡献法来分析均摊复杂度。假设两个集合分别为 \(A\) 和 \(B\),且 \(|A| < |B|\)。我们将 \(A\) 暴力加入到 \(B\) 中,这样 \(A\) 中的元素所在的集合大小变成 \(|A| + |B|\),即至少变成了原来的两倍。因此,每个元素至多被加入 \(\log n\) 次,总的复杂度为 \(O(n \log n)\)。

在具体实现步骤中,我们首先对每一种颜色使用\(vector\)存起来。每次修改时,根据启发式合并的方法来暴力合并,然后处理此次合并对答案的影响(答案是不增的)。为了处理颜色映射问题,如果我们把颜色 \(1\) 染成颜色 \(2\) 并且 \(|S_1| > |S_2|\),那么我们应该把颜色 \(2\) 加入到颜色 \(1\) 的集合。为了处理这种情况,我们只需要记录一下该颜色的集合中实际的颜色即可

代码

#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
const int N=2e5+3;
const int LOGN=18;
using i64 = long long;
int n,m,q;
vector<int> pos[10*N];
int ans=0;
void solve()
{
cin>>n>>m;
vector<int> a(n+2);
for(int i=1;i<=n;i++){
cin>>a[i];
pos[a[i]].push_back(i);
}
a[0]=a[n+1]=0;
for(int i=0;i<=n;i++) ans+=(a[i]!=a[i+1]);
//cout<<ans<<endl;
while(m--)
{
int op;
cin>>op;
if(op==2)
{
cout<<ans-1<<endl;
continue;
}
else if(op==1)
{
int x,y;
cin>>x>>y;
if(x==y) continue;
if(pos[x].size()>pos[y].size()) pos[x].swap(pos[y]);
auto modify = [&](int p,int col) -> void{
ans-=(a[p]!=a[p-1])+(a[p]!=a[p+1]);
a[p]=col;
ans+=(a[p]!=a[p-1])+(a[p]!=a[p+1]);
};
if(pos[y].empty()) continue;
int col=a[pos[y][0]];
for(auto p : pos[x])
{
modify(p,col);
pos[y].push_back(p);
}
pos[x].clear();
//cout<<ans-1<<endl;
}
} }
signed main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int T;
T=1;
//cin>>T;
while(T--)
{
solve();
}
return 0;
}

树上启发式合并

遍历节点 u 的步骤

在遍历节点 u 时,我们按照以下步骤进行操作:

  1. 遍历轻儿子并计算答案

    • 首先遍历节点 u 的轻(非重)儿子。
    • 计算这些轻儿子的答案,但不保留它们对 cnt 数组的影响。
  2. 遍历重儿子并保留影响

    • 接着遍历节点 u 的重儿子。
    • 计算重儿子的答案,并保留它对 cnt 数组的影响。
  3. 再次遍历轻儿子的子树结点

    • 最后,再次遍历节点 u 的轻儿子的子树结点。
    • 加入这些结点的贡献,以得到节点 u 的最终答案。

通过这种方式,我们可以有效地计算节点 u 的答案,同时确保重儿子的贡献被保留,轻儿子的贡献在需要时可以重新计算。

int n,m;
int c[N];
int l[N],r[N],id[N],sz[N],hs[N],tot;
vector<int> e[N];
int cnt[N]; //每一个颜色出现次数
int maxcnt; //众数出现次数
int sumcnt,ans[N]; //众数出现的和
void dfs_init(int u,int f)
{
l[u] = ++tot;
id[tot] = u;
sz[u] = 1;
hs[u] = -1;
for(auto v : e[u])
{
if(v==f) continue;
dfs_init(v,u);
sz[u] += sz[v];
if(hs[u] == -1 || sz[v] > sz[hs[u]]) hs[u] = v;
}
r[u] = tot;
}
void dfs_solve(int u,int f,bool keep)
{
for(auto v : e[u])
{
if(v != f && v != hs[u])
{
dfs_solve(v,u,false);
}
}
if(hs[u] != -1){
dfs_solve(hs[u],u,true);
//重儿子集合
}
auto add = [&](int x){
};
auto del = [&](int x){
};
for(auto v : e[u]){
if(v!=f && v != hs[u]){ //v是轻儿子
// 把v子树里所有点加入到重儿子集合中
for(int x=l[v];x<=r[v];x++)
add(id[x]);
}
}
add(u);
ans[u]=sumcnt;
//把u 本身加入
if(!keep){
//清空
for(int x=l[u];x<=r[u];x++){
del(id[x]);
}
}
}

题目——模板

E. Lomsat gelral

给你一棵有根的树,根位于顶点 1 。每个顶点都涂有某种颜色。

如果在顶点 v 的子树中,没有其他颜色比颜色 c 出现的次数更多,那么我们就称颜色 c 在顶点 v 的子树中占主导地位。因此,在某个顶点的子树中,可能会有两种或两种以上的颜色占主导地位。

顶点 v 的子树是顶点 v 和其他所有包含顶点 v 的顶点。

对于每个顶点 v 求顶点 v 的子树中所有支配色的总和。

代码:

#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std; const int N = 3e5+10;
using i64 = long long; int n,m;
int c[N];
int l[N],r[N],id[N],sz[N],hs[N],tot;
vector<int> e[N];
int cnt[N]; //每一个颜色出现次数
int maxcnt; //众数出现次数
int sumcnt,ans[N]; //众数出现的和
void dfs_init(int u,int f)
{
l[u] = ++tot;
id[tot] = u;
sz[u] = 1;
hs[u] = -1;
for(auto v : e[u])
{
if(v==f) continue;
dfs_init(v,u);
sz[u] += sz[v];
if(hs[u] == -1 || sz[v] > sz[hs[u]]) hs[u] = v;
}
r[u] = tot;
}
void dfs_solve(int u,int f,bool keep)
{
for(auto v : e[u])
{
if(v != f && v != hs[u])
{
dfs_solve(v,u,false);
}
}
if(hs[u] != -1){
dfs_solve(hs[u],u,true);
//重儿子集合
}
auto add = [&](int x){
x=c[x];
cnt[x]++;
if(cnt[x] > maxcnt) maxcnt=cnt[x],sumcnt=0;
if(cnt[x] == maxcnt) sumcnt+=x;
};
auto del = [&](int x){
x=c[x];
cnt[x]--;
};
for(auto v : e[u]){
if(v!=f && v != hs[u]){ //v是轻儿子
// 把v子树里所有点加入到重儿子集合中
for(int x=l[v];x<=r[v];x++)
add(id[x]);
}
}
add(u);
ans[u]=sumcnt;
//把u 本身加入
if(!keep){
//清空
maxcnt=0;
sumcnt=0;
for(int x=l[u];x<=r[u];x++){
del(id[x]);
}
}
}
signed main()
{ cin>>n;
for(int i=1;i<=n;i++) cin>>c[i];
for(int i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs_init(1,0);
dfs_solve(1,0,0);
for(int i=1;i<=n;i++) cout<<ans[i]<<" \n"[i==n];
}

[IOI2011] Race

P4149 [IOI2011] Race

题目描述

给一棵树,每条边有权。求一条简单路径,权值和等于 \(k\),且边的数量最小。

输入格式

第一行包含两个整数 \(n,k\),表示树的大小与要求找到的路径的边权和。

接下来 \(n-1\) 行,每行三个整数 \(u_i,v_i,w_i\),代表有一条连接 \(u_i\) 与 \(v_i\),边权为 \(w_i\) 的无向边。

注意:点从 \(0\) 开始编号

输出格式

输出一个整数,表示最小边数量。

如果不存在这样的路径,输出 \(-1\)。

样例 #1

样例输入 #1

4 3
0 1 1
1 2 2
1 3 4

样例输出 #1

2

提示

对于 \(100\%\) 的数据,保证 \(1\leq n\leq 2\times10^5\),\(0\leq k,w_i\leq 10^6\),\(0\leq u_i,v_i<n\)。

思路:

设\(dep1[u]\)表示\(u\)的深度,\(dep2[u]\)表示\(u\)到根节点的路径长度.对于任意两个点\(u,v\),最近公共祖先为\(p\),他们之间的简单路径值\(dep2[u]+dep2[v]-2×dep2[p]\).考虑启发式合并,对于每一个\(p\),用一个\(map\),记录每一个路径值到根节点的最短距离\(val\),然后遍历每一个轻儿子\(v\),是否存在已经记录的结点\(u\),使得\(dep2[u]=k-dep2[v]+2×dep2[u]\),然后将轻儿子加入到重儿子集合中。

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+10,mod=998244353;
typedef long long ll;
typedef pair<int,int> PII;
int T;
int n,m,k,q;
vector<PII> e[N];
int dep1[N],dep2[N];
int l[N],r[N],dfn[N],hs[N],sz[N],tot;
int ans;
map<int,int> val;
void dfs_init(int u,int f)
{
l[u]=++tot;
hs[u]=-1;
sz[u]=1;
dfn[tot]=u;
//cout<<u<<endl;
for(auto [v,w] : e[u])
{
if(v==f) continue;
dep1[v]=dep1[u]+1;
dep2[v]=dep2[u]+w;
dfs_init(v,u);
sz[u]+=sz[v];
if(hs[u] == -1 || sz[hs[u]] < sz[v]) hs[u]=v;
}
r[u]=tot;
}
void dfs_solve(int u,int f,int keep)
{
//cout<<hs[u]<<endl;
for(auto [v,w] : e[u])
{
if(v!=hs[u]&&v!=f)
dfs_solve(v,u,0);
}
if(hs[u]!=-1) dfs_solve(hs[u],u,1);
auto query = [&](int w)
{
int d2=k+2*dep2[u]-dep2[w];
if(val.count(d2))
ans=min(ans,val[d2]+dep1[w]-2*dep1[u]);
};
auto add = [&](int w)
{
if(val.count(dep2[w]))
val[dep2[w]]=min(val[dep2[w]],dep1[w]);
else val[dep2[w]]=dep1[w];
};
for(auto [v,w] : e[u])
{
if(v==f||v==hs[u]) continue;
for(int x=l[v];x<=r[v];x++)
query(dfn[x]);
for(int x=l[v];x<=r[v];x++)
add(dfn[x]);
}
query(u);add(u);
if(!keep){
val.clear();
}
}
void solve()
{
cin>>n>>k;
for(int i=1;i<n;i++)
{
int u,v,w;
cin>>u>>v>>w;
++u,++v;
e[u].push_back({v,w});
e[v].push_back({u,w});
}
ans=n+1;
dfs_init(1,0);
dfs_solve(1,0,0);
if(ans<n+1) cout<<ans<<endl;
else cout<<"-1"<<endl;
}
signed main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
T=1;
//cin>>T;
while(T--)
{
solve();
}
return 0;
}

树上启发式合并——dsu on tree的更多相关文章

  1. 神奇的树上启发式合并 (dsu on tree)

    参考资料 https://www.cnblogs.com/zhoushuyu/p/9069164.html https://www.cnblogs.com/candy99/p/dsuontree.ht ...

  2. 树上启发式合并 (dsu on tree)

    这个故事告诉我们,在做一个辣鸡出题人的比赛之前,最好先看看他发明了什么新姿势= =居然直接出了道裸题 参考链接: http://codeforces.com/blog/entry/44351(原文) ...

  3. 【CF600E】Lomset gelral 题解(树上启发式合并)

    题目链接 题目大意:给出一颗含有$n$个结点的树,每个节点有一个颜色.求树中每个子树最多的颜色的编号和. ------------------------- 树上启发式合并(dsu on tree). ...

  4. dsu on tree 树上启发式合并 学习笔记

    近几天跟着dreagonm大佬学习了\(dsu\ on\ tree\),来总结一下: \(dsu\ on\ tree\),也就是树上启发式合并,是用来处理一类离线的树上询问问题(比如子树内的颜色种数) ...

  5. 树上启发式合并(dsu on tree)学习笔记

    有丶难,学到自闭 参考的文章: zcysky:[学习笔记]dsu on tree Arpa:[Tutorial] Sack (dsu on tree) 先康一康模板题吧:CF 600E($Lomsat ...

  6. dsu on tree (树上启发式合并) 详解

    一直都没出过算法详解,昨天心血来潮想写一篇,于是 dsu on tree 它来了 1.前置技能 1.链式前向星(vector 建图) 2.dfs 建树 3.剖分轻重链,轻重儿子 重儿子 一个结点的所有 ...

  7. 【Luogu U41492】树上数颜色——树上启发式合并(dsu on tree)

    (这题在洛谷主站居然搜不到--还是在百度上偶然看到的) 题目描述 给一棵根为1的树,每次询问子树颜色种类数 输入输出格式 输入格式: 第一行一个整数n,表示树的结点数 接下来n-1行,每行一条边 接下 ...

  8. CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths 树上启发式合并(DSU ON TREE)

    题目描述 一棵根为\(1\) 的树,每条边上有一个字符(\(a-v\)共\(22\)种). 一条简单路径被称为\(Dokhtar-kosh\)当且仅当路径上的字符经过重新排序后可以变成一个回文串. 求 ...

  9. 树上启发式合并(dsu on tree)

    树上启发式合并属于暴力的优化,复杂度O(nlogn) 主要解决的问题特点在于: 1.对于树上的某些信息进行查询 2.一般问题的解决不包含对树的修改,所有答案可以离线解决 算法思路:这类问题的特点在于父 ...

  10. hdu6191(树上启发式合并)

    hdu6191 题意 给你一棵带点权的树,每次查询 \(u\) 和 \(x\) ,求以 \(u\) 为根结点的子树上的结点与 \(x\) 异或后最大的结果. 分析 看到子树,直接上树上启发式合并,看到 ...

随机推荐

  1. Java模拟高并发测试

    线程类,设置有一个公共资源 package cn.org.chris.concurrent; import java.util.concurrent.locks.Lock; import java.u ...

  2. 国产RK3568J基于FSPI的ARM+FPGA通信方案分享

    近年来,随着中国新基建.中国制造 2025 规划的持续推进,单 ARM 处理器越来越难胜任工业现场的功能要求,特别是如今能源电力.工业控制.智慧医疗等行业,往往更需要 ARM + FPGA 架构的处理 ...

  3. 使用sqlcel导入数据时出现“a column named '***' already belongs to this datatable”问题的解决办法

    我修改编码为GBK之后,选择导入部分字段,如下: 这样就不会出现之前的问题了,完美 ----------------------------------------------- 但是出现一个问题,我 ...

  4. AT_abc215F 题解

    考虑二分答案. 假设当前二分的答案为 \(k\),那么对于每个点,距离大于等于 \(k\) 的点构成了平面上 \(4\) 个子平面. 那么只需查询子平面中是否存在点即可,类似于窗口的星星,把问题转换成 ...

  5. ubuntu 同时安装python2 和 python3 版本的 gunicorn

    前言 最近在学习使用 gunicorn 部署 flask 项目.发现使用 pip3 安装完 gunicorn后,如如果再使用 pip2 安装 gunicorn,后安装的 gunicorn 就会覆盖掉原 ...

  6. SpringBoot AOP完美记录用户操作日志,附源码

    记录内容 接口名称 浏览器名称 操作系统 请求ip 接口入参.出参 接口耗时 .... 表结构 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- -- ...

  7. 【算法】用c#实现计算方法中的经典降幂优化策略,减少计算复杂度

    对于给定的数组[x1,x2,x3,-,xn],计算幂的累积:x1^(x2^(x3^(-^xn))的最后一位(十进制)数字. 例如,对于数组[3,4,2],您的代码应该返回1,因为3^(4^2)=3^1 ...

  8. 妙趣横生:利用Echarts实现SpreadJS引用从属关系的可视化魅力

    最新技术资源(建议收藏) https://www.grapecity.com.cn/resources/ 在金融行业,我们经常会有审计审查的需求,对某个计算结果进行审查,但是这个计算结果可能依赖多个单 ...

  9. MySQL ibdata1文件太大的解决办法

    在MySQL数据库中,如果不指定innodb_file_per_table=1参数单独保存每个表的数据,MySQL的数据都会存放在ibdata1文件里,时间久了这个文件就会变的非常大. 下面是参考网上 ...

  10. [oeasy]python0098_个人计算机浪潮_IBM5100_微软成立_苹果II_VisCalc

    个人计算机浪潮 回忆上次内容 个人电脑(PC) 在爱好者之间疯传 人人都有一台计算机 从attair-8800到apple-1 个人电脑 离普通人 更近了 如果 人人都有 自己的电脑 谁还去 用终端连 ...