Дано число, найдите последовательность, чтобы добраться до него

Я работаю над этой задачей:

A bot can do either of the 2 instructions:

  • [F]orward: move ahead by +1 and double its speed
  • [B]ack: Stop, change direction and reset its speed.

The initial speed is 1.

Given a distance, return a sequence that will satisfy it. The sequence can only have a maximum of 3 'B's. If a solution is not possible with 3 'B's then return empty string.

For example,

findSeq(2) -> FFBF (1+2-1)
findSeq(3) -> FF (1+2)
findSeq(4) -> FFFBFF (1+2+4 - (1+2))

Вот что у меня есть до сих пор:

def findSeq(dist):
    curr = 0
    seq = ""
    speed = 1
    mul = 1
    
    while (curr != dist):
        seq += 'F'
        curr += (speed*mul)
        speed *= 2
        
        if curr > dist and mul != -1:
            seq += 'B'
            mul = -1
            speed = 1

        elif curr < dist:
            mul = 1
        
        if curr == dist:
            return seq

        
        if seq.count('B') == 3 and curr != dist:
            return ''
    
    return seq

Это работает для findSeq(2), findSeq(3) и findSeq(4). Однако он начинает ломаться для findSeq(5) -> (дает FFFBFFFBFF)

Я не уверен, что мне не хватает.

Задача аналогична решению dist = A - B + C - D, где ABCD — числа из множества {0, 1, 3, 7, 15, ...}. Например, если dist = 5 решение 7 - 1 + 0 - 1, что переводится как FFFBFBBF

user3386109 18.03.2022 21:42

Должен ли быть [F]orward: move ahead by +1 and double its speed вместо [F]orward: move ahead by +speed and then double its speed?

kcsquared 18.03.2022 21:45

Вроде должно быть [F]orward: move in the current direction by the current speed, and then double the speed и [B]ack: don't move, reverse the current direction, set the speed to 1.

user3386109 18.03.2022 21:49

Я предполагаю: 1) бот начинается в начале строки; 2. бот должен переместиться в заданную точку на линии, представленной целым числом; 3) бот всегда перемещается за одну единицу времени, в которой выражается скорость (например, если бы скорость равнялась одному миллиметру в секунду, каждое движение длилось бы одну секунду, первое покрывало бы один миллиметр, следующее покрытие двух мм и т. д.); а движение «назад» заставляет бота изменить направление. Верный? Я предлагаю вам отредактировать, чтобы уточнить, особенно то, что под «продвинуться вперед на +1» вы подразумеваете «продвинуться вперед на одну единицу времени»...

Cary Swoveland 19.03.2022 01:26

... Фактически, указание единицы времени (например, «секунды») прояснит вопрос, не уменьшая его общности.

Cary Swoveland 19.03.2022 01:29
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
3
5
155
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Проблема с вашим кодом была во втором повороте. Обратите внимание, что когда curr < dist вы меняете направление, но не сбрасываете скорость и не добавляете «B» к последовательности. Изменение

elif curr < dist:
    mul = 1

к

elif curr < dist and mul == -1:
    seq += 'B'
    mul = 1
    speed = 1

Должно решить вашу проблему. Кроме того, вы заявили, что путь может иметь не более 3 'B', поэтому вам нужно изменить

if seq.count('B') == 3 and curr != dist:
    return ''

к

if seq.count('B') == 4 and curr != dist:
    return ''

Алгоритм анализа

Небольшое представление о том, на какие расстояния может достигать эта функция (ограничено всего тремя поворотами). Когда dist можно записать как 2**n - 1 для любого целого числа больше 0, эта функция достигнет dist без каких-либо поворотов, и путь будет 'F' * n. Это потому, что двоичное представление 2**n - 1 представляет собой последовательность n '1'. Примеры:

  1. 7 = 2**3-1 = 0b111 --> findSeq(0b111) = 'FFF'
  2. 15 = 2**4-1 = 0b1111 --> findSeq(0b1111) = 'FFFF'
  3. 31 = 2**5-1 = 0b11111 --> findSeq(0b11111) = 'FFFFF'

и т.д

Чтобы достичь числа 5, которое имеет двоичное представление 0b101, функция сделает следующее:

  1. достичь 7
  2. повернуться, вернуться к 4 (что является степенью 2, 4 = 0b100)
  3. развернуться, перейти на 5
print(findSeq(5))  # 'FFFBFFBF'

Это означает, что ваш алгоритм работает следующим образом: начиная с самого старшего бита (MSB), который является самым левым битом, для любого изменения с 1 на 0 вам нужно будет использовать 1 ход, а для другого изменения с 0 на 1 вам нужно будет использовать еще один ход. Это происходит потому, что по правилам движения, изменения от 1 до 0 в двоичном представлении, функция сбрасывается до наибольшего числа, которое меньше dist и имеет двоичное представление из s последовательности единиц, за которыми следует последовательность нулей (например, 4 = 0b100 имеет 1 «1», за которым следует 2 «0»).

Это означает, что с помощью вашего алгоритма вы можете найти путь к очень большим числам, но для некоторых малых достижимых чисел путь не будет найден. Взяв очень большое число 895, представленное 0b1101111111. Вы можете видеть, что есть только одно изменение с «1» на «0» (биты 8 и 7, когда младший бит находится в ячейке 0), функция сможет найти путь:

  1. достигнув 1023 (0b1111111111)
  2. вернуться к 768 (0b1100000000), имеет 2 '1', за которыми следуют 8 '0'
  3. перейти к 895 (0b1101111111)
print(findSeq(0b1101111111))  # 'FFFFFFFFFFBFFFFFFFFBFFFFFFF'
print(findSeq(895))  # 'FFFFFFFFFFBFFFFFFFFBFFFFFFF'

Изменение dist на 892 приведет к дополнительному ходу:

  1. вернуться к 892 (0b1101111100)
print(findSeq(0b1101111100))  # 'FFFFFFFFFFBFFFFFFFFBFFFFFFFBFF'
print(findSeq(892))  # 'FFFFFFFFFFBFFFFFFFFBFFFFFFFBFF'

Но для относительно небольших чисел, таких как 21, которое представлено 0b10101, функция не найдет путь, хотя указан правильный путь («FFFFBFBFFF»):

  1. Продвиньтесь на 4 шага до 15
  2. Обернитесь, вернитесь на 1 шаг к 14
  3. Снова повернитесь, сделайте 3 шага до 14 + 7 = 21.

Предложение решения

Это хороший пример задачи поиска по графу. В задаче поиска графа вам сначала нужно определить состояния. Состояния в этой задаче представляют собой кортежи, где каждый кортеж состоит из последовательности и текущего местоположения, то есть (seq, current_loc). После определения состояний любой алгоритм поиска по графу может решить вашу проблему, где двумя наиболее известными решениями будут BFS и DFS. Так как я хотел получить путь с кратчайшим количеством шагов, я реализовал BFS. Дополнительную информацию см. здесь.

Решение BFS выглядит следующим образом:

import math
import collections
def findSeq(dist):
    state_queue = collections.deque([])  # Pending states which have not been explored yet
    upper_lim   = 2**math.ceil(math.log2(dist))
    state = ['', 0]  # Starting state ; state is made out of the path at location [0], the 'B' count at location [1] and the distance at location [2]
    found = True
    while state[1] != dist:
        # adding states to the queue according to conditions:
        steps   = len(state[0].split('B')[-1])
        b_count = state[0].count('B')
        if 0 <= state[1] < upper_lim:  # advancing makes sense
            state_1 = [state[0] + 'F', state[1] + ((-1)**b_count) * 2**steps]
            state_queue.append(state_1)
        if b_count < 3 and state[1] >= 0:
            state_2 = [state[0] + 'B', state[1]]
            state_queue.append(state_2)
        try:
            state = state_queue.popleft()
        except IndexError:
            found = False
            break
    if found:
        return state[0]
    else:
        return ''

Обратите внимание, что это не оптимально, так как состояния в графе могут быть определены более эффективно, но это решение для всех случаев. Кроме того, граф можно представить в виде дерева, что означает, что вы не будете возвращаться к одному и тому же состоянию дважды (в текущем определении состояния), поэтому вам не нужно сохранять набор посещенных состояний.

Что касается вашего последнего предложения о 21, вы говорите, что текущий код неверен, потому что он не находит путь, или что 21 недоступен? Потому что FFFFBFBFFF = 15 -1 + 7 должно получить 21.

kcsquared 19.03.2022 05:47

Да, я имел в виду, что код был неверным, так как он не находит путь к достижимым расстояниям, например, 21, я это отредактирую. Спасибо за отзыв

Tomer Geva 19.03.2022 06:17

В качестве дополнения я думаю, что это означает, что в предложении This means that when starting from the Most Significant Bit (MSB) which is the leftmost-bit, for any change from 1 to 0, you will need to use 1 turn, and for another change from 0 to 1 you will need to use another turn также есть опечатка, поскольку b10101 имеет 4 изменения цифр, но нужно всего 2 поворота.

kcsquared 19.03.2022 06:33

Да, я добавил туда уточнение, что это верно только при использовании предложенного алгоритма, спасибо

Tomer Geva 19.03.2022 06:38
Ответ принят как подходящий

Несколько замечаний: последовательность из n F команд перемещает поезд на 2 ^ n - 1 в текущем направлении. Без ограничения общности все решения имеют вид F^k1 B F^k2 B F^k3 B F^k4, где k1, k2, k3, k4 >= 0, потому что, если мы не будем двигать поезд, мы можем просто положить некоторые из k равны 0.

Это дает переформулировку задачи: для данного n найти неотрицательные k1, k2, k3, k4 такие, что (2^k1 - 1) - (2^k2 - 1) + (2^k3 - 1) - (2 ^к4 — 1) = п.

Единицы в сумме сокращаются, поэтому вам нужно найти 2 ^ k1 + 2 ^ k3 - 2 ^ k2 - 2 ^ k4 = n.

Если n имеет N битов, то без ограничения общности каждое из четырех чисел может быть не более чем N+1.

Я уверен, что есть более умный подход, но просто поиск (N + 2) ^ 4 возможностей дает решение O ((log n) ^ 4).

import itertools

def findSeq(n):
    N = max(1, n.bit_length())
    for k1, k2, k3, k4 in itertools.product(tuple(range(N+2)), repeat=4):
        if (1<<k1) - (1<<k2) + (1<<k3) - (1<<k4) == n:
            return 'B'.join('F' * x for x in (k1, k2, k3, k4))
    return ''

for i in range(100):
    print(i, findSeq(i))

@MattTimmermans предлагает улучшение O ((logn) ^ 2): перечисление всех чисел в форме q = 2 ^ k1 + 2 ^ k3, а затем проверка того, имеет ли qn ту же форму.

def findSeq2(n):
    N = max(1, n.bit_length())
    b2 = {(1<<k1)+(1<<k2): (k1, k2) for k1, k2 in itertools.product(tuple(range(N+2)), repeat=2)}
    for k1, k3 in b2.values():
        r = (1 << k1) + (1 << k3) - n
        if r in b2:
            k2, k4 = b2[r]
            return 'B'.join('F' * x for x in (k1, k2, k3, k4))      
    return ''

Решение O (log ^ 2 n): найдите все числа в форме q = 2 ^ k1 + 2 ^ k2 и посмотрите, имеет ли qn ту же форму, что легко проверить за постоянное время.

Matt Timmermans 19.03.2022 13:53

Я предлагаю алгоритм, который выводит решение прямо из заданного числа, без поиска.

Наблюдения

Как уже отмечали другие, это сводится к нахождению терминов в форме (2a - 1) - (2b - 1) + (2c - 1) - (2d - 1).

Кроме того, обратите внимание, что когда у вас есть решение, вы всегда можете добавить немного «B» в конце, что не изменит отображаемое расстояние. Таким образом, мы можем сказать, что нужны ровно 3 буквы «Б» и нужны все четыре термина (приведенные выше). Это также означает, что мы можем исключить -1 в каждом члене, так как они компенсируют друг друга.

Когда мы смотрим на двоичное представление этих терминов, у нас должно быть что-то вроде этого:

0b1000000 - 0b100 + 0b10000 - 0b10

... где количество конечных нулей в этих числах является переменным.

Играя с этими терминами, становится ясно, что невозможно сгенерировать значение, которое - в его двоичном представлении - имеет 4 группы смежных 1-цифр (или более). Например: 0b1010101 нельзя закодировать с помощью инструкций «F» и трех «B».

Случай 1: двоичное представление имеет одну группу 1-битов

Когда есть только одна соседняя группа 1-цифр, это легко.

Например, 0b11100 можно получить с помощью FFFFFBFF. Первая группа «F» — это до тех пор, пока есть цифры, а вторая группа «F» — до тех пор, пока есть конечные нули.

Случай 2: двоичное представление имеет две группы единичных битов.

Когда есть две группы 1-цифр, это тоже довольно просто:

Например, 0b11001110 можно получить с помощью FFFFFFFFBFFFFFFBFFFFBF. Обратите внимание, что каждая группа «F» короче предыдущей и представляет собой количество двоичных цифр, которые остаются, когда группа одинаковых цифр удаляется с левой стороны. Так:

  • 11001110 переводится как 8xF,
  • от 001110 до 6xF,
  • 1110 до 4xF и
  • от 0 до 1xF.

Случай 3: двоичное представление имеет три группы единичных битов.

Случай, когда есть три группы 1-цифр, является самым сложным. При более глубоком анализе оказывается, что по крайней мере одна разделяющая группа 0 (которая разделяет две группы единиц) должна иметь длину 1 (т. е. только один разделяющий ноль). Так, например, нет решения для 0b1001001, потому что обе группы нулей имеют размер 2. Во-вторых, оставшаяся группа цифр не смежный для этого единственного 0 также должна быть одинарной.

Итак: 0b1010011 тоже не имеет решения, потому что правая группа (которая не является смежной с одиночным заключенным 0) состоит из двух единиц. Но у 0b11011001 есть решение, потому что есть изолированный 0 (между единицами), а другая оставшаяся группа единиц тоже одна (справа).

Реализация

С этими правилами можно иметь быстрый алгоритм, поскольку он на самом деле не поиск для решения, а выводит из двоичного представления данного числа:

import re

def encode(position):
    binary = bin(position)[2:]
    # Split in sizes of same digits. First group will consist of 1s
    sizes = list(map(len, re.findall(r"1+|0+", binary)))
    if len(sizes) > 6:
        return ""  # Not possible
    # Create sequences of F, corresponding 
    #    to the number of binary digits that follow
    digits = ["F" * sizes[-1]]
    for size in reversed(sizes[:-1]):
        digits.append(digits[-1] + "F" * size)
    digits.reverse()
    # Simple cases:
    if len(sizes) <= 4:
        return "B".join(digits)
    # The case where there are 3 groups of 1s in the binary representation:
    digits.append("")  # Make sure that digits[5] is defined
    if sizes[0] == 1 and sizes[3] == 1:  # The isolated single zero is at group 3
        return "B".join((digits[1], digits[4], digits[2], digits[5]))
    if sizes[4] == 1 and sizes[1] == 1:  # The isolated single zero is at group 1
        return "B".join((digits[0], digits[2], digits[5], digits[3]))
    return ""  # Not possible.

Некоторый код драйвера:

# Helper function to verify a solution
def decode(code):
    # Naive implementation according to rules
    position = 0
    direction = 1
    speed = 1
    for ch in code:
        if ch == "B":
            direction = -direction
            speed = 1
        else:
            position += speed * direction
            speed *= 2
    return position


for i in range(1, 73):
    code = encode(i)
    decoded = decode(code)
    print(i, code, decoded)

Первое натуральное число, для которого нет решения, — это 73, имеющее двоичное представление 0b1001001.

Это мое любимое решение; Я думаю, что можно доказать наблюдения (не более 2 групп последовательных единиц или 3 группы последовательных единиц, по крайней мере, с одним внутренним пробелом, являющимся одним нулем и несмежным одиночным) посредством анализа возможных битовых шаблонов при вычитании числа вида (2^b + 2^d) из (2^a + 2^c).

kcsquared 20.03.2022 06:26

Другие вопросы по теме