Saturday, 22 August 2020

Longest Path In a Tree

 I recently tried out SPOJ (Sphere Online Judge), and its pretty good for practicing algorithms. Here is a pretty simple SPOJ question.

The tree is undirected, meaning the connections can be given in any order.

The general idea is this. Start at any node, and traverse in depth first order, keeping track of the depth of the subtree under each node. We will also make sure we do not end up going in circles. Then, we just take the deepest child of the root, the second deepest child of the root, and add their depths and add 2 more edges : ones which connect the root to the two deepest children. There are a few edge cases. Try to think which, and how you'll handle them.

For trees, I really prefer to make a class and solve the question. I actually have a basic class ready for Binary Search Trees with all types of traversals (bread first, depth first and inOrder). I just had to copy that class in, and modify it for n-ary trees. Here is the class

#include<iostream>
#include<vector>
#include<queue>

struct Node {
	int val;
	int parent, left, right;
};

class BST {
private:
	std::vector<Node> tree;
public:
	Node* root = nullptr;

	BST(std::vector<Node> _tree) :tree(_tree){
		if (_tree.size() != 0) {
			root = &tree[0];
		}
		else {
			root = nullptr;
		}
	}

	void InorderTraversal(Node* root) {
		if (root == nullptr) {
			return;
		}
		if (root->left != -1) {
			InorderTraversal(&tree[root->left]);
		}
		std::cout << root->val << " ";
		if (root->right != -1) {
			InorderTraversal(&tree[root->right]);
		}
	}

	void PreOrderTraversal(Node* root) {
		if (root == nullptr) {
			return;
		}
		if (root->left != -1) {
			InorderTraversal(&tree[root->left]);
		}
		if (root->right != -1) {
			InorderTraversal(&tree[root->right]);
		}
		std::cout << root->val << " ";
	}

	void BreadthFirstTraversal(Node* root) {
		if (root == nullptr) {
			return;
		}
		std::queue<Node*> Q;
		Q.push(root);
		while (!Q.empty()) {
			std::cout << Q.front()->val << " ";
			if (Q.front()->left != -1) {
				Q.push(&tree[Q.front()->left]);
			}
			if (Q.front()->right != -1) {
				Q.push(&tree[Q.front()->right]);
			}
			Q.pop();
		}
	}
};
I modified this class for n-ary trees by removing the left, right and parent data in Node, and replacing that with a 
std::vector<int> neighbors;

this stores the index of all the nearest neighbors of that node, and since the tree is non-directional, if there exists an edge between 1 and 2, 1 appears in 2's neighbor list, and 2 appears in 1's neighbor list. So how do we avoid going in circles?

We add another boolean variable called "visited" to the node struct, which will be false by default.

bool visited  = false;

We will be using a depth first search in order to sift through the vertices, and find the depth. So let us add another data point to our struct which keeps track of the maximum depth of the subtree under a vertex.

int maxDepthOfSubtree = 0;

While we do the DFS, we increase the depth as we move back up from the deepest point of the tree, meaning after our recursive call. So this variable is updated from the bottom up, not top to bottom, which is opposite to the direction of out DFS.

Our DFS first of all sets the visited status of the current node to true, then recursively scans each of it's non-visited neighbors. When the recursion returns, the value of maxDepthOfSubtree will be correct for all the neighbors, and so we can just add 1 to the maximum neighbor, and assign this value to maxDepthOfSubtree of the current node.

for (int child : root->children) {
	if (!tree[child].visited) {
		DFS(&tree[child]);
		if (root->depthOfSubtree < tree[child].depthOfSubtree + 1) {
			root->depthOfSubtree = tree[child].depthOfSubtree + 1;
		}
	}
}

Now to return our answer, we simply compare the maxDepthOfSubtree of all the neighbors of our first node (which we will call the root), and add the two maximum subtree depths, and add 2 to them, edge cases being zero children and 1 child.

int MaxDist() {
	DFS(root);
	std::vector<int> depths;
	for (int child : root->children) {
		depths.push_back(tree[child].depthOfSubtree);
	}

	std::sort(depths.begin(), depths.end(), std::greater<int>());

	if (depths.size() >= 2) {
		return depths[0] + depths[1] + 2;
	}
	else if (depths.size() == 1) {
		return depths[0] + 1;
	}
	else {
		return 0;
	}
}

To be honest there isnt anything special about the root, unlike Binary Search Trees. Here, the root can be ANY node and it'll work. But since it IS definitely a tree, any node can be made the root, and it will remain a tree. That's why it works.

Tuesday, 18 August 2020

[keeping bribes minimum] New Year Chaos

 This question was particularly nasty for me, because the logic was so elegant, simple, yet elusive. My 3rd semester has started here at IIT Madras, and there is no mercy, so I haven't been coding too much recently. Maybe that's the cause, but a better reason might be that its a solid 65% success rate problem.

So, what's the deal?

My incorrect approach:

At every spot, Q[i] SHOULD have been equal to i + 1, but if it's not, the element there has either value greater or smaller than i + 1. We figure out the number of swaps by incrementing a swap counter by 

swapCounter += Q[i] - i - 1

only if this quantity is greater than zero, and then I reasoned that the ones where it is smaller than zero must have been swapped with some greater element. I thought that the smaller elements were the RESULT of a swap with an element strictly greater than itself. This logic misses cases like

3 2 1

My algorithm will totally ignore 2 and 1, but in reality in order to achieve such a configuration, we must  also switch 1 and 2 at some point, and that's where my algorithm fails. I realized this, and tried to work around this base solution to accommodate this case, but finally, failed.

The Correct Approach:

A very simple, elegant, no fancy math way of keeping track of swaps is to update 3 variables

expectedFirst, expectedSecond, expectedThird.

Initially, these are 1, 2, 3 respectively. However, we only update all 3, only the later 2, or only the 3rd one selectively depending upon the value we encounter at the queue.

    int expectedFirst = 1;
    int expectedSecond = 2;
    int expectedThird = 3;
    
    for (unsigned int i = 0; i < q.size(); ++i) {
        if (q[i] == expectedFirst) {
            expectedFirst = expectedSecond;
            expectedSecond = expectedThird;
            ++expectedThird;
        } else if (q[i] == expectedSecond) {
            ++totalBribes;
            expectedSecond = expectedThird;
            ++expectedThird;
        } else if (q[i] == expectedThird) {
            totalBribes += 2;
            ++expectedThird;
        } else {
            cout << "Too chaotic" << endl;
            return;
        }
    }

Ok. Let's break this down. What's the difference between this? 

At each step it compares the current number in the queue to what it expects to be. That is the smallest possible number, the next smallest, and so on.

When it encounters

3 2 1

1. it sees 3, updates totalBribes by 2, then sets the expected values to (1, 2, 4)

2. it sees 2, updates totalBribes by 1, then sets the expected values to (1, 3, 4)

3. it sees 1, updates totalBribes by 0  (as 1 == 1), then sets expected values to (3, 4, 5)

It needs to keep track of only 3 because the max number of bribes which can be given by any one person is 2, and one spot for the number itself.

There! Now that's elegance. I wonder when I will acquire such godlike abilities ;-;

I guess the correct way is "I never lose, I win, or I learn."

I've definitely learned today.