Быстрое вычисление членов линейной рекуррентной последовательности — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
м (rollbackEdits.php mass rollback)
 
(не показано 29 промежуточных версий 2 участников)
Строка 1: Строка 1:
Пусть нам дана [[Теорема о связи между рациональностью производящей функции и линейной рекуррентностью задаваемой ей последовательности#def_linear.|линейная реккурента]] размера <tex>k</tex>. А именно: <tex>a_n = c_1 \cdot a_{n - 1} + c_2 \cdot a_{n - 2} + \cdots + c_k \cdot a_{n - k}</tex>, а так же заданы <tex>k</tex> первых членов последовательности. Требуется уметь вычислять произвольное <tex>a_n</tex>.
+
Пусть нам дана [[Теорема о связи между рациональностью производящей функции и линейной рекуррентностью задаваемой ей последовательности#def_linear.|линейная рекуррентная последовательность]](далее ЛРП) порядка <tex>k</tex>. А именно: <tex>a_n = c_1 \cdot a_{n - 1} + c_2 \cdot a_{n - 2} + \cdots + c_k \cdot a_{n - k}</tex> при <tex>n \geqslant k</tex>, а так же заданы <tex>k</tex> первых членов последовательности. Требуется уметь вычислять произвольное <tex>a_n</tex>.
  
Самый простой способ сделать это {{---}} последовательно считать каждый <tex>a_i</tex>,  пока <tex>i</tex> не станет равен <tex>n</tex>. Однако этот способ не самый эффективный, ведь он, очевидно, требует <tex>O(n \cdot k)</tex> времени. Хочется уметь как-то быстрее решать эту задачу. Рассмотрим два способа это сделать.
+
Самый простой способ сделать это {{---}} последовательно считать каждый <tex>a_i</tex>,  пока <tex>i</tex> не станет равен <tex>n</tex>. Однако этот способ не самый эффективный, он, очевидно, требует <tex>O(n \cdot k)</tex> времени, но можно сделать это быстрее. Рассмотрим два способа это сделать.
  
== Умножение матриц (за <tex>O(k^3 \cdot logn)</tex>) ==
+
== Умножение матриц (за <tex>O(k^3 \cdot \log n)</tex>) ==
  
Заметим, что линейные рекурренты хорошо выражаются через матрицы. Запишем наши первые <tex>k</tex> членов последовательности в столбик.  
+
Заметим, что ЛРП хорошо выражаются через матрицы. Запишем наши первые <tex>k</tex> членов последовательности в столбик.  
 
<tex>A_0 = \begin{pmatrix}
 
<tex>A_0 = \begin{pmatrix}
 
a_{k - 1}\\
 
a_{k - 1}\\
Строка 14: Строка 14:
 
Так же выпишем следующую матрицу перехода:
 
Так же выпишем следующую матрицу перехода:
 
<tex>T = \begin{pmatrix}
 
<tex>T = \begin{pmatrix}
c_1 & c_2 & c_3 & \cdots & c_k\\
+
c_1 & c_2 & c_3 & \cdots & c_{k - 1} & c_k\\
1 & 0 & 0 & \cdots & 0\\
+
1 & 0 & 0 & \cdots & 0 & 0\\
0 & 1 & 0 & \cdots & 0\\
+
0 & 1 & 0 & \cdots & 0 & 0\\
\vdots & \vdots & \vdots & ~ & \vdots \\
+
\vdots & \vdots & \vdots & ~ & \vdots & \vdots \\
0 & 0 & 0 & \cdots & 1\\
+
0 & 0 & 0 & \cdots & 1 & 0\\
 
\end{pmatrix}</tex>
 
\end{pmatrix}</tex>
  
Заметим, что умножив <tex>A_0</tex> слева на <tex>T</tex>, мы получим столбик <tex>A_1</tex> следующего вида:  
+
Заметим, что умножив <tex>T</tex> на <tex>A_0</tex>, мы получим столбик <tex>A_1</tex> следующего вида:  
 
<tex>A_1 = T \cdot A_0 = \begin{pmatrix}
 
<tex>A_1 = T \cdot A_0 = \begin{pmatrix}
 
a_{k}\\
 
a_{k}\\
Строка 28: Строка 28:
 
a_1
 
a_1
 
\end{pmatrix}</tex>
 
\end{pmatrix}</tex>
Аналогично, домножив <tex>A_1</tex> слева на <tex>T</tex>, получим  
+
Аналогично, домножив <tex>T</tex> на <tex>A_1</tex>, получим  
 
<tex>A_2 = T \cdot A_1 = \begin{pmatrix}
 
<tex>A_2 = T \cdot A_1 = \begin{pmatrix}
 
a_{k + 1}\\
 
a_{k + 1}\\
Строка 36: Строка 36:
 
\end{pmatrix}</tex>
 
\end{pmatrix}</tex>
  
Продолжая так для любого <tex>i</tex>, мы получим столбик <tex>A_i</tex>, состоящий из <tex>k</tex> подряд идущий членов последовательности, начиная с <tex>a_i</tex>. Пользуясь ассоциативностью произведения матриц, можно записать, что <tex>A_i = T^i \cdot A_0</tex>. Из этого соотношения вытекает алгоритм вычисления произвольного <tex>a_n</tex>:
+
Продолжая так, для любого целого неотрицательного <tex>i</tex>, мы получим столбик <tex>A_i</tex>, состоящий из <tex>k</tex> подряд идущих членов последовательности, начиная с <tex>a_i</tex>. Пользуясь ассоциативностью произведения матриц, можно записать, что <tex>A_i = T^i \cdot A_0</tex>. Из этого соотношения вытекает алгоритм вычисления произвольного <tex>a_n</tex>:
  
 
# Инициализировать матрицы <tex>A_0</tex> и <tex>T</tex>
 
# Инициализировать матрицы <tex>A_0</tex> и <tex>T</tex>
Строка 42: Строка 42:
 
# Посчитать <tex>A_n</tex> как <tex>T^n \cdot A_0</tex> и взять из него <tex>a_n</tex>
 
# Посчитать <tex>A_n</tex> как <tex>T^n \cdot A_0</tex> и взять из него <tex>a_n</tex>
  
Используя быстрое возведение в степень, второй пункт будет тратить <tex>O(k^3 \cdot logn)</tex> времени, умножение же в третьем пункте выполняется за <tex>O(k^2)</tex>.
+
Используя быстрое возведение в степень во втором пункте, мы будем тратить <tex>O(k^3 \cdot \log n)</tex> времени. Умножение же в третьем пункте выполняется за <tex>O(k^2)</tex>.
  
Итого мы получили алгоритм за <tex>O(k^3 \cdot logn)</tex>.
+
Итого мы получили алгоритм, работающий за <tex>O(k^3 \cdot \log n)</tex>.
  
== Связь с многочленами (за <tex>O(k^2 \cdot logn)</tex>) ==
+
== Связь с многочленами (за <tex>O(k^2 \cdot \log n)</tex>) ==
  
Вспомним, что по [[Теорема о связи между рациональностью производящей функции и линейной рекуррентностью задаваемой ей последовательности|теореме о связи рекурренты и многочленов]] наша реккурента эквивалента некому многочлену <tex>A(t) = \dfrac{P(t)}{Q(t)}</tex>, при этом <tex>Q(t) = 1 - c_1 \cdot t - c_2 \cdot t^2 - \cdots - c_k \cdot t^k</tex>. Домножим числитель и знаменатель на <tex>Q(-t)</tex>. Новый знаменатель <tex>R(t) = Q(t) \cdot Q(-t)</tex>. При этом <tex>r_n = \sum\limits_{i = 0}^{n} q_i \cdot q_{n - i} \cdot (-1)^{n - i}</tex>. Нетрудно заметить, что при нечётных <tex>n</tex> коэффициенты <tex>r_n</tex> обращаются в <tex>0</tex>, a <tex>r_0 = 1</tex>.
+
Вспомним, что по [[Теорема о связи между рациональностью производящей функции и линейной рекуррентностью задаваемой ей последовательности|теореме о связи ЛРП и многочленов]] наша ЛРП эквивалента некому отношению многочленов <tex>A(t) = \dfrac{P(t)}{Q(t)}</tex>, при этом <tex>Q(t) = q_0 + q_1 \cdot t + \cdots q_k \cdot t^k = 1 - c_1 \cdot t - c_2 \cdot t^2 - \cdots - c_k \cdot t^k</tex>. Домножим числитель и знаменатель на <tex>Q(-t)</tex>. Новый знаменатель <tex>R(t) = Q(t) \cdot Q(-t)</tex>. При этом <tex>r_n = \sum\limits_{i = 0}^{n} q_i \cdot q_{n - i} \cdot (-1)^{n - i}</tex>. Нетрудно заметить, что при нечётных <tex>n</tex> коэффициенты <tex>r_n</tex> обращаются в <tex>0</tex>, a <tex>r_0 = 1</tex>.
  
Отсюда мы получаем, что многочлен <tex>R(t)</tex> имеет вид: <tex>R(t) = 1 + r_2 \cdot t^2 + r_4 \cdot t^4 + \cdots + r_{2k} \cdot t^{2k}</tex>. Однако вспомним о связи с рекуррентой, а именно мы получили, что <tex>a_n = -r_2 \cdot a_{n - 2} - r_4 \cdot a_{n - 4} - \cdots - r_{2k} \cdot a_{n - 2k}</tex>
+
Отсюда мы получаем, что многочлен <tex>R(t)</tex> имеет вид: <tex>R(t) = 1 + r_2 \cdot t^2 + r_4 \cdot t^4 + \cdots + r_{2k} \cdot t^{2k}</tex>. Однако вспомним о связи с ЛРП, тогда мы получили, что <tex>a_n = -r_2 \cdot a_{n - 2} - r_4 \cdot a_{n - 4} - \cdots - r_{2k} \cdot a_{n - 2k}</tex>
  
Иными словами мы получили новое рекуррентное соотношение для данной последовательности, где каждый элемент зависит от элементов с номерами, имеющими такую же чётность, что номер исходного. То есть по сути наша последовательность разделилась на две независимых: с чётными и нечётными номерами. Можно сказать, что мы теперь ищем не <tex>a_n</tex> из исходной последовательности, а <tex>a'_{n~div~2}</tex> из подпоследовательности элементов c номерами, имеющими ту же чётность, что и <tex>n</tex>. Заметим, что этот процесс можно проделывать далее пока <tex>n \geqslant k</tex>, ведь в итоге искомый окажется среди <tex>k</tex> первых. Всё, что нам нужно,{{---}} поддерживать первые <tex>k</tex> элементов для каждой новой последовательности.
+
Иными словами мы получили новое рекуррентное соотношение для данной последовательности, где каждый элемент зависит от элементов с номерами, имеющими такую же чётность, что номер исходного. То есть по сути наша последовательность разделилась на две независимых: с чётными и нечётными номерами. Можно сказать, что мы теперь ищем не <tex>a_n</tex> из исходной последовательности, а <tex>a'_{n~div~2}</tex> из подпоследовательности элементов c номерами, имеющими ту же чётность, что и <tex>n</tex>. Заметим, что этот процесс можно проделывать далее пока <tex>n \geqslant k</tex>, ведь в итоге искомый элемент окажется среди <tex>k</tex> первых. Всё, что нам нужно,{{---}} поддерживать первые <tex>k</tex> элементов для каждой новой последовательности.
  
 
Исходя из всего вышесказанного получаем алгоритм:
 
Исходя из всего вышесказанного получаем алгоритм:
  
<code>
+
    get_nth(n, a[], <tex>Q</tex>)
    '''while''' (n <= k) {
+
        '''while''' n <tex>\geqslant</tex> k
        count a[k], a[k + 1], ..., a[2k - 1];
+
            '''for''' i = k '''to''' 2k - 1
        Q(t) = Q(t) * Q(-t);
+
                a[i] = <tex>\sum\limits_{j = 1}^{k}</tex> -q[j] <tex>\cdot</tex> a[i - j]
        leave a[i] with (i % 2 == n % 2);
+
            <tex>R = Q(t) \cdot Q(-t)</tex>
        Q: t^2i -> t^i;
+
            '''filter''' a[i] '''with''' (i '''mod''' 2 == n '''mod''' 2)
        n = n div 2;
+
            <tex>Q = R(\sqrt{t})</tex>
    }
+
            n = n '''div''' 2
    return a[n];
+
        '''return''' a[n]
</code>
 
  
Вычисление <tex>a[k], a[k + 1], \cdots , a[2k - 1]</tex> занимает <tex>O(k^2)</tex> времени, ибо их всего <tex>k</tex>, а каждое считается за <tex>O(k)</tex>. Умножение многочленов длины порядка <tex>k</tex> также занимает <tex>O(k^2)</tex> времени. Итераций внешнего цикла будет <tex>O(logn)</tex> в силу того, что мы делим <tex>n</tex> на <tex>2</tex> каждый раз.  
+
Вычисление <tex>a[k], a[k + 1], \cdots , a[2k - 1]</tex> занимает <tex>O(k^2)</tex> времени, ибо их всего <tex>k</tex>, а каждый считается за <tex>O(k)</tex>. Умножение многочленов длины порядка <tex>k</tex> также занимает <tex>O(k^2)</tex> времени. Итераций внешнего цикла будет <tex>O(\log n)</tex> в силу того, что мы делим <tex>n</tex> на <tex>2</tex> каждый раз.  
  
Итого мы получили алгоритм, работающий за <tex>O(k^2 \cdot logn)</tex>
+
Итого мы получили алгоритм, работающий за <tex>O(k^2 \cdot \log n)</tex>
  
 
==См. также==
 
==См. также==
 
* [[Теорема о связи между рациональностью производящей функции и линейной рекуррентностью задаваемой ей последовательности|Теорема о связи между рациональностью производящей функции и линейной рекуррентностью задаваемой ей последовательности]]
 
* [[Теорема о связи между рациональностью производящей функции и линейной рекуррентностью задаваемой ей последовательности|Теорема о связи между рациональностью производящей функции и линейной рекуррентностью задаваемой ей последовательности]]
 
* [[Производящая функция| Производящая функция]]
 
* [[Производящая функция| Производящая функция]]
 +
 +
== Источники информации ==
 +
* [https://studfiles.net/preview/5826382/page:21/ Вычисление ЛРП с помощью матриц]
  
  
 
[[Категория: Дискретная математика и алгоритмы]]
 
[[Категория: Дискретная математика и алгоритмы]]
[[Категория: Комбинаторика]]
 
 
[[Категория: Производящие функции]]
 
[[Категория: Производящие функции]]

Текущая версия на 19:11, 4 сентября 2022

Пусть нам дана линейная рекуррентная последовательность(далее ЛРП) порядка k. А именно: an=c1an1+c2an2++ckank при nk, а так же заданы k первых членов последовательности. Требуется уметь вычислять произвольное an.

Самый простой способ сделать это — последовательно считать каждый ai, пока i не станет равен n. Однако этот способ не самый эффективный, он, очевидно, требует O(nk) времени, но можно сделать это быстрее. Рассмотрим два способа это сделать.

Умножение матриц (за O(k3logn))

Заметим, что ЛРП хорошо выражаются через матрицы. Запишем наши первые k членов последовательности в столбик. A0=(ak1ak2a0) Так же выпишем следующую матрицу перехода: T=(c1c2c3ck1ck1000001000 00010)

Заметим, что умножив T на A0, мы получим столбик A1 следующего вида: A1=TA0=(akak1a1) Аналогично, домножив T на A1, получим A2=TA1=(ak+1aka2)

Продолжая так, для любого целого неотрицательного i, мы получим столбик Ai, состоящий из k подряд идущих членов последовательности, начиная с ai. Пользуясь ассоциативностью произведения матриц, можно записать, что Ai=TiA0. Из этого соотношения вытекает алгоритм вычисления произвольного an:

  1. Инициализировать матрицы A0 и T
  2. Возвести матрицу T в степень n
  3. Посчитать An как TnA0 и взять из него an

Используя быстрое возведение в степень во втором пункте, мы будем тратить O(k3logn) времени. Умножение же в третьем пункте выполняется за O(k2).

Итого мы получили алгоритм, работающий за O(k3logn).

Связь с многочленами (за O(k2logn))

Вспомним, что по теореме о связи ЛРП и многочленов наша ЛРП эквивалента некому отношению многочленов A(t)=P(t)Q(t), при этом Q(t)=q0+q1t+qktk=1c1tc2t2cktk. Домножим числитель и знаменатель на Q(t). Новый знаменатель R(t)=Q(t)Q(t). При этом rn=ni=0qiqni(1)ni. Нетрудно заметить, что при нечётных n коэффициенты rn обращаются в 0, a r0=1.

Отсюда мы получаем, что многочлен R(t) имеет вид: R(t)=1+r2t2+r4t4++r2kt2k. Однако вспомним о связи с ЛРП, тогда мы получили, что an=r2an2r4an4r2kan2k

Иными словами мы получили новое рекуррентное соотношение для данной последовательности, где каждый элемент зависит от элементов с номерами, имеющими такую же чётность, что номер исходного. То есть по сути наша последовательность разделилась на две независимых: с чётными и нечётными номерами. Можно сказать, что мы теперь ищем не an из исходной последовательности, а an div 2 из подпоследовательности элементов c номерами, имеющими ту же чётность, что и n. Заметим, что этот процесс можно проделывать далее пока nk, ведь в итоге искомый элемент окажется среди k первых. Всё, что нам нужно,— поддерживать первые k элементов для каждой новой последовательности.

Исходя из всего вышесказанного получаем алгоритм:

   get_nth(n, a[], Q) 
       while n  k
            for i = k to 2k - 1
                a[i] = kj=1 -q[j]  a[i - j]
            R=Q(t)Q(t)
            filter a[i] with (i mod 2 == n mod 2)
            Q=R(t)
            n = n div 2
       return a[n]

Вычисление a[k],a[k+1],,a[2k1] занимает O(k2) времени, ибо их всего k, а каждый считается за O(k). Умножение многочленов длины порядка k также занимает O(k2) времени. Итераций внешнего цикла будет O(logn) в силу того, что мы делим n на 2 каждый раз.

Итого мы получили алгоритм, работающий за O(k2logn)

См. также

Источники информации