подсчет палиндромов быстрая оптимизация

Здравствуйте, у меня вопрос по оптимизации алгоритма подсчета палиндромов.

Задача: найти количество палиндромов в строке.

в моей функции я использую метод «в лоб», это похоже на O (n ^ 2), вы, ребята, можете помочь сделать это за O (n) или O (nlogn)

func isPalindrome(string: String) -> Bool {
    let str = (string.lowercased())
    let strWithoutSpace = str.components(separatedBy: .whitespaces).joined(separator: "")
    let strArray = Array(strWithoutSpace.characters)
    var i = 0
    var j = strArray.count-1
    while i <= j {
        if strArray[i] != strArray[j] { return false }
        i+=1
        j-=1
    }
    return true
}
func palindromsInString(string: String) -> Int {
    var arrayOfChars = Array(string.characters)
    var count = 0
    for i in 0..<arrayOfChars.count-1 {
        for x in i+1..<arrayOfChars.count {
            if isPalindrome(string: String(arrayOfChars[i...x])) {
                count+=1
            }
        }
    }
    return count
}

и да в моем случае одна буква не может быть палиндромом


person DmitrievichR    schedule 20.10.2016    source источник
comment
Вероятно, принадлежит обмену Code Review?   -  person matt    schedule 20.10.2016
comment
@matt хорошо, я попробую опубликовать там   -  person DmitrievichR    schedule 20.10.2016
comment
Я не думаю, что такая задача, как нахождение количества палиндромов в строке, должна быть задачей O (n).   -  person Redu    schedule 20.10.2016
comment
@Redu и первоначальный автор, посмотрите мой ответ, который, как я считаю, является решением O (n) или приближается к O (n).   -  person Tim Fuqua    schedule 21.10.2016


Ответы (3)


Я не знаком с алгоритмом Манахера, но мне всегда нравилось разрабатывать эффективные алгоритмы, поэтому я решил попробовать это.

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

extension String {
    func isPalindrome() -> Bool {
        if length < 2 { return false }
        let str = lowercased()
        let strArray = Array(str.characters)
        var i = 0
        var j = strArray.count-1
        while i <= j {
            if strArray[i] != strArray[j] { return false }
            i+=1
            j-=1
        }
        return true
    }
}

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

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

Идея наивного решения состоит в том, чтобы создать массивы подстрок исходной строки и проверить, является ли каждая подстрока палиндромом или нет. Способ, которым я определяю подстроки, состоит в том, чтобы начать с наибольшей возможной подстроки (исходной строки), а затем получить 2 подстроки длины string.length-1, а затем string.length-2 и так далее, пока, наконец, я не получу все подстроки длины 2 (я игнорирую подстроки длины 1, потому что вы сказали, что строка длины 1 не может быть палиндромом).

то есть: подстроки «теста», длина которых больше 1, будут:

["тест"] ["тес", "эст"] ["тэ", "эс", "ст"]

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

Наивное решение:

extension String {
    var length: Int { return characters.count }

    func substringsOfLength(length: Int) -> [String] {
        if self.length == 0 || length > self.length { return [] }

        let differenceInLengths = self.length - length

        var firstIndex = startIndex
        var lastIndex = index(startIndex, offsetBy: length)
        var substrings: [String] = []

        for _ in 0..<differenceInLengths {
            substrings.append(substring(with: Range(uncheckedBounds: (firstIndex, lastIndex))))
            firstIndex = index(after: firstIndex)
            lastIndex = index(after: lastIndex)
        }
        substrings.append(substring(with: Range(uncheckedBounds: (firstIndex, lastIndex))))

        return substrings
    }
}

extension String {
    func containsAPalindromeNaive(ignoringWhitespace: Bool = true) -> Int {
        let numChars = length

        if numChars < 2 { return 0 }

        let stringToCheck = (ignoringWhitespace ? self.components(separatedBy: .whitespaces).joined(separator: "") : self).lowercased()
        var outerLoop = numChars
        var count: Int = 0

        while outerLoop > 0 {
            let substrings = stringToCheck.substringsOfLength(length: outerLoop)
            for substring in substrings {
                if substring.isPalindrome() {
                    count += 1
                }
            }
            outerLoop -= 1
        }

        return count
    }
}

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

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

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

Идея состоит в том, что палиндромы могут существовать только в строке, которая заканчивается одной и той же буквой. Поэтому вам нужно только проверять подстроки внутри строки по индексам конкретной буквы. В слове «татуировка» у вас 3 разные буквы: «т», «а», «о». Сопоставление символов будет выглядеть так:

t: [0,2,3]
a: [1]
o: [4,5]

Теперь я знаю, что палиндромы могут существовать только в этом слове между (0,2), (2,3) и (4,5). Поэтому мне нужно проверить только 3 подстроки(0,2), (0,3), (2,3) и (4,5). Поэтому мне нужно проверить только 4 подстроки. Это идея. И как только вы проверили все возможные подстроки, заканчивающиеся определенной буквой, вы можете игнорировать любые другие найденные подстроки, начинающиеся с этой буквы, потому что вы уже проверили их.

Во втором проходе я просматриваю каждый символ в строке, и если я еще не проверил эту букву, я просматриваю пары индексов перестановки, сгенерированные generateOrderedPairIndexPermutations для индексов в сопоставлении символов и проверьте подстроки, чтобы увидеть, являются ли они палиндромом. Затем я делаю 2 оптимизации здесь. Во-первых, если расстояние между индексом начального символа и индексом конечного символа меньше 3, это должен быть палиндром (расстояние 1 означает, что они следуют друг за другом, а расстояние 2 означает, что между ними находится одна буква, поэтому также гарантированно будет палиндромом). Во-вторых, поскольку я уже знаю, что первый и последний символы совпадают, мне не нужно проверять всю подстроку, только от второй буквы до предпоследней буквы. Итак, если подстрока будет «тест», и я всегда гарантирую себе, что подстрока зарезервирована одной и той же буквой, мне на самом деле не нужно проверять «тест», и вместо этого я могу просто проверить «es». Это небольшая оптимизация, но, тем не менее, хорошая.

Умное решение:

extension Collection {
    func generateOrderedPairIndexPermutations() -> [(Index,Index)] {
        if count < 2 {
            return []
        }

        var perms: [(Index,Index)] = []

        var firstIndex = startIndex

        while firstIndex != endIndex {
            var secondIndex = index(firstIndex, offsetBy: 1)
            while secondIndex != endIndex {
                perms.append((firstIndex,secondIndex))
                secondIndex = index(secondIndex, offsetBy: 1)
            }
            firstIndex = index(firstIndex, offsetBy: 1)
        }

        return perms
    }
}

extension String {
    func generateCharacterMapping() -> [Character : [Int]] {
        var characterMapping: [Character : [Int]] = [:]

        for (index, char) in characters.enumerated() {
            if let indicesOfChar = characterMapping[char] {
                characterMapping[char] = indicesOfChar + [index]
            } else {
                characterMapping[char] = [index]
            }
        }

        return characterMapping
    }

    func containsAPalindromeSmart(ignoringWhitespace: Bool = true) -> Int {
        let numChars = length

        if numChars < 2 { return 0 }

        let stringToCheck = (ignoringWhitespace ? self.components(separatedBy: .whitespaces).joined(separator: "") : self).lowercased()
        let characterMapping = stringToCheck.generateCharacterMapping()

        var count: Int = 0
        var checkedChars: Set<Character> = Set()

        for char in stringToCheck.characters {
            if checkedChars.contains(char) == false {
                if let characterIndices = characterMapping[char], characterIndices.count > 1 {
                    let perms = characterIndices.generateOrderedPairIndexPermutations()
                    for (i,j) in perms {
                        let startCharIndex = characterIndices[i]
                        let endCharIndex = characterIndices[j]

                        if endCharIndex - startCharIndex < 3 {
                            count += 1
                        } else {
                            let substring = stringToCheck.substring(with: Range(uncheckedBounds: (stringToCheck.index(stringToCheck.startIndex, offsetBy: startCharIndex+1), stringToCheck.index(stringToCheck.startIndex, offsetBy: endCharIndex))))
                            if substring.isPalindrome() {
                                count += 1
                            }
                        }
                    }
                    checkedChars.insert(char)
                }
            }
        }

        return count
    }
}

Я чувствовал себя довольно хорошо об этом решении. Но я понятия не имел, насколько быстро это было на самом деле.Это было очень быстро

Используя XCTest для измерения производительности, я прогнал каждый алгоритм через несколько тестов производительности. Используя эту строку в качестве основного источника: "Здесь несколько палиндромов""Что я видел, машину или кошку", обновлено на основе предложений использовать более строгую входную строку< /strong>, длина которого составляет 3319 символов, если убрать пробелы и написать строчными буквами ("здесь есть несколько палиндромов""wasitacaroracatisaw"< /strong>), я также создал копии, в которых эта строка повторялась дважды ("therearemultiplepalindromesinheretherearmultiplepalindromesinhere"wasitacaroracatisawwasitacaroracatisaw), 4 раза, 8 раз и 10 раз. попытка определить O () алгоритма, увеличение количества букв и измерение коэффициента масштабирования - это путь.

Чтобы получить более точные данные, я провел каждый тест через 10 итераций (мне бы хотелось больше итераций, но исходное решение и мое наивное решение не завершаются своевременно в тестах выше раза 4). Вот данные о таймингах, которые я собрал (скриншот электронной таблицы было проще, чем вводить его здесь снова):

ОБНОВЛЕНО Таблица таймингов

ОБНОВЛЕНО Грин — автор; Красный — наивное решение; Orange — умное решение График времени

Как видите, и ваше исходное решение, и мое Наивное решение работают за квадратичное время (у вашего решения коэффициент квадратичной корреляции r=0,9997, а у моего Наивного решения коэффициент r=0,9999; !). Мое наивное решение, кажется, находится под вашим решением, но они оба увеличиваются квадратично и, следовательно, равны O (n ^ 2), как мы уже знали.

Самое интересное в моем умном решении то, что оно кажется линейным! Мои небольшие 5-точечные данные, полученные с помощью регрессионного калькулятора, имеют коэффициент корреляции r=0,9917! Так что, если это не линейно, это так близко, что мне все равно.

Все решения теперь работают в квадратичном времени. Вздох. Но, по крайней мере, ошибка была обнаружена, устранена, и в этот день наука победила. Облом, что мое «Умное решение» на самом деле не оказалось линейным. Тем не менее, я отмечу, что если входная строка еще не является гигантским палиндромом (подобным тому, который я в итоге изменил), то оптимизации «Умного решения» заставят ее работать намного быстрее, хотя все еще в квадратичном времени. .

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

Обновлено для устранения ошибки, обнаруженной Краскевичем.

person Tim Fuqua    schedule 21.10.2016
comment
Ваше интеллектуальное решение, похоже, реализует довольно хорошую оптимизацию с помощью таблицы поиска и действительно может быть жизнеспособным, если оно позаботится и о пробелах, но я все еще не думаю, что это может быть O (n), поскольку объем работы, который он будет определенно меняться в зависимости от входной строки. Например, у него есть длинный палиндром, такой как «Была ли это машина или кошка, которую я видел среди длинных предложений», я полагаю, что у него гораздо больше дел. Это может быть линейная временная сложность, такая как O (xn), но я полагаю, что не O (n). Хотя все равно очень умно. - person Redu; 21.10.2016
comment
Что возвращает это решение для строки, состоящей из нескольких повторений одной и той же буквы? Если я правильно понял, он вернет 2 для строки aaa, но должно быть 3. - person kraskevich; 21.10.2016
comment
@kraskevich в задаче и да в моем экземпляре одна буква не может быть палиндромом - person DmitrievichR; 21.10.2016
comment
Я знаю, что это не так. аа, аа, ааа — все палиндромы. Если это решение считает все разные палиндромы, оно тоже неверно (попробуйте, например, тестовый набор aabcaa). - person kraskevich; 21.10.2016
comment
@kraskevich вы абсолютно правы. Есть ошибка. Вместо того, чтобы проверять только последовательные пары индексов, я должен проверять все пары. Я исправлю это сегодня и обновлю ответ. Я не могу себе представить, что это слишком сильно тормозит. - person Tim Fuqua; 21.10.2016

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

Вы можете найти описание и реализацию этого алгоритма в этот вопрос.

person kraskevich    schedule 20.10.2016
comment
эй, спасибо за ответ, я пытаюсь решить эту проблему, используя алгоритм Манахера раньше. И у меня много проблем с этим. Может быть, есть какой-то алгоритм, который легче понять для начала, еще раз спасибо ;) - person DmitrievichR; 20.10.2016
comment
@DmitrievichR Я не знаю ни одного действительно простого алгоритма, который быстрее, чем O(n ^ 2) для этой проблемы. - person kraskevich; 20.10.2016
comment
@kraskevich и первоначальный автор, посмотрите мой ответ о том, что я считаю решением O (n) или приближается к O (n). - person Tim Fuqua; 21.10.2016

Вот решение «функционального программирования», на которое гораздо меньше влияет экспоненциальный характер процесса, чем на принятый ответ. (также намного меньше кода)

Это в 80% раз быстрее на коротких (19) струнах и в 90 раз быстрее на более длинных (190). Я формально не доказал это, но кажется линейным O(n)?.

public func countPalindromes(in text:String) -> Int
{
   let words  = text.lowercased()
                    .components(separatedBy:CharacterSet.letters.inverted)
                    .filter{!$0.isEmpty}
                    .joined(separator:"") 

   let sdrow  = String(words.characters.reversed())

   let splits = zip( sdrow.characters.indices.dropFirst().reversed(),
                     words.characters.indices.dropFirst() 
                   )
                .map{ (sdrow.substring(from:$0),words.substring(from:$1), words[$1...$1] ) }

   let count  = splits.map{$0.1.commonPrefix(with:$0.0)}  // even
                      .filter{ !$0.isEmpty }
                      .reduce(0){$0 + $1.characters.count}
              + splits.map{ $1.commonPrefix(with:$2 + $0)} // odd
                      .filter{$0.characters.count > 1 }
                      .reduce(0){$0 + $1.characters.count - 1}
   return count
}

// how it works ...

// words   contains the stripped down text (with only letters)
//
// sdrow   is a reversed version of words
//
// splits  creates split pairs for each character in the string.
//         Each tuple in the array contains a reversed left part, a right part
//         and the splitting character
//         The right part includes the splitting character 
//         but the left part does not.
//
//         [(String,String,String)] for [(left, right, splitChar)]
//
//         The sdrow string produces the left part in reversed letter order .
//         This "mirrored" left part will have a common prefix with the
//         right part if the split character's position is in the middle (+1)
//         of a palindrome that has an even number of characters
//
//         For palindromes with an odd number of characters, 
//         the reversed left part needs to add the splitting character
//         to match its common prefix with the right part.
//
// count   computes the total of odd and even palindromes from the
//         size of common prefixes. Each of the common prefix can produce
//         as many palindromes as its length (minus one for the odd ones)

[EDIT] Я также сделал процедурную версию для сравнения, зная, что компилятор может лучше оптимизировать процедурный код, чем его декларативный аналог.

Это расширение типа Array (поэтому он может подсчитывать палиндромы чего-либо сопоставимого).

extension Array where Element:Comparable
{
   public func countPalindromes() -> Int
   {
      if count < 2 { return 0 }

      var result = 0

      for splitIndex in (1..<count)
      {
         var leftIndex      = splitIndex - 1
         var rightIndex     = splitIndex
         var oddPalindrome  = true
         var evenPalindrome = true
         while leftIndex >= 0 && rightIndex < count
         {
            if evenPalindrome  
            && self[leftIndex] == self[rightIndex]
            { result += 1 }
            else
            { evenPalindrome = false }

            if oddPalindrome   
            && rightIndex < count - 1
            && self[leftIndex] == self[rightIndex+1]
            { result += 1 }
            else
            { oddPalindrome = false }

            guard oddPalindrome || evenPalindrome
            else { break }

            leftIndex  -= 1
            rightIndex += 1
         }
      }      
      return result
   }

} 

public func countPalindromesFromArray(in text:String) -> Int
{
   let words  = text.lowercased()
                    .components(separatedBy:CharacterSet.letters.inverted)
                    .filter{!$0.isEmpty}
                    .joined(separator:"") 
   return Array(words.characters).countPalindromes()
}

Он работает от 5 до 13 раз быстрее, чем декларативный, и до 1200 раз быстрее, чем принятый ответ.

Увеличивающаяся разница в производительности с декларативным решением говорит мне, что это не O (n). Процедурная версия может быть O(n), потому что ее время зависит от количества палиндромов, но будет пропорционально размеру массива.

person Alain T.    schedule 01.05.2017