1

This is not a homework that I'm struggling to do but I am trying to solve a problem (here is the link if interested https://open.kattis.com/problems/azulejos).

Here you actually don't have to understand the problem but what I would like to accomplish now is that I want to select only one element from multiple lists and they do not overlap with each other.

For example, at the end of my code, I get an output:

{1: [1, 2, 3], 2: [1, 2, 3, 4], 3: [2, 4], 4: [1, 2, 3, 4]}

I would like to transform this into, for example,

{3: 4, 2: 2, 4:1, 1: 3} -- which is the sample answer that is in the website.

But from my understanding, it can also be simply

{1: 3, 2: 2, 3: 4, 4: 1}

I am struggling to select only one integer that does not overlap with the others. The dictionary I produce in my code contains lists with multiple integers. And I would like to pick only one from each and they are all unique

import sys

n_tiles_row = int(sys.stdin.readline().rstrip())
# print(n_tiles_row) ==> 4

# BACK ROW - JOAO
back_row_price = sys.stdin.readline().rstrip()
# print(back_row_price) ==> 3 2 1 2

back_row_height = sys.stdin.readline().rstrip()
# print(back_row_height) ==> 2 3 4 3

# FRONT ROW - MARIA
front_row_price = sys.stdin.readline().rstrip()
# print(front_row_price) ==> 2 1 2 1

front_row_height = sys.stdin.readline().rstrip()
# print(front_row_height) ==> 2 2 1 3

br_num1_price, br_num2_price, br_num3_price, br_num4_price = map(int, back_row_price.split())
# br_num1_price = 3; br_num2_price = 2; br_num3_price = 1; br_num4_price = 2;

br_num1_height, br_num2_height, br_num3_height, br_num4_height = map(int, back_row_height.split())
# 2 3 4 3

fr_num1_price, fr_num2_price, fr_num3_price, fr_num4_price = map(int, front_row_price.split())
# 2 1 2 1

fr_num1_height, fr_num2_height, fr_num3_height, fr_num4_height = map(int, front_row_height.split())
# 2 2 1 3

back_row = {1: [br_num1_price, br_num1_height], 
2: [br_num2_price, br_num2_height], 
3: [br_num3_price, br_num3_height], 
4: [br_num4_price, br_num4_height]}
# {1: [3, 2], 2: [2, 3], 3: [1, 4], 4: [2, 3]}

front_row = {1: [fr_num1_price, fr_num1_height], 
2: [fr_num2_price, fr_num2_height], 
3: [fr_num3_price, fr_num3_height], 
4: [fr_num4_price, fr_num4_height]}
# {1: [2, 2], 2: [1, 2], 3: [2, 1], 4: [1, 3]}

_dict = {1: [], 
2: [], 
3: [], 
4: []
}

for i in range(n_tiles_row):
    _list = []
    for n in range(n_tiles_row):
        if(list(back_row.values())[i][0] >= list(front_row.values())[n][0] 
        and list(back_row.values())[i][1] >= list(front_row.values())[n][1]):
            _list.append(list(front_row.keys())[n])
            _dict[list(back_row.keys())[i]] = _list

print(_dict)
# {1: [1, 2, 3], 2: [1, 2, 3, 4], 3: [2, 4], 4: [1, 2, 3, 4]}

Please let me know if there is another approach to this problem.

Jaoa
  • 95
  • 9
  • I am not quite getting if you are struggling with the formulation or the implementation of the algorithm. If it is only the implementation, could you formulate the algorithm in the question? if you have troubles with the formulation, what is your current thinking? – norok2 Oct 30 '19 at 23:59
  • Also, it looks to me that there are multiple possible solutions, e.g. `{1: 1, 2: 3, 3: 2, 4: 4}` or `{1: 2, 2: 3, 3: 4, 4: 1}`. – norok2 Oct 31 '19 at 00:02
  • @norok2 thanks for your answer. Yes, there are multiple possible solutions. And I am not sure how to formulate the algorithm. Of course, I can brute force this by checking every single possible combinations and only get the unique items, however that is too much computation especially when there are a lot of input – Jaoa Oct 31 '19 at 00:05

2 Answers2

0

A, possibly inefficient, way of solving the issue, is to generate all possible "solutions" (with values potentially not present in the lists corresponding to a specific key) and settle for a "valid" one (for which all values are present in the corresponding lists).

One way of doing this with itertools.permutation (that is able to compute all possible solutions satisfying the uniqueness constraint) would be:

import itertools


def gen_valid(source):
    keys = source.keys()
    possible_values = set(x for k, v in source.items() for x in v)
    for values in itertools.permutations(possible_values):
        result = {k: v for k, v in zip(keys, values)}
        # : check that `result` is valid
        if all(v in source[k] for k, v in result.items()):
            yield result


d = {1: [1, 2, 3], 2: [1, 2, 3, 4], 3: [2, 4], 4: [1, 2, 3, 4]}


next(gen_valid(d))
# {1: 1, 2: 2, 3: 4, 4: 3}

list(gen_valid(d))
# [{1: 1, 2: 2, 3: 4, 4: 3},
#  {1: 1, 2: 3, 3: 2, 4: 4},
#  {1: 1, 2: 3, 3: 4, 4: 2},
#  {1: 1, 2: 4, 3: 2, 4: 3},
#  {1: 2, 2: 1, 3: 4, 4: 3},
#  {1: 2, 2: 3, 3: 4, 4: 1},
#  {1: 3, 2: 1, 3: 2, 4: 4},
#  {1: 3, 2: 1, 3: 4, 4: 2},
#  {1: 3, 2: 2, 3: 4, 4: 1},
#  {1: 3, 2: 4, 3: 2, 4: 1}]

This generates n! solutions.

The "brute force" approach using a Cartesian product over the lists, produces prod(n_k) = n_1 * n_1 * ... * n_k solutions (with n_k the length of each list). In the worst case scenario (maximum density) this is n ** n solutions, which is asymptotically much worse than the factorial. In the best case scenario (minimum density) this is 1 solution only. In general, this can be either slower or faster than the "permutation solution" proposed above, depending on the "sparsity" of the lists.

For an average n_k of approx. n / 2, n! is smaller/faster for n >= 6.

For an average n_k of approx. n * (3 / 4), n! is smaller/faster for n >= 4.

In this example there are 4! == 4 * 3 * 2 * 1 == 24 permutation solutions, and 3 * 4 * 2 * 4 == 96 Cartesian product solutions.

norok2
  • 25,683
  • 4
  • 73
  • 99
0

Here is a solution using the same syntax as the code you provided.

The trick here was to order the tiles first by price ascending (the question asked for non-descending) then by height descending such that the tallest tile of the next lowest price in the back row would be matched the tallest tile of the next lowest price in the front row. To do this sorting I utilized Python's sorted() function. See a Stack Overflow example here.

I assumed if there was no such match then immediately break and print according to the problem you linked.

As a side note, you had originally claimed that a python dictionary {3: 4, 2: 2, 4:1, 1: 3} was equivalent to {1: 3, 2: 2, 3: 4, 4: 1}. While you are correct, you must remember that in Python dictionary objects are unsorted by default so it is not easy to compare keys this way.

import sys

n_tiles_row = int(sys.stdin.readline().rstrip())
# print(n_tiles_row) ==> 4

# BACK ROW - JOAO
back_row_price = sys.stdin.readline().rstrip()
# print(back_row_price) ==> 3 2 1 2

back_row_height = sys.stdin.readline().rstrip()
# print(back_row_height) ==> 2 3 4 3

# FRONT ROW - MARIA
front_row_price = sys.stdin.readline().rstrip()
# print(front_row_price) ==> 2 1 2 1

front_row_height = sys.stdin.readline().rstrip()
# print(front_row_height) ==> 2 2 1 3

# preprocess data into lists of ints
back_row_price = [int(x) for x in back_row_price.strip().split(' ')]
back_row_height = [int(x) for x in back_row_height.strip().split(' ')]
front_row_price = [int(x) for x in front_row_price.strip().split(' ')]
front_row_height = [int(x) for x in front_row_height.strip().split(' ')]

# store each tile into lists of tuples
front = list()
back = list()
for i in range(n_tiles_row):
    back.append((i, back_row_price[i], back_row_height[i]))  # tuples of (tile_num, price, height)
    front.append((i, front_row_price[i], front_row_height[i]))

# sort tiles by price first (as the price must be non-descending) then by height descending
back = sorted(back, key=lambda x: (x[1], -x[2]))
front = sorted(front, key=lambda x: (x[1], -x[2]))

# print(back) ==> [(2, 1, 4), (1, 2, 3), (3, 2, 3), (0, 3, 2)]
# print(front) ==> [(3, 1, 3), (1, 1, 2), (0, 2, 2), (2, 2, 1)]

possible_back_tile_order = list()
possible_front_tile_order = list()
for i in range(n_tiles_row):
    if back[i][2] > front[i][2]:  # if next lowest priced back tile is taller than next lowest priced front tile
        possible_back_tile_order.append(back[i][0])
        possible_front_tile_order.append(front[i][0])
    else:
        break

if len(possible_back_tile_order) < n_tiles_row:  # check that all tiles had matching pairs in back and front
    print("impossible")
else:
    print(possible_back_tile_order)
    print(possible_front_tile_order) 
matt123788
  • 419
  • 5
  • 16
  • "in Python dictionary objects are unsorted by default" -> "in Python versions prior to 3.7 ..." (actually 3.6, but it was a guaranteed feature only in 3.7). – norok2 Oct 31 '19 at 01:06
  • A fine solution! However, Kattis does not see this solution as a valid answer. – Jaoa Oct 31 '19 at 01:12
  • @Jaoa why is this answer incorrect on Kattis? I print the solution as a list object rather than a string so maybe that is the issue. – matt123788 Oct 31 '19 at 03:02
  • @Jaoa change the last two lines to `print(' '.join([str(x+1) for x in possible_back_tile_order])) print(' '.join([str(x+1) for x in possible_front_tile_order]))` – matt123788 Oct 31 '19 at 03:17
  • @matt123788 I am not quite sure why it is incorrect. It passed the first few tests but fails later. There must be some issue with the code that it is not catching some of the cases. I am too wondering what the problem is. I am wondering if it's the for loop ```for i in range(n_tiles_row):``` that it only runs for 4 times in our case but may require more in some cases. I am not quite sure at the moment but I will continue investigating. – Jaoa Oct 31 '19 at 03:30
  • @matt123788 Also could you please explain how ```key=lambda x: (x[1], -x[2])``` works? Thank you! – Jaoa Oct 31 '19 at 03:33