Skip to content
Related Articles

Related Articles

Improve Article

Locking and Unlocking of Resources arranged in the form of n-ary Tree

  • Difficulty Level : Medium
  • Last Updated : 21 May, 2021

Given an n-ary tree of resources arranged hierarchically such that height of tree is O(Log N) where N is total number of nodes (or resources). A process needs to lock a resource node in order to use it. But a node cannot be locked if any of its descendant or ancestor is locked. 
Following operations are required for a given resource node: 
 

  • islock()- returns true if a given node is locked and false if it is not. A node is locked if lock() has successfully executed for the node.
  • Lock()- locks the given node if possible and updates lock information. Lock is possible only if ancestors and descendants of current node are not locked.
  • unLock()- unlocks the node and updates information.

How design resource nodes and implement above operations such that following time complexities are achieved. 
 

    islock()  O(1)
    Lock()    O(log N)
    unLock()  O(log N)

We strongly recommend you to minimize your browser and try this yourself first. 
It is given that resources need to be stored in the form of the n-ary tree. Now the question is, how to augment the tree to achieve the above complexity bounds. 

Some General Questions:

Q1. Why doesn’t setting Lock = true alone solve the purpose?
A1. Because for the approaches where we move towards the parent to check if locking is possible if a request comes for any node between a locked node and the root, then there’s no way of telling that a descendent node is locked. Hence, variables like isLockable are used to maintain this info.



Q2. Why not lock all nodes from the current node to the root?
A2. Because locking, in general, is a resource-intensive operation and performing locking for all nodes right up to the root would be a waste of resources. Hence, lightweight solutions such as introducing a isLockable variable are used.

Method 1 (Simple)
A Simple Solution is to store a boolean variable isLocked with every resource node. The boolean variable isLocked is true if the current node is locked, else false. 
Let us see how operations work using this Approach. 
 

  • isLock(): Check isLocked of the given node.
  • Lock(): If isLocked is set, then the node cannot be locked. Else check all descendants and ancestors of the node and if any of them is locked, then also this node cannot be locked. If none of the above conditions is true, then lock this node by setting isLocked as true.
  • unLock(): If isLocked of given node is false, nothing to do. Else set isLocked as false.

Time Complexities: 

isLock()  O(1) 
Lock()    O(N)
unLock()  O(1)

Lock is O(N) because there can be O(N) descendants. 

  
Method 2 (Time Complexities according to question) 
The idea is to augment tree with following three fields: 
 

  1. A boolean field isLocked (same as above method).
  2. Parent-Pointer to access all ancestors in O(Log n) time.
  3. Count-of-locked-descendants. Note that a node can be ancestor of many descendants. We can check if any of the descendants is locked using this count.

Let us see how operations work using this Approach. 
 

  • isLock(): Check isLocked of the given node.
  • Lock(): Traverse all ancestors. If none of the ancestors is locked, Count-of-locked-descendants is 0 and isLocked is false, set isLocked of current node as true. And increment Count-of-locked-descendants for all ancestors. Time complexity is O(Log N) as there can be at most O(Log N) ancestors.
  • unLock(): Traverse all ancestors and decrease Count-of-locked-descendants for all ancestors. Set isLocked of current node as false. Time complexity is O(Log N)

Thanks to Utkarsh Trivedi for suggesting this approach. 

Method 3 (Time Complexities according to question and better memory usage) The idea is to augment tree with following three fields:

  1. A boolean field isLocked (same as above method).
  2. Parent-Pointer to access all ancestors in O(Log n) time.
  3. isLockable. We can check if any of the descendants is locked using this variable. isLockable is true if none of the descendents are locked else false.

Let us see how operations work using this Approach.

  • isLock(): Check isLocked of the given node.
  • Lock(): Traverse all ancestors. If none of the ancestors is locked, isLockable is true and isLocked is false, set isLocked of current node as true. And mark isLockable false for all ancestors. Time complexity is O(Log N) as there can be at most O(Log N) ancestors.
  • unLock(): Traverse all ancestors and mark isLockable true for all ancestors. Set isLocked of current node as false. Time complexity is O(Log N)

C++




#include <bits/stdc++.h>
using namespace std;
 
class narytree {
public:
    bool isLock;
    bool isLockable;
    narytree* parent;
    vector<narytree*> children;
    narytree()
    {
        isLock = false;
        isLockable = true;
        parent = NULL;
    }
    narytree(narytree* parent)
    {
        isLock = false;
        isLockable = true;
        this->parent = parent;
    }
};
 
bool isLock(narytree* node) { return node->isLock; }
 
void Lock(narytree* node)
{
    if (node->isLockable == false)
        return;
 
    narytree* T = node;
    bool flag = false;
    while (T != NULL) {
        if (T->isLock == true) {
            flag = true;
            break;
        }
        T = T->parent;
    }
    if (flag)
        return;
    else {
        node->isLock = true;
        T = node;
        // marking isLockable as false for ancestor nodes.
        while (T != NULL) {
            T->isLockable = false;
            T = T->parent;
        }
    }
}
 
void unLock(narytree* node)
{
    if (node->isLock == false)
        return;
    narytree* T = node;
    node->isLock = false;
    // marking isLoackable as true for ancestor nodes.
    while (T != NULL) {
        T->isLockable = true;
        T = T->parent;
    }
}
 
int main()
{
    // Creating N-Array Tree
    narytree* root = new narytree();
 
    narytree* t1 = new narytree(root);
    narytree* t2 = new narytree(root);
    narytree* t3 = new narytree(root);
 
    root->children.push_back(t1);
    root->children.push_back(t2);
    root->children.push_back(t3);
 
    narytree* t5 = new narytree(root->children[0]);
    root->children[0]->children.push_back(t5);
    narytree* t4 = new narytree(root->children[1]);
    root->children[1]->children.push_back(t4);
 
    // Locking t4 node.
    Lock(t4);
    //    Checking if the t4 node is locked.
    cout << "t4 node is locked:"
         << ((isLock(t4) == true) ? "true" : "false")
         << "\n";
    Lock(t2);
    cout << "t2 node is locked:"
         << ((isLock(t2) == true) ? "true" : "false")
         << "\n";
    // Unlocking t4 node.
    unLock(t4);
    //    Now we should be able to lock the tree.
    Lock(t2);
    cout << "t2 node is locked:"
         << ((isLock(t2) == true) ? "true" : "false");
 
    return 0;
}

Thanks to Adarsh Singh for suggesting this approach. 

This article is contributed by Abhay Rathi. Please write comments if you find anything incorrect, or you want to share more information about the topic discussed above.

Attention reader! Don’t stop learning now. Get hold of all the important DSA concepts with the DSA Self Paced Course at a student-friendly price and become industry ready.  To complete your preparation from learning a language to DS Algo and many more,  please refer Complete Interview Preparation Course.

In case you wish to attend live classes with experts, please refer DSA Live Classes for Working Professionals and Competitive Programming Live for Students.




My Personal Notes arrow_drop_up
Recommended Articles
Page :