1

I've been struggling with this for a while now. While I had some long imperative method that worked, I decided to redesign this part:

  • Take a list of lists
  • Make a list of the first item of each sublist, then again the first item, but from the last sublist the second item, then the third, until that last list is exhausted, do the same for the N-1 sublist, which basically gives a kind of product of all these lists

    In other words: [abc][FG][98] should be evaluated like (apply function f to each item, comma's are for readability): [aF9,aF8,aG9,aG8,bF9,bF8,bG9,bG8,cF9,cF8,cG9,cG8]

  • Flatten that and apply a function to each item of the result, which itself returns a list

  • These lists are then flattened to return a singly linked list
  • When a sublist is empty (call this E), then only sublists 0 .. E-1 are evaluated (not in my example)

Here's a working example which clearly shows its recursive nature, but obviously only goes as deep as three lists:

let rec nestedApply f inp =
    match inp with
    | [] -> []
    | [x] -> x |> List.collect f
    | [x;y] -> 
        x
        |> List.collect (fun a -> y |> List.collect (fun b -> [a;b]))
        |> List.collect f
    | [x;y;z] -> 
        x
        |> List.collect (fun a -> y |> List.collect (fun b -> z |> List.collect (fun c -> [a;b;c])))
        |> List.collect f
    | head::tail ->
        // ??? I gave the following several attempts but couldn't get it right
        nestedApply f tail
        |> List.collect (fun a -> a::head)
        |> List.collect f

I'd prefer a solution that doesn't blow up the stack. Eventually I'll need this to lazy-eval, so I probably resort to sequences, but with lists I think the algorithm will be easiest to think about.

Example: nestedApply (fun a -> [a]) [[1 .. 5];[6;7];[11;12]]

Example output:

[1; 6; 11; 1; 6; 12; 1; 7; 11; 1; 7; 12; 2; 6; 11; 2; 6; 12; 2; 7; 11; 2; 7;
 12; 3; 6; 11; 3; 6; 12; 3; 7; 11; 3; 7; 12; 4; 6; 11; 4; 6; 12; 4; 7; 11; 4;
 7; 12; 5; 6; 11; 5; 6; 12; 5; 7; 11; 5; 7; 12]

As an aside, since this seems like a pretty "normal" algorithm, though it isn't a cartesian product, what is a typical well-known algorithm that this relates closest to?

Abel
  • 56,041
  • 24
  • 146
  • 247

2 Answers2

3

(OP's note: this high level answer led to my implementation in the other answer, thanks Vivek!)

  • The data set you have is called as N-ary tree.

  • Here, every node/list element may have number of children between 0 <= children <= N.

Algorithm steps:

  • Run through the first list of lists.
  • Apply depth first search to every element in the list.
  • Make child elements return themselves as a list.
  • Make a new empty lists at parent level and add parent element to each returned child list and add it to lists.
  • Return the lists.

Pseudocode:

function possibleLists(curr_list){
  my_new_lists = [];
  for each element in curr_list:
     child_lists = possibleLists(element)
     for each child_list in child_lists:
         child_list.add(element)
         my_new_lists.add(child_list)    
     if child_lists.size() == 0: // needed if there were no further deep levels than the level of curr_list elements
         my_new_lists.add(new List().add(child_list)) // you can return current instance to have this kind of chaining.      

  return my_new_lists;
}

Note: If you want to achieve tail-recursion, then you will have to pass path of elements visited to the next recursive call in parameters as a list to be appended in it's child elements.

P.S- I am not a F# coder, so can help you with pseudo code at the maximum.

Abel
  • 56,041
  • 24
  • 146
  • 247
nice_dev
  • 17,053
  • 2
  • 21
  • 35
  • Thanks, it certainly helps to know what type of problem it is, I didn't consider this to be a tree-traversal problem, this gives me other ways of looking at it. But it is precisely the imperative algorithm that I was trying to avoid. – Abel Sep 30 '18 at 16:51
  • @Abel why were you trying to avoid it? – nice_dev Sep 30 '18 at 17:14
  • I don't want to use mutability, besides, using a functional language, there's typically a readable yet simple algorithm for cases like this, in fact I think I just found mine (thanks in part to your pointers on N-ary trees, which had me look in a different way at the problem, though I think your suggestion is on any depth, i.e. _list of list of list etc_, whereas the data type here is only "list of of lists"). – Abel Sep 30 '18 at 17:55
  • (btw, the simplest reason for not using mutability in this case is multi-threading, as multiple threads use this function simultaneously, but I left that out of the original question, the other more obvious reason is clean code in FP) – Abel Sep 30 '18 at 18:05
  • @Abel Better do it for list of list of list etc because it can save you some refactoring in the future. Ignore this if you have already done it because I don't understand your `F#` code quite well. – nice_dev Oct 01 '18 at 11:35
  • I am not sure how would you handle mutability since multiple threads are dealing with the same function. Probably `clone` it and use it? – nice_dev Oct 01 '18 at 11:37
  • 1
    In F# everything is immutable by default (except when you use OO objects or the mutable keyword), which is why functional programming is so clean and gains popularity. The functionality I was after is similar to nested for loops to any depth, so the list of list of list etc won't happen, won't need refactoring. Nodes need to be represented in a different way, generally, since F# is more rigidly typed than C#, but that won't be an issue here. – Abel Oct 01 '18 at 11:42
  • @Abel ok, fair enough :) – nice_dev Oct 01 '18 at 11:49
  • @Abel Glad to help :) – nice_dev Dec 24 '18 at 06:31
2

Thanks to @vivek_23 for providing pointers on the N-ary trees, I read some posts on tree traversal and the like, which wasn't entirely what this was about (please correct me if I'm wrong), but it lead me to a simple, and I believe elegant, solution:

let rec nestedApply f acc inp =
    match inp with
    | [] -> f acc
    | head::tail -> 
        [
            for x in head do
                yield! nestedApply f (x::acc) tail
        ]

In this case, the apply-function f acts on the small sub-lists which have the same length each iteration, but for my particular case it didn't matter (also, it sped things up if the apply-function didn't need to care about the orders of the subsets). To get exactly the behavior as in the original question, use it like this:

> nestedApply List.rev [] [[1 .. 5];[6;7];[11;12]];;
val it : int list =
  [1; 6; 11; 1; 6; 12; 1; 7; 11; 1; 7; 12; 2; 6; 11; 2; 6; 12; 2; 7; 11; 2; 7;
   12; 3; 6; 11; 3; 6; 12; 3; 7; 11; 3; 7; 12; 4; 6; 11; 4; 6; 12; 4; 7; 11; 4;
   7; 12; 5; 6; 11; 5; 6; 12; 5; 7; 11; 5; 7; 12]

A slightly neater solution hides the accumulator:

let nestedApply f inp =
    let rec innerLoop acc inp =
        match inp with
        | [] -> f acc
        | head::tail -> 
            [
                for x in head do
                    yield! innerLoop (x::acc) tail
            ]

    innerLoop [] inp
Abel
  • 56,041
  • 24
  • 146
  • 247