📌  相关文章
📜  查找给定树中每个子树的 MEX(1)

📅  最后修改于: 2023-12-03 15:10:46.500000             🧑  作者: Mango

查找给定树中每个子树的 MEX

在树结构中,MEX可以定义为缺失的最小非负整数。给定一颗树,我们需要找到每个子树的MEX值。

本文将会介绍使用深度优先搜索和树上前缀和算法来解决这个问题。同时,我们也会提供相关的算法C++代码。

1. 深度优先搜索算法

我们可以使用深度优先搜索算法来遍历树(先根遍历),并查找每个子树的MEX值。

对于每个节点,我们需要遍历它的所有子节点,并保存它们的MEX值。然后,在计算该节点的MEX值时,我们可以根据其子节点的MEX值,来得到其本身的MEX值。具体的计算方法见下图:

MEX值计算示意图

我们可以使用一个数组 mx[] 来记录每个节点的MEX值。具体来说,对于节点 u,我们先遍历其所有子节点,并递归计算每个子节点的MEX值。然后,我们将子节点的MEX值插入到集合 s 中,并将 mx[u] 的值初始化为 s 中的最小未出现的非负整数。最后,我们返回 s 作为结果。

下面是深度优先搜索算法的C++代码:

int mx[N];
bool vis[N];
vector<int> adj[N];

int dfs(int u){
    vis[u] = true;

    vector<int> s;
    for(int v : adj[u]){
        if(!vis[v]){
            s.emplace_back(dfs(v));
        }
    }

    sort(s.begin(), s.end());
    s.erase(unique(s.begin(), s.end()), s.end());

    int i = 0;
    while(i < s.size() && s[i] == i) ++i;

    return mx[u] = i;
}

在上述代码中,我们使用一个 vis 数组来记录每个节点是否被遍历过。在遍历时,我们只对未被遍历过的节点进行处理。同时,为了方便起见,我们使用了C++ STL中的 vector 容器来管理集合 s。具体来说,我们可以使用 s.emplace_back(x) 在末尾插入一个元素 x,使用 s.erase(it1, it2) 删除区间 [it1, it2) 中的元素,并使用 sort(s.begin(), s.end()) 将容器中的元素排序。

2. 树上前缀和算法

使用深度优先搜索算法可以为我们的问题解决提供有效的算法实现。然而,它的时间复杂度是 $O(N^2)$ 的($N$ 是树的节点数),因为对于每个节点,我们都需要回溯到其所有祖先节点来计算MEX值。如果树很大,这个算法的时间效率将很低。

在本节中,我们将介绍一种更快的算法,称为树上前缀和算法。这种算法基于以下性质:对于一个连通的子图,其MEX值等于集合 [0, k] 中未出现的最小整数,其中 k 是该子图中节点编号的最大值。

我们可以使用前缀和来计算每个节点子树的最大节点值。然后,我们可以使用深度优先搜索算法来计算每个节点的MEX值。具体来说,对于节点 u,我们可以使用一个 cnt[] 数组来记录其子树中每个节点编号的出现次数。然后,我们可以使用前缀和算法来计算其子树的最大节点值 mx[u]。最后,我们可以使用深度优先搜索算法来计算节点 u 的MEX值。

下面是树上前缀和算法的C++代码:

int mx[N], cnt[N];
vector<int> adj[N];

void dfs1(int u, int p){
    cnt[u] = 1;
    for(int v : adj[u]){
        if(v != p){
            dfs1(v, u);
            cnt[u] += cnt[v];
            mx[u] = max(mx[u], mx[v]);
        }
    }
    mx[u] = max(mx[u], cnt[u]);
}

void dfs2(int u, int p){
    vector<int> s;
    for(int v : adj[u]){
        if(v != p){
            dfs2(v, u);
            s.emplace_back(mx[v]);
        }
    }
    sort(s.begin(), s.end());
    s.erase(unique(s.begin(), s.end()), s.end());

    int k = s.size();
    for(int i = 0; i < k; ++i){
        if(s[i] > i) break;
        if(i == k-1){
            mx[u] = max(mx[u], i+1);
            break;
        }
    }
}

int main(){
    int n, root;
    cin >> n >> root;

    for(int i = 1; i < n; ++i){
        int u, v;
        cin >> u >> v;
        adj[u].emplace_back(v);
        adj[v].emplace_back(u);
    }

    dfs1(root, 0);
    dfs2(root, 0);

    for(int i = 1; i <= n; ++i){
        cout << mx[i]-1 << ' ';
    }
    cout << endl;

    return 0;
}

在上述代码中,我们使用了两个深度优先搜索函数 dfs1()dfs2()。在 dfs1() 函数中,我们计算每个节点子树的最大节点值。具体来说,我们使用 cnt[] 数组来记录每个节点的子树大小(即子节点个数),并使用 mx[] 数组来记录子树中节点编号的最大值。当我们遍历到节点 u 的某个子节点 v 时,我们通过递归调用 dfs1(v, u) 来计算其子树大小和最大节点值,并使用 cnt[u] += cnt[v]mx[u] = max(mx[u], mx[v]) 来更新节点 u 的子树大小和最大节点值。

在计算完每个节点子树的最大节点值后,我们可以使用 dfs2() 函数来计算每个节点的MEX值。具体来说,对于节点 u,我们先遍历其所有子节点,并递归计算每个子节点的MEX值。然后,我们将子节点的最大节点值 mx[v] 插入到集合 s 中,并使用前缀和算法来找到该子树的MEX值。最后,我们将节点 u 的MEX值更新为子树MEX值加1。

至此,我们已经介绍了两种算法来计算树中每个子树的MEX值。相比于深度优先搜索算法,树上前缀和算法具有更高的时间效率(时间复杂度为 $O(N\log N)$)和更简洁的代码(不需要使用集合数据结构)。