Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1766. Tree of Coprimes - Leetcode Solution

Problem Description

In the Tree of Coprimes problem, you are given a rooted tree with n nodes, where each node i (0-indexed) has an integer value nums[i]. The tree is described by a list of edges, and the root is always node 0.

For each node i, you need to find the nearest ancestor of node i (excluding itself) such that the value at that ancestor is coprime with nums[i]. Two numbers are coprime if their greatest common divisor (gcd) is 1. If there is no such ancestor, return -1 for that node.

The output should be an array res of length n, where res[i] is the index of the nearest ancestor of node i whose value is coprime with nums[i], or -1 if none exists.

  • Each node has exactly one parent except the root.
  • The tree is connected and acyclic.
  • Values in nums are positive integers (typically ≤ 50 for this problem).

Thought Process

The straightforward approach is, for each node, to walk up its ancestry and check the gcd of its value with each ancestor’s value. This is a brute-force solution and can be very slow, especially if the tree is deep and the number of nodes is large.

To optimize, we can traverse the tree in a depth-first search (DFS) manner, keeping track of the most recent occurrence of each value along the path from the root to the current node. Since values are small (usually ≤ 50), we can use a map or array to quickly find the nearest ancestor with a value coprime to the current node’s value.

The key insight is to maintain, for each possible value, the latest node (and its depth) encountered so far in the current path. This allows us to efficiently check all potential coprime ancestors in O(1) per value.

Solution Approach

  • Build the tree: Use an adjacency list to represent the tree from the list of edges.
  • Precompute coprime pairs: For all values between 1 and the maximum possible value (e.g., 50), precompute which pairs are coprime using gcd.
  • DFS Traversal: Start DFS from the root. At each node:
    • For the current node’s value, look up all values that are coprime to it (using the precomputed pairs).
    • For each coprime value, check if it exists in the current path (using a map from value to (node, depth)).
    • Among all ancestors with coprime values, pick the one with the largest depth (nearest ancestor).
    • Before visiting children, update the map to include the current node’s value and depth.
    • After visiting children, restore the map to remove the current node’s value (backtracking).
  • Result: For each node, store the index of the nearest coprime ancestor (or -1 if none).

This approach is efficient because it leverages the small range of possible values and avoids redundant computations by using DFS and precomputation.

Example Walkthrough

Sample Input:

  • nums = [2, 3, 3, 2]
  • edges = [[0,1], [1,2], [1,3]]

Tree structure:

        0(2)
         |
         1(3)
        /   \
     2(3)   3(2)
  

Step-by-step:

  • Node 0: Root node, has no ancestors, so result is -1.
  • Node 1: Ancestor is 0 (value 2). gcd(3,2)=1 (coprime), so result is 0.
  • Node 2: Ancestors: 1 (3), 0 (2). Check:
    • gcd(3,3)=3 (not coprime)
    • gcd(3,2)=1 (coprime), so nearest is 0
    Result is 0.
  • Node 3: Ancestors: 1 (3), 0 (2). Check:
    • gcd(2,3)=1 (coprime), so nearest is 1
    Result is 1.

Final output: [-1, 0, 0, 1]

Time and Space Complexity

  • Brute-force: For each node, walking up the ancestry could be O(n) per node, so O(n^2) overall.
  • Optimized DFS approach:
    • DFS visits each node once: O(n).
    • For each node, checking coprime values is O(1) (since value range is small, e.g., ≤50).
    • Precomputing coprime pairs is O(V^2), where V is the value range (e.g., 2500 for V=50).
    • Overall: O(n + V^2) time and O(n + V) space, which is efficient for the given constraints.

Summary

The Tree of Coprimes problem is an elegant application of DFS with path tracking and precomputation. By leveraging the small value range and using a map to track the most recent occurrence of each value along the current path, we can efficiently find the nearest coprime ancestor for each node. This approach is much faster than brute-force and demonstrates how precomputation and careful state management during DFS can lead to optimal solutions for tree problems with value constraints.

Code Implementation

from math import gcd
from collections import defaultdict

class Solution:
    def getCoprimes(self, nums, edges):
        n = len(nums)
        # Build tree
        tree = [[] for _ in range(n)]
        for u, v in edges:
            tree[u].append(v)
            tree[v].append(u)

        # Precompute coprime pairs
        MAXV = 50
        coprime = [[] for _ in range(MAXV + 1)]
        for i in range(1, MAXV + 1):
            for j in range(1, MAXV + 1):
                if gcd(i, j) == 1:
                    coprime[i].append(j)

        res = [-1] * n
        # For each value, maintain (node, depth)
        last_seen = {}

        def dfs(u, parent, depth):
            # Find nearest ancestor with coprime value
            max_depth = -1
            ancestor = -1
            for val in coprime[nums[u]]:
                if val in last_seen:
                    anc_node, anc_depth = last_seen[val]
                    if anc_depth > max_depth:
                        max_depth = anc_depth
                        ancestor = anc_node
            res[u] = ancestor

            # Save current state and update
            prev = last_seen.get(nums[u], None)
            last_seen[nums[u]] = (u, depth)
            for v in tree[u]:
                if v != parent:
                    dfs(v, u, depth + 1)
            # Restore state
            if prev is not None:
                last_seen[nums[u]] = prev
            else:
                del last_seen[nums[u]]

        dfs(0, -1, 0)
        return res
      
#include <vector>
#include <algorithm>
#include <numeric>
#include <unordered_map>
using namespace std;

class Solution {
public:
    vector<int> getCoprimes(vector<int>& nums, vector<vector<int>>& edges) {
        int n = nums.size();
        vector<vector<int>> tree(n);
        for (auto& e : edges) {
            tree[e[0]].push_back(e[1]);
            tree[e[1]].push_back(e[0]);
        }

        // Precompute coprime pairs
        const int MAXV = 50;
        vector<vector<int>> coprime(MAXV + 1);
        for (int i = 1; i <= MAXV; ++i) {
            for (int j = 1; j <= MAXV; ++j) {
                if (gcd(i, j) == 1)
                    coprime[i].push_back(j);
            }
        }

        vector<int> res(n, -1);
        unordered_map<int, pair<int, int>> last_seen; // value -> (node, depth)

        function<void(int, int, int)> dfs = [&](int u, int parent, int depth) {
            int max_depth = -1, ancestor = -1;
            for (int val : coprime[nums[u]]) {
                if (last_seen.count(val)) {
                    auto [anc_node, anc_depth] = last_seen[val];
                    if (anc_depth > max_depth) {
                        max_depth = anc_depth;
                        ancestor = anc_node;
                    }
                }
            }
            res[u] = ancestor;

            auto prev = last_seen.count(nums[u]) ? make_optional(last_seen[nums[u]]) : nullopt;
            last_seen[nums[u]] = {u, depth};
            for (int v : tree[u]) {
                if (v != parent)
                    dfs(v, u, depth + 1);
            }
            if (prev)
                last_seen[nums[u]] = *prev;
            else
                last_seen.erase(nums[u]);
        };

        dfs(0, -1, 0);
        return res;
    }
};
      
import java.util.*;

class Solution {
    public int[] getCoprimes(int[] nums, int[][] edges) {
        int n = nums.length;
        List<List<Integer>> tree = new ArrayList<>();
        for (int i = 0; i < n; ++i) tree.add(new ArrayList<>());
        for (int[] e : edges) {
            tree.get(e[0]).add(e[1]);
            tree.get(e[1]).add(e[0]);
        }

        // Precompute coprime pairs
        int MAXV = 50;
        List<Integer>[] coprime = new ArrayList[MAXV + 1];
        for (int i = 1; i <= MAXV; ++i) {
            coprime[i] = new ArrayList<>();
            for (int j = 1; j <= MAXV; ++j) {
                if (gcd(i, j) == 1)
                    coprime[i].add(j);
            }
        }

        int[] res = new int[n];
        Arrays.fill(res, -1);
        Map<Integer, int[]> lastSeen = new HashMap<>(); // value -> [node, depth]

        dfs(0, -1, 0, nums, tree, coprime, lastSeen, res);
        return res;
    }

    private void dfs(int u, int parent, int depth, int[] nums, List<List<Integer>> tree,
                     List<Integer>[] coprime, Map<Integer, int[]> lastSeen, int[] res) {
        int maxDepth = -1, ancestor = -1;
        for (int val : coprime[nums[u]]) {
            if (lastSeen.containsKey(val)) {
                int[] anc = lastSeen.get(val);
                if (anc[1] > maxDepth) {
                    maxDepth = anc[1];
                    ancestor = anc[0];
                }
            }
        }
        res[u] = ancestor;

        int[] prev = lastSeen.get(nums[u]);
        lastSeen.put(nums[u], new int[]{u, depth});
        for (int v : tree.get(u)) {
            if (v != parent)
                dfs(v, u, depth + 1, nums, tree, coprime, lastSeen, res);
        }
        if (prev != null)
            lastSeen.put(nums[u], prev);
        else
            lastSeen.remove(nums[u]);
    }

    private int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
}
      
var getCoprimes = function(nums, edges) {
    const n = nums.length;
    const tree = Array.from({length: n}, () => []);
    for (const [u, v] of edges) {
        tree[u].push(v);
        tree[v].push(u);
    }

    // Precompute coprime pairs
    const MAXV = 50;
    const coprime = Array.from({length: MAXV + 1}, () => []);
    for (let i = 1; i <= MAXV; ++i) {
        for (let j = 1; j <= MAXV; ++j) {
            if (gcd(i, j) === 1) coprime[i].push(j);
        }
    }

    const res = Array(n).fill(-1);
    const lastSeen = new Map(); // value -> [node, depth]

    function dfs(u, parent, depth) {
        let maxDepth = -1, ancestor = -1;
        for (const val of coprime[nums[u]]) {
            if (lastSeen.has(val)) {
                const [ancNode, ancDepth] = lastSeen.get(val);
                if (ancDepth > maxDepth) {
                    maxDepth = ancDepth;
                    ancestor = ancNode;
                }
            }
        }
        res[u] = ancestor;

        const prev = lastSeen.has(nums[u]) ? lastSeen.get(nums[u]) : null;
        lastSeen.set(nums[u], [u, depth]);
        for (const v of tree[u]) {
            if (v !== parent) dfs(v, u, depth + 1);
        }
        if (prev !== null) lastSeen.set(nums[u], prev);
        else lastSeen.delete(nums[u]);
    }

    function gcd(a, b) {
        while (b !== 0) {
            [a, b] = [b, a % b];
        }
        return a;
    }

    dfs(0, -1, 0);
    return res;
};