Одна игровая Проблема буквы?

Недавно при собеседовании мне дали следующую проблему:

  1. Запишите сценарий, способный к работе командной строки как Python

  2. Это должно взять в двух словах на командной строке (или дополнительно если Вы предпочли бы, чтобы это могло запросить пользователя для предоставления этих двух слов через консоль).

  3. Учитывая те два слова: a. Удостоверьтесь, что они имеют равную длину b. Удостоверьтесь, что они - оба слова, существующие в словаре допустимых слов на английском языке, который Вы загрузили.

  4. Раз так вычислите, можно ли достигнуть второго слова сначала серией шагов следующим образом a. Можно изменить одну букву за один раз b. Каждый раз, когда Вы изменяете букву, получающееся слово должно также существовать в словаре c. Вы не можете добавить или удалить буквы

  5. Если эти два слова достижимы, сценарий должен распечатать путь, который ведет как единственный, кратчайший путь от одного слова до другого.

  6. Вы можете/usr/share/dict/words для своего словаря слов.

Мое решение состояло из использования поиска в ширину для нахождения кратчайшего пути между двумя словами. Но по-видимому который не был достаточно хорош для получения задания :(

Парни знают то, что я, возможно, сделал неправильно? Огромное спасибо.

import collections
import functools
import re

def time_func(func):
    import time

    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        timed = time.time() - start

        setattr(wrapper, 'time_taken', timed)
        return res

    functools.update_wrapper(wrapper, func)
    return wrapper

class OneLetterGame:
    def __init__(self, dict_path):
        self.dict_path = dict_path
        self.words = set()

    def run(self, start_word, end_word):
        '''Runs the one letter game with the given start and end words.
        '''
        assert len(start_word) == len(end_word), \
            'Start word and end word must of the same length.'

        self.read_dict(len(start_word))

        path = self.shortest_path(start_word, end_word)
        if not path:
            print 'There is no path between %s and %s (took %.2f sec.)' % (
                start_word, end_word, find_shortest_path.time_taken)
        else:
            print 'The shortest path (found in %.2f sec.) is:\n=> %s' % (
                self.shortest_path.time_taken, ' -- '.join(path))

    def _bfs(self, start):
        '''Implementation of breadth first search as a generator.

        The portion of the graph to explore is given on demand using get_neighboors.
        Care was taken so that a vertex / node is explored only once.
        '''
        queue = collections.deque([(None, start)])
        inqueue = set([start])

        while queue:
            parent, node = queue.popleft()
            yield parent, node

            new = set(self.get_neighbours(node)) - inqueue
            inqueue = inqueue | new
            queue.extend([(node, child) for child in new])

    @time_func
    def shortest_path(self, start, end):
        '''Returns the shortest path from start to end using bfs.
        '''
        assert start in self.words, 'Start word not in dictionnary.'
        assert end in self.words, 'End word not in dictionnary.'

        paths = {None: []}
        for parent, child in self._bfs(start):
            paths[child] = paths[parent] + [child]
            if child == end:
                return paths[child]
        return None

    def get_neighbours(self, word):
        '''Gets every word one letter away from the a given word.

        We do not keep these words in memory because bfs accesses 
        a given vertex only once.
        '''
        neighbours = []

        p_word = ['^' + word[0:i] + '\w' + word[i+1:] + '$' 
            for i, w in enumerate(word)]
        p_word = '|'.join(p_word)

        for w in self.words:
            if w != word and re.match(p_word, w, re.I|re.U):
                neighbours += [w]
        return neighbours

    def read_dict(self, size):
        '''Loads every word of a specific size from the dictionnary into memory.
        '''
        for l in open(self.dict_path):
            l = l.decode('latin-1').strip().lower()
            if len(l) == size:
                self.words.add(l)

if __name__ == '__main__':
    import sys
    if len(sys.argv) not in [3, 4]:
        print 'Usage: python one_letter_game.py start_word end_word'
    else:
        g = OneLetterGame(dict_path = '/usr/share/dict/words')
        try:
            g.run(*sys.argv[1:])
        except AssertionError, e:
            print e

Спасибо за все большие ответы. Я думаю, что действительно получило меня, то, что я действительно выполняю итерации по ВСЕМ словам в словаре каждый раз для рассмотрения возможных соседей слова. Вместо этого я, возможно, использовал инвертированный индекс, как указано Duncan и Matt Anderson.* подход определенно помог бы также. Большое спасибо теперь я знаю то, что я сделал неправильно.

Вот тот же код с инвертированным индексом:

import collections
import functools
import re

def time_func(func):
    import time

    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        timed = time.time() - start

        setattr(wrapper, 'time_taken', timed)
        return res

    functools.update_wrapper(wrapper, func)
    return wrapper

class OneLetterGame:
    def __init__(self, dict_path):
        self.dict_path = dict_path
        self.words = {}

    def run(self, start_word, end_word):
        '''Runs the one letter game with the given start and end words.
        '''
        assert len(start_word) == len(end_word), \
            'Start word and end word must of the same length.'

        self.read_dict(len(start_word))

        path = self.shortest_path(start_word, end_word)
        if not path:
            print 'There is no path between %s and %s (took %.2f sec.)' % (
                start_word, end_word, self.shortest_path.time_taken)
        else:
            print 'The shortest path (found in %.2f sec.) is:\n=> %s' % (
                self.shortest_path.time_taken, ' -- '.join(path))

    def _bfs(self, start):
        '''Implementation of breadth first search as a generator.

        The portion of the graph to explore is given on demand using get_neighboors.
        Care was taken so that a vertex / node is explored only once.
        '''
        queue = collections.deque([(None, start)])
        inqueue = set([start])

        while queue:
            parent, node = queue.popleft()
            yield parent, node

            new = set(self.get_neighbours(node)) - inqueue
            inqueue = inqueue | new
            queue.extend([(node, child) for child in new])

    @time_func
    def shortest_path(self, start, end):
        '''Returns the shortest path from start to end using bfs.
        '''
        assert self.in_dictionnary(start), 'Start word not in dictionnary.'
        assert self.in_dictionnary(end), 'End word not in dictionnary.'

        paths = {None: []}
        for parent, child in self._bfs(start):
            paths[child] = paths[parent] + [child]
            if child == end:
                return paths[child]
        return None

    def in_dictionnary(self, word):
        for s in self.get_steps(word):
            if s in self.words:
                return True
        return False

    def get_neighbours(self, word):
        '''Gets every word one letter away from the a given word.
        '''
        for step in self.get_steps(word):
            for neighbour in self.words[step]:
                yield neighbour

    def get_steps(self, word):
        return (word[0:i] + '*' + word[i+1:] 
            for i, w in enumerate(word))

    def read_dict(self, size):
        '''Loads every word of a specific size from the dictionnary into an inverted index.
        '''
        for w in open(self.dict_path):
            w = w.decode('latin-1').strip().lower()
            if len(w) != size:
                continue
            for step in self.get_steps(w):
                if step not in self.words:
                    self.words[step] = [] 
                self.words[step].append(w)

if __name__ == '__main__':
    import sys
    if len(sys.argv) not in [3, 4]:
        print 'Usage: python one_letter_game.py start_word end_word'
    else:
        g = OneLetterGame(dict_path = '/usr/share/dict/words')
        try:
            g.run(*sys.argv[1:])
        except AssertionError, e:
            print e

И сравнение синхронизации:

% Python one_letter_game_old.py счастливый привет кратчайший путь (найденный за 91,57 секунды.):
=> счастливый - гарпия - арфы - олени - остановы - залы - ад - привет

% Python one_letter_game.py счастливый привет кратчайший путь (найденный за 1,71 секунды.):
=> счастливый - гарпия - арфы - олени - остановы - залы - ад - привет

8
задан Nitesh 1 May 2012 в 07:43
поделиться

5 ответов

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

  1. Поиск в ширину будет посещать все пути длиной на единицу короче, чем требуется, плюс все пути необходимой длины, прежде чем он сможет дать вам ответ. Поиск лучшего первого (A *) в идеале пропустит самые нерелевантные пути.

  2. Вы проверяете каждое слово в словаре на предмет кандидатуры в качестве соседа каждый раз, когда ищете соседей. Как предлагает Дункан, вы можете построить структуру данных, чтобы искать соседей, а не искать их.

Вот еще одно решение вашей проблемы:

import collections
import heapq
import time

def distance(start, end):
    steps = 0
    for pos in range(len(start)):
        if start[pos] != end[pos]:
            steps += 1
    return steps


class SearchHeap(object):
    def __init__(self):
        self.on_heap = set()
        self.heap = []

    def push(self, distance, word, path):
        if word in self.on_heap:
            return
        self.on_heap.add(word)
        heapq.heappush(self.heap, ((distance, len(path)), word, path))

    def __len__(self):
        return len(self.heap)

    def pop(self):
        return heapq.heappop(self.heap)


class OneLetterGame(object):
    _word_data = None

    def __init__(self, dict_path):
        self.dict_path = dict_path

    def run(self, start_word, end_word):
        start_time = time.time()
        self._word_data = collections.defaultdict(list)
        if len(start_word) != len(end_word):
            print 'words of different length; no path'
            return

        found_start, found_end = self._load_words(start_word, end_word)
        if not found_start:
            print 'start word %r not found in dictionary' % start_word
            return
        if not found_end:
            print 'end word %r not found in dictionary' % end_word
            return

        search_start_time = time.time()
        path = self._shortest_path(start_word, end_word)
        search_time = time.time() - search_start_time
        print 'search time was %.4f seconds' % search_time

        if path:
            print path
        else:
            print 'no path found from %r to %r' % (start_word, end_word)

        run_time = time.time() - start_time
        print 'total run time was %.4f seconds' % run_time

    def _load_words(self, start_word, end_word):
        found_start, found_end = False, False
        length = len(start_word)
        with open(self.dict_path) as words:
            for word in words:
                word = word.strip()
                if len(word) == length:
                    if start_word == word: found_start = True
                    if end_word == word: found_end = True
                    for bucket in self._buckets_for(word):
                        self._word_data[bucket].append(word)
        return found_start, found_end

    def _shortest_path(self, start_word, end_word):
        heap = SearchHeap()
        heap.push(distance(start_word, end_word), start_word, (start_word,))
        while len(heap):
            dist, word, path = heap.pop()
            if word == end_word:
                return path
            for neighbor in self._neighbors_of(word):
                heap.push(
                    distance(neighbor, end_word), 
                    neighbor, 
                    path + (neighbor,))
        return ()

    def _buckets_for(self, word):
        buckets = []
        for pos in range(len(word)):
            front, back = word[:pos], word[pos+1:]
            buckets.append(front+'*'+back)
        return buckets

    def _neighbors_of(self, word):
        for bucket in self._buckets_for(word):
            for word in self._word_data[bucket]:
                yield word

if __name__ == '__main__':
    import sys
    if len(sys.argv) not in [3, 4]:
        print 'Usage: python one_letter_game.py start_word end_word'
    else:
        matt = OneLetterGame(dict_path = '/usr/share/dict/words')
        matt.run(*sys.argv[1:])

И сравнение времени:

% python /tmp/one_letter_alex.py canoe happy
The shortest path (found in 51.98 sec.) is:
=> canoe -- canon -- caxon -- taxon -- taxor -- taxer -- taper -- paper -- papey -- pappy -- happy

% python /tmp/one_letter_matt.py canoe happy
search time was 0.0020 seconds
('canoe', 'canon', 'caxon', 'taxon', 'taxor', 'taxer', 'taper', 'paper', 'papey', 'pappy', 'happy')
total run time was 0.2416 seconds
10
ответ дан 5 December 2019 в 11:23
поделиться

Может быть, они ожидали поиска A * с расстоянием редактирования в качестве оценки?

1
ответ дан 5 December 2019 в 11:23
поделиться

Я согласен, что было бы странно ожидать, что ваш ответ на этот тест по программированию станет единственной причиной, по которой они выбрали кого-то другого, но на самом деле с вашим кодом есть проблемы. Вы выполняете линейный поиск по словарю для каждого шага пути или каждого потенциального пути. Это может занять много времени для большого словаря и множества потенциальных путей. Также это довольно очевидно вы не тестировали его тщательно, поскольку он не работает, когда нет пути.

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

words = {}

def get_keys(word):
    """Generate keys from a word by replacing each letter in turn by an asterisk"""
    for i in range(len(word)):
        yield word[:i]+'*'+word[i+1:]

def get_steps(word):
    """Find the set of all words that can link to the given word."""
    steps = []
    for key in get_keys(word):
        steps.extend(words[key])
    steps = set(steps) - set([word])
    return steps

# Load the dictionary
for word in ('start', 'stare', 'spare', 'spore'):
    for key in get_keys(word):
        if key not in words:
            words[key] = []
        words[key].append(word)

print(words)
print(get_steps('stare'))
3
ответ дан 5 December 2019 в 11:23
поделиться

Возможно, вы не хотели работать в такой придурочной компании. Я лично не верю в обзоры кода. Я думаю, что если вы достаточно хорошо справляетесь с проверкой портфолио и прошлых рекомендаций, то нет необходимости в таких проверках кода на месте. Компании с такой жесткой политикой - это те компании, которые никогда не добьются успеха, потому что все, кто в них работает - это задроты, которые думают о коде 24 часа в сутки 7 дней в неделю. Просто мои 2 цента.

1
ответ дан 5 December 2019 в 11:23
поделиться

Может быть, вы забыли добавить шебанг? >-|

А может, им просто не понравился ваш стиль кодирования... Например, я бы не стал создавать класс для такой простой задачи, это чрезмерная инженерия решения (хотя, конечно, я не настолько придирчив, чтобы основывать решение о найме только на этом).

0
ответ дан 5 December 2019 в 11:23
поделиться
Другие вопросы по тегам:

Похожие вопросы: