web-dev-qa-db-fra.com

Mise en œuvre de la justification de texte avec la programmation dynamique

J'essaie de comprendre le concept de programmation dynamique via le cours sur MIT OCW here . L’explication de la vidéo OCW est excellente, mais j’ai l’impression que je ne la comprends pas vraiment tant que j’ai incorporé l’explication dans le code. Lors de la mise en œuvre, je me réfère à certaines notes de la note de conférence ici , en particulier à la page 3 de la note.

Le problème est que je ne sais pas comment traduire une partie de la notation mathématique en code. Voici une partie de la solution que j'ai mise en œuvre (et pensez l'avoir mise en œuvre correctement):

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)

Maintenant, la partie que je ne comprends pas se trouve aux points 3 à 5 des notes de cours. Je ne comprends littéralement pas et je ne sais pas par où commencer pour les mettre en œuvre. Jusqu'ici, j'ai essayé d'itérer la liste de mots et de compter le mal de chacun des prétendus fins de lignes, comme ceci:

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?

Mais alors, je ne sais pas comment je peux continuer la fonction, et pour être honnête, je ne comprends pas cette ligne:

dag = [badness(paragraph[i:j], page_width) + justifier(paragraph[j:], page_width) for j in range(i + 1, par_len + 1)] 

et comment je renverrai justifier en tant que int (puisque j’ai déjà décidé de stocker la valeur de retour dans result, qui est une liste. Devrais-je créer une autre fonction et renvoyer à partir de là? Devrait-il y avoir une récursion?

Pourriez-vous s'il vous plaît me montrer quoi faire ensuite et expliquer en quoi c'est une programmation dynamique? Je ne vois vraiment pas où se trouve la récursivité ni quel est le sous-problème.

Merci avant.

18
bertzzie

Si vous avez du mal à comprendre l’idée de base de la programmation dynamique, voici mon point de vue:

La programmation dynamique sacrifie essentiellement complexité d'espace pour complexité temporelle (mais l'espace supplémentaire que vous utilisez est généralement très peu comparé au temps que vous gagnez, rendant la programmation dynamique totalement vaut la peine si mis en œuvre correctement). Vous stockez les valeurs de chaque appel récursif au fur et à mesure (dans un tableau ou un dictionnaire, par exemple), ce qui vous permet d'éviter de procéder à un nouveau calcul lorsque vous rencontrez le même appel récursif dans une autre branche de l'arborescence de la récursivité.

Et non, vous not devez utiliser la récursivité. Voici ma mise en œuvre de la question sur laquelle vous travailliez en utilisant simplement des boucles. J'ai suivi très attentivement le TextAlignment.pdf lié par AlexSilva. J'espère que vous trouvez cela utile.

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
19
Joohwan

Pour ceux qui sont encore intéressés par ceci: La clé est de revenir en arrière à partir de la fin du texte (comme mentionné ici ). Si vous le faites, vous ne faites que comparer des éléments déjà mémorisés.

Dites, words est une liste de chaînes à emballer selon textwidth. Ensuite, dans la notation de la conférence, la tâche se réduit à trois lignes de code:

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)])

Avec:

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

Il mentionne que l'on peut ajouter une seconde liste pour suivre les positions de rupture. Vous pouvez le faire en modifiant le code en:

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]

Pour récupérer le texte, utilisez simplement la liste des positions de rupture:

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

Résultat: (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.

On pourrait être tenté d’ajouter des espaces. C'est assez délicat (car on peut trouver diverses règles esthétiques) mais un essai naïf pourrait être:

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

Qu'est-ce qui vous donne: (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.
4
Suuuehgi

je viens de voir la conférence et je pensais mettre ici tout ce que je pouvais comprendre. J'ai mis dans le code dans le même format que celui de l'interrogateur. J'ai utilisé la récursion ici, comme l'explique la conférence.
Le point 3 définit la récurrence. Il s’agit en gros d’une approche de fond, dans laquelle vous calculez plus tôt une valeur de la fonction se rapportant à une entrée plus élevée, puis vous l’utilisez pour calculer l’entrée de la valeur la plus basse.
La conférence explique comme suit:
DP (i) = min (DP (j) + badness (i, j))
pour j qui varie de i + 1 à n.
Ici, je varie de n à 0 (de bas en haut!).
En tant que DP (n) = 0,
DP (n-1) = DP (n) + méchanceté (n-1, n)
et ensuite vous calculez D(n-2) à partir de D(n-1) et D(n) et prenez un minimum d'eux.
De cette façon, vous pouvez descendre jusqu'à i = 0 et c'est la réponse finale du mal!
Au point 4, comme vous pouvez le constater, deux boucles se déroulent ici. Un pour moi et l'autre à l'intérieur je pour j.
Par conséquent, lorsque i = 0, j(max) = n, i = 1, j(max) = n-1, ... i = n , j(max) = 0.
donc temps total = addition de ceux-ci = n (n + 1)/2.
D'où O (n ^ 2).
Le point 5 identifie simplement la solution que DP [0]!
J'espère que cela t'aides!

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 __== '__main__':
    main()
1
Rindojiterika

C'est ce que je pense selon votre définition.

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]
0
user6043912

Implémentation Java Etant donné la largeur de trait maximale L, l'idée de justifier le texte T est de considérer tous les suffixes du texte (considérer les mots au lieu des caractères pour former des suffixes de manière précise). La programmation dynamique n'est rien d'autre que "Attention minutieuse" ". Si vous envisagez l'approche de la force brute, vous devez procéder comme suit.

  1. pensez à mettre 1, 2, .. n mots à la première ligne.
  2. pour chaque cas décrit dans le cas 1 (par exemple, les mots sont placés dans la ligne 1), considérons les cas où vous mettez 1, 2, .. n -i mots dans la deuxième ligne, puis les mots restants dans la troisième ligne, etc.

Au lieu de cela, considérons simplement le problème pour déterminer le coût de la mise d'un mot au début d'une ligne. En général, nous pouvons définir DP (i) comme étant le coût pour considérer le (i-1) mot comme le début d'une ligne.

Comment pouvons-nous former une relation de récurrence pour DP (i)?

Si jth Word est le début de la ligne suivante, la ligne en cours contiendra les mots [i: j) (j exclusif) et le coût du jth Word correspondant au début de la ligne suivante sera DP (j). D'où DP (i) = DP (j) + coût de l'insertion des mots [i: j) dans la ligne courante. Comme nous voulons minimiser le coût total, DP (i) peut être défini comme suit.

Relation réccurente:

DP (i) = min {DP (j) + coût d’ajout de mots [i: j dans la ligne courante} pour tout j dans [i + 1, n]

Remarque j = n signifie qu'il ne reste plus de mots à insérer dans la ligne suivante.

Le cas de base: DP (n) = 0 => à ce stade, il ne reste plus de mot à écrire.

Pour résumer:

  1. Sous-problèmes: suffixes, mots [: i]
  2. Devinez: par où commencer la ligne suivante, # de choix n - i -> O (n)
  3. Récurrence: DP (i) = min {DP (j) + coût d’insertion des mots [i: j) dans la ligne actuelle} Si nous utilisons la mémorisation, l’expression à l’intérieur de l’accolade devrait prendre O(1) heure, et la boucle est exécutée O(n) fois (nombre de fois). i Varie de n à 0 => La complexité totale est donc ramenée à O (n ^ 2).

Maintenant, même si nous avons calculé le coût minimum pour justifier le texte, nous devons également résoudre le problème initial en gardant une trace de la valeur j définie comme minimum dans l'expression ci-dessus, afin que nous puissions ensuite utiliser le même résultat pour imprimer le texte justifié. texte. L'idée est de garder le pointeur parent.

J'espère que cela vous aide à comprendre la solution. Ci-dessous, la simple mise en œuvre de l'idée ci-dessus.

 public class TextJustify {
    class IntPair {
        //The cost or badness
        final int x;

        //The index of Word at the beginning of a line
        final int y;
        IntPair(int x, int y) {this.x=x;this.y=y;}
    }
    public List<String> fullJustify(String[] words, int L) {
        IntPair[] memo = new IntPair[words.length + 1];

        //Base case
        memo[words.length] = new IntPair(0, 0);


        for(int i = words.length - 1; i >= 0; i--) {
            int score = Integer.MAX_VALUE;
            int nextLineIndex = i + 1;
            for(int j = i + 1; j <= words.length; j++) {
                int badness = calcBadness(words, i, j, L);
                if(badness < 0 || badness == Integer.MAX_VALUE) break;
                int currScore = badness + memo[j].x;
                if(currScore < 0 || currScore == Integer.MAX_VALUE) break;
                if(score > currScore) {
                    score = currScore;
                    nextLineIndex = j;
                }
            }
            memo[i] = new IntPair(score, nextLineIndex);
        }

        List<String> result = new ArrayList<>();
        int i = 0;
        while(i < words.length) {
            String line = getLine(words, i, memo[i].y);
            result.add(line);
            i = memo[i].y;
        }
        return result;
    }

    private int calcBadness(String[] words, int start, int end, int width) {
        int length = 0;
        for(int i = start; i < end; i++) {
            length += words[i].length();
            if(length > width) return Integer.MAX_VALUE;
            length++;
        }
        length--;
        int temp = width - length;
        return temp * temp;
    }


    private String getLine(String[] words, int start, int end) {
        StringBuilder sb = new StringBuilder();
        for(int i = start; i < end - 1; i++) {
            sb.append(words[i] + " ");
        }
        sb.append(words[end - 1]);

        return sb.toString();
    }
  }
0
self_noted