Search code examples
pythoncombinationsiterablehamming-distance

How to generates a list which elements are at a fix distance from a desired list


I have a list of possibilities and a desired input:

possibles = [20, 30, 40, 50, 60, 70, 80, 100]
desired = [20, 30, 40]

I want to generate the close by lists. Example:

# Distance of 1 (i.e. 1 element changes to a close-by)
[30, 30, 40]
[20, 40, 40]
[20, 30, 30]
[20, 30, 50]

# Distance of 2:
[40, 30, 40]
[30, 30, 50]
[30, 40, 40]
...

My current version is only varying one element at a time, thus, as soon as the distance is above 1, I'm missing a lot of combination.

def generate_close_by(possibles, desired):
    for k in range(1, 4):
        for i, elt in enumerate(desired):
            id = possibles.index(elt)

            new = desired[:]
            if id < len(possibles)-k-1:
                new[i] = possibles[id+k]
                yield (new)

            if id > k:
                new[i] = possibles[id-k]
                yield (new)

# Output
[30, 30, 40]
[20, 40, 40]
[20, 30, 50]
[20, 30, 30]
[40, 30, 40]
[20, 50, 40]
[20, 30, 60]
[50, 30, 40]
[20, 60, 40]
[20, 30, 70]

I'm quite sure a module should already exist to do this kind of iteration (itertools?), could you point me to the write function?

Thanks.

EDIT:

Update on the tries...

I'm trying to generate a list the same size of desired in which each element correspond to how much I have to move the element of desired.

desired = [20, 30, 40]
# Distance of 1:
distance = [1, 0, 0]
distance = [0, 1, 0]
distance = [0, 0, 1]
distance = [-1, 0, 0]
distance = [0, -1, 0]
distance = [0, 0, -1]

And then plan was to try to create the new list, and if it can't (out of bounds), it just continues. Not working yet, but might be a good approach.


Solution

  • I guess I will show a more long winded approach that can be more easily generalizable.

    I first write down the problem.

    possible_pts = [20, 30, 40, 50, 60, 70, 80, 100]
    starting_pt_in_idx = [0, 1, 2]
    distance = 2
    

    There are 3 axes that can "change". I first find the combinations of axis changes.

    N = len(starting_pt_in_idx)
    axis = list(range(N))
    
    import itertools
    axismoves = list(itertools.combinations_with_replacement(axis, distance))
    print(axismoves)
    

    Next, we bin it. For example, if I see axis-0 appearing twice, it becomes [2,0,0].

    abs_movements = []
    for combi in axismoves:
        move_bin = [0] * N
        for i in combi:
            move_bin[i] += 1
        abs_movements.append(move_bin)
    print(abs_movements)
    

    The above gave the absolute movements. To find the actual movement, we must take into consideration the change can be positive or negative along that axis.

    import copy
    actual_movements = []
    for movement in abs_movements:
        actual_movements.append(movement)
        for i, move in enumerate(movement):
            if move != 0:
                _movement = copy.deepcopy(movement)
                _movement[i] = - move
                actual_movements.append(_movement)
    print(actual_movements)
    

    The final step is to translate the index into actual positions. So first we write this helper function.

    def translate_idx_to_pos(idx_vect, points):
        idx_bounds = [0, len(points) - 1]
        pos_point = [0] * len(idx_vect)
        for i, idx_pos in enumerate(idx_vect):
            if idx_pos < idx_bounds[0] or idx_pos > idx_bounds[1]:
                return None
            else:
                pos_point[i] = points[idx_pos]
        return pos_point
    

    Using the actual movements to act on the starting point index, then translating it back to the positions.

    from operator import add
    final_pts = []
    for movement in actual_movements:
        final_pt_in_idx = list(map(add, starting_pt_in_idx, movement))
        final_point = translate_idx_to_pos(final_pt_in_idx, possible_pts)
        if final_point is not None:
            final_pts.append(final_point)
    
    print(final_pts)
    

    This gives

    [40, 30, 40]
    [30, 40, 40]
    [30, 20, 40]
    [30, 30, 50]
    [30, 30, 30]
    [20, 50, 40]
    [20, 40, 50]
    [20, 20, 50]
    [20, 40, 30]
    [20, 30, 60]
    [20, 30, 20]