Счётчик Кнута — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Переписывание конспекта заново)
Строка 1: Строка 1:
'''Счетчик Кнута''' (англ. ''Knuth's Counter'') - структура данных, представленная избыточной двоичной системой счисления, где добавление единицы к любому разряду выполняется за <tex>O(1)</tex>.
+
'''Счетчик Кнута''' (англ. ''Knuth's Counter'') &mdash; структура данных, представленная избыточной двоичной системой счисления, где добавление единицы к числу выполняется за <tex>O(1)</tex>.
 +
 
 +
Неотрицательное целое число <tex>N</tex> в ''избыточной двоичной системе счисления'' записывается в виде последовательности разрядов <tex>d_n d_{n-1} \dotsc  d_2 d_1</tex>, где <tex>n</tex> обозначает количество разрядов в числе, <math>d_i</math> &mdash; <tex>i</tex>&ndash;й разряд числа <tex>(1 \le i \le n)</tex>, причем <math>d_i \in \{0,1,2\}</math> и <math>\sum\limits_{i=1}^n d_i \cdot 2^i = N.
 +
</math>
  
''Избыточная двоичная система счисления'' - двоичная система счисления, где кроме 0 и 1 допустима 2 в записи числа.
 
 
== Алгоритм ==
 
== Алгоритм ==
''Примечание: этот алгоритм работает не за истинное <tex>O(1)</tex>. Например, чтобы прибавить 1 к 0111...111, нужно <tex>O(n)</tex> действий.''
 
 
Имеется число, записанное в избыточной двоичной системе счисления, необходимо добавить 1 к какому-либо разряду. Будем поддерживать следующий инвариант: в следующем за любой двойкой разряде всегда стоит 0.
 
 
Разберем случаи добавления единицы:
 
* a) Если 1 нужно добавить к 2, то в текущем разряде установить 1, в следующем 1.
 
* б) Если 1 нужно добавить к 1 и в следующем разряде 0, то в текущем разряде установить 2.
 
* в) Если 1 нужно добавить к 1 и в следующем разряде 1, то в текущем разряде установить 0, повторить алгоритм для следующего разряда.
 
* г) Если 1 нужно добавить к 1 и в следующем разряде 2, то в следующем за двойкой разряде установить 1, в разряде с двойкой установить 1, в текущем установить 0.
 
* д) Если 1 нужно добавить к 0 и в предыдущем разряде 2, в следующем 0, то в предыдущем разряде установить 0, в текущем 2.
 
* е) Если 1 нужно добавить к 0 и в предыдущем разряде 2, в следующем 1, то в предыдущем разряде установить 0, повторить алгоритм для следующего разряда.
 
* ж) Если 1 нужно добавить к 0 и в предыдущем разряде 2, в следующем 2, то в предыдущем разряде установить 0, повторить алгоритм для следующего разряда.
 
* з) Если 1 нужно добавить к 0 и в предыдущем разряде не 2, установить в текущем разряде 1.
 
 
== Доказательство ==
 
Покажем, что инвариант не нарушится.
 
 
Инвариант можно нарушить в 2 случаях: когда к единице прибавляют 1, и в следующем разряде стоит не 0, и когда к нулю прибавляется 1, а в предыдущем разряде стоит 2.
 
 
В первом случае, если в следующем разряде 1, алгоритм будет устанавливать в текущий разряд 0 и запускаться от следующего разряда. Если в следующем разряде 2, то, согласно инварианту, следующий за 2 разряд 0, и, если установить в следующий за двойкой и в разряд с двойкой единицы, то инвариант не нарушится.
 
 
Во втором случае, если в следующем разряде 0, то предыдущий установится в 0, текущий в 2 и так как в следующем 0 инвариант не нарушится. Если в следующем разряде не 0, то предыдущий установится в 0 и алгоритм запустится от следующего.
 
 
== Пример ==
 
Рассмотрим пример для каждого варианта, добавление 1 происходит в 3 разряд.
 
 
а) <tex>200_{2} \Rightarrow 1100_{2}</tex>
 
 
б) <tex>0100_{2} \Rightarrow 0200_{2}</tex>
 
 
в) <tex>1100_{2} \Rightarrow 1000_{2}</tex> повторение алгоритма для 4 разряда, по варианту б <tex>1000_{2} \Rightarrow 2000_{2}</tex>
 
 
г) <tex>2100_{2} \Rightarrow 11000_{2}</tex>
 
 
д) <tex>10020_{2} \Rightarrow 10200_{2}</tex>
 
 
е) <tex>1020_{2} \Rightarrow 1000_{2}</tex> повторение алгоритма для 4 разряда, по варианту б <tex>1000_{2} \Rightarrow 2000_{2}</tex>
 
  
ж) <tex>2020_{2} \Rightarrow 2000_{2}</tex> повторение алгоритма для 4 разряда, по варианту а <tex>2000_{2} \Rightarrow 11000_{2}</tex>
+
Оригинальный алгоритм представлен в Clancy, Knuth и состоит из двух правил:
  
з) <tex>010_{2} \Rightarrow 110_{2}</tex>  
+
# Найти младший разряд <tex>d_i</tex> равный <tex>2</tex> и, если таковой имеется, заменить последовательность <tex>d_{i+1}d_i</tex> на <tex>(d_{i+1}+1)0</tex>
 +
# Заменить <tex>d_1</tex> на <tex>d_1+1</tex>.
  
== Амортизационная оценка алгоритма ==
+
Чтобы достичь необходимой оценки и не выполнять каждый раз поиск для первого правила, можно хранить односвязный список позиций двоек в числе. Тогда чтобы найти младший разряд равный двум нужно просто взять первый элемент списка. Также, непосредственно перед изменением значений разрядов в правилах необходимо выполнять следующее:
Воспользуемся методом предоплаты. Будем считать, что каждый раз когда мы начинаем выполнять алгоритм мы берем 2 монетки. Одна будет тратится на изменение текущего разряда и одна запасаться. Таким образом, если в разряде стоит 1, то для него запасена 1 монетка, если стоит 2, то запасено 2 монетки. Проверим все варианты.
 
  
а) Так как в текущем разряде было 2, то уже запасено 2 монеты, а так же согласно инварианту после 2 стоит 0, тогда возьмем одну из запасенных у двойки монет и потратим их на присвоение следующему разряду 1, вторую запасенную передадим этой единице. Одну монету потратим на установку в текущий разряд 1, вторую запасем для это единицы.
+
# Если <tex>d_{i+1}=1</tex>, то заменить первый элемент списка с <math>i</math> на <math>i+1</math>, иначе удалить его.
 +
# Если <tex>d_1=1</tex>, то добавить в начало списка 1.
  
б) Так как в текущем разряде было 1, то прибавим наши монеты к уже запасенной от единицы. Одну монету потратим, что бы установить 2, останется 2 монеты.
+
Проблемой может оказаться появление двух последовательных двоек, при этом перое правило может породить
 +
тройку, что недопустимо. Однако если между любой парой двоек всегда будет находится хотя бы один
 +
0, то такой ситуации не возникнет. Покажем, что если это условие с нулем выполняется, то оно
 +
выполняется и после инкремента, рассмотрев основные случаи
 +
(в обозначениях левой части предполагается что правая двойка самая младшая из всех до инкремента):
 +
* <math>\dotsc 2\dotsc 0\dotsc 12\dotsc </math> переходит в <math>\dotsc 2\dotsc 0\dotsc 20\dotsc </math>, при этом в младшем разряде может появиться новая 2-ка и между ней следующей будет хотя бы один ноль.
 +
* <math>\dotsc 2\dotsc 0\dotsc 02\dotsc </math> переходит в <math>\dotsc 2\dotsc 0\dotsc 10\dotsc </math>, ситуация почти аналогична предыдущей.
 +
* <math>\dotsc 202\dotsc </math> переходит в <math>\dotsc 210\dotsc </math>, также гарантируется наличие нуля при появлении двойки в младшем разряде
 +
* Появление двух двоек, если уже есть ровно одна возможно, тогда <math>\dotsc 12\dotsc 1</math> переходит в <math>\dotsc 20\dotsc 2</math>, ясно что в этом случае между ними также будет хотя бы один ноль.
  
в) Так как в текущем разряде было 1, то прибавив наши монеты к запасенной получим 3. Одну монету потратим, что бы установить 0, оставшиеся 2 потратим на повторение алгоритма для следующего разряда.
+
В таблице можно увидеть как будет изменятья представление при применении данных правил десять раз к нулю (представления чисел от 0 до 10):
  
г) Так как в текущем разряде было 1, в следующем 2, то уже запасено 3 монеты и еще 2 наши. 3 монеты потратим, что бы следующему за 2 разряду установить 1, разряду с 2 установить 1, текущему установить 0. Останется 2 монеты, которые распределятся между двумя единицами.
+
{| class="wikitable"
 +
|-
 +
! Шаг
 +
! Представление
 +
! Шаг
 +
! Представление
 +
|-
 +
| 0
 +
| 0
 +
| 5
 +
| 21
 +
|-
 +
| 1  
 +
| 1
 +
| 6
 +
| 102
 +
|-
 +
| 2  
 +
| 2
 +
| 7
 +
| 111
 +
|-
 +
| 3  
 +
| 11
 +
| 8
 +
| 112
 +
|-
 +
| 4
 +
| 12
 +
| 9
 +
| 121
 +
|}
  
д) Так как в текущем разряде было 0, в предыдущем разряде 2, в следующем 0, то уже запасено 2 монеты. Одну монету потратим на изменение предыдущего разряда, одну на изменение текущего разряда, наши две монеты запасем для двойки в текущем разряде.
+
== Обобщение на системы с произвольным основанием ==
  
е) Так как в текущем разряде было 0, в предыдущем разряде 2, в следующем 1, то уже запасено 3 монеты. Одну монету потратим на изменение предыдущего разряда, одну сохраним с единицей, наши две монеты потратим на повторение алгоритма на следующем разряде. Останется одна лишняя монета.
+
В общем случае подобные счётчики называются ''<math>b</math>-ричными избыточными счетчиками'' (''ИС''), которые похожи на счетчик Кнута,
 +
но основание системы может быть произвольным, то есть <math>d_i \in \{0,1,\dotsc ,b\}</math> и <math>\sum\limits_{i=1}^n d_i \cdot b^i = N</math>
 +
, где <math>b</math> &mdash; основание. Такие счетчики позволяют прибавить единицу к любому разряду, то есть увеличить число на <math>b^i</math>
 +
за <math>O(1)</math>
 +
Назовем такое представление ''регулярным'', если между дувумя разрядами равными <math>b</math> есть хотя бы один разряд отличный от
 +
<math>b-1</math>.
 +
Операция ''исправления'' (''fix'') разряда <math>d_i=b</math> в регулярной <math>b</math>-ричного
 +
счетчика <math>d</math> увеличивает <math>d_{i+1}</math> на 1 и устанавливает
 +
<math>d_i</math> в 0, образуая новый регулярный счетчик, представляющий то же число,
 +
что и <math>d</math>.
 +
Чтобы добавить 1 к разряду <math>d_i</math> регулярного ИС <math>d</math>,
 +
нужно сделать следующее:
 +
# Если <math>d_i=b</math>, исправить <math>d_i</math>.
 +
# Если <math>d_i=b-1</math> и самый младший значащий разряд <math>d_j</math>, такой, что <math>j>i</math> и <math>d_j \ne b-1</math>, равен <math>b</math> (т.е. <math>d_j=b</math>), применить операцию исправления к разряду <math>d_j</math>.
 +
# Добавить 1 к <math>d_i</math>.
 +
# Если <math>d_i=b</math>, исправить <math>d_i</math>.
  
ж) Так как в текущем разряде было 0, в предыдущем разряде 2, в следующем 2, то уже запасено 4 монеты. Одну монету потратим на изменение предыдущего разряда, две сохраним с двойкой, наши две монеты потратим на повторение алгоритма на следующем разряде. Останется одна лишняя монета.
+
Для реализации данной схемы, мы используем односвязный список разрадов от младших
 +
к старшим. В дополнение каждый разряд <math>d_i</math> равный <math>b-1</math>
 +
будет иметь указатель на самый младший разряд <math>d_j</math>, такой,
 +
что <math>j>i</math> и <math>d_j \ne b-1</math>, если он равен <math>b</math>,
 +
иначе этот указатель будет на произвольный разряд <math>d_j</math> (<math>j>i</math>).
 +
Теперь, во время увеличения разряда <math>d_i</math> на 1, будем проверять
 +
разряд по указателю вперед (п. 2).
  
з) Одну монету потратим на изменение разряда, оставшуюся запасем с единицей.
+
Такое представление позволяет увеличиать произвольный разряд за константное
 +
время. Обновление указателя вперед происходит следующим образом: когда <math>d_{i+}</math> становится равен <math>b-1</math> при исправлении разряда <math>d_{i-1}</math>, устанавливаем указатель вперед разряда <math>d_{i}</math> на <math>d_{i+1}</math>, если <math>d_{i+1}=b</math>, либо копируем указатель вперед из <math>d_{i+1}</math> в <math>d_{i}</math>, если <math>d_{i+1}=b-1</math>.
 +
При собственно добавлении единицы к разряду <math>d_i</math>, также необходимо обновлять его указатель вперед аналогичным образом,
 +
если этот разряд становится равен <math>b-1</math>.
  
Получается 2 монет достаточно для прибавления 1 к любому разряду, тогда наш алгоритм работает в среднем за <tex>O(1)</tex>
+
== Литература ==
 +
* H. Kaplan и R. E. Tarjan. New heap data structures. 1998
 +
* M. J. Clancy и D. E. Knuth. A programming and problem-solving seminar. Technical Report STAN-CS-77-606, Department of Computer Sciencr, Stanford University, Palo Alto, 1977.
 +
* G. S. Brodal. Worst case priority queues. ''Proc. 7th annual ACM-SIAM Symposium on Discrete Algorithms (SODA 96)'', страницы 52-58. ACM Press, 1996.
 +
* H. Kaplan и R. E. Tarjan. Purely functional representations of catenable sorted lists. ''Proceedings of the 28th Annual ACM Symposium of Computing'', страницы 202-211. ACM Press, 1996
  
== Смотри также ==
+
== Смотрите также ==
 
* [[Амортизационный анализ]]
 
* [[Амортизационный анализ]]
 
* [[Представление целых чисел: прямой код, код со сдвигом, дополнительный код]]
 
* [[Представление целых чисел: прямой код, код со сдвигом, дополнительный код]]

Версия 20:48, 15 июня 2014

Счетчик Кнута (англ. Knuth's Counter) — структура данных, представленная избыточной двоичной системой счисления, где добавление единицы к числу выполняется за [math]O(1)[/math].

Неотрицательное целое число [math]N[/math] в избыточной двоичной системе счисления записывается в виде последовательности разрядов [math]d_n d_{n-1} \dotsc d_2 d_1[/math], где [math]n[/math] обозначает количество разрядов в числе, [math]d_i[/math][math]i[/math]–й разряд числа [math](1 \le i \le n)[/math], причем [math]d_i \in \{0,1,2\}[/math] и [math]\sum\limits_{i=1}^n d_i \cdot 2^i = N. [/math]

Алгоритм

Оригинальный алгоритм представлен в Clancy, Knuth и состоит из двух правил:

  1. Найти младший разряд [math]d_i[/math] равный [math]2[/math] и, если таковой имеется, заменить последовательность [math]d_{i+1}d_i[/math] на [math](d_{i+1}+1)0[/math]
  2. Заменить [math]d_1[/math] на [math]d_1+1[/math].

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

  1. Если [math]d_{i+1}=1[/math], то заменить первый элемент списка с [math]i[/math] на [math]i+1[/math], иначе удалить его.
  2. Если [math]d_1=1[/math], то добавить в начало списка 1.

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

  • [math]\dotsc 2\dotsc 0\dotsc 12\dotsc [/math] переходит в [math]\dotsc 2\dotsc 0\dotsc 20\dotsc [/math], при этом в младшем разряде может появиться новая 2-ка и между ней следующей будет хотя бы один ноль.
  • [math]\dotsc 2\dotsc 0\dotsc 02\dotsc [/math] переходит в [math]\dotsc 2\dotsc 0\dotsc 10\dotsc [/math], ситуация почти аналогична предыдущей.
  • [math]\dotsc 202\dotsc [/math] переходит в [math]\dotsc 210\dotsc [/math], также гарантируется наличие нуля при появлении двойки в младшем разряде
  • Появление двух двоек, если уже есть ровно одна возможно, тогда [math]\dotsc 12\dotsc 1[/math] переходит в [math]\dotsc 20\dotsc 2[/math], ясно что в этом случае между ними также будет хотя бы один ноль.

В таблице можно увидеть как будет изменятья представление при применении данных правил десять раз к нулю (представления чисел от 0 до 10):

Шаг Представление Шаг Представление
0 0 5 21
1 1 6 102
2 2 7 111
3 11 8 112
4 12 9 121

Обобщение на системы с произвольным основанием

В общем случае подобные счётчики называются [math]b[/math]-ричными избыточными счетчиками (ИС), которые похожи на счетчик Кнута, но основание системы может быть произвольным, то есть [math]d_i \in \{0,1,\dotsc ,b\}[/math] и [math]\sum\limits_{i=1}^n d_i \cdot b^i = N[/math] , где [math]b[/math] — основание. Такие счетчики позволяют прибавить единицу к любому разряду, то есть увеличить число на [math]b^i[/math] за [math]O(1)[/math] Назовем такое представление регулярным, если между дувумя разрядами равными [math]b[/math] есть хотя бы один разряд отличный от [math]b-1[/math]. Операция исправления (fix) разряда [math]d_i=b[/math] в регулярной [math]b[/math]-ричного счетчика [math]d[/math] увеличивает [math]d_{i+1}[/math] на 1 и устанавливает [math]d_i[/math] в 0, образуая новый регулярный счетчик, представляющий то же число, что и [math]d[/math]. Чтобы добавить 1 к разряду [math]d_i[/math] регулярного ИС [math]d[/math], нужно сделать следующее:

  1. Если [math]d_i=b[/math], исправить [math]d_i[/math].
  2. Если [math]d_i=b-1[/math] и самый младший значащий разряд [math]d_j[/math], такой, что [math]j\gt i[/math] и [math]d_j \ne b-1[/math], равен [math]b[/math] (т.е. [math]d_j=b[/math]), применить операцию исправления к разряду [math]d_j[/math].
  3. Добавить 1 к [math]d_i[/math].
  4. Если [math]d_i=b[/math], исправить [math]d_i[/math].

Для реализации данной схемы, мы используем односвязный список разрадов от младших к старшим. В дополнение каждый разряд [math]d_i[/math] равный [math]b-1[/math] будет иметь указатель на самый младший разряд [math]d_j[/math], такой, что [math]j\gt i[/math] и [math]d_j \ne b-1[/math], если он равен [math]b[/math], иначе этот указатель будет на произвольный разряд [math]d_j[/math] ([math]j\gt i[/math]). Теперь, во время увеличения разряда [math]d_i[/math] на 1, будем проверять разряд по указателю вперед (п. 2).

Такое представление позволяет увеличиать произвольный разряд за константное время. Обновление указателя вперед происходит следующим образом: когда [math]d_{i+}[/math] становится равен [math]b-1[/math] при исправлении разряда [math]d_{i-1}[/math], устанавливаем указатель вперед разряда [math]d_{i}[/math] на [math]d_{i+1}[/math], если [math]d_{i+1}=b[/math], либо копируем указатель вперед из [math]d_{i+1}[/math] в [math]d_{i}[/math], если [math]d_{i+1}=b-1[/math]. При собственно добавлении единицы к разряду [math]d_i[/math], также необходимо обновлять его указатель вперед аналогичным образом, если этот разряд становится равен [math]b-1[/math].

Литература

  • H. Kaplan и R. E. Tarjan. New heap data structures. 1998
  • M. J. Clancy и D. E. Knuth. A programming and problem-solving seminar. Technical Report STAN-CS-77-606, Department of Computer Sciencr, Stanford University, Palo Alto, 1977.
  • G. S. Brodal. Worst case priority queues. Proc. 7th annual ACM-SIAM Symposium on Discrete Algorithms (SODA 96), страницы 52-58. ACM Press, 1996.
  • H. Kaplan и R. E. Tarjan. Purely functional representations of catenable sorted lists. Proceedings of the 28th Annual ACM Symposium of Computing, страницы 202-211. ACM Press, 1996

Смотрите также