I'm a web developer specialising in front-end solutions combining clean coding with intuitive user interfaces and striking design approaches. I'm moving into the sector from a background in graphic design. I'm looking for junior web development roles where I can contribute from the outset with my strong creative problem solving skills, while also building my knowledge and experience of the sector. You can view some of my sample projects here, and read more about my work on my coding blog.

Postcards from the edge

25 Feb 2021

I’ve been spending some time working through problem solving challenges on HackerRank; here’s one I really enjoyed. It seemed easy to solve at first glance, but it threw up some interesting edge cases, and after a good start it took me a while longer to write a comprehensive function which would handle all the test inputs.

The full problem description is available on the challenge page, but here’s my summary:

-You start with two strings, s and t.

-You can perform two operations on string s: delete the last character in the string, or add a new character to the end of the string

Given the value k, is it possible to mutate string s into string t in exactly k moves, only using these two operations? Return Yes or No.

Here’s an example of how this might work:

s = "superman"
t = "supernoodle"
k = 9

//delete the letters "n", "a" and "m" from the end of string s (3 moves)
//add the letters "n", "o", "o", "d", "l" and "e" to the end of string s (6 moves)

6 + 3 = 9 = k

return "Yes"

Great! Notice that because the first five letters of string s perfectly match the first five letters of string t we don’t need to mutate them, which saves us several moves. If we had to delete the whole of string s, our total number of moves would be s.length + t.length, or 8 + 11 = 19 but instead it is now going to be s.length - {length of matching part} + t.length - {length of matching part}, or 8 - 3 + 11 - 3 = 9.

A good way to track whether we have any matching leading characters would be to iterate through string s and record any characters that match. I would do this with:

let match = "";

    for(let i=0; i<s.length; i++){
        if (s[i]===t[i]){
            match += s[i];
        }
        else {
            break;
        }

We can now step through string s, updating the variable match for as long s[i]===t[i]. As soon as s[i]!==t[i] we will break out of the loop, but we will still have our match variable to hand.

Following our earlier logic, we can now easily calculate the minimum number of moves to mutate s into t:

let ans = t.length - match.length + s.length - match.length

Or to simplify:

let ans = t.length + s.length - 2*match.length

This is excellent, because it works for all cases of s and t. Check out the following examples:

s ="bulbous"
t= "bulbasaur"

s= "charizard"
t= "charred"

In case 1, where s.length < t.length, our loop will stop after s[3] because we’ve declared i<s.length. This would also work for s.length === t.length.

In case 2, where s.length > t.length our loop will stop after s[3] because t[4] is undefined and doesn’t satisfy s[i]===t[i].

The method for breaking out of the loop changes, but the end result is the same. In each case, we know that match = 4, so we can go ahead and calculate that ans = 8 for each example. For these examples I’ve intentionally chosen words which share leading characters, but this is also fine for words that don’t, or words that have zero length. In both cases match.length would be zero, and ans would simply be t.length + s.length. The question doesn’t ask us to solve for words with zero length, but it’s nice to know we’ve got this covered!

It feels like we’ve nearly solved this! Now we just need to compare ans against k, returning Yes if they are the same, and No if they are different.

Right? Wrong. The ans we’ve obtained isn’t the only answer to this problem, it’s just the minimum one. In my summary I cheekily glossed over one important piece of information from the question rubric: “performing [the delete function] on an empty string results in an empty string”. I’m sure a lot of people answering this question will have noticed this and understood how it affects the solution straight away, but I’d already done all the previous work before picking up on this, which is why I’m only mentioning it now.

If we can use up moves by deleting on an empty string, we can take as many moves as we like to mutate string s into string t. Here’s an example:

s="abdomen"
t="abracadabra"

The minimum score for mutating s into t is s.length+t.length-2*match.length, or 7+11-4 = 14. However, with our new knowledge, we could instead delete all of s, then use up some arbitrary number of moves deleting on an empty string, then finish by adding all of the letters to make up t. Our score would now be s.length+t.length + {some arbitrary number of moves}. This is important, because we can now see that as long as k >= s.length+t.length we can always come up with a score that will match k.

We can now update our script. Straight off the bat, before we even bother to look at the content of strings s and t we can say:

 if (k>=s.length+t.length){
        return "Yes";
    }

So that’s one major loophole dealt with; surely we’ve now figured this problem out? Well not quite. There’s a slightly different way for us to manipulate our score without deleting on an empty string, and this one is not explicitly pointed out in the rubric. Here’s a further example:

s="moo"
t="moomin"

Clearly our minimum score here is 3. However, we can extend our score by writing and deleting some or all of the string over and over again. The score for something like moo>moom>moomi>moomin>moomi>moomin would be 5. The score for moo>mo>m>mo>moo>moom>moomi>moomin would be 7. Like before, we can keep extending this forever to pump our score, but this time there is a restriction. While deleting on an empty string allows us to increase our score in increments of 1, with this new technique whenever we delete a character we must replace it, so we can only increase our score in increments of 2.

In other words, we can generate any score in the form minAns + n*2, where minAns is the lowest possible score, and n is the number of paired delete/add moves we want to make to extend our score. When dealing with odd and even numbers we know that odd+even === odd and even+even === even, so if our minAns is odd any new answer we get from this technique must be odd as well. Likewise, if minAns is even, any new answer must be even too. We can now check our score against k using this test:

if (k%2 === 0 && minAns%2 === 0 || k%2 !==0 && minAns%2 !== 0){
        return "Yes";
    }
    else {
        return "No";
    }

This edge case is quite tricky to spot because it only applies in a very limited set of circumstances. Using the empty string deletion trick we figured out that:

if (k>=s.length+t.length){
        return "Yes";
}

…so our test above will only be needed if k<s.length+t.length but larger than our minimum answer. Here’s a final example:

s="jig"
t="jigglypuff"

minAns = 7

//Yes: k>=13, k=9, k=11
//No: k<7

The results No for k<7 and Yes for k>=13 are easy to figure out, but the extra test is needed to pick out those two loose values (9 and 11) where minAns < k < s.length+t.length.

With a little bit of tidying, my complete solution for this challenge looks like this:

function appendAndDelete(s, t, k) {

    if (k>=s.length+t.length){
        return "Yes";
    }
    else{
        let match = "";
        for(let i=0; i<s.length; i++){
            if (s[i]===t[i]){
                match += s[i];
            }
            else {
                break;
            }
        }
        let minAns = t.length + s.length - 2*match.length;
        if (k < minAns){
            return "No";
        }
        else if (k%2 === 0 && minAns%2 === 0 || k%2 !==0 && minAns%2 !== 0){
            return "Yes";
        }
        else {
            return "No";
        }
    }
}

I’ve come across so many situations while learning to code where I’ve been caught out by pesky edge cases; on this occasion I could have saved some time if I’d read the question more carefully before writing my solution. An optimal approach could have involved carefully examining every facet of the problem at hand and reasoning out all the edge cases before writing any code at all. However, some edge cases will be hard to fathom out until they present themselves as bugs in the code, and I think there’s a lot to be said for the pragmatic approach; why not start off with a best guess and then examine, tinker and amend, until all those edges are nicely tidied away?