Cracking the LeetCode - 1095

Find in Mountain Array

Introduction

Another tricky LeetCode problem that I found interesting. I decided to do this one in Python first and then convert to C#. Also, instead of going to ChatGPT for help, I discussed with colleagues which was a fun exercise. Have a look :)

Problem

You may recall that an array arr is a mountain array if and only if:

  • arr.length >= 3

  • There exists some i with 0 < i < arr.length - 1 such that:

    • arr[0] < arr[1] < ... < arr[i - 1] < arr[i]

    • arr[i] > arr[i + 1] > ... > arr[arr.length - 1]

Given a mountain array mountainArr, return the minimum index such that mountainArr.get(index) == target. If such an index does not exist, return -1.

You cannot access the mountain array directly. You may only access the array using a MountainArray interface:

  • MountainArray.get(k) returns the element of the array at index k (0-indexed).

  • MountainArray.length() returns the length of the array.

Submissions making more than 100 calls to MountainArray.get will be judged Wrong Answer. Also, any solutions that attempt to circumvent the judge will result in disqualification.

Examples

Example 1:

Input: array = [1,2,3,4,5,3,1], target = 3
Output: 2
Explanation: 3 exists in the array, at index=2 and index=5. Return the minimum index, which is 2.

Example 2:

Input: array = [0,1,2,4,2,1], target = 3
Output: -1
Explanation: 3 does not exist in the array, so we return -1.

Constraints

  • 3 <= mountain_arr.length() <= 10<sup>4</sup>

  • 0 <= target <= 10<sup>9</sup>

  • 0 <= mountain_arr.get(index) <= 10<sup>9</sup>

Process

This ended up being quite a funny process. I don't think I fully understood the question when I started and I immediately just set up a loop to go through the length of the array and get each value. Confusion struck until I read that there was a limit of 100 calls to get the value of an element in the array (note to self: always read the full problem first). So I set up a binary search and was very confused once again as to why it wasn't working (note to self: see previous note). After a bit of debugging, I realised that this approach was bound to fail as a regular binary search would get lost on the mountain array. Here's an example:

Mountain array: [1,3,5,6,7,8,9,4,2,1]
Target value: 4

If using binary search, our first index will be in the middle (index: 5).
[1,3,5,6,7,(8),9,4,2,1,0]

Note that the target is lower, so the search continues to the left of the array. Oops! Already went the wrong way. The algorithm could then search every number in that half of the array and never find the target. The target was instead on the "other side of the mountain" so to speak.

I then explained the issue to my colleagues at work. They suggested, instead of splitting the array down the middle, to start with two pointers some index away from either end. Then, calculate the "slope of the mountain" (whether next value was decreasing or increasing) at those points and try to find each side of the mountain.

My biggest issue with this method was it felt like I was taking stabs in the dark until the algorithm could "get its bearings" on the mountain and even then it would still require some guesswork. A better solution would be to find the peak first and then "traversing the mountain" would be made easy from there.

How can I find the peak though? Well, binary search of course!

def find_peak(i,j):
    while i != j:
        index = (i+j)//2
        if mountain_arr.get(index) < mountain_arr.get(index+1):
            i = index + 1
        else:   
            j = index
    return i

The variables i and j work as my lower and upper bound, and the index represents the mid-point between these. It then checks for the "slope" of the mountain, i.e. if the value at the index is less than the next value along then the values are increasing up the mountain otherwise it must be going down the mountain. If values are increasing, the peak will be to the upper end of the array so increasing the lower boundary to be after the midpoint will decrease our search region in this direction. If the values decrease, the peak will be towards the lower end so moving the upper bound to the midpoint will decrease our search region in this direction instead. Eventually, the upper and lower bounds will settle on one value and this will be the peak of the mountain.

Now, all that's needed is to first check the values below the peak using a regular binary search. If the value isn't there, then check values above the peak with a slightly modified binary search (as the values are decreasing) and if the values isn't there either then it's not in the array.

Solution

Python

Here it is in Python. It's definitely a bit convoluted and could do with a cleanup but it works nicely enough.

class Solution:
    def findInMountainArray(self, target: int, mountain_arr: 'MountainArray') -> int:
        mlen = mountain_arr.length()
        start_index = 0
        end_index = mlen - 1

        def find_peak(i,j):
            while i != j:
                index = (i+j)//2
                if mountain_arr.get(index) < mountain_arr.get(index+1):
                    i = index + 1
                else:   
                    j = index
            return i

        def search_for_ans(i,j,reverse):
            while i != j:
                index = (i+j)//2
                if reverse:
                    if mountain_arr.get(index) > target:
                        i = index + 1
                    else:
                        j = index
                else:
                    if mountain_arr.get(index) < target:
                        i = index + 1
                    else:
                        j = index
            return i


        def find_ans(p):
            val = mountain_arr.get(p)
            if val == target:
                return p

            up_the_mountain = search_for_ans(0,p,False)
            if mountain_arr.get(up_the_mountain) == target:
                return up_the_mountain

            down_the_mountain = search_for_ans(p,mlen-1,True)
            if mountain_arr.get(down_the_mountain) == target:
                return down_the_mountain

            return -1

        peak = find_peak(start_index, end_index)
        return find_ans(peak)

C

I tried a few different things in this code to optimise but only managed to increase the runtime.

class Solution {
    public int FindInMountainArray(int target, MountainArray mountainArr) {
        int peak = FindPeak(mountainArr);
        int index = SearchMountainSide(mountainArr, target, 0, peak, false);
        if (index == -1)
        {
            index = SearchMountainSide(mountainArr, target, peak, mountainArr.Length() - 1, true);
        }

        return index != -1 ? index : -1;
    }

    public int FindPeak(MountainArray mountain)
    {
        int i = 0;
        int j = mountain.Length() - 1;
        while(i != j)
        {
            int index = (i + j) / 2;
            if(mountain.Get(index) < mountain.Get(index + 1))
            {
                i = index + 1;
            }
            else
            {
                j = index;
            }
        }
        return i;
    }

    public int SearchMountainSide(MountainArray mountain, int target, int i, int j, bool reverse)
    {
        while(i <= j)
        {
            int index = (i + j) / 2;
            int match = mountain.Get(index);
            if (match == target)
            {
                return index;
            }

            if (reverse)
            {
                if (match > target)
                {
                    i = index + 1;
                }
                else
                {
                    j = index - 1;
                }
            }
            else  
            {
                if (match < target)
                {
                    i = index + 1;
                }
                else
                {
                    j = index - 1;
                }
            }

        }
        return -1;
    }
}

Conclusion

I think I made this challenge much harder on myself than it ended up being. By the time I finally got my head around the requirements, I was convinced there was no possible way this could be done straightforwardly. Talking to colleagues only made things worse as I found myself arguing the difficulty of the challenge and shutting down some solutions that were close to being right but I was set on the challenge requiring a cleverer approach. However, once I tried finding the peak of the mountain and found how easily it could be done, everything else fell into place.

Differences I noticed between working in Python and C# this time around were low. I found the biggest difference was the visual structure of the code and I must say I prefer how Python looks and even find it more readable. Surprisingly, it also ran quicker and with less memory so I do wonder if C# could be re-written to achieve better speeds as it usually performs better.

I had a few ideas for others working that will work on this problem. Firstly, I think it would be interesting to try and get the answer without ever finding the peak. This was my first idea and while I wasn't happy the direction the solution was going at the time I think it would be a fun challenge to try out. Secondly, I'd love to know if there is a faster alternative to the binary search. A quick Google search gives back "y-fast trees" and "fusion trees" so this might be a good place to start.