4

For a shorter ver., only read the paragraphs that immediately follow the BOLD sentences and it reduces to only 3 paragraphs.

Problem Statement : Given a tree with N nodes rooted at node 1. Each node is associated with a value. Determine the closest ancestor that contains the value coprime to the current node value. (Note that it is node value and not node number.)

Here is my algorithm :

Define the lists as : adj[ ] is the adjacency list (a list of lists which is constructed when taking inputs from the user), vis[ ] denotes if a node is visited, children[ ] is a list of lists that stores the children of each node, when exists. Since this is a tree, we will construct adj[ ] such that adj[node] = list of children of node. This helps us with not worrying about whether a node is visited or not.

Create a list parent[ ] that stores the parent of each node. Do it as :

def search_parent(node):
        for i in adj[node] :
                parent[i] = node
                search_parent(i)

Our main algorithm is to start at node 1 and mark it as ans[1] = -1 since it can not have an ancestor. Traverse through the nodes in the DFS manner. Check for the coprime ancestor by setting a variable v and a while loop such that if gcd(node, v) == 1 : ans[node] = v else make v = parent[v]. In this way, we check if the parent is coprime, if not, we check if parent[parent] is coprime and so on, till we hit the base case.

Pseudocode for the main problem :

ans[1] = -1
parent[1] = 0
def dfs(root) :
        loop node in adj[root] :
                v = root
                while (5 > 0) :
                    if gcd(val[node],val[v]) == 1 :
                        ans[node] = v
                        dfs(node)
                    else :
                        v = parent[v]
                        if v == 0 :
                            ans[node] = -1
                            dfs(node)
               
            

The code can be reduced in complexity by a constant factor if instead of list parent, we chose dictionary parent. Then when v = parent[1] is reached, we can directly make parent[1] = -1 and ans[node] = -1 is returned in the next step of the while loop, following which the while loop terminates. On the other hand, the current code goes through the if condition upto O(depth(node)) times for every node.

The GCD can be evaluated in O(log_2 max(val[node])) time. The while loop runs in a time proportional to O(depth(node)). Suppose b is the max branching factor of the graph. Then, the overall complexity will be O(|V| + |E| + sum(b^{r <= d} log_2 max(val[node]))) = O(N log_2 max(val)).

1. Is there a more optimized code (average time/space complexity wise)?

2. Is the algorithm correct or there are loop holes in the logic or maybe in some boundary cases?

  • 3
    1. What is the range of possible values? If it is smallish positive integers, that might suggest one strategy. If it is potentially huge/unbounded, then something else may be needed. 2. Is this a one time operation per a given fixed size known tree? Or would you want to maintain the quality even as members are added and removed from the tree or the values in some nodes changes? 3. What is the expected size for the tree? How small is N? Can it be sometimes/frequently/always huge? 4. If the tree or its values change over time, can additional intermediate info be stored per node? – Eric Dec 15 '20 at 21:14
  • @Eric Arbitrary is the answer to all your questions. – oldsailorpopoye Dec 18 '20 at 14:03
  • Possibly better suited for https://math.stackexchange.com/ – sancho.s ReinstateMonicaCellio Dec 21 '20 at 07:26
  • Why do you define `vis[]` if you're not using it? – user66554 Dec 21 '20 at 08:16
  • And what is the difference between `adj[]` and `children[]` - the latter not being used? – user66554 Dec 21 '20 at 08:20
  • What's the point of `while (5 > 0)`, how do you intend to get out of that infinite loop? – user66554 Dec 21 '20 at 08:23

1 Answers1

0

I turned your algorithm into Python so I could run some test cases:

from math import gcd

# Example Tree:
# Node numbers     Node values
# (array index or
# dictionary key)
#     0                1
#    / \              / \
#   1   2            7   1
#  / \   \          / \   \
# 3   4   5        14 14   2
#        / \              / \
#       6   7            6   6
adj = [
        [1, 2], # 0
        [3, 4], # 1
        [5],    # 2
        [],     # 3
        [],     # 4
        [6, 7], # 5
        [],     # 6
        []      # 7
]

val = [1, 7, 1, 14, 14, 2, 6, 6]

parent = [-1, 0, 0, 0, 0, 0, 0, 0]

def search_parent(node):
    for i in adj[node]:
        parent[i] = node
        search_parent(i)

search_parent(0)

ans = [-1, 0, 0, 0, 0, 0, 0, 0]

def dfs(root):
    for node in adj[root]:
        a = None
        v = root
        while a is None:
            if gcd(val[node], val[v]) == 1: % coprime
                a = v
                break
            else: % not coprime
                v = parent[v]
                if v == 0:
                    a = -1
                break
        ans[node] = a

        if node != 0:
            dfs(node) # continuing dfs no matter whether it's coprime or not.

dfs(0)

print("Nodes:   {0}".format(list(range(0,8))))
print("Values:  {0}".format(val))
print("Parents: {0}".format(parent))
print("Answers: {0}".format(ans))

Question 2: Correctness

So regarding question 2 (correctness), let's consider a few test cases - though it might just be that I don't understand what you expect.

If all values have the trivial value 1 then the closest coprime is always the parent:

Nodes:   [0, 1, 2, 3, 4, 5, 6, 7]
Values:  [1, 1, 1, 1, 1, 1, 1, 1]
Parents: [-1, 0, 0, 1, 1, 2, 5, 5]
Answers: [-1, 0, 0, 1, 1, 2, 5, 5]

Now consider the case where not the parent but the grandparent is the ancestor. In the case of the branch 2 this looks correct, but not for the branch 1 where the node which it's children are coprime to is the root node, running into the special case if v == 0: a = -1:

Nodes:   [0, 1, 2, 3, 4, 5, 6, 7]
Values:  [1, 7, 1, 14, 14, 2, 6, 6]
Parents: [-1, 0, 0, 1, 1, 2, 5, 5]
Answers: [-1, 0, 0, -1, -1, 2, 2, 2]

I think you rather expect if v == 0: a = 0 here:

Answers: [-1, 0, 0, 0, 0, 2, 2, 2]

However, the case you originally had in mind is that the root node is coprime, where if v == 0: a = -1 is correct:

Nodes:   [0, 1, 2, 3, 4, 5, 6, 7]
Values:  [7, 7, 1, 14, 14, 2, 6, 6]
Parents: [-1, 0, 0, 1, 1, 2, 5, 5]
Answers: [-1, -1, 0, -1, -1, 2, 2, 2]

So those two cases probably need to be distinguished.

Question 1: Complexity

Regarding complexity, it seems you computed it wrong - I don't think you can add the cost of the parent lookup to the cost of the depth-first search. For you have to do the parent lookup for every node.

The problem is that you're going down to the descendants with the for while going up to the ancestors with the while. If you want any improvement you'd have to pick one direction, e.g. going only down to the descendants with for, and computing intermediate values to pass to the call to dfs.

However, I don't think you can get rid of the while loop, you merely can reduce it in the best case. For example, instead of recursing over all ancestors you could construct a list of relevant ancestors by adding node to the list only if val[v] % val[node] == 0 and removing v from the list if val[node] % val[v] == 0 before recursively calling dfs.

Probably not worth it (didn't do any test runs, though).

user66554
  • 558
  • 2
  • 14