<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Programação dinâmica - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Aprenda a codificar - de graça. Tutoriais de programação em Python, JavaScript, Linux e muito mais. ]]>
        </description>
        <link>https://www.freecodecamp.org/portuguese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Programação dinâmica - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/portuguese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 29 May 2026 16:05:38 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/portuguese/news/tag/programacao-dinamica/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Desmistificando a programação dinâmica ]]>
                </title>
                <description>
                    <![CDATA[ Escrito por: Alaina Kafkes Como criar e programar algoritmos em programação dinâmica Talvez você tenha ouvido falar sobre isso na preparação para entrevistas de programação. Talvez você a tenha enfrentado em um curso de algoritmos. Talvez você esteja tentando aprender a programar por conta própria e tenha descoberto em algum ]]>
                </description>
                <link>https://www.freecodecamp.org/portuguese/news/desmistificando-a-programacao-dinamica/</link>
                <guid isPermaLink="false">66bb568783dd720400e0d6c9</guid>
                
                    <category>
                        <![CDATA[ Programação dinâmica ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Daniel Rosa ]]>
                </dc:creator>
                <pubDate>Mon, 26 Aug 2024 01:03:40 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/portuguese/news/content/images/2024/08/1_7QbvB25maQRxi7LGYOAfYw.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>Artigo original:</strong> <a href="https://www.freecodecamp.org/news/demystifying-dynamic-programming-3efafb8d4296/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Demystifying Dynamic Programming</a>
      </p><p>Escrito por: Alaina Kafkes</p><h4 id="como-criar-e-programar-algoritmos-em-programa-o-din-mica"><strong>Como criar e programar algoritmos em programação dinâmica</strong></h4><p>Talvez você tenha ouvido falar sobre isso na preparação para entrevistas de programação. Talvez você a tenha enfrentado em um curso de algoritmos. Talvez você esteja tentando aprender a programar por conta própria e tenha descoberto em algum lugar que é importante entender a programação dinâmica. Usar programação dinâmica (ou <em>DP</em>, do inglês, <em>dynamic programming</em>) para escrever algoritmos é importante e, ao mesmo tempo, assustador.</p><p>Quem pode culpar aqueles que se esquivam dela? A programação dinâmica parece intimidante porque é mal ensinada. Muitos tutoriais se concentram no resultado – <em>explicando</em> o algoritmo, em vez do processo – que é <em>encontrar</em> o algoritmo . Isso incentiva os alunos a decorar/memorizar , não a compreender.</p><p>Durante minha aula de algoritmos este ano, montei meu próprio processo para resolver problemas que exigem programação dinâmica. Partes desse processo vêm do <a href="https://sites.northwestern.edu/hartline/">meu professor de algoritmos</a> (a quem eu devo muito por esse entendimento!) e partes vêm da minha própria análise aprofundada de algoritmos de programação dinâmica.</p><p>Antes de compartilhar meu processo, no entanto, vamos começar com o básico. O que é a programação dinâmica?</p><h3 id="defini-o-de-programa-o-din-mica"><strong>Definição de programação dinâmica</strong></h3><p>A programação dinâmica tem a ver com <strong>dividir um problema de otimização</strong> em subproblemas mais simples e <strong>armazenar a solução para cada subproblema</strong> de modo que cada subproblema seja resolvido apenas uma vez.</p><p>Para falar a verdade, essa definição pode não fazer todo o sentido até que você veja um exemplo de um subproblema. Tudo bem. Esse exemplo virá na próxima seção.</p><p>O que espero transmitir é que a DP é uma técnica útil para problemas de otimização, aqueles problemas que buscam a solução máxima ou mínima dadas certas restrições, porque examina todos os subproblemas possíveis e nunca recalcula a solução para qualquer subproblema. Isso garante correção e eficiência, o que não podemos dizer da maioria das técnicas usadas para algoritmos de resolução ou tratamento de um problema. Isso, por si só, torna a DP especial.</p><p>Nas próximas duas seções, explicarei o que é um <strong>subproblema</strong> e, em seguida, explicarei por que armazenar soluções — uma técnica conhecida como <strong>memoização</strong> — é importante na programação dinâmica.</p><h3 id="subproblemas-em-subproblemas-em-subproblemas"><strong>Subproblemas em subproblemas em subproblemas</strong></h3><p>Os subproblemas são versões menores do problema original. Na verdade, os subproblemas geralmente parecem uma versão reformulada do problema original. Se formulados corretamente, os subproblemas se acumulam para obter a solução para o problema original.</p><p>Para dar uma ideia melhor de como isso funciona, vamos encontrar o subproblema em um exemplo de problema de programação dinâmica.</p><p>Faça de conta que você está de volta à década de 1950 trabalhando em um computador IBM-650. Você sabe o que isso significa – cartões perfurados! Seu trabalho é lidar com o IBM-650 por um dia. Você recebe um número natural <em>n</em> de cartões perfurados para executar. Cada cartão perfurado <em>deve </em>ser executado em algum horário de início predeterminado <em>s_i</em> e parar de funcionar em algum horário de término predeterminado <em>f_i</em>. Apenas um cartão perfurado pode ser executado no IBM-650 de cada vez. Cada cartão perfurado também tem um valor associado <em>v_i </em>com base em sua importância para a empresa.</p><p><strong>Problema</strong>: como responsável pelo IBM-650, você deve determinar o cronograma ideal de cartões perfurados que maximize o valor total de todos os cartões perfurados executados.</p><p>Como vou passar por esse exemplo em detalhes no decorrer deste artigo, vou apenas provocá-lo com seu subproblema por enquanto:</p><p><strong>Subproblema</strong>: estabelecer o cronograma de valor máximo para cartões perfurados <em>de i</em> a <em>n</em>, de modo que os cartões perfurados sejam classificados por hora de início.</p><p>Observe como o subproblema divide o problema original em componentes que constroem a solução. Com o subproblema, você pode encontrar o cronograma de valor máximo para cartões perfurados <em>n-1</em> a <em>n </em>e, em seguida, para cartões perfurados <em>n-2</em> a <em>n </em>e assim por diante. Ao encontrar as soluções para cada subproblema, você pode resolver o problema original em si: o cronograma de valor máximo para cartões perfurados de 1 a <em>n</em>. Como o subproblema se parece com o problema original, os subproblemas podem ser usados para resolver o problema original.</p><p>Na programação dinâmica, depois de resolver cada subproblema, você deve memorizá-lo ou armazená-lo. Vamos descobrir o porquê na seção a seguir.</p><h3 id="motiva-o-para-a-memoiza-o-com-os-n-meros-da-sequ-ncia-de-fibonacci"><strong>Motivação para a <em>memoização</em> com os números da sequência de Fibonacci</strong></h3><p>Ao receber a instrução de implementar um algoritmo que calcule o <a href="https://www.mathsisfun.com/numbers/fibonacci-sequence.html" rel="noopener">valor de Fibonacci</a> (texto em inglês) para determinado número, o que você faria? A maioria das pessoas que eu conheço escolheria um <a href="https://softwareengineering.stackexchange.com/questions/25052/in-plain-english-what-is-recursion" rel="noopener">algoritmo recursivo</a> (texto em inglês). Um algoritmo assim teria esta aparência em Python:</p><pre><code>def fibonacciVal(n):
  if n == 0:
    return 0
  elif n == 1:
    return 1
  else:
    return fibonacciVal(n-1) + fibonacciVal(n-2)</code></pre><p>Esse é um algoritmo que alcança seu objetivo, mas o custo para isso é <em>enorme</em>. Por exemplo, vamos examinar o que o algoritmo faz quando precisa calcular o valor de Fibonacci para n = 5 (abreviado como F(5)).</p><p>Cada cálculo deve ser feito na ordem para encontrar o valor de Fibonacci para n = 5. O subproblema para n = 2, por exemplo, é resolvido <strong><strong>t</strong>rês vezes<strong>.</strong></strong> Para um exemplo relativamente pequeno (n = 5), é uma quantidade de cálculos excessivamente repetida e desperdiçada!</p><p>Se, ao invés de calcular o valor de Fibonacci para n = 2 três vezes, criássemos um algoritmo que calcula o valor uma vez, armazena-o e acessa o valor armazenado para toda e qualquer ocorrência posterior de n = 2, teríamos uma melhoria e tanto, não? É <em><em>exa</em>tamente</em> isso que a <em><strong>memoização</strong></em> faz.</p><p>Com isso em mente, escrevi uma solução usando programação dinâmica para o problema do valor de Fibonacci:</p><pre><code>def fibonacciVal(n):
  memo = [0] * (n+1)
  memo[0], memo[1] = 0, 1
  for i in range(2, n+1):
    memo[i] = memo[i-1] + memo[i-2]
    return memo[n]</code></pre><p>Perceba como a solução do valor de retorno vem do <em>array </em>de <em>memoização</em> memo[], que é preenchido iterativamente pelo <em>loop</em> for. "Iterativamente", neste caso, significa que memo[2] é calculado e armazenado antes de memo[3], memo[4], … e memo[<em><em>n</em></em>]. Como memo[ ] é preenchido nessa ordem, a solução para cada subproblema (n = 3) pode ser resolvida pelas soluções de seus subproblemas anteriores (n = 2 e n = 1), dado que esses valores já foram armazenados em memo[] anteriormente.</p><p>A memoização tem como vantagem tornar desnecessário o recálculo, o que torna o algoritmo mais eficiente. Desse modo, a memoização garante que a programação dinâmica é eficiente, mas é a escolha do subproblema certo que garante que um "programa dinâmico" examine todas as possibilidades para encontrar a melhor.</p><p>Agora que tratamos da memoização e dos subproblemas, é o momento de aprender o processo da programação dinâmica em si. Aperte o cinto.</p><h3 id="meu-processo-de-programa-o-din-mica"><strong>Meu processo de programação dinâmica</strong></h3><h4 id="passo-1-identificar-o-subproblema-em-palavras-"><strong>Passo 1: identificar o subproblema em palavras.</strong></h4><p>Com frequência, os programadores começarão a escrever o código <em>antes</em> de exercitar seu pensamento crítico sobre o problema. Isso não é bom. Uma estratégia para botar seu cérebro para funcionar antes mesmo de tocar no teclado é usar palavras no seu próprio idioma para descrever o subproblema que você identificou dentro do problema original.</p><p>Se estiver resolvendo um problema que precise da programação dinâmica, pegue um pedaço de papel e pense nas informações necessárias para resolver o problema. Escreva o subproblema tendo isso em consideração.</p><p>Por exemplo, no problema dos cartões perfurados, afirmei que o subproblema pode ser escrito como "o cronograma de valor máximo para cartões perfurados de <em><em>i</em></em> até <em><em>n</em></em>, de modo que os cartões sejam ordenados de acordo com o tempo de início". Eu encontrei esse subproblema ao me dar conta de que, para determinar o cronograma de valor máximo para os cartões perfurados de 1 até <em><em>n</em></em> dessa maneira, eu precisaria encontrar a resposta para os seguintes subproblemas:</p><ul><li>O cronograma de valor máximo para os cartões perfurados de <em><em>n-1</em></em> até <em><em>n</em></em> de modo que os cartões sejam ordenados pelo tempo de início</li><li>O cronograma de valor máximo para os cartões perfurados de <em><em>n-</em>2</em> até <em><em>n</em></em> de modo que os cartões sejam ordenados pelo tempo de início</li><li>O cronograma de valor máximo para os cartões perfurados de <em><em>n-</em>3</em> até <em><em>n</em></em> de modo que os cartões sejam ordenados pelo tempo de início</li><li>e assim por diante até</li><li>O cronograma de valor máximo para os cartões perfurados de <em>2</em> até <em><em>n</em></em> de modo que os cartões sejam ordenados pelo tempo de início</li></ul><p>Se você consegue identificar um subproblema que se baseia em subproblemas anteriores para resolver o problema em questão, está no caminho certo.</p><h4 id="passo-2-escrever-o-subproblema-como-uma-decis-o-matem-tica-recorrente-"><strong>Passo 2: escrever o subproblema como uma decisão matemática recorrente.</strong></h4><p>Depois de identificar um subproblema em palavras, é hora de escrevê-lo matematicamente. Por quê? Bem, a <strong>recorrência matemática, </strong>ou decisão repetida, que você encontrar acabará sendo o que você colocará em seu código. Além disso, ao escrever o subproblema, você examina matematicamente seu subproblema em palavras do Passo 1. Se for difícil colocar seu subproblema do Passo 1 em termos da matemática no seu código, pode ser que ele seja o subproblema errado!</p><p>Há duas perguntas que me faço toda vez que tento encontrar uma recorrência:</p><ul><li>Que decisão eu devo tomar a cada passo?</li><li>Se meu algoritmo está no passo <em><em>i</em></em>, de quais informações eu precisarei para decidir o que fazer no passo <em><em>i+1</em></em>? (algumas vezes, temos que: se meu algoritmo está no passo <em><em>i</em></em>, de quais informações eu precisei para decidir o que fazer no passo <em><em>i-1</em></em>?)</li></ul><p>Vamos retornar ao problema dos cartões perfurados e fazer essas perguntas.</p><p><strong>Que decisão eu tomo a cada passo<strong>?</strong></strong> Suponha que os cartões perfurados sejam classificados por hora de início, conforme mencionado anteriormente. Para cada cartão perfurado compatível com o cronograma até o momento (sua hora de início é após a hora de término do cartão perfurado que está sendo executado no momento), o algoritmo deve escolher entre duas opções: executar ou não executar o cartão perfurado.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.freecodecamp.org/portuguese/news/content/images/2024/08/1_b69mBGpwu6bGJrIlodV6Jw.jpeg" class="kg-image" alt="1_b69mBGpwu6bGJrIlodV6Jw" width="465" height="329" loading="lazy"><figcaption>Este programa dinâmico escolhe entre duas opções a cada passo, como nosso velho amigo, Hamlet – ser ou não ser!</figcaption></figure><p><strong>Se meu algoritmo estiver no passo <em>i</em>, de quais informações eu necessito para decidir o que fazer no passo</strong> <strong><strong><em><em>i+1</em></em>?</strong></strong> Para decidir entre as duas opções, o algoritmo precisa saber o próximo cartão perfurado compatível na ordem. O próximo cartão perfurado compatível para um determinado cartão perfurado <em>p</em> é o cartão perfurado <em>q</em> tal que <em>s_q</em> (a hora de início predeterminada para o cartão perfurado <em>q</em>) ocorre após <em>f_p </em>(a hora de término predeterminada para o cartão perfurado <em>p</em>) e a diferença entre <em>s_q</em> e <em>f_p</em> é minimizada. Abandonando a linguagem matemática, o próximo cartão perfurado compatível é aquele com a hora de início mais próxima após o cartão perfurado atual terminar de ser executado.</p><p><strong>Se meu algoritmo está no passo <em>i</em>, de quais informações eu precisei para decidir o que fazer no passo</strong> <strong><strong><em><em>i-1</em></em>?</strong></strong> O algoritmo precisa saber sobre decisões futuras: aquelas feitas para os cartões perfurados de <em>i</em> a <em>n </em>para decidir se executa ou não executa o cartão perfurado <em>i-1</em>.</p><p>Agora que respondemos a essas perguntas, talvez você tenha começado a formar uma decisão matemática recorrente em sua mente. Caso contrário, tudo bem, fica mais fácil escrever recorrências à medida que você é exposto a problemas de programação mais dinâmicos.</p><p>Sem mais delongas, aqui está nossa recorrência:</p><pre><code>OPT(i) = max(v_i + OPT(proximo[i]), OPT(i+1))</code></pre><p>Essa recorrência matemática requer alguma explicação, especialmente para aqueles que não escreveram uma antes. Eu uso OPT (<em>i</em>) para representar o cronograma do valor máximo para cartões perfurados <em>de i</em> a <em>n</em>, de modo que os cartões perfurados sejam classificados por hora de início. Parece familiar, certo? OPT(•) é nosso subproblema do Passo 1.</p><p>Para determinar o valor de OPT(<em>i</em>), consideramos duas opções e queremos aproveitar ao <em>máximo </em>essas opções para atingir nossa meta: o cronograma de <em>valor máximo</em> para todos os cartões perfurados. Uma vez que escolhemos a opção que dá o resultado máximo na etapa <em>i</em>, memorizamos seu valor como OPT(<em>i</em>).</p><p>As duas opções — executar ou não o cartão perfurado <em><em>i</em></em> — são representadas matematicamente assim:</p><pre><code>v_i + OPT(proximo[i])</code></pre><p>Esta sentença representa a decisão de executar o cartão perfurado <em>i</em>. Ela adiciona o valor obtido com a execução do cartão perfurado <em>i</em> para OPT(proximo[<em>i</em>]), onde proximo[<em>i</em>] representa o próximo cartão perfurado compatível após o cartão perfurado <em>i</em>. OPT(proximo[<em>i</em>]) fornece o agendamento de valor máximo para cartões perfurados proximo[<em>i</em>] a <em>n</em>, de modo que os cartões perfurados sejam classificados por hora de início. A soma desses dois valores produz o agendamento de valor máximo para cartões perfurados de <em>i</em> a <em>n</em>, de modo que os cartões perfurados sejam classificados por hora de início se o cartão perfurado <em>i</em> for executado.</p><pre><code>OPT(i+1)</code></pre><p>Por outro lado, esta sentença representa a decisão de não executar o cartão perfurado <em>i</em>. Se o cartão perfurado <em>i</em> não for executado, seu valor não será obtido. OPT(<em>i+1</em>) fornece o agendamento de valor máximo para cartões perfurados <em>i+1</em> a <em>n</em>, de modo que os cartões perfurados sejam classificados por hora de início. Portanto, OPT(<em>i+1</em>) fornece o cronograma de valor máximo para cartões perfurados <em>i</em> a <em>n</em>, de modo que os cartões perfurados sejam classificados por hora de início se o cartão perfurado<em> i </em>não for executado.</p><p>Desse modo, a decisão tomada a cada passo dos problemas dos cartões perfurados é codificada matematicamente para refletir o subproblema do Passo 1.</p><h4 id="passo-3-resolver-o-problema-original-usando-os-passos-1-e-2-"><strong>Passo 3: resolver o problema original usando os passos 1 e 2.</strong></h4><p>No passo 1, escrevemos o subproblema do problema do cartão perfurado em palavras. No passo 2, escrevemos uma decisão matemática recorrente que corresponde a esses subproblemas. Como podemos resolver o problema original com essas informações?</p><pre><code>OPT(1)</code></pre><p>É simples assim. Como o subproblema que encontramos no passo 1 é o cronograma de valor máximo para cartões perfurados <em>de i</em> a <em>n</em>, de modo que os cartões perfurados sejam classificados por hora de início, podemos escrever a solução para o problema original como o cronograma de valor máximo para cartões perfurados de 1 a <em>n</em>, de modo que os cartões perfurados sejam classificados por hora de início. Como os passos 1 e 2 andam de mãos dadas, o problema original também pode ser escrito como OPT(1).</p><h4 id="passo-4-determinar-as-dimens-es-do-array-de-memoiza-o-e-a-dire-o-na-qual-ele-deve-ser-preenchido-"><strong>Passo 4: determinar as dimensões do array de memoização e a direção na qual ele deve ser preenchido.</strong></h4><p>Achou o passo 3 simples demais para ser verdade? Com certeza, ele dá essa impressão. Você pode se perguntar como OPT(1) pode ser a solução para nosso programa dinâmico se ele depende de OPT(2), OPT(proximo[1]) e assim por diante?</p><p>Você está correto em perceber que OPT(1) depende da solução de OPT(2). Isso vem diretamente após o passo 2:</p><pre><code>OPT(1) = max(v_1 + OPT(proximo[1]), OPT(2))</code></pre><p>Essa, porém, não é uma questão terrível. Pense no exemplo de <em>memoização</em> de Fibonacci. Para encontrar o valor de Fibonacci para <em>n</em> = 5, o algoritmo se baseia no fato de que os valores de Fibonacci para <em>n</em> = 4, <em>n</em> = 3, <em>n</em> = 2, <em>n</em> = 1 e <em>n</em> = 0 já foram <em>memoizados</em>. Se preenchermos nossa tabela de <em>memoização</em> na ordem correta, a dependência que OPT(1) tem dos outros subproblemas não é grande coisa.</p><p>Como podemos identificar a direção correta para preencher a tabela de <em>memoização</em>? No problema do cartão perfurado, uma vez que sabemos que OPT(1) depende das soluções para OPT(2) e OPT (proximo[1]), e que os cartões perfurados 2 e proximos[1] têm horários de início após o cartão perfurado 1 devido à classificação, podemos inferir que precisamos preencher nossa tabela de <em>memoização</em> de OPT (<em>n</em>) para OPT (1).</p><p>Como determinamos as dimensões desse <em>array</em> de <em>memoização</em>? Aqui está um truque: as dimensões do <em>array</em> são iguais ao número e tamanho das variáveis das quais OPT(•) depende. No problema do cartão perfurado, temos OPT(<em>i</em>), o que significa que OPT(•) depende apenas da variável <em>i</em>, que representa o número do cartão perfurado. Isso sugere que nosso <em>array</em> de <em>memoização</em> será unidimensional e que seu tamanho será <em>n</em>, pois há <em>n</em> cartões perfurados no total.</p><p>Se soubermos que <em>n</em>= 5, então nosso <em>array</em> de <em>memoização</em> pode ficar assim:</p><pre><code>memo = [OPT(1), OPT(2), OPT(3), OPT(4), OPT(5)]</code></pre><p>Porém, como a maioria das linguagens de programação <a href="https://en.wikipedia.org/wiki/Zero-based_numbering" rel="noopener">inicia a indexação de arrays em 0</a> (texto em inglês), pode ser mais conveniente criar esse <em>array</em> de <em>memoização </em>para que seus índices se alinhem com os números de cartões perfurados:</p><pre><code>memo = [0, OPT(1), OPT(2), OPT(3), OPT(4), OPT(5)]</code></pre><h4 id="passo-5-programar-"><strong>Passo 5: programar!</strong></h4><p>Para programar nosso programa dinâmico, reunimos os passos de 2 a 4. A única informação nova de que você precisará para escrever um programa dinâmico é um caso base, que você pode encontrar ao mexer em seu algoritmo.</p><p>Um programa dinâmico para o problema do cartão perfurado será mais ou menos assim:</p><pre><code>def punchcardSchedule(n, valores, proximo): # Inicia o array de memoização - passo 4
    memo = [0] * (n+1)   # Define o caso base
    memo[n] = valores[n]   # Cria a tabela de memoização de n a 1 - passo 2
    for i in range(n-1, 0, -1):
        memo[i] = max(v_i + memo[proximo[i]], memo[i+1])  # Retorna a solução do problema original OPT(1) - passo 3
	return memo[1]</code></pre><p>Parabéns por escrever seu primeiro programa dinâmico! Agora que você iniciou os trabalhos, vou mostrar para você em um tipo diferente de programa dinâmico.</p><h3 id="paradoxo-da-escolha-exemplo-de-programa-o-din-mica-com-m-ltiplas-op-es"><strong>Paradoxo da escolha: exemplo de programação dinâmica com múltiplas opções</strong></h3><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.freecodecamp.org/portuguese/news/content/images/2024/08/1_ZOy2VkYyok5a5YoBEUGMtg.jpeg" class="kg-image" alt="1_ZOy2VkYyok5a5YoBEUGMtg" width="400" height="477" loading="lazy"><figcaption>Na imagem: "Alô? Preciso de ajuda para escolher uma linha de ajuda." – não relacionado à programação dinâmica, mas uma ilustração clara de como ter decisões de múltipla escolha pode se tornar um desafio rapidamente.</figcaption></figure><p>Embora o exemplo anterior de programação dinâmica tivesse uma decisão de duas opções — executar ou não executar um cartão perfurado — alguns problemas exigem que várias opções sejam consideradas antes que uma decisão possa ser tomada em cada etapa.</p><p>É hora de um novo exemplo.</p><p>Faça de conta que você está vendendo as pulseiras da amizade para <em>n</em> clientes, e que o valor desse produto aumenta monotonicamente. Isso significa que o produto tem preços {<em>p_1</em>, ..., <em>p_n</em>} tais que <em>p_i ≤ p_j</em> se o cliente <em>j</em> vier depois do cliente <em>i</em>. Esses <em>n</em> clientes têm valores {<em>v_1</em>, ..., <em>v_n</em>}. Um determinado cliente <em>i</em> comprará uma pulseira da amizade pelo preço <em>p_i</em> se e somente se &nbsp;p_i ≤ <em>v_i</em>; caso contrário, a receita obtida desse cliente será 0. Suponha que os preços sejam números naturais.</p><p><strong><strong>Problem</strong>a</strong>: você deve encontrar o conjunto de preços que garanta a máxima receita possível com a venda de suas pulseiras da amizade.</p><p>Reserve um segundo para pensar em como você pode resolver esse problema antes de examinar minhas soluções para os passos 1 e 2.</p><h4 id="passo-1-identificar-o-subproblema-em-palavras--1"><strong>Passo 1: identificar o subproblema em palavras.</strong></h4><p><strong><strong>Subproblem</strong>a</strong>: a receita máxima obtida de clientes de <em><em>i</em></em> a <em><em>n </em></em>de modo que o preço para o cliente &nbsp;<em><em>i-1</em></em> tenha sido definido em <em><em>q</em></em>.</p><p>Encontrei esse subproblema percebendo que, para determinar a receita máxima para os clientes de 1 a <em>n</em>, precisaria encontrar a resposta para os seguintes subproblemas:</p><ul><li>A receita máxima obtida dos clientes <em>n-1</em> a <em>n</em> de modo que o preço do cliente <em>n-2 </em>tenha sido definido em <em>q</em>.</li><li>A receita máxima obtida dos clientes <em>n-2</em> a <em>n</em> de modo que o preço do cliente <em>n-3 </em>tenha sido definido em <em>q</em>.</li><li>(e assim por diante)</li></ul><p>Observe que introduzi uma segunda variável <em>q</em> no subproblema. Fiz isso porque, para resolver cada subproblema, preciso saber o preço que estabeleci para o cliente <em>antes </em>desse subproblema. A variável <em>q</em> garante a natureza monotônica do conjunto de preços, enquanto a variável <em>i</em> mantém o controle do cliente atual.</p><h4 id="passo-2-escrever-o-subproblema-como-uma-decis-o-matem-tica-recorrente--1"><strong>Passo 2: escrever o subproblema como uma decisão matemática recorrente.</strong></h4><p>Há duas perguntas que me faço toda vez que tento encontrar uma recorrência:</p><ul><li>Que decisão preciso tomar a cada passo?</li><li>Se meu algoritmo está no passo <em><em>i</em></em>, de quais informações eu precisarei para decidir o que fazer no passo <em><em>i+1</em></em>? (algumas vezes, temos que: se meu algoritmo está no passo <em><em>i</em></em>, de quais informações eu precisei para decidir o que fazer no passo <em><em>i-1</em></em>?)</li></ul><p>Vamos voltar ao problema da pulseira da amizade e fazer essas perguntas.</p><p><strong>Que decisão eu tomo a cada passo?</strong> Eu decido a que preço vender minha pulseira da amizade para o cliente atual. Como os preços devem ser números naturais, sei que devo definir meu preço para o cliente <em>i </em>na faixa de <em>q – </em>o preço definido para o cliente <em>i-1 – </em>a <em>v_i </em>– o preço máximo pelo qual o cliente <em>comprará</em> uma pulseira da amizade.</p><p><strong>Se meu algoritmo estiver na etapa <em>i</em>, quais informações ele precisaria para decidir o que fazer na etapa <em>i + 1</em>?</strong> Meu algoritmo precisa saber o preço definido para o cliente <em>i</em> e o valor do cliente <em>i+1 </em>para decidir em qual número natural definir o preço para o cliente <em>i+1</em>.</p><p>Com esse conhecimento, posso escrever matematicamente a recorrência:</p><pre><code>OPT(i,q) = max~([Receita(v_i, a) + OPT(i+1, a)])
de modo que max~ encontra o máximo entre todos os a no intervalo q ≤ a ≤ v_i</code></pre><p>Mais uma vez, essa recorrência matemática requer alguma explicação. Como o preço para o cliente <em>i-1</em> é <em>q</em>, para o cliente <em>i</em>, o preço <em>a</em> permanece no inteiro <em>q</em> ou muda para algum inteiro entre <em>q+1</em> e <em>v_i</em>. Para encontrar a receita total, adicionamos a receita do cliente <em>i</em> à receita máxima obtida dos clientes <em>i+1</em> a <em>n</em>, de modo que o preço do cliente <em>i </em>foi definido em <em>a</em>.</p><p>Em outras palavras, para maximizar a receita total, o algoritmo deve encontrar o preço ideal para o cliente <em>i, </em> verificando todos os preços possíveis entre <em>q</em> e <em>v_i</em>. Se <em>v_i</em> ≤ <em>q</em>, então o preço <em>a</em> deve permanecer em <em>q</em>.</p><h4 id="e-quanto-aos-outros-passos"><strong>E quanto aos outros passos?</strong></h4><p>Ao trabalharmos os passos 1 e 2, temos a parte mais difícil da programação dinâmica. Como exercídio, sugiro que você faça os passos 3, 4 e 5 por conta própria para ver se entendeu.</p><h3 id="an-lise-em-tempo-real-dos-programas-din-micos"><strong>Análise em tempo real dos programas dinâmicos</strong></h3><p>Agora, vamos à parte divertida de escrever algoritmos: a análise do tempo de execução. Usarei a notação big-O ao longo dessa discussão. Se você ainda não está familiarizado com o big-O, sugiro que leia sobre ele <a href="https://www.interviewcake.com/article/java/big-o-notation-time-and-space-complexity">aqui</a> (texto em inglês).</p><p>Geralmente, o tempo de execução de um programa dinâmico é composto pelos seguintes recursos:</p><ul><li>Pré-processamento</li><li>Quantas vezes o <em>loop for</em> é executado</li><li>Quanto tempo leva para a recorrência ser executada em uma iteração do <em>loop for</em></li><li>Pós-processamento</li></ul><p>No geral, o tempo de execução assume a seguinte forma:</p><p><code>Pré-processamento + Loop * Recorrência + Pós-processamento</code></p><p>Vamos realizar uma análise de tempo de execução do problema do cartão perfurado para nos familiarizarmos com o big-O para programas dinâmicos. Aqui está o programa dinâmico do problema do cartão perfurado:</p><pre><code>def punchcardSchedule(n, valores, proximo): # Inicia o array de memoização - passo 4
	memo = [0] * (n+1)   # Define o caso de base
    memo[n] = valores[n]   # Cria a tabela de memoização de n a 1 - passo 2
    for i in range(n-1, 0, -1):
    	memo[i] = max(v_i + memo[proximo[i]], memo[i+1])  # Retorna a solução para o problema original OPT(1) - passo 3
    return memo[1]</code></pre><p>Vamos detalhar seu tempo de execução:</p><ul><li>Pré-processamento: aqui, isso significa construir o <em>array</em> de <em>memoização</em>. O(<em>n</em>).</li><li>Quantas vezes o <em>loop for</em> é executado: O(<em>n</em>).</li><li>Quanto tempo leva para a recorrência ser executada em uma iteração do <em>loop for</em>: a recorrência leva tempo constante para ser executada porque toma uma decisão entre duas opções em cada iteração. O(1).</li><li>Pós-processamento: nada ocorre aqui! O(1).</li></ul><p>O tempo de execução geral do programa dinâmico do problema do cartão perfurado é O(<em>n</em>) O(<em>n</em>) * O(1) + O(1), ou, de modo simplificado, O(<em>n</em>).</p><h3 id="voc-chegou-l-"><strong>Você chegou lá!</strong></h3><p>É isso — você está um passo mais perto de se tornar um gênio da programação dinâmica!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.freecodecamp.org/portuguese/news/content/images/2024/08/1_iFMwuyC5ym3f_Ep6L_GEEA.jpeg" class="kg-image" alt="1_iFMwuyC5ym3f_Ep6L_GEEA" srcset="https://www.freecodecamp.org/portuguese/news/content/images/size/w600/2024/08/1_iFMwuyC5ym3f_Ep6L_GEEA.jpeg 600w, https://www.freecodecamp.org/portuguese/news/content/images/2024/08/1_iFMwuyC5ym3f_Ep6L_GEEA.jpeg 800w" sizes="(min-width: 720px) 720px" width="800" height="969" loading="lazy"><figcaption>Margaret Hamilton: uma das várias mestres da programação da nossa história!</figcaption></figure><p>Um último conselho: <strong>continue praticando programação dinâmica</strong>. Não importa o nível de frustração que esses algoritmos possam parecer ter, escrever repetidamente programas dinâmicos fará com que os subproblemas e recorrências cheguem até você com mais naturalidade. Aqui está uma lista de <em>crowdsourcing</em> de <a href="https://www.quora.com/What-are-the-top-10-most-popular-dynamic-programming-problems-among-interviewers">problemas clássicos de programação dinâmica</a> (texto em inglês) para você experimentar.</p><p>Então, faça suas entrevistas, suas aulas e<strong> viva</strong> (logicamente) com seu novo conhecimento de programação dinâmica!</p><p>Agradeço muito a <a href="https://stebenn.wordpress.com/" rel="noopener">Steven Bennett</a>, <a href="https://medium.com/@eeclaire" rel="noopener">Claire Durand</a> e <a href="https://medium.com/@prithajnath" rel="noopener">Prithaj Nath</a> por terem revisado este artigo originalmente. Agradeço também ao <a href="https://sites.northwestern.edu/hartline/" rel="noopener">Professor Hartline</a> por me empolgar com a ideia de aprender sobre programação dinâmica ao ponto de me fazer escrever tanto sobre ela.</p><p>Gostou do que leu? Compartilhe este artigo. Tem sugestões ou perguntas? Entre em contato com a autora pelo <a href="https://twitter.com/alainakafkes" rel="noopener">Twitter</a> (em inglês).</p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
