Puzzles

Below are various puzzles and games.

Number Puzzles

Primality Test

Given a positive integer n,{n,} write an algorithm that returns true if n{n} is prime, and false otherwise.

A naive solution is to iterate up to n.{n.} This would get the job done, but it runs on Θ(n){\bigTheta{n}} time. We can bring that down to Θ(n){\bigTheta{\sqrt{n}}} time by relying on the fact that the largest divisor of an integer n{n} (say some integer d{d}), will always be d2.{d^2.} In other words, we only need to check for divisors up to n.{\sqrt{n}.}

primality-test

  • Input: An integer n.{n.}
  • Output: 1{1} if nP,{n \in \primes,} 0{0} otherwise.
  1. if n<2{n \lt 2} return 0{0}
  2. else
    1. init i2{\let{i}{2}}
    2. while i2n{i^2 \le n} do
      1. if n rem i=0{n \rem i = 0} return 0{0}
      2. else increment i{\df{increment } i}
      3. goto line[3]{\line{3}}
  3. return 1{1}

Note that we use the square of the divisor rather than n.{\sqrt{n}.} Square roots are fairly nasty objects — both in real analysis and on computers — causing problems on the comparison operators because of floating point arithmetic.

Array Puzzles

Rain Trapping

Given n{n} non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining.

function f(height:number[]) {
	const L = height.length;
	let left = 0;
	let right = L-1;
	let leftmax = height[left];
	let rightmax = height[right];
	let result = 0;
	while (left < right) {
		if (leftmax < rightmax) {
			left++;
			leftmax = leftmax > height[left] ? leftmax : height[left];
			result += leftmax-height[left]
		} else {
			right--;
			rightmax = rightmax > height[right] ? rightmax : height[right];
			result += rightmax - height[right];
		}
	}
	return result;
} 

String Puzzles

For the string puzzles, unless otherwise stated, characters are mapped to ASCII.

Decompress

Given a string of the form nS,{nS,} where n{n} is an integer and S{S} is a string, write an algorithm that returns the decompressed form of the string — the string S{S} repeated n{n} times. For example, the string 2c{2c} results in cc.{cc.} The string 4a2b{4a2b} results in aaaabb.{aaaabb.} We assume the strings are well-formed.

The algorithm below is one possible approach. The idea is to have one pointer i{i} behind another pointer j.{j.} We use j{j} to move through the string S.{S.} If the character S[j]{S\ix{j}} is a numeral (represented by the is-digit{\df{is-digit}} function below), we we increment j.{j.} Otherwise, we enter an innerloop that concatenates the string from i{i} to j.{j.}

decompress

  • Input: A string S.{S.}
  • Output: An uncompressed string S.{S.}
  1. i0{\let{i}{0}}
  2. j0{\let{j}{0}}
  3. resultnew empty string{\let{result}{\df{new empty string}}}
  4. while j<length S{j \lt \len S}
    1. if is-digit(S[j]){\df{is-digit}(S \ix{j})} then increment j{\df{increment }j}
    2. else
      1. ncast int S.slice(i,j){\let{n}{\df{cast int}}~S.\df{slice}(i,j)}
      2. k0{\let{k}{0}}
      3. while k<n{k \lt n}
        1. result++S[j]{result \inc S \ix{j}}
      4. increment j{\df{increment~}j}
      5. ij{\let{i}{j}}
  5. return result{result}

For example, suppose the string is 2a3b11a.{2a3b11a.} Initially, both i{i} and j{j} are on the first character:

j      2a3b11ai            j=0 \begin{array}{c:c:c:c:c:c:c} j & ~ & ~ & ~ & ~ & ~ & ~ \\ \hdashline 2 & a & 3 & b & 1 & 1 & a \\ \hdashline i & ~ & ~ & ~ & ~ & ~ & ~ \\ \end{array} ~~~~~~ j = 0

This is a numeral, so j{j} moves forward:

 j     2a3b11ai            j=1 \begin{array}{c:c:c:c:c:c:c} ~ & j & ~ & ~ & ~ & ~ & ~ \\ \hdashline 2 & a & 3 & b & 1 & 1 & a \\ \hdashline i & ~ & ~ & ~ & ~ & ~ & ~ \\ \end{array} ~~~~~~ j = 1

In doing so, j{j} is no longer on a numeral, so we take the slice from i{i} to j.{j.} That's the numeral 2,{2,} so we cast it to an integer, then loop up to that cast. At each iteration, we concatenate the the character j{j} is currently pointing to. At the end of that loop, we increment j{j} and update i{i} to j.{j.}

 j     2a3b11a i           j=2      aa \begin{array}{c:c:c:c:c:c:c} ~ & j & ~ & ~ & ~ & ~ & ~ \\ \hdashline 2 & a & 3 & b & 1 & 1 & a \\ \hdashline ~ & i & ~ & ~ & ~ & ~ & ~ \\ \end{array} ~~~~~~ j = 2 ~~~~~~ aa

At the next iteration, we have j{j} on another numeral:

  j    2a3b11a i           j=2      aa \begin{array}{c:c:c:c:c:c:c} ~ & ~ & j & ~ & ~ & ~ & ~ \\ \hdashline 2 & a & 3 & b & 1 & 1 & a \\ \hdashline ~ & i & ~ & ~ & ~ & ~ & ~ \\ \end{array} ~~~~~~ j = 2 ~~~~~~ aa

The previous process repeats:

   j   2a3b11a  i          j=3      aabbb \begin{array}{c:c:c:c:c:c:c} ~ & ~ & ~ & j & ~ & ~ & ~ \\ \hdashline 2 & a & 3 & b & 1 & 1 & a \\ \hdashline ~ & ~ & i & ~ & ~ & ~ & ~ \\ \end{array} ~~~~~~ j = 3 ~~~~~~ aabbb

resulting in the state:

   j   2a3b11a   i         j=3      aabbb \begin{array}{c:c:c:c:c:c:c} ~ & ~ & ~ & j & ~ & ~ & ~ \\ \hdashline 2 & a & 3 & b & 1 & 1 & a \\ \hdashline ~ & ~ & ~ & i & ~ & ~ & ~ \\ \end{array} ~~~~~~ j = 3 ~~~~~~ aabbb

Now, when j{j} encounters another numeral followed by another numeral, we simply keep incrementing:

     j 2a3b11a   i         j=5      aabbb \begin{array}{c:c:c:c:c:c:c} ~ & ~ & ~ & ~ & ~ & j & ~ \\ \hdashline 2 & a & 3 & b & 1 & 1 & a \\ \hdashline ~ & ~ & ~ & i & ~ & ~ & ~ \\ \end{array} ~~~~~~ j = 5 ~~~~~~ aabbb

Leaving us with:

      j2a3b11a    i        j=6      aabbbaaaaaaaaaaa \begin{array}{c:c:c:c:c:c:c} ~ & ~ & ~ & ~ & ~ & ~ & j \\ \hdashline 2 & a & 3 & b & 1 & 1 & a \\ \hdashline ~ & ~ & ~ & ~ & i & ~ & ~ \\ \end{array} ~~~~~~ j = 6 ~~~~~~ aabbbaaaaaaaaaaa

In the worst case, this algorithm runs on O(nm),{\bigO{n \by m},} where n{n} is the length of the string, and m{m} is the length of an expansion. The time complexity may be worse, however, depending on how the slice{\df{slice}} and ++{\inc} (concatenation) functions are implemented. In a language like JavaScript, those operations run on O(n).{\bigO{n}.} These bottlenecks can be avoided by pushing the characters to the array, then joining all of the array elements after the loops. This problem is not encountered in languages like C++.

Compress

Given a string S,{S,} write an algorithm that replaces 2 or more consecutive terms of the same character into the number of occurrences followed by the character. For example, aaab3ab.{aaab \to 3ab.}

This implementation uses the same approach as the uncompress problem. Note the use of the do-while loop. Unlike the uncompress algorithm, this approach takes Θ(n).{\bigTheta{n}.} There's an inner-while loop, but that loop is purely there to make the code more concise. The j{j} pointer increments so long as j{j} and i{i} point to terms of the same character.

compress

  • Input: A string S.{S.}
  • Output: A compressed string, result.{result.}
  1. Llength S{\let{L}{\len S}}
  2. i0,{\let{i}{0},} j0{\let{j}{0}}
  3. resultnew empty string{\let{result}{\df{new empty string}}}
  4. while (j<L){(j \lt L)}
    1. do jj+1{\let{j}{j+1}}
      1. while S[i]=S[j]{S\ix{i} = S\ix{j}}
    2. if ji>1{j-i \gt 1} then
      1. result concat S[i]{result \df{ concat } S \ix{i}}
  5. return result{result}

Anagrams

Given two strings S1{S_1} and S2,{S_2,} write an algorithm that returns true (or 1) if S1{S_1} and S2{S_2} are anagrams, and false (or 0) otherwise. Anagrams are strings that contain the same characters, disregarding order.

an incorrect, tempting solution would be to iterate over the first string, accumulating the sum of each element's character code. Then iterate over the second string, subtracting each element's character code from the sum. If two strings have the same set of characters (an anagram), the result should be 0, so we return 1, otherwise we return 0. Before any of these operations occur, we immediately return if the two strings do not have the same length (strings of unequal lengths are not anagrams by definition):

anagram

  • Input: a string s1{s_1} and a string s2.{s_2.}
  • Output: Boolean.
  1. if length s1length s2{\len s_1 \neq \len s_2} return false{\df{false}}
  2. init
    1. sum0{\let{sum}{0}}
    2. i0{\let{i}{0}}
  3. if i<length s1{i \lt \len s_1} start loop body
    1. sumsum+(cast int)s1[i]{\let{sum}{sum + (\df{cast int})s_1\ix{i}}}
    2. sumsum(cast int)s2[i]{\let{sum}{sum - (\df{cast int})s_2\ix{i}}}
    3. ii+1{\let{i}{i+1}}
  4. goto line[4]{\line{4}} end loop body
  5. return sum=0{sum=0}

The temptation here is strong: We have a linear time complexity and constant memory complexity. Except there's a case it doesn't cover: ac{ac} and bb.{bb.} If the characters were encoded via ASCII, we would have 97+99=196{97+99=196} and 98+98=196.{98+98=196.} This yields 196196=0,{196-196=0,} in which case we return true. Clearly, however, the two strings are not anagrams.

A correct approach would be to use a hash table:

anagram

  • Input: a string s1{s_1} and a string s2.{s_2.}
  • Output: Boolean.
  1. if length s1length s2{\len s_1 \neq \len s_2} return false{\df{false}}
  2. init
    1. Hnew hash-table{\let{H}{\df{new hash-table}}}
    2. i0{\let{i}{0}}
  3. if i<length s1{i \lt \len{s_1}} start loop 1 body
    1. if s1[i]H{s_1 \ix{i} \in H} then H(s1[i],0){H \push (s_1 \ix{i}, 0)}
    2. increment H[s1[0]]{{\df{increment}}~{H \ix{s_1 \ix{0}}}}
    3. increment i{\df{increment}~i}
  4. goto line[4]{\line{4}} end loop 1 body
  5. i0{\let{i}{0}}
  6. if i<length s2{i \lt \len{s_2}} start loop 2 body
    1. if (H[s2[i]]=){(H \ix{s_2 \ix{i}} = \nil)} then return false{\df{false}}
    2. else decrement H[s2[i]]{\df{decrement} ~ H \ix{s_2 \ix{i}}}
    3. increment i{\df{increment}~i}
  7. goto line[10]{\line{10}} end loop 2 body
  8. for each xH{x \in H} do start loop 3 body
    1. if x0{x \neq 0} return false{\df{false}}
  9. return true{\df{true}} end loop 3 body

Here, we have a linear time complexity, O(n+m),{\bigO{n + m},} where n{n} is the length of either string, and m{m} is the number of entries in the hash table. This comes at the cost of O(n){\bigO{n}} memory complexity. The time complexity also depends on the hash table implementation.

Most Frequent Character

Given a string S,{S,} write an algorithm that returns the most frequent character of the string. If there are ties, return the earliest character.

A hash table would work here.

most frequent character

  • Input: a string S.{S.}
  • Output: a string result,{result,} the most frequent character.
  1. init
    1. Llength S{\let{L}{\len{S}}}
    2. Hnew hash-table{\let{H}{\df{new hash-table}}}
    3. i0{\let{i}{0}}
    4. resultS[0]{\let{result}{S \ix{0}}}
  2. if i<L{i \lt L} then begin loop body
    1. if S[i]H{S \ix{i} \in H} then
      1. increment H[S[i]]{\df{increment }H \ix{S \ix{i}}}
    2. else H(S[i],1){H \push (S \ix{i}, 1)}
  3. goto line[5]{\line{5}} end loop body
  4. init max0{\let{max}{0}}
  5. for each (key,value)H{(key,value) \in H} do
    1. if value>max{value \gt max} then
      1. maxvalue{\let{max}{value}}
      2. resultkey{\let{result}{key}}
  6. return result{result}

This algorithm has a running time of O(n+m),{\bigO{n+m},} where n{n} is the length of the string and m{m} is the number of entries in the hash table. Given that mn,{m \le n,} we have O(n).{\bigO{n}.} Like other hash table solutions, a complete time complexity analysis requires an examining the hash table's implementation. The memory complexity here is O(m),{\bigO{m},} where m{m} is the size of the hash table.

Roman Numeral Conversion

Write an algorithm that converts a given Roman numeral into its Hindu-arabic equivalent.

romnum

Let S{S} be a finite, non-empty string of length n{n} over the alphabet Σ={I,V,X,L,C,D,M}.{\Sigma = \set{\tx{I}, \tx{V}, \tx{X}, \tx{L}, \tx{C}, \tx{D}, \tx{M}}.} Further, let M{M} be a mapping from ΣzZ+,{\Sigma \to z \in \uint^+,} defined as

M={(I,1),(IV,4),(V,5)(IX,9),(X,10),(XL,40)(L,50),(XC,90),(C,100)(CD,400),(D,500),(CM,900)(M,1000)}. M = \lset{ \eqs{ & (\tx{I},1),(\tx{IV},4),(\tx{V},5) \\ & (\tx{IX},9),(\tx{X},10),(\tx{XL},40) \\ & (\tx{L},50),(\tx{XC},90),(\tx{C},100) \\ & (\tx{CD},400),(\tx{D},500),(\tx{CM},900) \\ & (\tx{M},1000) } }.

For each character siS=(s0,s1,,sn1){s_i \in S = (s_0, s_1, \ldots, s_{n-1})} with nN{n \in \NN}, the function romnum{\df{romnum}} returns the sum

i=0n1M(si). \dsum{i=0}{n-1} M(s_i).
  1. variable  out0{\var ~ \let{out}{0}}
  2. variable  ilength (S)1{\var ~ \let{i}{\len (S) - 1}}
  3. while i0{i \ge 0} do
    1. switch  true{\df{switch} ~~ \df{true}}
      1. case  S[i]=M{\df{case} ~~ S \ix{i} = {\tx{M}}}
        1. if  S[i1]={\if ~ S \ix{i-1} = {\tx{C}} ~ \nc} ii1,  outout+900{\let{i'}{i - 1},~~\let{out'}{out + 900}}
        2. else  outout+1000{\else ~ \let{out'}{out + 1000}}
      2. case  S[i]=D{\df{case} ~~ S\ix{i} = {\tx{D}}}
        1. if  S[i1]={\if ~ S \ix{i-1} = {\tx{C}} ~ \nc} ii1,  outout+400{\let{i'}{i - 1},~~\let{out'}{out + 400}}
        2. else  outout+500{\else ~ \let{out'}{out + 500}}
      3. case  S[i]=C{\df{case} ~~ S\ix{i} = {\tx{C}}}
        1. if  S[i1]={\if ~ S \ix{i-1} = {\tx{X}} ~ \nc} ii1,  outout+90{\let{i'}{i - 1},~~\let{out'}{out + 90}}
        2. else  outout+100{\else ~ \let{out'}{out + 100}}
      4. case  S[i]=L{\df{case} ~~ S \ix{i} = {\tx{L}}}
        1. if  S[i1]={\if ~ S \ix{i-1} = {\tx{X}} ~ \nc} ii1,  outout+40{\let{i'}{i - 1},~~\let{out'}{out + 40}}
        2. else  outout+50{\else ~ \let{out'}{out + 50}}
      5. case  S[i]=  such that  X{\df{case} ~~ S \ix{i} = \st{\tx{X}}}
        1. if  S[i1]={\if ~ S \ix{i-1} = {\tx{I}} ~ \nc} ii1,  outout+9{\let{i'}{i - 1},~~\let{out'}{out + 9}}
        2. else  outout+10{\else ~ \let{out'}{out + 10}}
      6. case  S[i1]=V{\df{case} ~~ S \ix{i - 1} = {\tx{V}}}
        1. if  S[i1]={\if ~ S \ix{i-1} = {\tx{I}} ~ \nc} ii1,  outout+4{\let{i'}{i - 1},~~\let{out'}{out + 4}}
        2. else  outout+5{\else ~ \let{out'}{out + 5}}
      7. default outout+1{\df{default} ~ \let{out'}{out + 1}}
  4. return out{out}
{3}{7}{1}{4}     {2}     {43}     {5}{8}{19}{21} \ax{ \set{3} & \to & \set{7} & \to & \set{1} & \to & \set{4} \\ \uarr & ~ & ~ & ~ & ~ & ~ & \darr \\ \set{2} & ~ & ~ & ~ & ~ & ~ & \set{43} \\ \uarr & ~ & ~ & ~ & ~ & ~ & \darr \\ \set{5} & \gets & \set{8} & \gets & \set{19} & \gets & \set{21} \\ }