Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Punch/Combine multiple strings into a single (shortest possible) string that includes all the chars of each strings in forward direction

Tags:

My purpose is to punch multiple strings into a single (shortest) string that will contain all the character of each string in a forward direction. The question is not specific to any language, but more into the algorithm part. (probably will implement it in a node server, so tagging nodejs/javascript).

So, to explain the problem:

Let's consider I have few strings

["jack", "apple", "maven", "hold", "solid", "mark", "moon", "poor", "spark", "live"] 

The Resultant string should be something like:

"sjmachppoalidveonrk" 

jack: sjmachppoalidveonrk

apple: sjmachppoalidveonrk

solid: sjmachppoalidveonrk

====================================>>>> all in the forward direction

These all are manual evaluation and the output may not 100% perfect in the example.

So, the point is all the letters of each string have to exist in the output in FORWARD DIRECTION (here the actual problem belongs), and possibly the server will send the final strings and numbers like 27594 will be generated and passed to extract the token, in the required end. If I have to punch it in a minimal possible string it would have much easier (That case only unique chars are enough). But in this case there are some points:

  1. Letters can be present multiple time, though I have to reuse any letter if possible, eg: for solid and hold o > l > d can be reused as forward direction but for apple (a > p) and spark (p > a) we have to repeat a as in one case it appears before p for apple, and after p for sparks so either we need to repeat a or p. Even, we cannot do p > a > p as it will not cover both the case because we need two p after a for apple

  2. We directly have no option to place a single p and use the same index twice in a time of extract, we need multiple p with no option left as the input string contains that

  3. I am (not) sure, that there is multiple outputs possible for a set of strings. but the concern is it should be minimal in length, the combination doesn't matter if its cover all the tokens in a forward direction. all (or one ) outputs of minimal possible length need to trace.
  4. Adding this point as an EDIT to this post. After reading the comments and knowing that it's already an existing problem is known as shortest common supersequence problem we can define that the resultant string will be the shortest possible string from which we can re generate any input string by simply removing some (0 to N) chars, this is same as all inputs can be found in a forward direction in the resultant string.

I have tried, by starting with an arbitrary string, and then made an analysis of next string and splitting all the letters, and place them accordingly, but after some times, it seems that current string letters can be placed in a better way, If the last string's (or a previous string's) letters were placed according to the current string. But again that string was analysed and placed based on something (multiple) what was processed, and placing something in the favor of something that is not processed seems difficult because to that we need to process that. Or might me maintaining a tree of all processed/unprocessed tree will help, building the building the final string? Any better way than it, it seems a brute force?

Note: I know there are a lot of other transformation possible, please try not to suggest anything else to use, we are doing a bit research on it.

like image 803
Koushik Chatterjee Avatar asked Jul 23 '17 19:07

Koushik Chatterjee


2 Answers

I came up with a somewhat brute force method. This way finds the optimal way to combine 2 words then does it for each element in the array.

This strategy works by trying finding the best possible way to combine 2 words together. It is considered the best by having the fewest letters. Each word is fed into an ever growing "merged" word. Each time a new word is added the existing word is searched for a matching character which exists in the word to be merged. Once one is found both are split into 2 sets and attempted to be joined (using the rules at hand, no need 2 add if letter already exists ect..). The strategy generally yields good results.

The join_word method takes 2 words you wish to join, the first parameter is considered to be the word you wish to place the other into. It then searches for the best way to split into and word into 2 separate parts to merge together, it does this by looking for any shared common characters. This is where the splits_on_letter method comes in.

The splits_on_letter method takes a word and a letter which you wish to split on, then returns a 2d array of all the possible left and right sides of splitting on that character. For example splits_on_letter('boom', 'o') would return [["b","oom"],["bo","om"],["boo","m"]], this is all the combinations of how we could use the letter o as a split point.


The sort() at the beginning is to attempt to place like elements together. The order in which you merge the elements generally effects the results length. One approach I tried was to sort them based upon how many common letters they used (with their peers), however the results were varying. However in all my tests I had maybe 5 or 6 different word sets to test with, its possible with a larger, more varying word arrays you might find different results.

Output is

spmjhooarckpplivden 


var words = ["jack", "apple", "maven", "hold", "solid", "mark", "moon", "poor", "spark", "live"];  var result = minify_words(words);  document.write(result);    function minify_words(words) {      // Theres a good sorting method somewhere which can place this in an optimal order for combining them,      // hoever after quite a few attempts i couldnt get better than just a regular sort... so just use that      words = words.sort();        /*          Joins 2 words together ensuring each word has all its letters in the result left to right      */      function join_word(into, word) {          var best = null;          // straight brute force each word down. Try to run a split on each letter and           for(var i=0;i<word.length;i++) {              var letter = word[i];              // split our 2 words into 2 segments on that pivot letter              var intoPartsArr = splits_on_letter(into, letter);              var wordPartsArr = splits_on_letter(word, letter);              for(var p1=0;p1<intoPartsArr.length;p1++) {                  for(var p2=0;p2<wordPartsArr.length;p2++) {                      var intoParts = intoPartsArr[p1], wordParts = wordPartsArr[p2];                      // merge left and right and push them together                      var result = add_letters(intoParts[0], wordParts[0]) + add_letters(intoParts[1], wordParts[1]);                      if(!best || result.length <= best.length) {                          best = result;                      }                  }              }          }            // its possible that there is no best, just tack the words together at that point          return best || (into + word);      }        /*          Splits a word at the index of the provided letter      */      function splits_on_letter(word, letter) {          var ix, result = [], offset = 0;;          while((ix = word.indexOf(letter, offset)) !== -1) {              result.push([word.substring(0, ix), word.substring(ix, word.length)]);              offset = ix+1;          }          result.push([word.substring(0, offset), word.substring(offset, word.length)]);          return result;      }          /*          Adds letters to the word given our set of rules. Adds them starting left to right, will only add if the letter isnt found      */      function add_letters(word, addl) {          var rIx = 0;          for (var i = 0; i < addl.length; i++) {              var foundIndex = word.indexOf(addl[i], rIx);              if (foundIndex == -1) {                  word = word.substring(0, rIx) + addl[i] + word.substring(rIx, word.length);                  rIx += addl[i].length;              } else {                  rIx = foundIndex + addl[i].length;              }          }          return word;      }        // For each of our words, merge them together      var joinedWords = words[0];      for (var i = 1; i < words.length; i++) {          joinedWords = join_word(joinedWords, words[i]);      }      return joinedWords;  }
like image 64
ug_ Avatar answered Jan 06 '23 16:01

ug_


A first try, not really optimized (183% shorter):

function getShort(arr){  var perfect="";  //iterate the array  arr.forEach(function(string){    //iterate over the characters in the array    string.split("").reduce(function(pos,char){          var n=perfect.indexOf(char,pos+1);//check if theres already a possible char      if(n<0){              //if its not existing, simply add it behind the current          perfect=perfect.substr(0,pos+1)+char+perfect.substr(pos+1);        return pos+1;      }      return n;//continue with that char     },-1);   })   return perfect; } 

In action


This can be improved trough simply running the upper code with some variants of the array (200% improvement):

var s=["jack",...]; var perfect=null; for(var i=0;i<s.length;i++){  //shift  s.push(s.shift());  var result=getShort(s);  if(!perfect || result.length<perfect.length) perfect=result; } 

In action

Thats quite close to the minimum number of characters ive estimated ( 244% minimization might be possible in the best case)

Ive also wrote a function to get the minimal number of chars and one to check if a certain word fails, you can find them here

like image 22
Jonas Wilms Avatar answered Jan 06 '23 16:01

Jonas Wilms