3

I have a list of five attributes, each attribute has five different values. I want to generate the Cartesian product of them and filter all unique permutations.

Some background:

I need them to be my input values to solve a logic puzzle. Where I check rules against them to find the right solution.

from itertools import product

# input
names = ['Dana', 'Ingo', 'Jessica', 'Sören', 'Valerie']
ages = [26, 27, 30, 33, 35]
tops = ['Blouse', 'Poloshirt', 'Pullover', 'Sweatshirt', 'T-Shirt']
colors = ['blue', 'yellow', 'green', 'red', 'black']
sizes = ['XS', 'S', 'M', 'L', 'XL']

all_attributes = [names, ages, tops, colors, sizes]

# cartesian product (superset)
inputs = list(product(*all_attributes))

# the following code you do that...

Perhaps a simplified example can make it clear.

Data:

[['Dana', 'Ingo'], [26, 27]]

Cartesian Product of Data:

[('Dana', 26), ('Dana', 27), ('Ingo', 26), ('Ingo', 27)]

What I want:

[[('Dana', 26), ('Ingo', 27)],
 [('Dana', 27), ('Ingo', 26)],
 [('Ingo', 26), ('Dana', 27)],
 [('Ingo', 27), ('Dana', 26)]]

What I don't want:

[[('Dana', 26), ('Ingo', 26)], ...

I don't want multiple occurrences of the same value. The place matters, so it should have permutative character and that for a list of lists with five elements. I guess the output size will be enormous and maybe that isn't possible to compute so it would be nice to specify some place values which are fixed. For example, I Want to set 'Dana' as a first Element name.

Output:

[[('Dana', 26), ('Ingo', 27),
 [('Dana', 27), ('Ingo', 26)]]

Maybe you can tell me, out of curiosity, what the specific mathematical names for the concepts are, which I need?


The puzzle:

There are five friends (Dana, Ingo, Jessica, Sören, Valerie) waiting in line at the cash register of a shopping center. They are all of different ages (26, 27, 30, 33, 35) and want to buy different tops (Blouse, Poloshirt, Pullover, Sweatshirt, T-Shirt) for themselves. The tops have different colors (blue, yellow, green, red, black) and sizes (XS, S, M, L, XL).

Rules:

  1. The top 'Dana' wants to buy is 'XL'. Behind her (but not directly behind) is someone with a 'black' top.
  2. 'Jessica' waits directly in front of a person who wants to buy a 'Poloshirt'.
  3. The second person in line wants to buy a 'yellow' top.
  4. 'T-Shirt' isn't 'red'.
  5. 'Sören' wants to buy a 'Sweatshirt'. The person who waits directly in front of him is older than the one behind him.
  6. 'Ingo' needs a top in size 'L'.
  7. The last person in line is 30 years old.
  8. The oldest person is going to buy the top with the smallest size.
  9. The person who waits directly behind 'Valerie', wants to buy a 'red' top, which is bigger than size 'S'.
  10. The youngest person wants to buy a 'yellow' top.
  11. Jessica is going to buy a 'Blouse'.
  12. The third person waiting in line wants to buy a top of size 'M'.
  13. The 'Poloshirt' is 'red' or 'yellow' or 'green'.
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
Algore87
  • 43
  • 8
  • If this is indeed for a logic puzzle, I would recommend a different approach. Any time your problem space runs into billions for something you are probably expected to be able to solve by hand, you're probably underthinking it. As a start, use the additional rules to prune the search space before generating the permutations, not after. – Mad Physicist May 24 '19 at 02:04
  • yes your right twice, I want to specify different values on fixed places to shrink the problem space and this puzzle is easier solved by hand, paper and scissor but I got curious for a mechanism to generate those inputs. I appreciate your comment. – Algore87 May 24 '19 at 02:08
  • 1
    Thanks. Let me see if I can find you a way that doesn't permute. – Mad Physicist May 24 '19 at 02:53
  • 1
    First simplification: add one more list, which is the spot in line. No more need for permutations, just a product. Each of 5 layers has 5! ways to arrange: search space is 5*120 now. Much more manageable. – Mad Physicist May 24 '19 at 03:02
  • I'm in the middle of writing an answer using graphs. Search space of 405 edges, 30 nodes. Temporarily deleted since I need a desktop to test but currently only have mobile. Tomorrow morning... – Mad Physicist May 24 '19 at 05:11
  • 1
    Ok, I figured out how to do it, but the processing is a little long. I'm going to post a module on GitHub and reference it. It's basically a general purpose solver for this sort of problem. I use an adjacency matrix graph representation. The problem space is only a few hundred elements, so it runs pretty fast. – Mad Physicist May 24 '19 at 21:59
  • that would be amazing. – Algore87 May 25 '19 at 04:14
  • Sorry this is taking so long. I'm working on a puzzle solver package for pypi now... – Mad Physicist May 29 '19 at 19:48
  • your welcome but I'd be glad if you post your repo to this package if you are done. Looking forward to and happy coding. – Algore87 May 30 '19 at 13:14
  • 1
    I'm done. Got it on readthedocs, pypi, github. Enjoy. – Mad Physicist May 30 '19 at 21:16
  • 1
    FWIW, I just found out this is called a zebra puzzle by reading through the tag suggestions. – Mad Physicist May 30 '19 at 22:12

2 Answers2

2

This will do it, but take a very long time. I reduced the list size because your options as requested have 24,883,200,000 permutations:

from itertools import permutations, product

names = ['Dana', 'Ingo']
ages = [26, 27]
tops = ['Hemd', 'Poloshirt']
colors = ['blau', 'gelb']
sizes = ['XS', 'S']

options = []

# Generate the Cartesian product of all permutations of the options.
for name,age,top,color,size in product(*map(permutations,[names,ages,tops,colors,sizes])):
    # Build the option list. zip() transposes the individual lists.
    option = list(zip(name,age,top,color,size))
    options.append(option)
    print(option)

Output:

[('Dana', 26, 'Hemd', 'blau', 'XS'), ('Ingo', 27, 'Poloshirt', 'gelb', 'S')]
[('Dana', 26, 'Hemd', 'blau', 'S'), ('Ingo', 27, 'Poloshirt', 'gelb', 'XS')]
[('Dana', 26, 'Hemd', 'gelb', 'XS'), ('Ingo', 27, 'Poloshirt', 'blau', 'S')]
[('Dana', 26, 'Hemd', 'gelb', 'S'), ('Ingo', 27, 'Poloshirt', 'blau', 'XS')]
[('Dana', 26, 'Poloshirt', 'blau', 'XS'), ('Ingo', 27, 'Hemd', 'gelb', 'S')]
[('Dana', 26, 'Poloshirt', 'blau', 'S'), ('Ingo', 27, 'Hemd', 'gelb', 'XS')]
[('Dana', 26, 'Poloshirt', 'gelb', 'XS'), ('Ingo', 27, 'Hemd', 'blau', 'S')]
[('Dana', 26, 'Poloshirt', 'gelb', 'S'), ('Ingo', 27, 'Hemd', 'blau', 'XS')]
[('Dana', 27, 'Hemd', 'blau', 'XS'), ('Ingo', 26, 'Poloshirt', 'gelb', 'S')]
[('Dana', 27, 'Hemd', 'blau', 'S'), ('Ingo', 26, 'Poloshirt', 'gelb', 'XS')]
[('Dana', 27, 'Hemd', 'gelb', 'XS'), ('Ingo', 26, 'Poloshirt', 'blau', 'S')]
[('Dana', 27, 'Hemd', 'gelb', 'S'), ('Ingo', 26, 'Poloshirt', 'blau', 'XS')]
[('Dana', 27, 'Poloshirt', 'blau', 'XS'), ('Ingo', 26, 'Hemd', 'gelb', 'S')]
[('Dana', 27, 'Poloshirt', 'blau', 'S'), ('Ingo', 26, 'Hemd', 'gelb', 'XS')]
[('Dana', 27, 'Poloshirt', 'gelb', 'XS'), ('Ingo', 26, 'Hemd', 'blau', 'S')]
[('Dana', 27, 'Poloshirt', 'gelb', 'S'), ('Ingo', 26, 'Hemd', 'blau', 'XS')]
[('Ingo', 26, 'Hemd', 'blau', 'XS'), ('Dana', 27, 'Poloshirt', 'gelb', 'S')]
[('Ingo', 26, 'Hemd', 'blau', 'S'), ('Dana', 27, 'Poloshirt', 'gelb', 'XS')]
[('Ingo', 26, 'Hemd', 'gelb', 'XS'), ('Dana', 27, 'Poloshirt', 'blau', 'S')]
[('Ingo', 26, 'Hemd', 'gelb', 'S'), ('Dana', 27, 'Poloshirt', 'blau', 'XS')]
[('Ingo', 26, 'Poloshirt', 'blau', 'XS'), ('Dana', 27, 'Hemd', 'gelb', 'S')]
[('Ingo', 26, 'Poloshirt', 'blau', 'S'), ('Dana', 27, 'Hemd', 'gelb', 'XS')]
[('Ingo', 26, 'Poloshirt', 'gelb', 'XS'), ('Dana', 27, 'Hemd', 'blau', 'S')]
[('Ingo', 26, 'Poloshirt', 'gelb', 'S'), ('Dana', 27, 'Hemd', 'blau', 'XS')]
[('Ingo', 27, 'Hemd', 'blau', 'XS'), ('Dana', 26, 'Poloshirt', 'gelb', 'S')]
[('Ingo', 27, 'Hemd', 'blau', 'S'), ('Dana', 26, 'Poloshirt', 'gelb', 'XS')]
[('Ingo', 27, 'Hemd', 'gelb', 'XS'), ('Dana', 26, 'Poloshirt', 'blau', 'S')]
[('Ingo', 27, 'Hemd', 'gelb', 'S'), ('Dana', 26, 'Poloshirt', 'blau', 'XS')]
[('Ingo', 27, 'Poloshirt', 'blau', 'XS'), ('Dana', 26, 'Hemd', 'gelb', 'S')]
[('Ingo', 27, 'Poloshirt', 'blau', 'S'), ('Dana', 26, 'Hemd', 'gelb', 'XS')]
[('Ingo', 27, 'Poloshirt', 'gelb', 'XS'), ('Dana', 26, 'Hemd', 'blau', 'S')]
[('Ingo', 27, 'Poloshirt', 'gelb', 'S'), ('Dana', 26, 'Hemd', 'blau', 'XS')]
Mark Tolonen
  • 166,664
  • 26
  • 169
  • 251
  • the order of the list items should not be ordered by the elements name. `[('Sören',..), ('Dana',...),...] should be an example of the input too. – Algore87 May 24 '19 at 01:42
  • @Algore87 Why? That's just the same combination for the people with the people listed in a different order. If you actually want that just add names to the iteration. I'll update with shorter code that does that since that's what you want. Does your logic puzzle care what order the people are listed? – Mark Tolonen May 24 '19 at 01:45
  • I need that input to solve a logical puzzle. In that puzzle, you have a queue where those people with the different attribute permutations wait. There should be a possibility of an input like [('Dana', 33, 'T-Shirt', 'blau', 'XL), ('Sören', 26, 'Sweatshirt', 'gelb', 'S'), ('Jessica', 27, 'Hemd', 'schwarz', 'M'), ('Valerie', 35, 'Poloshirt', 'XS'), ('Ingo', 30, 'Pullover', 'rot', 'L')]. I guess input size gets extremely big so I should specify some fixed values. For example I want 'Ingo' on place 5, and the age of the second person is equal to 26. This should shrink the input size at generation – Algore87 May 24 '19 at 01:55
  • Can you post the puzzle? I'm curious now – Mad Physicist May 24 '19 at 02:06
  • Sure, what is the best approach to share an image and / or more text in the comments. [solution@Imgur](https://i.imgur.com/vIKnhj0.jpg) [puzzle@Imgur](https://i.imgur.com/1Bsi3Fu.jpg) I can translate it, if you want, but it's not that hard of a puzzle, I just want to solve it with a program. – Algore87 May 24 '19 at 02:21
  • 1
    @Algore. You can edit it into the end of your question. Based on our relative skills in each other's languages, I think it would be much faster if you translated it. – Mad Physicist May 24 '19 at 02:27
2

There is a fundamental problem with the approach of using permutations for the type of logic puzzle you have. The issue is not even that there are so many of them that your solver is unlikely to finish in a reasonable amount of time. The issue is that you don't have an automated way of checking the rules against the problem: having all the possibilities in front of you is pointless unless you have a method to verify them.

To address these issues, I have created a project called Puzzle Solvers:

This is a small project that currently contains a single class of interest: puzzle_solvers.elimination.Solver. This class implements most of the operations you need to solve the process-of-elimination type problem presented in your question. All of the logic is documented, and the tutorial is a walkthrough of your exact problem. I will explain the basics here, so you can understand what I did, and perhaps improve it.

Step 1 is to recognize that position in the queue is an attribute just like age, name, etc. That means that the order is not relevant any more.

Step 2 is to recognize that this is a graph problem in disguise. You have 30 nodes: all the possible attributes (six of them) of each of five individuals. The graph starts out almost complete. Only the edges between attributes of a given type are missing, so you start with 375 instead of the complete 435 edges.

The final goal is to have one edge between each class of attribute among five connected components in the graph. The final number of edges is therefore 5 * 15 = 75.

So how do you remove edges? The simple rules like "'T-Shirt' isn't 'red'" are quite straightforward: just remove that edge. Rules like "The last person in the queue is 30 years old." are simple too, and more profitable in terms of edge removal. All edges between ages that aren't 30 and position 5 are removed, as well as all edges between positions that aren't 5 and age 30. I added a couple of half-baked utility wrappers to check greater than and less than conditions and remove edges that represented impossible combinations.

The most important aspect of the solver is the fact that any time an edge is removed, it completely follows through with all of the logical implications of that operation. Imagine that you have "The 'Poloshirt' is 'red' or 'yellow' or 'green'" and that you have reached a point in your puzzle where none of those three colors is linked to say age 30. That means that whoever is wearing the poloshirt can not be 30 years old. In fact 'Poloshirt' can not have any edges with endpoints that are not shared by those three colors. If you recursively follow through with those inferences, you get a complete solution.

I'm sorry for the shameless plug of my package, but in justification, I did write it just to answer this question.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • Your right, the question evolved into the puzzle problem. At the beginning I just want to ask how to generate various inputs. I solved those kinds of puzzles by writing logical functions where I check against. I fill a container with all the rule functions, iterate over them and check if everyone returns true. That was the approach I choose because it was the first which came to my mind. I am glad you build that solver. Checking it out soon! Thank you very much. – Algore87 May 31 '19 at 17:38
  • @Algore87. The combination approach is prohibitively expensive in almost all cases, so I generally assume XY problem in that case. It's why graph theory is so important. It's basically a huge tool for representing problems in a way that immediately reduces the problem space. – Mad Physicist May 31 '19 at 20:22
  • @Algore87. I've updated my package to v0.0.1b1. It's about doubled in size, and I can now solve the original zebra puzzle without any looping. I still haven't managed to handle he first rule in your puzzle in a single statement though. On an unrelated note, you should probably select an answer some time. – Mad Physicist Jun 06 '19 at 23:09