Динамическое программирование по профилю — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Пример)
м (rollbackEdits.php mass rollback)
 
(не показано 5 промежуточных версий 3 участников)
Строка 7: Строка 7:
  
 
== Общие принципы ==
 
== Общие принципы ==
Обычно дана таблица и надо посчитать количество замощений этой таблицы некоторыми фигурами (замощение шахматной доски доминошками). Можно перебрать все варианты и выбрать из них удовлетворяющие условию. Но можно воспользоваться методом динамического программирования по профилю и сократить время по одной размерности до линейной. Затем пусть у нас есть правило по которому надо заполнить и для него нам надо <tex>k</tex> предыдущих линий. Тогда можно перебрать все замощения  длиной <tex>k\times n</tex>. В итоге нужно заполнить данную таблицу этими замощениями. Получается, что если перебирать все варианты нам понадобиться <tex>O(a^{nm})</tex> времени, а если перебирать только состояния и переходить по ним нам потребуется <tex>O(a^{kn}m)</tex> времени (где <tex>a</tex> {{---}} количество способов замощения одной клетки).
+
Обычно дана таблица и надо посчитать количество замощений этой таблицы некоторыми фигурами (замощение шахматной доски доминошками). Можно перебрать все варианты и выбрать из них удовлетворяющие условию. Но можно воспользоваться методом динамического программирования по профилю и сократить время по одной размерности до линейной. Затем пусть у нас есть правило по которому надо заполнить и для него нам надо <tex>k</tex> предыдущих линий. Тогда можно перебрать все замощения  длиной <tex>k\times n</tex>. В итоге нужно заполнить данную таблицу этими замощениями. Получается, что если перебирать все варианты нам понадобится <tex>O(a^{nm})</tex> времени, а если перебирать только состояния и переходить по ним нам потребуется <tex>O(a^{kn}m)</tex> времени (где <tex>a</tex> {{---}} количество способов замощения одной клетки).
  
 
== '''Задача о замощении домино''' ==
 
== '''Задача о замощении домино''' ==
Строка 36: Строка 36:
 
==='''Реализация'''===
 
==='''Реализация'''===
 
  <font color=green>// n, m {{---}} размер таблицы </font>  
 
  <font color=green>// n, m {{---}} размер таблицы </font>  
   '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} \verb|<<| \ \mathtt{n}) - \mathtt{1}</tex>
+
   '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} << \ \mathtt{n}) - \mathtt{1}</tex>
     '''for''' <tex>\mathtt{j} = \mathtt{0}..(\mathtt{1} \verb|<<| \ \mathtt{n}) - \mathtt{1}</tex>
+
     '''for''' <tex>\mathtt{j} = \mathtt{0}..(\mathtt{1} << \ \mathtt{n}) - \mathtt{1}</tex>
 
         '''if''' можно перейти из <tex>\mathtt{i}</tex> в <tex>\mathtt{j}</tex> профиль  
 
         '''if''' можно перейти из <tex>\mathtt{i}</tex> в <tex>\mathtt{j}</tex> профиль  
 
             <tex>\mathtt{d}[\mathtt{i}][\mathtt{j}] = \mathtt{1}</tex>
 
             <tex>\mathtt{d}[\mathtt{i}][\mathtt{j}] = \mathtt{1}</tex>
Строка 44: Строка 44:
 
  <tex>\mathtt{a}[\mathtt{0}][\mathtt{0}] = \mathtt{1}</tex> <font color=green>// Так как мы можем начать только с профиля где все клетки 0 </font>  
 
  <tex>\mathtt{a}[\mathtt{0}][\mathtt{0}] = \mathtt{1}</tex> <font color=green>// Так как мы можем начать только с профиля где все клетки 0 </font>  
 
  '''for''' <tex>k = \mathtt{1}..\mathtt{m} - \mathtt{1} </tex>
 
  '''for''' <tex>k = \mathtt{1}..\mathtt{m} - \mathtt{1} </tex>
     '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} \verb|<<| \ \mathtt{n}) - \mathtt{1}</tex>
+
     '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} << \ \mathtt{n}) - \mathtt{1}</tex>
         '''for''' <tex>\mathtt{j} = \mathtt{0}..(\mathtt{1} \verb|<<| \ \mathtt{n}) - \mathtt{1}</tex>
+
         '''for''' <tex>\mathtt{j} = \mathtt{0}..(\mathtt{1} << \ \mathtt{n}) - \mathtt{1}</tex>
 
      <tex>\mathtt{a}[\mathtt{k}][\mathtt{i}] = \mathtt{a}[\mathtt{k}][\mathtt{i}] + \mathtt{a}[\mathtt{k} - \mathtt{1}][\mathtt{j}]  \cdot  \mathtt{d}[\mathtt{j}][\mathtt{i}]</tex>
 
      <tex>\mathtt{a}[\mathtt{k}][\mathtt{i}] = \mathtt{a}[\mathtt{k}][\mathtt{i}] + \mathtt{a}[\mathtt{k} - \mathtt{1}][\mathtt{j}]  \cdot  \mathtt{d}[\mathtt{j}][\mathtt{i}]</tex>
 
  <tex>\mathtt{ans} = \mathtt{0}</tex>
 
  <tex>\mathtt{ans} = \mathtt{0}</tex>
  '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} \verb|<<| \ \mathtt{n}) - \mathtt{1}</tex>
+
  '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} \lt \lt \ \mathtt{n}) - \mathtt{1}</tex>
 
     '''if''' можно закончить <tex>\mathtt{i}</tex> профилем
 
     '''if''' можно закончить <tex>\mathtt{i}</tex> профилем
 
         <tex>\mathtt{ans} = \mathtt{ans} + \mathtt{a}[\mathtt{m} - \mathtt{1}][\mathtt{i}]</tex>
 
         <tex>\mathtt{ans} = \mathtt{ans} + \mathtt{a}[\mathtt{m} - \mathtt{1}][\mathtt{i}]</tex>
Строка 82: Строка 82:
 
==='''Реализация'''===
 
==='''Реализация'''===
 
  <font color=green>// n, m {{---}} размер таблицы </font>   
 
  <font color=green>// n, m {{---}} размер таблицы </font>   
  '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} \verb|<<| \ \mathtt{n}) - \mathtt{1}</tex>
+
  '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} << \ \mathtt{n}) - \mathtt{1}</tex>
     '''for''' <tex>\mathtt{j} = \mathtt{0}..(\mathtt{1} \verb|<<| \ \mathtt{n}) - \mathtt{1}</tex>
+
     '''for''' <tex>\mathtt{j} = \mathtt{0}..(\mathtt{1} << \ \mathtt{n}) - \mathtt{1}</tex>
 
         '''if''' можно перейти из <tex>\mathtt{i}</tex> в <tex>\mathtt{j}</tex> профиль  
 
         '''if''' можно перейти из <tex>\mathtt{i}</tex> в <tex>\mathtt{j}</tex> профиль  
 
             <tex>\mathtt{d}[\mathtt{i}][\mathtt{j}]\ =\ \mathtt{1}</tex>
 
             <tex>\mathtt{d}[\mathtt{i}][\mathtt{j}]\ =\ \mathtt{1}</tex>
 
  '''else'''
 
  '''else'''
 
      <tex>\mathtt{d}[\mathtt{i}][\mathtt{j}]\ =\ \mathtt{0}</tex>
 
      <tex>\mathtt{d}[\mathtt{i}][\mathtt{j}]\ =\ \mathtt{0}</tex>
  '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} \verb|<<| \ \mathtt{n}) - \mathtt{1}</tex>
+
  '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} << \ \mathtt{n}) - \mathtt{1}</tex>
 
     <tex>\mathtt{a}[0][\mathtt{i}]\ = \mathtt{1}</tex> <font color=green >// Так как мы можем начать c любого профиля</font>
 
     <tex>\mathtt{a}[0][\mathtt{i}]\ = \mathtt{1}</tex> <font color=green >// Так как мы можем начать c любого профиля</font>
 
  '''for''' <tex>\mathtt{k} = \mathtt{1}.. \mathtt{m} - \mathtt{1} </tex>
 
  '''for''' <tex>\mathtt{k} = \mathtt{1}.. \mathtt{m} - \mathtt{1} </tex>
     '''for'''  <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} \verb|<<| \ \mathtt{n}) - \mathtt{1}</tex>
+
     '''for'''  <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} << \ \mathtt{n}) - \mathtt{1}</tex>
         '''for''' <tex>\mathtt{j} = \mathtt{0}..(\mathtt{1} \verb|<<| \ \mathtt{n}) - \mathtt{1}</tex>
+
         '''for''' <tex>\mathtt{j} = \mathtt{0}..(\mathtt{1} << \ \mathtt{n}) - \mathtt{1}</tex>
 
      <tex>\mathtt{a}[\mathtt{k}][\mathtt{i}] = \mathtt{a}[\mathtt{k}][\mathtt{i}] + \mathtt{a}[\mathtt{k} - 1][\mathtt{j}] \cdot \mathtt{d}[\mathtt{j}][\mathtt{i}]</tex>   
 
      <tex>\mathtt{a}[\mathtt{k}][\mathtt{i}] = \mathtt{a}[\mathtt{k}][\mathtt{i}] + \mathtt{a}[\mathtt{k} - 1][\mathtt{j}] \cdot \mathtt{d}[\mathtt{j}][\mathtt{i}]</tex>   
 
  <tex>\mathtt{ans} = \mathtt{0}</tex>
 
  <tex>\mathtt{ans} = \mathtt{0}</tex>
  '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} \verb|<<| \ \mathtt{n}) - \mathtt{1}</tex>
+
  '''for''' <tex>\mathtt{i} = \mathtt{0}..(\mathtt{1} << \ \mathtt{n}) - \mathtt{1}</tex>
 
     <tex>\mathtt{ans} = \mathtt{ans} + \mathtt{a}[\mathtt{m} - \mathtt{1}][\mathtt{i}]</tex> <font color=green>// Так как мы можем закончить любым профилем </font>
 
     <tex>\mathtt{ans} = \mathtt{ans} + \mathtt{a}[\mathtt{m} - \mathtt{1}][\mathtt{i}]</tex> <font color=green>// Так как мы можем закончить любым профилем </font>
 
  '''return''' <tex>\mathtt{ans}</tex>
 
  '''return''' <tex>\mathtt{ans}</tex>
Строка 132: Строка 132:
 
     '''if''' <tex>\mathtt{bit}(\mathtt{p, \mathtt{i} + \mathtt{1}}) == \mathtt{0}</tex>
 
     '''if''' <tex>\mathtt{bit}(\mathtt{p, \mathtt{i} + \mathtt{1}}) == \mathtt{0}</tex>
 
         '''if''' <tex>\mathtt{i} == \mathtt{n} - \mathtt{1}</tex>
 
         '''if''' <tex>\mathtt{i} == \mathtt{n} - \mathtt{1}</tex>
             '''println'''<tex>((\mathtt{p} - (\mathtt{2} \verb|<<| \ \mathtt{i})) \verb|<<| \ \mathtt{1}</tex>, " ", <tex>\mathtt{0})</tex>
+
             '''println'''<tex>((\mathtt{p} - (\mathtt{2} << \ \mathtt{i})) << \ \mathtt{1}</tex>, " ", <tex>\mathtt{0})</tex>
 
         '''else'''
 
         '''else'''
             '''println'''<tex>(\mathtt{p} - (\mathtt{2} \verb|<<| \ \mathtt{i})</tex>, " ", <tex>\mathtt{i} + \mathtt{1})</tex>
+
             '''println'''<tex>(\mathtt{p} - (\mathtt{2} << \ \mathtt{i})</tex>, " ", <tex>\mathtt{i} + \mathtt{1})</tex>
 
     '''else'''  
 
     '''else'''  
 
         '''if''' <tex>\mathtt{bit}(\mathtt{p}, \mathtt{i}) == \mathtt{0}</tex>
 
         '''if''' <tex>\mathtt{bit}(\mathtt{p}, \mathtt{i}) == \mathtt{0}</tex>
 
             '''if''' <tex>\mathtt{i} == \mathtt{n} - \mathtt{1} </tex>
 
             '''if''' <tex>\mathtt{i} == \mathtt{n} - \mathtt{1} </tex>
                 '''println'''<tex>((\mathtt{p} \verb|<<| \ \mathtt{1})</tex>, " ", <tex>\mathtt{0})</tex>
+
                 '''println'''<tex>((\mathtt{p} << \ \mathtt{1})</tex>, " ", <tex>\mathtt{0})</tex>
 
             '''else'''   
 
             '''else'''   
                 '''println'''<tex>(\mathtt{p} + (\mathtt{1} \verb|<<| \ \mathtt{n})</tex>, " ", <tex>(\mathtt{i} + \mathtt{1}) % \mathtt{n})</tex>
+
                 '''println'''<tex>(\mathtt{p} + (\mathtt{1} << \ \mathtt{n})</tex>, " ", <tex>(\mathtt{i} + \mathtt{1}) % \mathtt{n})</tex>
 
     '''if''' <tex>\mathtt{i} < \mathtt{n} - \mathtt{1}</tex> && <tex>\mathtt{bit}(\mathtt{p, \mathtt{i} + \mathtt{2}}) == \mathtt{0}</tex>
 
     '''if''' <tex>\mathtt{i} < \mathtt{n} - \mathtt{1}</tex> && <tex>\mathtt{bit}(\mathtt{p, \mathtt{i} + \mathtt{2}}) == \mathtt{0}</tex>
             '''println'''<tex>(\mathtt{p} + (\mathtt{4} \verb|<<| \ \mathtt{i})</tex>, " ", <tex>\mathtt{i} + \mathtt{1})</tex>
+
             '''println'''<tex>(\mathtt{p} + (\mathtt{4} << \ \mathtt{i})</tex>, " ", <tex>\mathtt{i} + \mathtt{1})</tex>
 
[[Файл:ok.jpg|640px|thumb|left|Возможные переходы]]
 
[[Файл:ok.jpg|640px|thumb|left|Возможные переходы]]
  

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

Определение:
Динамическое программирование по профилю (англ. dynamic programming with profile) — способ оптимизации перебора количества вариантов с помощью динамического программирования, когда одно из измерений небольшое.


Определение:
Профиль (англ. profile) — один из столбцов (строк), удовлетворяющий условию задачи. Обычно используется в качестве состояния динамики.


Общие принципы

Обычно дана таблица и надо посчитать количество замощений этой таблицы некоторыми фигурами (замощение шахматной доски доминошками). Можно перебрать все варианты и выбрать из них удовлетворяющие условию. Но можно воспользоваться методом динамического программирования по профилю и сократить время по одной размерности до линейной. Затем пусть у нас есть правило по которому надо заполнить и для него нам надо k предыдущих линий. Тогда можно перебрать все замощения длиной k×n. В итоге нужно заполнить данную таблицу этими замощениями. Получается, что если перебирать все варианты нам понадобится O(anm) времени, а если перебирать только состояния и переходить по ним нам потребуется O(aknm) времени (где a — количество способов замощения одной клетки).

Задача о замощении домино

Условие

Найти количество способов замостить таблицу n×m с помощью доминошек размерами 1×2,2×1.

Решение

Переходы (1-правильный переход, 2,3-неправильные)

Для удобства можно хранить профили в виде двоичных масок. В качестве состояния динамики будем использовать профили размерами n. В этом профиле 1 будет означать, что домино лежит горизонтально и заканчивается на этом столбце, иначе 0. Таких профилей будет 2n. Теперь проверим из какого профиля в какой можно перейти.

Из профиля i в профиль j можно перейти если выполняются условия:

  • Можно положить горизонтальные домино. То есть там где в j профиле стоит 1, в i профиле должен стоять 0.
  • Можно доложить в оставшиеся клетки вертикальные домино. То есть оставшиеся 0 в i профиле должны образовывать четные подстроки.

Пусть d[i][j]=1 если из профиля i можно перейти в j-ый, иначе 0.

Пусть так же a[k][i] — количество способов замощения первых k1 столбцов и заканчивавшийся на i-ом профиле. Тогда a[k][i]=2n1j=0a[k1][j]d[j][i]

Ответом будет a[m][i], где i — профиль, который может быть последним (т.е. все группы из 0 имеют четные размеры).

Реализация

// n, m — размер таблицы  
  for i=0..(1<< n)1
    for j=0..(1<< n)1
        if можно перейти из i в j профиль 
           d[i][j]=1
	else 
	    d[i][j]=0
a[0][0]=1 // Так как мы можем начать только с профиля где все клетки 0  
for k=1..m1
    for i=0..(1<< n)1
        for j=0..(1<< n)1
	    a[k][i]=a[k][i]+a[k1][j]d[j][i]
ans=0
for i=0..(1<< n)1
    if можно закончить i профилем
        ans=ans+a[m1][i]
return ans

Оценка сложности: подсчет d22n , и подсчет a22nm в итоге O(22nm).

Оценка памяти: O(22n+22nm), так же можно заметить что в массиве a для k состояния нам нужно только k1 состояние, при такой реализации нужно будет O(22n). Еще можно не считать массив d, а просто каждый раз перепроверять можем ли мы перейти в это состояние в итоге потребуется O(2n+1) памяти, но нам потребуется больше времени 22nmf(i,j), где f(i,j) время проверки возможности перехода из i в j равно n и тогда время получается O(22nmn).

Задача о симпатичных узорах

Условие

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

Симпатичне узоры.png

Решение

Для удобства можно хранить профиля в виде двоичных масок. В качестве состояния динамики будем использовать профили размера n. В этом профиле 1 будет означать что клетка закрашена в черный цвет, и 0 если в белый. Из профиля i в j-ый можно перейти если выполнено условие:

  • если поставить i и j профиль рядом, то не должно быть квадратов 2×2 одного цвета

Пусть d[i][j]=1 если из профиля i можно перейти в j-ый, иначе 0.

Пусть так же a[k][i] — количество способов раскрашивания первые k1 столбцов и заканчивавшийся на i-ом профиле. Тогда a[k][i]=2n1j=0a[k1][j]d[j][i]

Ответом будет 2n1i=0a[m][i]

Реализация

// n, m — размер таблицы   
for i=0..(1<< n)1
    for j=0..(1<< n)1
        if можно перейти из i в j профиль 
            d[i][j] = 1
	else
	    d[i][j] = 0
for i=0..(1<< n)1
    a[0][i] =1 // Так как мы можем начать c любого профиля
for k=1..m1
    for  i=0..(1<< n)1
        for j=0..(1<< n)1
	     a[k][i]=a[k][i]+a[k1][j]d[j][i]  
ans=0
for i=0..(1<< n)1
    ans=ans+a[m1][i] // Так как мы можем закончить любым профилем 
return ans

Оценка сложности: подсчет d22n , и подсчет a22nm в итоге O(22nm).

Оценка памяти: O(22n+22nm), так же можно заметить что в массиве a для k состояния нам нужно только k1 состояние, при такой реализации нужно будет O(22n). Еще можно не считать массив d, а просто каждый раз перепроверять можем ли мы перейти в это состояние в итоге потребуется O(2n+1) памяти, но нам потребуется больше времени 22nmf(i,j), где f(i,j) время проверки возможности перехода из i в j равно n и тогда время получается O(22nmn).

Динамика по изломанному профилю

Определение:
Изломанный профиль (англ. broken profile) — обобщение прямого профиля на случай, когда обработанным является не целое число столбцов, а некоторое количество столбцов и несколько первых клеток следующего столбца.


Это очень сильная оптимизация динамики по профилю. Идея в том, чтобы добиться как можно меньшего числа переходов (от одного профиля к другому).

Пример

Еще раз используем в качестве примера задачу о замощении. Базовая линия теперь будет ломаной: при прохождении через i-ю вертикаль сверху вниз, она переходит на предыдущую вертикаль и спускается до низу. Теперь профиль — это не только маска, но ещё и место излома.

Img34.gif

Профилем будет пара (p,i), в p будет информация о n+1 маленьком квадратике слева от базовой линии, имеющем с ней общие точки; i обозначает номер горизонтали, на которой произошел излом. Квадратики профиля будут нумероваться сверху вниз, так что угловой будет иметь номер i+1. Горизонтали будем нумеровать с нуля, так что i пробегает значения от 0 до n1.

Пусть d[pr1][pr2]=1 если из профиля pr1 = (p1,i1) можно перейти в pr2=(p2,i2), иначе 0.

  • Eсли i<n1, то i1+1=i2, иначе i2=0;
  • Mожно так положить доминошку, накрывающую квадратик с номером i+1, что после этого в p2 будет храниться в точности информация о соответствующих квадратиках.

Проще говоря, доминошку можно класть только двумя способами — как показано на рисунках (на квадратик с номером i+1 можно положить не более одной вертикальной и горизонтальной доминошки). То, что потом получается после сдвига вниз излома, и будет новым профилем. Заметим, что если клетка i+1 занята, то доминошку уже не надо класть, и (p,i) логично отождествить с (p,i+1). Условно пишется — "i+1", однако, нужно всегда иметь в виду возможность i=n1.

Легко заметить, что количество профилей увеличилось в 2n раз (добавилось число от 1 до n и еще один бит). Но зато количество переходов резко сократилось с 2n до 2.

//Для профиля (p, i) выводятся все переходы из него (нумерация горизонталей начинается с нуля и i = 0..n - 1)
// Функция bit(x,i), возвращающая единицу или ноль или i-й бит в двоичной записи числа x
print_all_links(p, i):
    if bit(p,i+1)==0
        if i==n1
            println((p(2<< i))<< 1, " ", 0)
        else
            println(p(2<< i), " ", i+1)
    else 
        if bit(p,i)==0
            if i==n1
               println((p<< 1), " ", 0)
            else   
               println(p+(1<< n), " ", (i+1)
    if i<n1 && bit(p,i+2)==0
            println(p+(4<< i), " ", i+1)
Возможные переходы

При такой реализации существует немало профилей только с одним переходом (например, у которых (i+1)-й бит равен единице). Отождествим все профили с один переходом с теми, кто их них получается. Это будет выглядеть так: пусть pr2 (и только он) получается из pr1, который, в свою очередь, получается из pr0. Тогда имеются такие соотношения: d[pr0,pr1]=1, d[pr1,pr2]=1. Отождествить pr1 и pr2 — это, по сути, заменить эти два соотношение на одно, то есть теперь d[pr0,pr1]=0 и d[pr1,pr2]=0, но d[pr0,pr2]=1, и так далее.

Таким образом, возможно сокращение профилей не менее чем вдвое. Хотя можно совершить дальнейшие оптимизации.

В итоге асимптотика составляет O(2nnm). Это доказывает, что данный метод значительно лучше простого способа подсчёта динамики.

См. также

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