How to write a function that connects each previous word to the next word by the shared letters

Viewed 300

Return the resulting string (removing duplicate characters in the overlap) and the minimum number of shared letters across all pairs of strings. Examples

join(["oven", "envier", "erase", "serious"]) ➞ ["ovenvieraserious", 2]
join(["move", "over", "very"]) ➞ ["movery", 3]
join(["to", "ops", "psy", "syllable"]) ➞ ["topsyllable", 1]

# "to" and "ops" share "o" (1)
# "ops" and "psy" share "ps" (2)
# "psy" and "syllable" share "sy" (2)
# the minimum overlap is 1

join(["aaa", "bbb", "ccc", "ddd"]) ➞ ["aaabbbcccddd", 0] Notes More specifically, look at the overlap between the previous words ending letters and the next word's beginning letters.

2 Answers

Code

def doit(arr)
  a, nbr = arr.each_cons(2).map do |s1,s2|
    n = [s1.size, s2.size].min.downto(1).find { |n| s1[-n..-1] == s2[0,n] }
    n.nil? ? [s2, 0] : [s2[n..-1],n]
  end.transpose
  [a.unshift(arr.first).join, nbr.min]
end

Examples

doit ["oven", "envier", "erase", "serious"]
    #=> ["ovenvieraserious", 2] 
doit ["move", "over", "very"]
    # ["movery", 3]
doit ["to", "ops", "psy", "syllable"]
    #["topsyllable", 1]

Explanation

The steps are as follows.

arr = ["oven", "envier", "erase", "serious"]

e = arr.each_cons(2)
  #=> #<Enumerator: ["oven", "envier", "erase", "serious"]:each_cons(2)>

see Enumerable#each_cons. We can see the elements that will be generated by this enumerator and passed to map by converting it to an array.

e.to_a
  #=> [["oven", "envier"], ["envier", "erase"], ["erase", "serious"]]

Continuing,

b = e.map do |s1,s2|
  n = [s1.size, s2.size].min.downto(1).find {|n| s1[-n..-1] == s2[0,n]}
  n.nil? ? [s2, 0] : [s2[n..-1],n]
end
  #=> [["vier", 2], ["ase", 2], ["rious", 2]]
a, nbr = b.transpose
  #=> [["vier", "ase", "rious"], [2, 2, 2]] 
a #=> ["vier", "ase", "rious"] 
nbr
  #=> [2, 2, 2]

Computing a and nbr from b.transpose makes use of Array Decomposition. Continuing,

c = a.unshift(arr.first)
  #=> ["oven", "vier", "ase", "rious"] 
d = c.join
  #=> "ovenvieraserious" 
e = nbr.min
  #=> 2 
[d, e]
  #=> ["ovenvieraserious", 2] 

Consider now the calculation of b. The first element is generated by e and passed to map, which passes it to its block, and values are assigned to the block variables (again, with the help of array decomposition).

s1, s2 = e.next
  #=> ["oven", "envier"]
s1
  #=> "oven"
s2
  #=> "envied"

The block calculation is now performed.

f = [s1.size, s2.size].min
  #=> [4,6].min
  #=> 4
g = f.downto(1).find { |n| s1[-n..-1] == s2[0,n] }
  #=> 2

In computing g the block calculation preceeds as follows.

 n = 4
 s1[-n..-1] == s2[0,n]
   #=> "oven" == "envi" => false

 n = 3
 s1[-n..-1] == s2[0,n]
   #=> "ven" == "env" => false

 n = 2
 s1[-n..-1] == s2[0,n]
   #=> "en" == "en" => true

This means that, when n #=> 2, the string consisting of the last n characters of s1 equals the first n characters of s2, and that is the largest value of n having that property.

Then

 n.nil? ? [s2, 0] : [s2[n..-1],n]
   #=> [s2[n..-1],n]
   #=> ["envied"[2..-1],2]
   #=> ["vied",2]

The next value of e is then generated, passed to map and the block variables are assigned new values:

s1, s2 = e.next
  #=> ["oven", "envier"]

The block calculation is then performed, and so on.

Here is my solution in js.


function joinOverlapping(words) {
    function joinWords(word1, word2) {
        let result = "";

        let j = 0;
        for(let i=0; i<word1.length; i++) {
            if(word1[i] != word2[j]) {
                result += word1[i];
            }else {
                result += word1[i];
                j += 1;
            }
        }
        
        result += word2.substring(j);
        return result;
    }

    //forward
    let joined_f = words[0];
    for(let i=1; i<words.length; i++) {
        joined_f = joinWords(joined_f, words[i]);
    }


    return joined_f;
    
}

Related