Префикс-функция — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Доказательство корректности алгоритма)
(Построение строки по префикс-функции: некоторые мелкие правки)
Строка 58: Строка 58:
 
==Построение строки по префикс-функции==
 
==Построение строки по префикс-функции==
 
===Постановка задачи===  
 
===Постановка задачи===  
Восстановить строку по префикс-функции за <tex>O(N)</tex> (алфавит неограничен).
+
Восстановить строку по префикс-функции за <tex>O(N)</tex>, считая алфавит неограниченным.
  
 
===Описание алгоритма===
 
===Описание алгоритма===
В дальнейшем нумерация символов в строках будет с <tex>1</tex>.
 
  
 
Пусть в массиве <tex>p</tex> хранятся значения префикс-функции, в <tex>s</tex> будет записан ответ. Пойдем по массиву <tex>p</tex> слева направо.
 
Пусть в массиве <tex>p</tex> хранятся значения префикс-функции, в <tex>s</tex> будет записан ответ. Пойдем по массиву <tex>p</tex> слева направо.
Пусть мы хотим узнать значение <tex>s[i]</tex>, посмотрим на значение <tex>p[i]</tex>,  если <tex>p[i] =0</tex> тогда в <tex>s[i]</tex> запишем новый символ, иначе <tex>s[i] = s[p[i]]</tex>. Обратим внимание, что  <tex>s[p[i]]</tex> мы посчитали раньше, так как <tex>p[i] < i</tex>. Так как подстрока с <tex>1</tex> по <tex>p[i]</tex> заканчивается в <tex>s[i]</tex>, то <tex>s[i]</tex> должен быть равен <tex>s[p[i]]</tex>.
 
  
===Псевдокод===
+
Пусть мы хотим узнать значение <tex>s[i]</tex>. Для этого посмотрим на значение <tex>p[i]</tex>: если <tex>p[i] =0</tex> тогда в <tex>s[i]</tex> запишем новый символ, иначе <tex>s[i] = s[p[i]]</tex>. Обратим внимание, что  <tex>s[p[i]]</tex> нам уже известно, так как <tex>p[i] < i</tex>.
  '''string''' buildFromPrefix('''int'''[] p)  
+
 
'''for''' i = 0 '''to''' p.length - 1
+
  '''string''' buildFromPrefix('''int'''[] p):
    '''if''' p[i] == 0     
+
  s = ""
 +
  '''for''' i = 0 '''to''' p.length - 1:
 +
      '''if''' p[i] == 0:      
 
           s += new char
 
           s += new char
    '''else'''
+
      '''else:'''
 
           s += s[p[i]]
 
           s += s[p[i]]
'''return''' s
+
  '''return''' s
  
 
===Доказательство корректности алгоритма===
 
===Доказательство корректности алгоритма===
 
Докажем, что если нам дали корректную префикс-функцию, то наш алгоритм построит строку с такой же префикс-функцией. Также заметим, что строк с такой префикс-функцией может быть много, и алгоритм строит только одну из них.
 
Докажем, что если нам дали корректную префикс-функцию, то наш алгоритм построит строку с такой же префикс-функцией. Также заметим, что строк с такой префикс-функцией может быть много, и алгоритм строит только одну из них.
  
Пусть <tex>p</tex> данная префикс-функция, <tex>s'</tex> правильная строка, <tex>s</tex> эту строку построил наш алгоритм, <tex> q </tex> массив значений префикс-функции для <tex>s</tex>.
+
Пусть <tex>p</tex> данная префикс-функция, <tex>s'</tex> правильная строка, строку <tex>s</tex> построил наш алгоритм, <tex> q </tex> массив значений префикс-функции для <tex>s</tex>.
  
 
Докажем корректность индукцией по длине массива префикс-функции полученной строки.
 
Докажем корректность индукцией по длине массива префикс-функции полученной строки.
  
База очевидна для строки длиной <tex>1</tex>.
+
* База очевидна для строки длиной <tex>1</tex>.
  
Переход: Пусть до <tex>n</tex>-ой позиции мы построили строку, что <tex>p[1..n - 1] = q[1..n - 1]</tex>. Возможно два варианта,
+
* Переход: пусть до <tex>n</tex>-ой позиции мы построили строку, что <tex>p[1..n - 1] = q[1..n - 1]</tex>. Возможны два случая:
  
 
<tex>1)</tex>  <tex>p[n] = 0</tex>. Тогда мы добавляем новый символ, поэтому <tex>q[n]</tex> тоже будет равно <tex>0</tex>. Также, предыдущие значения <tex>q</tex> не поменяются и останутся верными.
 
<tex>1)</tex>  <tex>p[n] = 0</tex>. Тогда мы добавляем новый символ, поэтому <tex>q[n]</tex> тоже будет равно <tex>0</tex>. Также, предыдущие значения <tex>q</tex> не поменяются и останутся верными.

Версия 14:43, 2 мая 2014

Префикс-функция строки [math]s[/math] — функция [math]\pi(i) = \max\limits_{k = 1..i - 1} \{ 0, k : [/math] [math]s[1..k] = s[i - k + 1..i] \}[/math].

Алгоритм

Наивный алгоритм вычисляет префикс функцию непосредственно по определению, сравнивая префиксы и суффиксы строк.

Псевдокод

Prefix_function ([math]s[/math])
     [math]\pi[/math] = [0,..,0]
     for i = 1 to n
         for k = 1 to i - 1
             if s[1..k] == s[i - k + 1..i]
                 [math]\pi[/math][i] = k
     return [math]\pi[/math]

Пример

Рассмотрим строку abcabcd, для которой значение префикс-функции равно [math][0,0,0,1,2,3,0][/math].

Шаг Строка Значение функции
[math]1[/math] a 0
[math]2[/math] ab 0
[math]3[/math] abc 0
[math]4[/math] abca 1
[math]5[/math] abcab 2
[math]6[/math] abcabc 3
[math]7[/math] abcabcd 0

Время работы

Всего [math]O(n^2)[/math] итераций цикла, на каждой из который происходит сравнение строк за [math]O(n)[/math], что дает в итоге [math]O(n^3)[/math].

Оптимизация

Вносятся несколько важных замечаний:

  • Следует заметить, что [math]\pi(i) \le \pi(i-1) + 1[/math]. По определению префикс функции верно, что [math]s[1..\pi(i)] = s[i - \pi(i) + 1..i][/math]. В частности, получается, что [math]s[1..\pi(i) - 1] = s[i - \pi(i) + 1..i - 1][/math]. Поскольку [math]\pi[/math] это наибольший префикс равный суффиксу, то [math]\pi(i - 1) \ge \pi(i) - 1[/math].
  • Избавимся от явных сравнений строк. Для этого подберем такое [math]k[/math], что [math]k = \pi(i) - 1[/math]. Делаем это следующим образом. За исходное [math]k[/math] необходимо взять [math]\pi(i - 1)[/math], что следует из первого пункта. В случае, когда символы [math]s[k+1][/math] и [math]s[i][/math] не совпадают, [math]\pi(k)[/math] — следующее потенциальное наибольшее значение [math]k[/math], что видно из рисунка. Последнее утверждение верно, пока [math]k\gt 0[/math], что позволит всегда найти его следующее значение. Если [math]k=0[/math], то [math]\pi(i)=1[/math] при [math]s[i] = s[1][/math] , иначе [math]\pi(i)=0[/math].

Prefix2.jpg

Псевдокод

Prefix_function ([math]s[/math])
     [math]\pi[/math][1] = 0
     k = 0
     for i = 2 to n
         while k > 0 && s[i] != s[k + 1]
             k = [math]\pi[/math][k]
         if s[i] == s[k + 1]
             k++
         [math]\pi[/math][i] = k
     return [math]\pi[/math]

Время работы

Время работы алгоритма составит [math]O(n)[/math]. Для доказательства этого нужно заметить, что итоговое количество итераций цикла [math]while[/math] определяет асимптотику алгоритма. Теперь стоит отметить, что [math]k[/math] увеличивается на каждом шаге не более чем на единицу, значит максимально возможное значение [math]k = n - 1[/math]. Поскольку внутри цикла [math]while[/math] значение [math]k[/math] лишь уменьшается, получается, что [math]k[/math] не может суммарно уменьшиться больше, чем [math]n-1[/math] раз. Значит цикл [math]while[/math] в итоге выполнится не более [math]n[/math] раз, что дает итоговую оценку времени алгоритма [math]O(n)[/math].

Построение строки по префикс-функции

Постановка задачи

Восстановить строку по префикс-функции за [math]O(N)[/math], считая алфавит неограниченным.

Описание алгоритма

Пусть в массиве [math]p[/math] хранятся значения префикс-функции, в [math]s[/math] будет записан ответ. Пойдем по массиву [math]p[/math] слева направо.

Пусть мы хотим узнать значение [math]s[i][/math]. Для этого посмотрим на значение [math]p[i][/math]: если [math]p[i] =0[/math] тогда в [math]s[i][/math] запишем новый символ, иначе [math]s[i] = s[p[i]][/math]. Обратим внимание, что [math]s[p[i]][/math] нам уже известно, так как [math]p[i] \lt i[/math].

string buildFromPrefix(int[] p):
  s = "" 
  for i = 0 to p.length - 1:
      if p[i] == 0:     
          s += new char
      else:
          s += s[p[i]]
  return s

Доказательство корректности алгоритма

Докажем, что если нам дали корректную префикс-функцию, то наш алгоритм построит строку с такой же префикс-функцией. Также заметим, что строк с такой префикс-функцией может быть много, и алгоритм строит только одну из них.

Пусть [math]p[/math] данная префикс-функция, [math]s'[/math] правильная строка, строку [math]s[/math] построил наш алгоритм, [math] q [/math] массив значений префикс-функции для [math]s[/math].

Докажем корректность индукцией по длине массива префикс-функции полученной строки.

  • База очевидна для строки длиной [math]1[/math].
  • Переход: пусть до [math]n[/math]-ой позиции мы построили строку, что [math]p[1..n - 1] = q[1..n - 1][/math]. Возможны два случая:

[math]1)[/math] [math]p[n] = 0[/math]. Тогда мы добавляем новый символ, поэтому [math]q[n][/math] тоже будет равно [math]0[/math]. Также, предыдущие значения [math]q[/math] не поменяются и останутся верными.

[math]2)[/math] [math]p[n] \gt 0[/math]. Предположим, что [math]q[n] \neq p[n] [/math]. Заметим, что подстрока с [math]1[/math] по [math]p[n][/math] оканчивается на [math]n[/math]-ом символе. По предположению индукции наш алгоритм построил правильную строку до [math]n - 1[/math] символа, следовательно, [math]p[n] \leqslant q[n][/math]. Представим, что [math]q[n] \gt p[n] [/math], тогда получается, что префикс с [math]1..q[n][/math] в строке [math]s'[/math], является подстрокой заканчивающийся на [math]n[/math]-ом символе, но тогда возникает противоречие с тем, что массив [math]p[/math] корректный. Также, предыдущие значения [math]q[/math] не поменяются и останутся верными.

Простой пример некорректного [math]p = {0,1,1} [/math], тогда по алгоритму получится строка [math]aaa[/math]. Очевидно, что префикс-функции не будут совпадать.

Литература

Кормен Т., Лейзерсон Ч., Ривест Р. Алгоритмы: построение и анализ. — 2-е изд. — М.: Издательский дом «Вильямс», 2007. — С. 1296.