Python-функция для размещения строки в алфавитном списке перестановок ее символов

1

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

Проблема:

С учетом строки верните позицию введенной строки в отсортированном в алфавитном порядке списке всех возможных перестановок символов в этой строке. Например, перестановки ABAB - это [AABB, ABAB, ABBA, BAAB, BABA, BBAA], где позиция ABAB в этом списке равна 2.

Логика Я использовал для решения проблемы:

Для больших входов невозможно (неэффективно) генерировать список перестановок, поэтому точка должна найти позицию без генерации алфавитного списка. Это можно сделать, найдя частоту символов. Для вышеприведенного примера первый charcater в ABAB равен A, поэтому до = 0 и после =.5 и между = 6, поэтому вы уменьшаете max на 0,5 * 6, что равно 3, для minn 1 и maxx из 3, оставить только [AABB, ABAB, ABBA], пермы с A в качестве первого символа! Затем оставшиеся символы - BAB. minn = 1 и maxx = 3, а между = 3. Итак, для B раньше было бы.33, а после было бы 0, поэтому увеличьте minn на 3 *.33 для minn 2 и maxx из 3, что равно [ABAB, ABBA], которые имеют AB как первые два символа! Продолжайте делать это для каждого персонажа, и он найдет вход в списке.

Мой код:

## Imports
import operator
from collections import Counter
from math import factorial
from functools import reduce
## Main function, returns list position
def listPosition(word):
    #turns string into list of numbers, A being 1, B being 2, and so on
    val = [ord(char) - 96 for char in word.lower()]
    #the result has to be between 1 and the number of permutations
    minn = 1
    maxx = npermutations(word)
    #so we just increase the min and decrease the max based on the sum of freq
    #of the characters less than and greater than each character
    for indx in range(len(word)):
            between = (maxx+1-minn)
            before,after = sumfreq(val[indx:],val[indx])
            minn = minn + int(round((between * before),0))
            maxx = maxx - int(between * after)
    return maxx #or minn, doesn't matter. they're equal at this point

## returns the number of permutations for the string (this works)
def npermutations(word):
    num = factorial(len(word))
    mults = Counter(word).values()
    den = reduce(operator.mul, (factorial(v) for v in mults), 1)
    return int(num / den)
## returns frequency as a percent for the character in the list of chars
def frequency(val,value):
    f = [val.count(i)/len(val) for i in val]
    indx = val.index(value)
    return f[indx]
#returns sum of frequencies for all chars < (before) and > (after) the said char
def sumfreq(val,value):
    before = [frequency(val,i) for i in [i for i in set(val) if i < value]]
    after = [frequency(val,i) for i in [i for i in set(val) if i > value]]
    return sum(before),sum(after)

tests= ['A','ABAB','AAAB','BAAA','QUESTION','BOOKKEEPER','ABCABC','IMMUNOELECTROPHORETICALLY','ERATXOVFEXRCVW','GIZVEMHQWRLTBGESTZAHMHFBL']
print(listPosition(tests[0]),"should equal 1")
print(listPosition(tests[1]),"should equal 2")
print(listPosition(tests[2]),"should equal 1")
print(listPosition(tests[3]),"should equal 4")
print(listPosition(tests[4]),"should equal 24572")
print(listPosition(tests[5]),"should equal 10743")
print(listPosition(tests[6]),"should equal 13")
print(listPosition(tests[7]),"should equal 718393983731145698173")
print(listPosition(tests[8]),"should equal 1083087583") #off by one digit?
print(listPosition(tests[9]),"should equal 5587060423395426613071") #off by a lot?
  • 2
    Я предполагаю, что ошибка округления с плавающей точкой. Там есть очень большие цифры. Попробуйте использовать целочисленную арифметику, воспользовавшись поддержкой Python bignum.
  • 0
    Вы игнорируете дубликаты? AABB появится 4 раза, если вы переставляете ABAB
Показать ещё 2 комментария
Теги:
python-3.x
algorithm
permutation

2 ответа

2
Лучший ответ

Как указывает @rici, это ошибка с плавающей запятой (см. Разбивается математика с плавающей запятой?). К счастью, у python есть fractions.

Несколько разумных применений fractions.Fraction исправляет проблему, не меняя тело кода, например:

from fractions import Fraction
...
## returns the number of permutations for the string (this works)
def npermutations(word):
    num = factorial(len(word))
    mults = Counter(word).values()
    den = reduce(operator.mul, (factorial(v) for v in mults), 1)
    return int(Fraction(num, den))
## returns frequency as a percent for the character in the list of chars
def frequency(val,value):
    f = [Fraction(val.count(i),len(val)) for i in val]
    indx = val.index(value)
    return f[indx]
...

In []:
print(listPosition(tests[0]),"should equal 1")
print(listPosition(tests[1]),"should equal 2")
print(listPosition(tests[2]),"should equal 1")
print(listPosition(tests[3]),"should equal 4")
print(listPosition(tests[4]),"should equal 24572")
print(listPosition(tests[5]),"should equal 10743")
print(listPosition(tests[6]),"should equal 13")
print(listPosition(tests[7]),"should equal 718393983731145698173")
print(listPosition(tests[8]),"should equal 1083087583")
print(listPosition(tests[9]),"should equal 5587060423395426613071")

Out[]:
1 should equal 1
2 should equal 2
1 should equal 1
4 should equal 4
24572 should equal 24572
10743 should equal 10743
13 should equal 13
718393983731145698173 should equal 718393983731145698173
1083087583 should equal 1083087583
5587060423395426613071 should equal 5587060423395426613071

обновленный

Основываясь на @m69 отличное объяснение здесь намного проще:

from math import factorial
from collections import Counter
from functools import reduce
from operator import mul

def position(word):
    charset = Counter(word)
    pos = 1    # Per OP 1 index
    for letter in word:
        chars = sorted(charset)
        for char in chars[:chars.index(letter)]:
            ns = Counter(charset) - Counter([char])
            pos += factorial(sum(ns.values())) // reduce(mul, map(factorial, ns.values()))
        charset -= Counter([letter])
    return pos

Это дает те же результаты, что и выше:

In []:
tests = ['A', 'ABAB', 'AAAB', 'BAAA', 'QUESTION', 'BOOKKEEPER', 'ABCABC',
         'IMMUNOELECTROPHORETICALLY', 'ERATXOVFEXRCVW', 'GIZVEMHQWRLTBGESTZAHMHFBL']
print(position(tests[0]),"should equal 1")
print(position(tests[1]),"should equal 2")
print(position(tests[2]),"should equal 1")
print(position(tests[3]),"should equal 4")
print(position(tests[4]),"should equal 24572")
print(position(tests[5]),"should equal 10743")
print(position(tests[6]),"should equal 13")
print(position(tests[7]),"should equal 718393983731145698173")
print(position(tests[8]),"should equal 1083087583")
print(position(tests[9]),"should equal 5587060423395426613071")

Out[]:
1 should equal 1
2 should equal 2
1 should equal 1
4 should equal 4
24572 should equal 24572
10743 should equal 10743
13 should equal 13
718393983731145698173 should equal 718393983731145698173
1083087583 should equal 1083087583
5587060423395426613071 should equal 5587060423395426613071
  • 1
    Я подумал, что это как-то связано с числами с плавающей запятой, но мое решение заключалось в использовании округления, которое не помогло. Я никогда не знал, что фракции существуют в Python! Это определенно помогло мне понять, где это пошло не так.
3

Вы можете использовать логику, которая требует целочисленной арифметики. Сначала создайте лексикографически первую перестановку:

BOOKKEEPER  ->  BEEEKKOOPR

Затем для каждой буквы вы можете подсчитать, сколько уникальных перестановок потребовалось, чтобы переместить их на свое место. Поскольку первая буква B уже установлена, мы можем ее игнорировать и смотреть на остальные буквы:

B EEEKKOOPR  (first)
B OOKKEEPER  (target)

Чтобы узнать, сколько перестановок требуется для вывода O на фронт, мы вычислим, сколько уникальных перестановок есть с E впереди, затем с K впереди:

E+EEKKOOPR -> 8! / (2! * 2! * 2!) = 40320 /  8 = 5040
K+EEEKOOPR -> 8! / (3! * 2!)      = 40320 / 12 = 3360

Где 8 - количество букв, подлежащих перестановке, а 2 и 3 - количество кратных букв. Итак, после 8400 перестановок мы находимся по адресу:

BO EEEKKOPR

Теперь, опять же, мы вычислим, сколько перестановок требуется, чтобы довести второй O до фронта:

E+EEKKOPR -> 7! / (2! * 2!) = 5040 / 4 = 1260
K+EEEKOPR -> 7! / (3!)      = 5040 / 6 =  840

Итак, после 10500 перестановок мы находимся по адресу:

BOO EEEKKPR

Затем мы вычислим, сколько перестановок требуется, чтобы довести K до фронта:

E+EEKKPR -> 6! / (2! * 2!) = 720 / 4 = 180

Итак, после 10680 перестановок мы находимся по адресу:

BOOK EEEKPR

Затем мы вычислим, сколько перестановок требуется, чтобы довести второй К до фронта:

E+EEKPR -> 5! / 2! = 120 / 2 = 60

Итак, после 10740 перестановок мы находимся по адресу:

BOOKK EEEPR

Следующие две буквы уже на месте, поэтому мы можем перейти к:

BOOKKEE EPR

Затем мы вычисляем, сколько перестановок требуется, чтобы получить P спереди:

E+PR -> 2! = 2

Итак, после 10742 перестановок мы находимся по адресу:

BOOKKEEP ER

И последние две буквы тоже уже в порядке, поэтому ответ 10743 (добавьте 1, потому что запрашивается индекс на основе 1).

  • 0
    Отличное объяснение, закодировал это решение и добавил в свой пост - но чувствую, что я превращаюсь в чужое домашнее задание :)
  • 0
    @AChampion Спасибо, я не говорю ни слова о Python. Код на удивление короткий.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню