Monday, 21 September 2020

[The Essence of Binary Search] Aggressive Cows

Ahh, a long and tiring day nears close, and boy have I learned a lot today!

Today we are going to talk about binary search, and more importantly how to sniff out binary search problems where the binary search part is absolutely not obvious. At least to me. Here is today's one lone problem I've tried today. I legit spent about 6 hours exploring this problem and the concepts related to it.

Aggressive Cows on SPOJ

Binary Search. And interesting and fast (O(logn)) method to look for values in a binary tree or a sorted array. But it is so much more than that. Let's dive into some mathematics:

At its core, what is Binary Search? At each step, we check the truth value of a statement (most of the time it is "is the value >= the number to be searched?"), and depending on that value, we decide whether we have found our element, or we go left, or right in our journey to hunt down the searched value.

What we do with binary search is find the maximum value which satisfies our statement. 

(since maximum value >= x is x provided x exists in the array, we use this to search for x)

So essentially,

"Binary search converts an optimisation problem to a decision problem"

Our proposition is true for all cases before the answer, and false after it. So our array essentially turns into a TF sequence like this:

TTTTTTFFFFFFFFFFFF

Let us refer to this type of sequence as a TF sequence from now on.

And the mission of binary search is to find the last T.

Therefore the functionality of binary search can be modified to find the least (or conversely the greatest) element which fulfills a proposition P, which has to generate a sequence of T's followed by a sequence of F (or conversely, a sequence of F followed by a sequence of T).

This idea is so powerful, that we can exploit this in a LOT of seemingly O(n2) solutions to solve it in O(nlogn).

Let us define our proposition 

Px(k) : "It is possible to arrange x cows in the barn such that distance between them is at least k"

Before using this, we must prove that Px(k) yields a TF sequence.

Proof:

If Px(k) is true, we have a situation like this:

COW.....k.....COW

Just shift that cow one step closer. COW.....(k-1).....COW

Therefore Px(k) --> Px(k - 1)

But the same need not be true for Px(k + 1) because if k = n and x = 2, both cows occupy the very ends, and  Px(n + 1) is not possible as the barn is not that long.

We have established, that if we find a valid Px(k0), Px(k) is also valid for all the k <= k0, 

Therefore, Px(k) generates a TF sequence over k = 1 to n.

Now, we could do this:

Evaluate the truth value of Px(k) for each k, and obtain the TF array, then find the last T. That would be O(n). Totally, it would be O(n2), as we evaluating Px(k) as k goes from 1 to n.

But why do that?

If Px(k) yields a TF array, we can use binary search to find the last T. 

We evaluate Px(k) only at THAT k which is required by binary search to obtain the answer.

Barn in the example testcase:     

 0   1    2    3    4    5   6   7   8   9

And we have a set of k's for which we want to find the greatest k for which Px(k) is true:

k = {1, 2, 3... 9}

define start = 0, end = 9. (n = 9, x = 3)

We find mid = k = floor( (start + end)/2 ). We check if Px(k) is valid for that k (=mid).

If we get T, we can look further to the right, as there may be more T's there.

If we hit F, there DEFINITELY isn't any T to the right, so we go left.

start = 0, end = size of the barn (last index in barn array)

while (start < end):

    if (checkPx(mid))

        start = mid + 1 

    else

        end = mid

1Figure out what you would return when this loop finishes executing!

Now how to check Px(mid)?

Again, we use greedy approach to do this:

 checkPx(k):

    Put the first cow in first stall.

    Move along the barn until you find a stall which is at least k

    If you find one:

        Put a cow there

    Repeat until a) there are no cows left -> return true

                        b) the barn ends with cows still remaining -> return false

 Ok now we know all the pieces of the puzzle!

Answer to question 1: would return start - 1, as start would become the first F, so decrement it by 1 to get the last T

Now, its time for some code:


#include <iostream>
#include <vector>
#include <algorithm>

bool checkMyK(int k, int cows, std::vector<int>& barn) {
	int currentPos = 1;
	int prevCowPos = 0;
	int numCowsPlaced = 1;
	while (currentPos < barn.size()) {
		if (barn[currentPos] - barn[prevCowPos] >= k) {
			numCowsPlaced++;
			if (numCowsPlaced == cows) {
				return true;
			}
			prevCowPos = currentPos;
		}
		currentPos++;
	}
	return false;
}

int modifiedBinarySearch(std::vector<int>& barn, int cows) {
	int start = 0, end = barn[barn.size() - 1];

	while (start < end) {
		int mid = (start + end) / 2;
		if (checkMyK(mid, cows, barn)) {
			start = mid + 1;
		}
		else {
			end = mid;
		}

		//std::cout << start << " " << end << "\n";
	}

	return start - 1;
}

int main() {
	int t;
	std::cin >> t;

	while (t--) {
		int n, cows;
		std::vector<int> barn;
		std::cin >> n >> cows;

		while (n--) {
			int val;
			std::cin >> val;
			barn.push_back(val);
		}

		std::sort(barn.begin(), barn.end());

		std::cout << modifiedBinarySearch(barn, cows) << "\n";
	}
}

More reading material and references:

https://www.topcoder.com/community/competitive-programming/tutorials/binary-search/

https://discuss.codechef.com/t/spoj-problem-aggrcow-aggressive-cows/11971/12 

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.

Sunday, 19 July 2020

[min heaps] Jesse and Cookies

The UC San Diego course on coursera has been really helping me learn a lot about Data Structures. The latest thing I've learned is about binary heaps, and priority queues. So, I've been solving some data structures problems on the side on HackerRank in order to gain a further understanding. One such problem is Jesse and Cookies.

This problem is really simple if you know what a min heap is. The way to do that in C++ (STL) is make a priority queue with a custom comparison function:

std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
std::priority_queue<int, std::vector<int>> maxHeap;

We need a minHeap here. What we do is this:

1. Push all the cookies into the minHeap
2. initialize integer operations = 0
3. while root of minHeap < k:
          int newSweetness = minHeap.getMin()
          minHeap.ExtractMin()
          newSweetness += 2 * minHeap.getMin()
          minHeap.ExtractMin()
          minHeap.Push(newSweetness)
          increment operations by 1
4. return operations

Why does this work?
Due to the property of the min Heap data structure we know that each node is >= to its parent. This means the smallest node is the root, and if that is >= k, every single node in the heap is >= k.

So for each iteration we check if the root is >= k. If not, we take the smallest node and the second smallest nodes out, and put in a new node with sweetness = (smallest) + 2*(second smallest)

The moment the root becomes >= k, we know we are done!

Wednesday, 15 July 2020

[stacked up odds] Two stacks, one queue

Task sounded simple. Implement a queue with 2 stacks, which as the methods Push(), Pop() and Front().

Well, the fundamental problem here is stack cannot give me the element at the bottom in a neat way. It has to be dirty. Also, how do I keep track of Pop() operations on the queue? 2 stacks. I need to keep track of elements in the inverse order of my stack.

Okay here's something: what if the reason we require two stacks is that we maintain, for each operation, a stack which is the right way up, and a second stack which has elements in the reverse order as the first? The first stack helps us push elements into the queue. The second stack helps with Pop() and Front(). I will call the first stack as the front stack, and second as the rear stack

Let's be naive first. Let us update the two stacks every single time any of the queue methods are called. The major testcase eater here is that there is no way to nicely invert a stack. I went with declaring a temporary stack, copying the elements of the front stack to it (std::stack has a nice copy constructor for this), and then running a while loop and pushing the top of the temporary stack to the rear stack and then popping the temporary stack, so that elements of the front stack get pushed onto the rear stack in reverse order. That was a mouthful. A lot of pushes and pops. But hopefully you got the idea.

If we do all this, and repeat it (update both stacks for every queue method call) this eats up a LOT of time. Half the testcases pass with this one.

I couldn't figure this part out, so I went to the discussion section. There I found what I had been missing. In order to explain this, I must make an analogy with what we do for dynamic arrays.

Dynamic Arrays, as you might know, are resizable arrays (std::vector for C++). Now note the parallel. There is no neat way of resizing a proper C++ array. It has to be dirty. We are going to have to re-allocate a new array, copy all the elements to the new array. There is simply no other way, as that is how the data structure works fundamentally.

So what do we do? We do it less often. Only when necessary.
Dynamic Arrays achieve a PushBack() complexity of amortized O(1) by doubling the array capacity when it is full. Similarly, we must do this reversal of stack and copying of stack elements only when it is needed.

What can change given this idea?
First, the rear stack need not be updated if it is not empty! The only time it needs to update is if it is empty. Because if we have elements left in the rear stack, the top() is still correct, no matter what we have pushed. Similarly the front stack doesn't need to still contain all the pushed elements. Once it updates rear stack, the front stack's job is essentially nothing, apart from storing the new incoming elements. Once it pushes those to the rear stack, it doesn't need to care about the elements that were transferred to the rear stack.
Therefore, that the temporary stack was unnecessary. We can directly pop from the front stack while updating the rear, instead of first making a copy.

We update the rear stack only on calls to Pop() or Front(). We needn't do so for Push(). Similarly we need to update the front stack only on calls to Push().

With all this done, the solution now passes all the testcases.
The lesson I learned was this: If there is doesn't exist any neat way of doing something. If there is something you just cannot bypass due to the fundamental nature of the tools you use to solve it, it has to be dirty. It can be done more efficiently by doing the dirty work only and only when required, and nowhere else.

PS: I pity those who noticed the pun in the title ._.

Monday, 6 July 2020

Between the Brackets #1 : Practice vs. Lectures



Between the Brackets is a new thing I'm starting on this blog. It's a series where we have discussions over the non-technical things in coding, my experiences, in general, coding and chill.

So let's get to the point. Which helps you more in improving your coding skills? A lot of practice, or watching lectures, from sources like Coursera or YouTube or MIT OCW?

I, so far have relied on sheer practice, and reading up things based on the questions I faced, as an when I feel I need to read it. This has definitely improved my coding skills, and indeed that's what my friends and seniors encouraged me do.

So we started this Team Resurgence thing, tri-weekly tournaments, discussions, and reading on demand. But it slowly fizzled out. Participation dropped from 10 to 3-4 people. Later, we simply stopped.

So its been a bit. A month. A very transformative month indeed. After two whole months of solving tournaments every other day, I got burnt. It was inevitable. I decided to double down a bit.

Recently, I started a course on Data Structures from UC San Diego on Coursera. I'm done with week one, and it's a breath of fresh air. One important lesson I have learnt is that coding is very hard if you do it only through trial and error. And while there are dozens of YouTube channels which can seem very appealing, they are often incomplete, since most such YouTubers are also employed (those who make coding videos) in a 9 to 5, so they do not make their videos "complete". After the amount of trial and error based learning I did, this course felt way better and had less hand holding for the basics. I already knew most basics already, so it was less annoying.

Now, about platforms like GeeksForGeeks. I find these platforms to be very cookbook style, and indeed they are useful, but I don't recommend this platform as a place for first time learners. I mean, you might find quick code and documentation if you forget something, but the explanations are very brief, and the accompanying video also is not very good. So I use GeeksForGeeks to figure out what to learn, or to refresh memory.

Now we come to MIT OCW.
MIT OCW is a great place to learn. I have done Multivariable Calculus from OCW and it helped me get the highest grade in the MA1101 course we have here at IIT Madras.
So naturally my first instinct was to start MIT 6.006 Discrete maths for CS. And that course was so hard that it made me quit it multiple times for weeks. I feel, specially in India, the Engineering admission exams are heavily trick based and focus on Calculus and Geometry, and less on Algebra and Combinatorics. So having trained so hard on continuous mathematics, it might take a while to start activating your discrete math spider sense.

Yes, I said spider sense. Concepts like induction and combinatorics require you to recognize patterns and try to propose solutions based heavily on past experiences. So beginners have to learn to not feel bad about being completely lost. I know, it sucks. But suck it up. Suck up the knowledge you gain by failing repeatedly. (I think I'm being a bit overly positive and hypocritical when I say this. I'm no expert, and I definitely feel really dumb sometimes while solving these. Also, I haven't yet mastered that art of not feeling bad about being lost. The point is we have to embrace this together, dear reader.)

So coming back to courses. I feel we must have a balance of the two. And auditing courses on Coursera and EdX is definitely better than relying on YouTube and MIT OCW. I'm going to have to stick to this course, and not leave it midway. In general, experiment. Pick and choose which course/channel you prefer. Your choice may not be mainstream, but if it works for you, go ahead and do it.

And that's what is Between the Brackets this week.

Monday, 25 May 2020

[Learning DSA series!] Trees: PreOrder Traversal

So, I was starting to get pretty bored of those standard array based questions. Graphs and trees seem mysterious and interesting. I don't want to pretend that this is a tutorial. It's not even close.

I'm just going to describe what I learn, my experiences, where I struggled and what not. If anyone at all is reading this, you will probably get some doubts cleared, if our doubt seems to match, and also get a sense that CS is hard. A lot of people online cast an image of being natural geniuses, and so-called "normies" like us feel like we can never catch up. It's mutual. I just want the guy struggling with trying to master coding like me, to feel better, and know that it's hard for everyone and not just for them. And, prolly share the fascination and interest in coding.

As CarryMinati would say "Shuru karte hain bina kisi bakchodi ke":

First, some terminology.

Tree: A data structure consisting of interconnected nodes (data points), like the root system of a tree.

Root: The topmost node of a tree, which has no parent (a node above its level to which it is connected).

Leaf node: A node with no children (nodes below its own level which are connected to it)

Binary Tree: A tree where each node has at most two children. A node can have none, one or two children.

So, let's get on with implementing our binary tree code. This is already given in the HackerRank question, you don't have to type this, but nevertheless it will be a good exercise to try and write one ourselves.

If you are not very comfortable with OOP, I'll try to make it as digestible as possible.

Let's start with the Node class. A class consists of some data (which can be of differing types) and some functions (called methods). Each node has 3 data components:
  • info: the type has to be fixed for certain languages like C++, although there are ways to generalize the data types in C++. Might want to look up Template Specialization. This is not a problem at all in Python, where data types are implicit. For our purposes, we will keep it at int or float values.
  • reference to left node
  • reference to right node
The node class does not need any methods. Let's code this.

class Node:
    def __init__(self, info):
        self.infoinfo
        self.left = None
        self.right = None

Remember that a constructor is the function which initializes a new object of the class, and we can use this to assign some values to our class data. In Python, __init__ function designates the constructor.

Every method in a Python which messes with class data has to have a "self" parameter, which is a reference to the self. This is passed automatically while initializing a new object, and we don't need to pass it manually. This is something exclusive to Python.

Now lets get to our BinarySearchTree Class. This class will contain only one data point: a ROOT node. This node will be the start of our tree. It will be an object of the Node Class we just made. What's important here is the method we are going to make to populate a tree with values.

This is a binary search tree. This means, there are two children to each node (left and right child) and the left child will store a value smaller than the parent node, and the right will store a value greater than the parent node. Let's code this:

class BinaryTree:
    def __init__(self):
        self.root = None

    def create(self, val):
        if self.root == None:
            self.root = Node(val)
        else:
            current = self.root

            while True:
                if val < current.info:
                    if current.left:
                        current = current.left
                    else:
                        current.left = Node(val)
                        break
                elif val > current.info:
                    if current.right:
                        current = current.right:
                    else:
                        current.right = Node(val)
                        break
                else:
                    break

Ok. Let's explain the create() method. If the root has not yet been given a value (empty tree) then give it a node with the given value (val). If the tree is already populated, compare val with the value of the current node (the first time it will be the root). If the value in the current node is greater than val, we need to go left. If there is no left child, we make one. If the left child already exists, then we set current node to be the left child and repeat the process of comparing val with the current node (which is now the left child). We do the same with the right side case too. This will ensure we go to a node which needs a child to accommodate the new val, but doesn't already have one. Then, we make the node there (which is the appropriate spot due to all the discussion so far) and we are done!

Now after all this, let's get to the problem itself. It asks us to print out the elements in a depth first order, preferring the left hand side (hence the "preoder" traversal). Depth first means that we go as far as possible taking left. When we reach a node where we can no longer go any further by turning left, we explore all the paths taking one right at a time bottom to top. Maybe this program I made in Python using graphics.py can help:
So we are going to go for a recursive approach. The first thing we do, the print the info value of the current node. Then we check if the node has a left child. If it does, we recursively call the same function for currentNode.left. Then, we check if the right child exists. If it does, we send currentNode.right in another recursive call. We exit the function if there are no children to a node (i.e. leaf node).

Here is the code:

def preOrder(root):
    print(str(root.info), end = " ")
    
    if root.left != None:
        preOrder(root.left)
    if root.right != None:
        preOrder(root.right)

And that's all for now. Phew. Super long article. We will explore more trees in the next one! Until then...