Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
启发式算法是指基于人类的经验和直观感觉,对一些算法的优化,最常见的有并查集的按秩合并,即对于两个大小不一样的集合,我们总是将小的集合合并到大的集合中,让高度小的子树成为高度大的树的子树,这个优化称为启发式算法,通过启发式优化,可以将部分树上问题的复杂度为的算法优化成。
树上启发式合并的详细介绍及复杂度证明见OI Wiki:树上启发式合并。
结合LeetCode上一道例题说明:
例题:LeetCode 2003. 每棵子树内缺失的最小基因值
有一棵根节点为 0 的 家族树 ,总共包含 n 个节点,节点编号为 0 到 n - 1 。给你一个下标从 0 开始的整数数组 parents ,其中 parents[i] 是节点 i 的父节点。由于节点 0 是 根 ,所以 parents[0] == -1 。
总共有 个基因值,每个基因值都用 闭区间 [1, ] 中的一个整数表示。给你一个下标从 0 开始的整数数组 nums ,其中 nums[i] 是节点 i 的基因值,且基因值 互不相同 。
请你返回一个数组 ans ,长度为 n ,其中 ans[i] 是以节点 i 为根的子树内 缺失 的 最小 基因值。
节点 x 为根的 子树 包含节点 x 和它所有的 后代 节点。
显然,对于某个节点而言,其最小缺失基因值一定大于等于其左右子树的最小缺失基因值,换言之,从叶结点到根节点的最小缺失基因值一定只增不减。因此,我们可以从根节点开始dfs,在dfs过程中,将每个子树的中包含的基因值用一个set存起来,然后返回一个最小缺失基因值。由于是set,单步复杂度均摊set合并操作采用启发式合并的方法,因此整体复杂度为。
class Solution {
public:
static const int N = 100005;
set<int> st[N];
vector<int> son[N];
vector<int> smallestMissingValueSubtree(vector<int>& p, vector<int>& nums) {
int n = nums.size();
for(int i = 1; i < n; i ++) son[p[i]].push_back(i);
vector<int> ans(n, 1);
auto dfs = [&] (auto&& dfs, int u) -> int {
int res = 1;
for(auto&& ne: son[u]) {
res = max(res, dfs(dfs, ne));
//swap实际上仅交换指针,很快
if(st[u].size() < st[ne].size()) swap(st[u], st[ne]);
//set,map自带merge函数,配合move快上加快,暴力写也没问题
st[u].merge(move(st[ne]));
}
st[u].emplace(nums[u]);
while(st[u].count(res)) ++ res;
return ans[u] = res;
};
dfs(dfs, 0);
return ans;
}
};