Реализация выравнивания текста с помощью динамического программирования
Я пытаюсь понять концепцию динамического программирования через курс MIT OCW здесь. Объяснение по видео в OCW великолепен и все, но я чувствую, что не понимаю его до тех пор, пока не внедрил объяснение в код. Во время реализации я ссылаюсь на некоторые примечания из лекции здесь, особенно на стр. 3 примечания.
Проблема в том, что я не знаю, как перевести некоторые математические обозначения в код. Вот часть решения, которое я реализовал (и думаю, что оно реализовано правильно):
import math
paragraph = "Some long lorem ipsum text."
words = paragraph.split(" ")
# Count total length for all strings in a list of strings.
# This function will be used by the badness function below.
def total_length(str_arr):
total = 0
for string in str_arr:
total = total + len(string)
total = total + len(str_arr) # spaces
return total
# Calculate the badness score for a word.
# str_arr is assumed be send as word[i:j] as in the notes
# we don't make i and j as argument since it will require
# global vars then.
def badness(str_arr, page_width):
line_len = total_length(str_arr)
if line_len > page_width:
return float('nan')
else:
return math.pow(page_width - line_len, 3)
Теперь часть, которую я не понимаю, находится в пунктах 3-5 в лекциях. Я буквально не понимаю и не знаю, с чего начать их внедрять. До сих пор я пробовал перебирать список слов и считал, что каждый из них якобы заканчивается, например:
def justifier(str_arr, page_width):
paragraph = str_arr
par_len = len(paragraph)
result = [] # stores each line as list of strings
for i in range(0, par_len):
if i == (par_len - 1):
result.append(paragraph)
else:
dag = [badness(paragraph[i:j], page_width) + justifier(paragraph[j:], page_width) for j in range(i + 1, par_len + 1)]
# Should I do a min(dag), get the index, and declares it as end of line?
Но тогда я не знаю, как я могу продолжить эту функцию, и, честно говоря, я не понимаю эту строку:
dag = [badness(paragraph[i:j], page_width) + justifier(paragraph[j:], page_width) for j in range(i + 1, par_len + 1)]
и как я вернусь justifier
как int
(так как я уже решил сохранить возвращаемое значение в result
, что является списком. Должен ли я делать еще одну функцию и рекурсировать оттуда? рекурсия вообще?
Не могли бы вы показать мне, что делать дальше, и объяснить, как это динамическое программирование? Я действительно не вижу, где рекурсия, и что такое подзадача.
Спасибо раньше.
Ответы
Ответ 1
Если у вас возникли проблемы с пониманием основной идеи самого динамического программирования, здесь я беру на себя это:
Динамическое программирование по существу приносит в жертву сложность пространства для временной сложности (но дополнительное пространство, которое вы используете, обычно очень мало по сравнению с тем временем, которое вы сохраняете, делая динамическое программирование вполне оправданным, если оно выполнено правильно). Вы сохраняете значения из каждого рекурсивного вызова по ходу (например, в массиве или в словаре), поэтому во втором случае вы можете избежать вычислений при выполнении того же рекурсивного вызова в другой ветке дерева рекурсии.
И нет, вам не нужно использовать рекурсию. Вот моя реализация вопроса, над которым вы работали, используя только циклы. Я внимательно следил за TextAlignment.pdf, связанным с AlexSilva. Надеюсь, вы найдете это полезным.
def length(wordLengths, i, j):
return sum(wordLengths[i- 1:j]) + j - i + 1
def breakLine(text, L):
# wl = lengths of words
wl = [len(word) for word in text.split()]
# n = number of words in the text
n = len(wl)
# total badness of a text l1 ... li
m = dict()
# initialization
m[0] = 0
# auxiliary array
s = dict()
# the actual algorithm
for i in range(1, n + 1):
sums = dict()
k = i
while (length(wl, k, i) <= L and k > 0):
sums[(L - length(wl, k, i))**3 + m[k - 1]] = k
k -= 1
m[i] = min(sums)
s[i] = sums[min(sums)]
# actually do the splitting by working backwords
line = 1
while n > 1:
print("line " + str(line) + ": " + str(s[n]) + "->" + str(n))
n = s[n] - 1
line += 1
Ответ 2
Для кого-то еще заинтересованного в этом: ключ должен двигаться назад от конца текста (как упоминалось здесь).
Если вы это сделаете, вы просто сравните уже запомненные элементы.
Скажем, words
- список строк, которые должны быть завернуты в соответствии с textwidth
. Затем в обозначениях лекции задача сводится к трем строкам кода:
import numpy as np
textwidth = 80
DP = [0]*(len(words)+1)
for i in range(len(words)-1,-1,-1):
DP[i] = np.min([DP[j] + badness(words[i:j],textwidth) for j in range(i+1,len(words)+1)])
С
def badness(line,textwidth):
# Number of gaps
length_line = len(line) - 1
for word in line:
length_line += len(word)
if length_line > textwidth: return float('inf')
return ( textwidth - length_line )**3
Он упоминает, что можно добавить второй список, чтобы отслеживать позиции нарушения. Вы можете сделать это, изменив код на:
DP = [0]*(len(words)+1)
breaks = [0]*(len(words)+1)
for i in range(len(words)-1,-1,-1):
temp = [DP[j] + badness(words[i:j],args.textwidth) for j in range(i+1,len(words)+1)]
index = np.argmin(temp)
# Index plus position in upper list
breaks[i] = index + i + 1
DP[i] = temp[index]
Чтобы восстановить текст, просто используйте список ломающихся позиций:
def reconstruct_text(words,breaks):
lines = []
linebreaks = []
i = 0
while True:
linebreaks.append(breaks[i])
i = breaks[i]
if i == len(words):
linebreaks.append(0)
break
for i in range( len(linebreaks) ):
lines.append( ' '.join( words[ linebreaks[i-1] : linebreaks[i] ] ).strip() )
return lines
Результат: (text = reconstruct_text(words,breaks)
)
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit
amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed
diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Возможно, у вас возникнет соблазн добавить некоторые пробелы. Это довольно сложно (поскольку можно придумать различные эстетические правила), но наивная попытка может быть:
import re
def spacing(text,textwidth,maxspace=4):
for i in range(len(text)):
length_line = len(text[i])
if length_line < textwidth:
status_length = length_line
whitespaces_remain = textwidth - status_length
Nwhitespaces = text[i].count(' ')
# If whitespaces (to add) per whitespace exeeds
# maxspace, don't do anything.
if whitespaces_remain/Nwhitespaces > maxspace-1:pass
else:
text[i] = text[i].replace(' ',' '*( 1 + int(whitespaces_remain/Nwhitespaces)) )
status_length = len(text[i])
# Periods have highest priority for whitespace insertion
periods = text[i].split('.')
# Can we add a whitespace behind each period?
if len(periods) - 1 + status_length <= textwidth:
text[i] = '. '.join(periods).strip()
status_length = len(text[i])
whitespaces_remain = textwidth - status_length
Nwords = len(text[i].split())
Ngaps = Nwords - 1
if whitespaces_remain != 0:factor = Ngaps / whitespaces_remain
# List of whitespaces in line i
gaps = re.findall('\s+', text[i])
temp = text[i].split()
for k in range(Ngaps):
temp[k] = ''.join([temp[k],gaps[k]])
for j in range(whitespaces_remain):
if status_length >= textwidth:pass
else:
replace = temp[int(factor*j)]
replace = ''.join([replace, " "])
temp[int(factor*j)] = replace
text[i] = ''.join(temp)
return text
Что дает: (text = spacing(text,textwidth)
)
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit
amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed
diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Ответ 3
Это то, что я думаю в соответствии с вашим определением.
import math
class Text(object):
def __init__(self, words, width):
self.words = words
self.page_width = width
self.str_arr = words
self.memo = {}
def total_length(self, str):
total = 0
for string in str:
total = total + len(string)
total = total + len(str) # spaces
return total
def badness(self, str):
line_len = self.total_length(str)
if line_len > self.page_width:
return float('nan')
else:
return math.pow(self.page_width - line_len, 3)
def dp(self):
n = len(self.str_arr)
self.memo[n-1] = 0
return self.judge(0)
def judge(self, i):
if i in self.memo:
return self.memo[i]
self.memo[i] = float('inf')
for j in range(i+1, len(self.str_arr)):
bad = self.judge(j) + self.badness(self.str_arr[i:j])
if bad < self.memo[i]:
self.memo[i] = bad
return self.memo[i]
Ответ 4
Я только что увидел лекцию, и мысль была бы поставлена здесь, что я мог понять. Я поставил код в том же формате, что и вопросщика. Я использовал рекурсию здесь, как объяснила лекция.
Точка № 3 определяет повторение. Это, в основном, дно для приближения, где вы вычисляете значение функции, относящееся к более высокому входу раньше, а затем используйте его для вычисления для более низкого значения входа.
Лекция объясняет это как:
DP (i) = min (DP (j) + badness (i, j))
для j, которая изменяется от я + 1 до n.
Здесь я варьируется от n до 0 (снизу вверх!).
Поскольку DP (n) = 0,
DP (n-1) = DP (n) + badness (n-1, n)
и затем вы вычисляете D (n-2) из D (n-1) и D (n) и берете минимум из них.
Таким образом, вы можете спуститься до я = 0 и получить окончательный ответ о плохом настроении!
В пункте № 4, как вы можете видеть, здесь есть две петли. Один для я и другой внутри я для j.
Следовательно, когда я = 0, j (max) = n, я = 1, j (max) = n-1,... я = n, j (max) = 0.
следовательно, общее время = добавление этих чисел = n (n + 1)/2.
Следовательно, O (n ^ 2).
Точка №5 просто определяет решение, которое DP [0]!
Надеюсь это поможет!
import math
justification_map = {}
min_map = {}
def total_length(str_arr):
total = 0
for string in str_arr:
total = total + len(string)
total = total + len(str_arr) - 1 # spaces
return total
def badness(str_arr, page_width):
line_len = total_length(str_arr)
if line_len > page_width:
return float('nan')
else:
return math.pow(page_width - line_len, 3)
def justify(i, n, words, page_width):
if i == n:
return 0
ans = []
for j in range(i+1, n+1):
#ans.append(justify(j, n, words, page_width)+ badness(words[i:j], page_width))
ans.append(justification_map[j]+ badness(words[i:j], page_width))
min_map[i] = ans.index(min(ans)) + 1
return min(ans)
def main():
print "Enter page width"
page_width = input()
print "Enter text"
paragraph = input()
words = paragraph.split(' ')
n = len(words)
#justification_map[n] = 0
for i in reversed(range(n+1)):
justification_map[i] = justify(i, n, words, page_width)
print "Minimum badness achieved: ", justification_map[0]
key = 0
while(key <n):
key = key + min_map[key]
print key
if __name__ == '__main__':
main()