Docsity
Docsity

Prepare-se para as provas
Prepare-se para as provas

Estude fácil! Tem muito documento disponível na Docsity


Ganhe pontos para baixar
Ganhe pontos para baixar

Ganhe pontos ajudando outros esrudantes ou compre um plano Premium


Guias e Dicas
Guias e Dicas

Cálculo de Média, Distância entre Pontos e Conversão de Moeda, Notas de aula de Programação Javascript

Uma série de exercícios básicos de programação em python, abordando tópicos como cálculo de média, distância entre pontos e conversão de moeda. Os exercícios envolvem a leitura de dados de entrada, realização de cálculos e exibição dos resultados. O documento pode ser útil para estudantes de programação em nível introdutório, servindo como material de estudo, exercícios e revisão de conceitos fundamentais de python. Os tópicos abordados são comumente encontrados em cursos introdutórios de ciência da computação, engenharia de software e áreas afins, podendo ser aplicados em diversas disciplinas relacionadas à programação e resolução de problemas computacionais.

Tipologia: Notas de aula

2024

Compartilhado em 12/03/2024

leandro-ferreira-tyv
leandro-ferreira-tyv 🇧🇷

1 documento


Pré-visualização parcial do texto

Baixe Cálculo de Média, Distância entre Pontos e Conversão de Moeda e outras Notas de aula em PDF para Programação Javascript, somente na Docsity! Introdução a computação Prof. Alexandre L. M. Levada - DC/UFSCar Notas de Aula Estrutura do curso Parte 1: Conceitos básicos de computação a) Hardware e software b) Modelo de Von Neumann c) CPU, memória, entrada e saída d) Linguagem de programação e) Sistema de numeração binário Parte 2: Programação em Python a) Plataformas para computação científica b) Estruturas sequenciais c) Estruturas de seleção d) Estruturas de repetição e funções e) Listas, strings, vetores, matrizes f) Ordenação de dados g) Dicionários h) Autômatos celulares i) Dilema do prisioneiro iterativo j) Recursão Bibliografia MENEZES, N. N. C. Introdução à programação com Python: algoritmos e lógica de programação para iniciantes. 2. ed. São Paulo: Novatec, 2014. (BCO) BORGES, L. E.; Python para Desenvolvedores. 2. ed., 2010. Disponível em https://ark4n.files.wordpress.com/2010/01/python_para_desenvolvedores_2ed.pdf MILLER, B.; RANUM, D.; Como Pensar como um Cientista da Computação: Versão Interativa. Disponível em: https://panda.ime.usp.br/pensepy/static/pensepy/index.html MASANORI, F.; Python para Zumbis (curso online). Material didático em: https://github.com/fmasanori/PPZ Curso online de Python em Codeacademy - http://www.codecademy.com/pt-BR/tracks/python Dive into Python, Mark Pilgrim, Apress, 2004 - disponível em http://www.diveintopython.net/ Repositório online de livros sobre programação Pythonl - http://pythonbooks.revolunet.com/ Matthes, E.; Curso Intensivo de Python: Uma Introdução Prática e Baseada em Projetos à Programação, Novatec, 2016. Ramalho, L; Python Fluente: Programação Clara, Concisa e Eficaz, Novatec, 2015. Lutz, M.; Ascher, D.; Aprendendo Python. 2. ed., Bookman, 2007. Ambiente virtual: http://www.moodle.ufscar.br/course/view.php?id=1618 Critério de avaliação: MF = 0.35*P1 + 0.35*P2 + 0.1*EX + 0.2*MT onde P1 e P2 são as notas das avaliações presenciais, EX é a média dos exercícios semanais e MT é a média dos trabalhos práticos. Critério para aprovação: MF >= 6.0 e frequência >= 75% Porque Python 3? - Evolução do Python 2 (mais moderno) - Sintaxe simples e de fácil aprendizagem - Linguagem de propósito geral que mais cresce na atualidade - Bibliotecas para programação científica Plataformas Python para desenvolvimento Para desenvolver aplicações científicas em Python, é conveniente instalar um ambiente de programação em que as principais bibliotecas para computação científica estejam presentes. Isso poupa-nos muito tempo e esforço, pois além de não precisarmos procurar cada biblioteca individualmente, não precisamos saber a relação de dependência entre elas (quais devem ser instaladas primeiro e quais tem que ser instaladas posteriormente). Dentre as plataformas Python para computação científica, podemos citar as seguintes: a) Anaconda - https://www.continuum.io/downloads Uma ferramenta multiplataforma com versões para Windows, Linux e MacOS. É muito parecido com o WinPython, com a exceção de que não pode ser instalado em um pen-drive externo. Muito útil para quem não usa Windows. b) WinPython - http://winpython.github.io/ Uma plataforma exclusiva para Windows que contém inúmeros pacotes indispensáveis, bem como um ambiente integrado de desenvolvimento muito poderoso (Spyder). Devido à fácil instalação (pode ser instalado diretamente em um pen-drive), é a opção mais recomendada para iniciantes. Por questões de compatibilidade com a disciplina, sugere-se a versão referente ao Python 3 O problema pode ser amplificado ao se realizar operações matemáticas com esse valor é como se o erro fosse sendo propagado nos cálculos). Existem outros valores para que isso ocorre: 0,2, 0,4, 0,6 Ex: Forneça as representações computacionais na base 2 (binário) para os seguintes números. Quais são os erros cometidos se considerarmos apenas 8 bits para a parte fracionária? a) 11,6 b) 27,4 c) 53,6 d) 31,2 Ambientes de desenvolvimento Após a instalação, há vários ambientes de desenvolvimento disponíveis, sendo os mais conhecidos: IDLEX, Jupyter e Spyder. Abra o programa escolhido e estará pronto para iniciar a programação. Ao iniciar o IDLEX, a janela principal nos fornece acesso ao modo interativo, onde é possível interagir em tempo real com o interpretador da linguagem Python. Pode-se criar variáveis, comparar valores, imprimir mensagens na tela, etc. Estruturas sequenciais Antes de iniciar a programação em si, iremos definir alguns comandos básicos da linguagem Python. Uma das primeiras perguntas que surgem quando vamos construir um programa é: como declarar variáveis? Python é uma linguagem dinâmica e portanto não é necessário declarar variáveis. Basta usá-las conforme a necessidade. Para ilustrar o funcionamento básico da linguagem, iremos introduzir os comandos de entrada e saída: O comando print imprime informações na tela e o comando input lê dados do teclado. Para demonstrar a utilização desses comandos, iremos para o modo de edição. Para isso, basta criar um novo arquivo e salvá-lo com uma extensão .py, por exemplo, entrada_saida.py # Leitura dos valores de entrada a = int(input('Entre com o valor de a: ')) b = int(input('Entre com o valor de b: ')) print('') # Pula linha # Calculo do(s) valor(es) de saída c = a + b # Impressão do(s) resultado(s) na saída # OBS: As 3 formas de utilizar o comando print são equivalentes print('O valor de %.2f + %.2f é igual a %.2f' %(a,b,c)) print('O valor de ',a,' + ',b,' é igual a ',c) print('O valor de {0} + {1} é igual a {2}'.format(a,b,c)) Note que ao ler um valor digitado pelo teclado com o comando input, utilizamos o comando int() para converter a string de leitura para um dado do tipo inteiro. Por padrão, toda informação lida através do teclado em Python é do tipo string, ou seja, não permite cálculos matemáticos. Para tratar os dados de entrada como números inteiros usamos int() e para tratar como números reais usamos float(). A seguir iremos resolver alguns exercícios básicos que envolvem estruturas sequenciais, isto é, uma sequencia linear de comandos. Ex1: Sabe-se que dado uma temperatura em graus Celsius, o valor na escala Farenheit é dado por: F= 9 5 C+32 Faça um programa que leia uma temperatura em Celsius e imprima na tela o valor em Farenheit # celsius para farenheit c = float(input('Entre com a temperatura em Celsius (C): ')) f = 9*c/5 + 32 print('%.1f graus Celsius equivalem a %.1f graus Farenheit' %(c, f)) Ex2: Faça um programa em Python que leia 3 números reais e imprima na tela a média dos três. n1 = float(input('Entre com a primeira nota: ')) n2 = float(input('Entre com a segunda nota: ')) n3 = float(input('Entre com a terceira nota: ')) media = (n1+n2+n3)/3 print('A média final é %.2f' %media) Ex3: A distância entre 2 pontos P = (x1, y1) e Q = (x2, y2) é definida por: d (P , Q)=√(x1−x2) 2 +( y1− y2) 2 Faça um programa que leias as coordenadas x1, y1, x2 e y2 e imprima a distância entre P e Q. x1 = float(input('Entre com a coordenada x do ponto P (x1): ')) y1 = float(input('Entre com a coordenada y do ponto P (y1): ')) x2 = float(input('Entre com a coordenada x do ponto Q (x2): ')) y2 = float(input('Entre com a coordenada y do ponto Q (y2): ')) distancia = ((x1 - x2)**2 + (y1 - y2)**2)**0.5 print('A distância entre os pontos é %.2f' %distancia) Ex4: A expressão utilizada para computar juros compostos é dada por: M=C (1+ J 100 ) P onde C é o capital inicial, J é a taxa de juros mensal (%), e P é o período em meses. Faça um programa que leia os valores de C, J e P e imprima a tela o valor final após a aplicação dos juros. C = float(input('Entre com o capital inicial (R$): ')) J = float(input('Entre com a taxa de juros (% ao mês): ')) P = float(input('Entre com o período em meses: ')) M = C*(1+J/100)**P print('O montante final será de %.2f reais' %M) Ex5: Num certo país da América do Sul, a moeda nacional é a Merreca (M$). No sistema monetário desse país só existem cédulas de M$ 100, M$ 50, M$ 10, M$ 5 e M$ 1. Dado um valor em Merrecas, faça um script que retorne a quantidade mínima de cédulas que totalizam o valor especificado. Por exemplo, se o valor for M$ 379, devemos ter: 3 cédulas de M$ 100 1 cédula de M$ 50 2 cédulas de M$ 10 0 cédulas de M$ 5 4 cédulas de M$ 1 valor = int(input('Entre com o valor em M$: ')) cedulas100 = valor // 100 resto = valor % 100 cedulas50 = resto // 50 resto = resto % 50 cedulas10 = resto // 10 resto = resto % 10 cedulas5 = resto // 5 resto = resto % 5 print('O valor digitado corresponde a:') print('%d cédulas de M$ 100' %cedulas100) print('%d cédulas de M$ 50' %cedulas50) print('%d cédulas de M$ 10' %cedulas10) print('%d cédulas de M$ 5' %cedulas5) print('%d cédulas de M$ 1' %resto) Estruturas de seleção Os programas desenvolvidos até o presente momento eram sempre compostos por sequências simples e lineares de comandos, no sentido de que em toda a execução todos os comandos serão executados. Em diversos problemas, é necessário tomar decisões antes de se executar um trecho de código. Por exemplo, para imprimir na tela uma mensagem que diz ‘Carro novo’, primeiramente devemos verificar se a idade do carro é menor ou igual a 3 anos. Caso contrário, gostaríamos de imprimir a mensagem ‘Carro usado’. Isso significa que dependendo do valor da variável idade, um de dois caminhos possíveis será escolhido. Ex8: Sabendo que a média final do curso é computada por MF = 0.35 x P1 + 0.35 x P2 + 0.2 x MT + 0.1 x EX onde P1 é a nota da primeira avaliação, P2 é a nota da segunda avaliação e NT é a nota dos trabalhos, faça um programa que leia P1, P2 e NT e imprima na tela a média final, bem como a situação do aluno: Reprovado, se a media final é inferior a 5; Recuperação, se a média final está entre 5 e 6 ou Aprovado, se a média final é maior ou igual a 6. p1 = float(input('Entre com a nota da P1: ')) p2 = float(input('Entre com a nota da P2: ')) mt = float(input('Entre com a média dos trabalhos: ')) ex = float(input('Entre com a média dos exercícios: ')) media = 0.35*p1 + 0.35*p2 + 0.2*mt + 0.1*ex print('A média final é %.2f' %media) if media < 5: print('REPROVADO') elif media >= 5 and media < 6: print('RECUPERAÇÃO') else: print('APROVADO') Ex9: O índice de massa corpórea (IMC) de uma pessoa é dado pelo seu peso (em quilogramas) dividido pelo quadrado de sua altura (em metros). Faça um programa que leia o peso e a altura de um indivíduo e imprima seu IMC, além da sua classificação, de acordo com a tabela a seguir. IMC <= 18.5 → magro 18.5 < IMC <= 25 → normal 25 < IMC <= 30 → sobrepeso IMC >= 30 → obeso peso = float(input('Entre com o peso (kg): ')) altura = float(input('Entre com a altura (m): ')) imc = peso/altura**2 print('IMC = %.2f' %imc) if imc <= 18.5: print('MAGRO') elif imc > 18.5 and imc <= 25: print('NORMAL') elif imc > 25 and imc <= 30: print('SOBREPESO') else: print('OBESO') Ex10: Uma equação do 2o grau é definida na sua forma padrão por ax2 +bx+c=0 . As soluções de uma equação desse tipo são dadas pela conhecida fórmula de Bhaskara: x1= −b−√Δ 2 a e x2= −b+√Δ 2 a , onde Δ=b2 −4 ac Faça um programa em que, dados os valores de a, b e c, imprima na tela as soluções reais da equação da equação do segundo grau. Note que a deve ser diferente de zero e Δ não pode ser negativo. Note que caso Δ seja igual a zero, a solução é única. # resolve uma simples equação do segundo grau print('Entre com os coeficientes A, B e C da equação de segundo grau') a = float(input('Entre com o valor de A: ')) b = float(input('Entre com o valor de B: ')) c = float(input('Entre com o valor de C: ')) if a == 0: print('Não é equação do segundo grau') else: delta = b**2 - 4*a*c if delta < 0: print('Não há soluções reais para equação') elif delta == 0: x = -b/2*a print('Solução única x = %.3f' %x) else: x1 = (-b - delta**(0.5))/2*a x2 = (-b + delta**(0.5))/2*a print('x1 = %.3f e x2 = %.3f' %(x1, x2)) Estruturas de repetição As estruturas de seleção são ferramentas importante para controlar o fluxo de execução de programas, porém são limitadas no sentido que nos permitem apenas ao programa ignorar blocos de instruções indesejados. Um outro tipo de estrutura muito importante e que nos permite a execução de um trecho de código diversas vezes são as estruturas de repetição. Suponha por exemplo que desejamos imprimir os números de 1 até 3 na tela. Ao invés de repetir três vezes o mesmo comando na tela, podemos simplesmente usar um comando de repetição. Uma maneira de implementar estruturas de repetição em Python é através do comando while. Considere o exemplo a seguir. A variável x inicia com valor 1. Na primeira passagem pelo loop, compara-se x com o valor 3 e verifica-se que 1 <= 3. Assim, como a condição é verdadeira, entra-se no laço. Dentro do laço imprime-se x na tela (será mostrado o valor 1) e em seguida x é incrementado em uma unidade, fazendo com que seu valor seja 2. O fluxo então retorna para a condição da repetição, onde a variável x é novamente compara com o valor 3, sendo verificado que 2 <= 3, e portanto mais uma vez iremos entrar no loop, imprimindo o valor 2 na tela e incrementando x novamente. Novamente, retornamos a condição do while onde compara-se x com o valor 3. Como 3 <= 3, entra-se de novo no loop, imprimindo o valor 3 na tela e incrementando o valor de x para 4. Por fim, ao voltarmos para a condição do loop, verifica-se que 4 não é menor ou igual a 3 (retorna Falso), e portanto não entra-se no loop. O comando while se encerra e o fluxo de execução é dirigido para a linha subsequente ao último comando do loop. Ex11: Faça um programa que imprima na tela todos os números pares de zero até um limite N definido pelo usuário e lido como entrada. Um contador é uma variável que é incrementada em uma unidade a cada passagem pelo laço (a variável x no exemplo anterior é um exemplo). Seu objetivo é contar quantas vezes a repetição está sendo realizada. Um outro tipo de variável muito importante em repetições são as variáveis chamadas de acumuladoras. A diferença entre um contador e um acumulador é que nos contadores o valor adicionado a cada passo é constante (em geral é uma unidade mas pode uma constante c), enquanto que nos acumuladores o valor adicionado é variável (muda de um passo para outro). n = int(input('Entre com o número de termos: ')) soma = 0 num = 1 den = 1 while den <= n: soma = soma + num/den num = num + 2 den = den + 1 print(soma) b) n = int(input('Entre com o número de termos: ')) soma = 0 num = 1 den = 1 sinal = 1 while num <= n: soma = soma + sinal*num/den num = num + 1 den = num**2 sinal = -1*sinal print(soma) c) n = int(input('Entre com o número de termos: ')) soma = 0 num = 1000 den = 1 sinal = 1 while den <= n: soma = soma + sinal*num/den num = num - 3 den = den + 1 sinal = -1*sinal print(soma) Ex17: A constante matemática pi desempenha um papel importante nas mais variadas áreas da ciência. Existem diversas séries que aproxima o valor real dessa constante e podem ser usadas para gerar aproximações computacionais para esse número irracional. Três dessas formas são apresentadas a seguir. Faça 3 programas que computam aproximações para a constante pi, sendo um para cada série em questão. Funções Assim como aprendemos a utilizar diversas funções da linguagem Python como print(), input() e range(), iremos aprender a criar nossas próprias funções. A ideia consiste em empacotar um trecho de código para reutilizá-lo sempre que necessário. Em Python criamos uma função com a palavra reservada def. Para indicar o valor de retorno da função utilizamos a palavra-chave return. Por exemplo, suponha que desejamos empacotar numa função o código que computa o fatorial de N. Basta chamar a função com o argumento desejado: fat(5) fat(10) Variáveis globais e locais Variáveis declaradas dentro dentro de uma função possuem escopo local, ou seja, não podem ser acessadas fora da função. Ao se usar o mesmo nome para designar variáveis dentro e fora da função, elas são objetos distintos. Para indicar que as duas variáveis são de fato a mesma, é necessário utilizar a palavra-chave global. A seguir serão apresentados alguns exemplos de problemas envolvendo estruturas de repetição e funções. Ex18: A função seno pode ser aproximada numericamente pela seguinte série Faça um programa que aceite como entrada o valor do ângulo x em radianos e o número de termos n da série e imprima na tela o valor computado de sen(x). # Calcula a raiz quadrada de um número usando o método de Newton def raiz_quadrada(a): x = 1 x_novo = a epson = 0.001 # tolerância (critério de parada) erro = abs(x - x_novo) while erro >= epson: x_novo = 0.5*(x + a/x) erro = abs(x - x_novo) print('x : %.10f ********** Erro: %.10f' %(x_novo, erro)) x = x_novo return x a = float(input('Entre com o valor de a: ')) print('A raíz quadrada de %.3f é %f' %(a, raiz_quadrada(a))) Ex23: Números primos são muito importantes em diversas aplicações que vão desde fatoração de números inteiros até criptografia de dados. Faça um programa que compute a soma de todos números primos menores que N, onde N é fornecido como entrada. a) Compute o valor da soma e o tempo gasto para computá-la se N=10000 b) Compute o valor da soma e o tempo gasto para computá-la se N=100000 c) Compute o valor da soma e o tempo gasto para computá-la se N=1000000 import time def verifica_primo(n): # Maior divisor de um número N é raiz de N divisor = n**0.5 primo = True while divisor > 1 and primo: if n % divisor == 0: primo = False else: divisor = divisor - 1 return primo # Inicio do programa N = int(input('Entre com o valor de N:')) inicio = time.time() soma = 1 for i in range(2, N): # Só precisa testar se é primo se i for ímpar if i % 2 != 0: if verifica_primo(i): soma = soma + i print('A soma dos primos menores que %d é %d' %(N,soma)) fim = time.time() print('Tempo: %f' %(fim – inicio)) Listas Tuplas e listas são variáveis compostas, isto é, estruturas de dados em Python muito úteis para armazenar uma coletânea de objetos de forma indexada, ou seja, em cada posição da lista ou da tupla temos uma variável. A figura a seguir ilustra essa ideia como um trem de dados. A única diferença entre uma tupla e uma lista, é que tuplas são objetos imutáveis, ou seja, uma vez criados não podem ser modificados. Em outras palavras, é impossível usar o operador de atribuição com uma tupla. As listas por outro lado podem ser modificadas sempre que necessário e portanto são muito mais utilizadas na prática do que as tuplas. # cria uma lista vazia lista = [] # cria uma lista com 3 notas notas = [7.5, 9, 8.3] # imprime a primeira nota print(notas[0]) # mudando a primeira nota notas[0] = 8.7 Veremos a seguir uma série de comandos da linguagem Python para minipulação de listas. import random # cria uma lista com 5 elementos la = [1,2,3,4,5] print('Lista la possui %d elementos' %len(la)) print(la) input('Pressione qualquer tecla para continuar...') lb = [] # cria uma lista com 5 números aleatórios entre 0 e 99 for i in range(5): lb.append(random.randint(0, 100)) print('Lista lb possui %d elementos aleatórios' %len(lb)) print(lb) input('Pressione qualquer tecla para continuar...') # concatena 2 listas e gera uma nova lc = la + lb print('Lista lc = la + lb possui %d elementos' %len(lc)) print(lc) input('Pressione qualquer tecla para continuar...') print('Percorre lista incrementando cada elemento') for i in range(len(lc)): lc[i] += 1 print(lc) input('Pressione qualquer tecla para continuar...') # insere elementos no final da lista lc.append(99) lc.append(72) print('Após inserção de 99 e 72 lc tem %d elementos' %len(lc)) print(lc) input('Pressione qualquer tecla para continuar...') # encontra o maior e o menor elementos da lista print('Menor elemento de lc é %d' %min(lc)) print('Maior elemento de lc é %d' %max(lc)) # ordena a lista print('Lista lc ordenada') lc.sort() print(lc) Ex24: Faça um programa que leia um vetor de 5 números inteiros e mostre o vetor Ex25: Faça um programa que leia 4 notas, mostre as notas e a média na tela Ex31: Dada a seguinte lista de 10 elementos L = [-8, -29, 100, 2, -2, 40, 23, -8, -7, 77] faça funções que recebam L como entrada e retorne: a) o menor número da lista b) o maior número da lista c) o maior número negativo da lista d) o menor número positivo da lista e) a média de todos os elementos da lista f) a média dos elementos positivos da lista g) a média dos elementos negativos da lista h) o desvio padrão dos elementos da lista Desvio=√ 1 n ∑i=1 n (x i− x̄ ) 2 , onde x̄ é a média i) a lista inversa S trings Strings são tipos de dados definidos como uma sequência de caracteres. Assemelham-se muito com listas, porém não é possível adicionar novos elementos ao conjunto (não posso fazer append, nem ordenar, nem inserir ou remover elemento). Por outro lado, há uma série de funções que strings possuem mas listas não. Como exemplo, podemos citar upper e lower para mudar os caracteres para maiúsculo ou minúsculo. Mas de longe a função mais conhecida e importante que apenas strings possuem é split (quebra em pedaços baseado num separador e monta uma lista). Como o próprio nome diz, a função split() recebe uma string como entrada e a quebra em diversos pedaços, retornando uma lista em que cada elemento é um pedaço da string original. Isso tudo de acordo com um caracter separador passado como parâmetro (caracteres típicos de separação são : ; , . - _). Se nenhum caracter especial for passado como parâmetro, assume-se que devemos quebrar a string nos espaços em branco. Por exemplo: digitos = '0 1 2 3 4 5 6 7 8' lista_digitos = digitos.split() digitos = '0,1,2,3,4,5,6,7,8' lista_digitos = digitos.split(',') O processo inverso, ou seja, pegar um lista de pedaços e juntar numa única string é feito com a função join(). O que essa função join faz nada mais é que pegar uma lista de strings e gerar uma única string numeros = ''.join(lista_digitos) numeros = ','.join(lista_digitos) numeros = '-'.join(lista_digitos) numeros = ':'.join(lista_digitos) Na prática o que essa função faz é equivalente ao seguinte código: lista = ['a','b','c','d','e','f'] g = '' for x in lista: g = g + x # concatena caracteres Ex32: Saber quantas palavras distintas existe em uma frase é uma tarefa que pode ser bastante complicada em determinadas linguagens de programação. Em Python, isso pode ser feito facilmente. Veja o exemplo a seguir. frase = 'Experiência não é o que acontece com um homem , é o que um homem faz com o que lhe acontece' lista = frase.lower().split() palavras = [] especiais = [',', ';', '-', ':', '.', '!', '?'] for x in lista: if (x not in palavras) and (x not in especiais): palavras.append(x) print(palavras) print(len(palavras)) Ex33: Na pacata vila campestre de Numberville, todos os telefones tem 6 dígitos. A companhia telefônica estabelece as seguintes regras sobre os números: 1. Não pode haver dois dígitos consecutivos idênticos, pois isso é chato; 2. A soma dos dígitos tem que ser par, porque isso é legal; 3. O último dígito não pode ser igual ao primeiro, porque isso dá azar; Dadas essas regras perfeitamente razoáveis, bem projetadas e maduras, quantos números de telefone na lista abaixo são válidos? 213752 216732 221063 221545 225583 229133 230648 233222 236043 237330 239636 240138 242123 246224 249183 252936 254711 257200 257607 261424 263814 266794 268649 273050 275001 277606 278997 283331 287104 287953 289137 291591 292559 292946 295180 295566 297529 300400 304707 306931 310638 313595 318449 319021 322082 323796 326266 326880 327249 329914 334392 334575 336723 336734 338808 343269 346040 350113 353631 357154 361633 361891 364889 365746 365749 366426 369156 369444 369689 372896 374983 375223 379163 380712 385640 386777 388599 389450 390178 392943 394742 395921 398644 398832 401149 402219 405364 408088 412901 417683 422267 424767 426613 430474 433910 435054 440052 444630 447852 449116 453865 457631 461750 462985 463328 466458 469601 473108 476773 477956 481991 482422 486195 488359 489209 489388 491928 496569 496964 497901 500877 502386 502715 507617 512526 512827 513796 518232 521455 524277 528496 529345 531231 531766 535067 535183 536593 537360 539055 540582 543708 547492 550779 551595 556493 558807 559102 562050 564962 569677 570945 575447 579937 580112 580680 582458 583012 585395 586244 587393 590483 593112 593894 594293 597525 598184 600455 600953 601523 605761 608618 609198 610141 610536 612636 615233 618314 622752 626345 626632 628889 629457 629643 633673 637656 641136 644176 644973 647617 652218 657143 659902 662224 666265 668010 672480 672695 676868 677125 678315 Obs: Copie e cole o conteúdo acima (números) num arquivo chamado numeros.txt # Condição 1: não pode haver 2 dígitos iguais em sequencia def condicao1(digitos): cond1 = True for i in range(len(digitos)-1): if (digitos[i] == digitos[i+1]): cond1 = False break return cond1 # Condição 2: soma dos dígitos deve ser par def condicao2(digitos): lista = list(digitos) # converte para lista soma = 0 for x in lista: soma = soma + int(x) # converte para inteiro! if soma % 2 == 0: cond2 = True # soma é par else: cond2 = False # soma é ímpar return cond2 # Condição 3: último dígito não pode ser igual ao primeiro def condicao3(digitos): if digitos[0] != digitos[-1]: cond3 = True else: cond3 = False return cond3 # Abre arquivo para leitura f = open('numeros.txt', 'r') # Lê dados do arquivo numa única string conteudo = f.read() # Quebra string em lista (cada palavra vira um elemento da lista) numeros = conteudo.split() total = 0 # Percorre todos os elementos da lista for num in numeros: if (condicao1(num) and condicao2(num) and condicao3(num)): total = total + 1 print('O total de números telefônicos válidos é %d' %total) Ex40: Dada uma lista L de inteiros positivos, encontre o comprimento da maior sublista tal que a soma de todos os seus elementos seja igual a k. Exemplo: Para L = [1, 3, 3, 5, 1, 4, 7, 2, 2, 2, 1, 8] e k = 6, existem várias subsequencias que somam 6, por exemplo, as subsequencias [3, 3], [5, 1] e [2, 2, 2]. O programa deve retornar o comprimento da maior delas, ou seja, 3. def max_sequence(L, k): sequencia = [] max_len = -1 # retorna -1 se não há subsequencia que soma k for i in L: sequencia.append(i) while sum(sequencia) > k: sequencia = sequencia[1:] if sum(sequencia) == k: max_len = max(max_len, len(sequencia)) return max_len Vetores Em Python, listas são objetos extremamente genéricos, podendo ser utilizados em uma gama de aplicações. Entretanto, o pacote Numpy (Numerical Python) oferece a definição de um tipo de dados específico para representar vetores: são os chamados arrays. Em resumo, um array é praticamente uma lista, mas com algumas adicionais que facilitam cálculos matemáticos. Um exemplo é o seguinte: suponha que você tenha uma lista em que cada elemento representa o gasto em reais de uma compra efetuada pelo seu cartão. L = [13.5, 8.0, 5.99, 27.30, 199.99, 57.21] Você deseja dividir cada um dos valores pelo gasto total, para descobrir a porcentagem referente a cada compra. Como você tem uma lista, você deve fazer o seguinte: lista = L for i in range(len(L)): lista[i] = L[i]/sum(L) Ou seja, não é permitido fazer lista = L/sum(L) → ERRO Mas se os dados tiverem sido armazenados num vetor, é possível fazer diretamente o comando acima: import numpy as np # criando um vetor (as duas maneiras são equivalentes) v = np.array(L) v = np.array([13.5, 8.0, 5.99, 27.30, 199.99, 57.21]) v = v/sum(v) → OK Algumas funções importantes que todo objeto vetor possui, supondo que nosso vetor chama-se v: v.max() - retorna o maior elemento do vetor v.min() - retorna o menor elemento do vetor v.argmax() - retorna o índice do maior elemento do vetor v.argmin() - retorna o índice do menor elemento do vetor v.sum() - retorna a soma dos elementos do vetor v.mean() - retorna a média dos elementos do vetor v.prod() - retorna o produto dos elementos do vetor v.T – retorna o vetor transposto v.clip(a, b) – o que é menor que a vira a e o que é maior que b vira b v.shape() - retorna as dimensões do vetor/matriz Para a referência completa das funções veja os links: https://docs.scipy.org/doc/numpy-dev/user/quickstart.html https://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html#array-methods Matrizes Matrizes em Python também são objetos criados com o comando np.array. Elas são vetores que possuem mais de uma dimensão, sendo tipicamente duas. Suponha que desejamos criar uma matriz 3 x 3 contendo os elementos de 1 até 9. [ 1 2 3 4 5 6 7 8 9] Isso é feito com o seguinte comando (no que as chaves): M = np.array([[1,2,3], [4,5,6], [7,8,9]]) A seguir veremos um exemplo onde algumas operações básicas com matrizes são realizadas. import numpy as np M = np.array([[1,2],[3,4]]) N = np.array([[5,6],[7,8]]) # soma de 2 matrizes C = M + N # subtração de 2 matrizes D = M – N # multiplicação elemento por elemento E = M*N # multiplicação de matrizes (também serve para produto escalar entre 2 vetores e produto matriz vetor) F = np.dot(M, N) # gera uma matriz com números aleatórios entre 1 e 99 G = np.random.randint(1, 100, (3, 3)) Ex41: Faça uma função que receba uma matriz quadrada n x n como entrada e retorne a sua matriz triangular superior. import numpy as np def triangular_superior(M): if M.shape[0] != M.shape[1]: print('Matriz não é quadrada.') return -1 else: for i in range(M.shape[0]): for j in range(M.shape[1]): # zerar tudo que está abaixo da diagonal if i > j: M[i,j] = 0 return M M = np.random.randint(1, 100, (5,5)) print(M) print() print(triangular_superior(M)) Modifique a função anterior para que ela retorne a matriz triangular inferior. Ex42: Dada uma matriz n x n faça funções para: a) calcular a soma dos elementos da diagonal principal. b) calcular a soma dos elementos da diagonal secundária. c) calcular a soma dos elementos de cada uma das linhas. d) calcular a soma dos elementos de cada uma das colunas. import numpy as np def soma_diagonal(M): if M.shape[0] != M.shape[1]: print('Matriz não é quadrada, não tem diagonal.') return -1 else: soma_diagonal = 0 for i in range(M.shape[0]): for j in range(M.shape[1]): if i == j: soma_diagonal += M[i,j] return soma_diagonal def soma_diagonal_secundária(M): if M.shape[0] != M.shape[1]: print('Matriz não é quadrada, não tem diagonal secundária') return -1 else: A = np.random.randint(1, 99, (5, 5)) B = np.array([[1, 0, 0],[0, 1, 0],[0, 0, 1]]) print(A) print(B) print(simetrica(A)) print(simetrica(B)) Dicionários Dicionários são estruturas de dados em Python que generalizam o conceito de lista, permitindo a criação de pares do tipo (chave, valor). Em uma lista, a chave é sempre um índice que indica a posição do elemento no conjunto (existe uma relação de ordem intrínseca pois o 0 vem antes do 1, que vem antes do 2, etc). Em um dicionário, a chave pode ser praticamente qualquer tipo de dados (string, float, tupla). É como se fosse uma etiqueta com um identificador único para cada valor armazenado na estrutura. Para inserir elementos num dicionário, é preciso relacionar uma chave a um valor: d = {} # cria dicionário vazio d['a'] = 'programação' d['b'] = 'em' d['c'] = 'python' No dicionário d, todos os elementos são armazenados como pares (chave, valor) {('a', 'programação'), ('b', 'em'), ('c', 'python')} Assim, não devemos acessar os elementos como: d[0] → ERRO X d[1] → ERRO X d[2] → ERRO X Mas sim d['a'] d['b'] d['c'] Para modificar um valor, basta fazer: d['a'] = 'teste' A posição dos pares (chave, valor) no conjunto não segue uma ordem, ou seja, não podemos garantir quem vem antes ou depois de quem. Um dos erros mais comuns que se tem é percorrer um dicionário como uma lista: for i in range(len(d)): print(d[i]) → ERRO X Funções Auxiliares A seguir veremos algumas funções muito úteis na manipulação de dicionários. d.clear() - limpa dicionário (mesmo que fazer d = {}) d.keys() - retorna um conjunto com todas as chaves Ex: 'a' in d.keys() - verifica se chave 'a' faz parte do dicionário d d.values() - retorna um conjunto com todos os valores Ex: 99 in d.values() - verifica que 99 é um dos valores armazenados no dicionário d d.items() - retorna um conjunto de todas as tuplas com pares (chave, valor) Ex: ('a', 99) in d.items() d.pop(chave) - retira par (chave, valor) com base na chave especificada Ex: d.pop('a') Ex45: Crie um dicionário em que as chaves são nomes e os valores são os números de telefones de 5 pessoas. Imprima na tela os nomes e telefones de cada um deles. d = {} d['José'] = '33079876' d['Ana'] = '81889275' d['João'] = '97490128' d['Maria'] = '34141796' d['André'] = '33216784' print('Lista Telefônica') for chave in d.keys(): print('%s: %s' %(chave, d[chave])) Outra forma de percorrer dicionário e imprimir os dados: print('Lista Telefônica') for chave, valor in d.items(): print('%s: %s' %(chave, valor)) Outras consultas incluem: a) Retornar os nomes e telefones de todas as pessoas cujo nome inicia com a letra A b) Retornar os nomes e telefones de todas as pessoas cujo número inicia com 3 e termina com 6 def imprime_A(d): for chave in d.keys(): if chave[0] == 'A': print('%s: %s' %(chave, d[chave])) def imprime_B(d): for chave, valor in d.items(): if valor[0] == '3' and valor[0] == '6': print('%s: %s' %(chave, valor)) A seguir veremos alguns problemas envolvendo dicionários na forma de exercícios. Ex46: Seja uma lista L que contém o nome de diversas pessoas: L = ['mark', 'henry', 'matthew', 'paul', 'luke', 'robert', 'joseph', 'carl', 'michael'] Escreva uma função que receba L como entrada e retorne um dicionário em que os nomes são agrupados pelo tamanho, ou seja, na chave 4 deve ser armazenada uma lista com os nomes de 4 letras, e assim por diante. def agrupa_nomes(L): d = {} for nome em L: chave = len(nome) if key not in d: # se não existe grupo, cria vazio d[chave] = [] d[chave].append(nome) return d Ex47: Em uma papelaria os produtos a seguir são vendidos considerando a tabela de preços: caderno R$ 10 pasta R$ 7.50 lápis RS 2.50 caneta R$ 3 borracha R$ 4 a) Monte um dicionário d em que a chave é o nome do produto e o valor é seu preço b) Escreva uma função para determinar o produto mais barato c) Devido a inflação, os produtos que custam menos de R$ 5 devem sofrer um aumento de preço de 20% enquanto os produtos que custam mais que R$ 5 devem sofrer um aumento de 10%. Escreva uma função que atualize a tabela de preços. # cria dicionário d = {} d['caderno'] = 10.0 d['pasta'] = 7.50 d['lapis'] = 2.50 d['caneta'] = 3.0 d['borracha'] = 4.0 # determina o produto mais barato def mais_barato(dic): menor_preco = max(d.values()) for chave, valor in d.items(): if valor < menor_preco: menor_preco = valor produto = chave saida = [produto, menor_preco] return saida # reajusta preços def reajusta_precos(dic): print('NOVA TABELA DE PREÇOS') for chave in dic.keys(): if dic[chave] < 5.0: dic[chave] = dic[chave]*1.2 else: dic[chave] = dic[chave]*1.1 print('%s : R$ %.2f' %(chave, dic[chave])) dic = {} for p in texto: if p not in dic: dic[p] = 1 else: dic[p] += 1 # Fecha arquivo arquivo.close() print('O nome Alice aparece %d vezes no texto' %dic['alice']) Pergunta: como faríamos para saber quais as 30 palavras que mais aparecem? Criando uma lista ordenada pelo número de ocorrências # Ranking: lista de palavras em ordem decrescente de ocorrências lista_palavras = sorted(dic, key=dic.get, reverse=True) for palavras in lista_palavras: print('%s : %d' %(palavras, dic[palavras])) Ex50: Crie uma função chamada frequencia que recebe como entrada uma string de caracteres que representam letras do alfabeto (i.e., aaabbbaabababccaabaacccc) e retorne como saída um dicionário que represente a frequência de cada caractere na cadeia. def frequencia(cadeia): d = {} for c in cadeia: if c not in d: d[c] = 1 else: d[c] += 1 return d cadeia = input('Entre com a cadeia de caracteres') f = frequencia(cadeia) for chave, valor in f.items(): print('%s : %d' %(chave, valor)) Ex51: Um dicionário recebe esse nome pois é uma estrutura muito utilizada para mapear ou traduzir uma string de entrada numa string de saída. Faça um pequeno tradutor de inglês – português utilizando algumas poucas palavras. d = {'I':'eu', 'the':'o', 'sky':'céu', 'you':'você', 'is': 'é', 'like':'gosto', 'love':'amo', 'dogs':'cachorros', 'cats':'gatos', 'in':'em', 'cars':'carros', 'blue':'azul'} def traducao(frase, dic): palavras = frase.split() saida = '' for p in palavras: saida = saida + ' ' + dic[p] return saida print('LISTA DE PALAVRAS') print(d) print() frase = input('Entre com uma frase: ') print(traducao(frase, d)) Ex52: Suponha uma turma com N alunos em que a avaliação final será realizada com base em 3 avaliações (provas P1, P2 e P3). Alunos com média inferior a 6 são reprovados e aqueles com média igual ou superior a 6 são aprovados. Sabendo disso responda: a) Escreva um trecho de código em Python referente aos dados a seguir. Considere um dicionário inicialmente vazio denominado dados, em que a chave é o nome do aluno e o valor é uma lista contendo as 3 notas que o aluno obteve. Alunos P1 P2 P3 João 6.5 5.0 7.0 Ana 8.5 4.0 7.5 Carlos 3.0 5.0 7.0 Maria 5.5 6.0 9.0 José 3.5 5.0 6.0 b) Deseja-se saber quais os aluno aprovados e reprovados. Escreva uma função que percorra o dicionário imprimindo na tela os nomes dos alunos e sua condição: aprovado ou reprovado. c) Os 3 melhores alunos da turma recebem uma bolsa de estudos como incentivo ao bom desempenho. Faça uma função que escreva na tela o nome dos alunos e as médias em ordem decrescente de nota, ou seja, um ranking dos estudantes. # cria dicionário d = {} d['João'] = [6.5, 5.0, 7.0] d['Ana'] = [8.5, 4.0, 7.5] d['Carlos'] = [3.0, 5.0, 7.0] d['Maria'] = [5.5, 6.0, 9.0] d['José'] = [3.5, 5.0, 6.0] # imprime na tela os aprovados e reprovados def verifica_aprovados(dic): for chave, valor in dic.items(): media = sum(valor)/len(valor) if media >= 6: print('%s aprovado(a): média = %.2f' %(chave, media)) else: print('%s reprovado(a): média = %.2f' %(chave, media)) # imprime na tela um ranking dos alunos por desempenho def gera_ranking(dic): medias_finais = {} for chave, valor in dic.items(): media = sum(valor)/len(valor) medias_finais[chave] = media # novo dicionário # retorna lista dos alunos em ordem decrescente de médias nomes = sorted(medias_finais, key=medias_finais.get, reverse=True) print('RANKING') for x in nomes: print('%s : %.2f' %(x, medias_finais[x])) Ex53: Criptografia é o processo de codificar mensagens utilizando um conjunto de símbolos de modo a dificultar a interpretação do seu conteúdo. Uma forma bastante simples de criptografar uma mensagem de texto consiste em realizar um mapeamento denominado de cifra de César. A ação de uma cifra de César é mover cada letra do alfabeto um número de vezes fixo abaixo no alfabeto. Este exemplo está com uma troca de três, então o B no texto normal se torna E no texto cifrado. a) Utilizando um dicionário para implementar a cifra de César, faça uma função encript que recebe como entrada 3 parâmetros: uma mensagem, um alfabeto e um valor K (deslocamento) e produz como saída a mensagem criptografada. b) Utilizando um dicionário para implementar a cifra de César, faça uma função decript que recebe como entrada 3 parâmetros: uma mensagem, um alfabeto e um valor K (deslocamento) e produz como saída a mensagem descriptografada. # Criptografa mensagem usando alfabeto e chave def encript(mensagem, alfabeto, chave): # aplica chave: alfabeto deslocado de k posições cifra = alfabeto[k:] + alfabeto[0:k] # cria 2 listas com os caracteres lista_alfabeto = list(alfabeto) lista_cifra = list(cifra) # cria mapeamento para os caracteres da mensagem # cria tuplas a partir de 2 listas e monta dicionário d = dict(zip(lista_alfabeto, lista_cifra)) # mapeia alfabeto # Criptografa mensagem saida = '' for c in texto.lower(): saida = saida + d[c] return saida # define a derivada de f(x) def df(x): return 3*x**2 + 1 #return 1 + np.sin(x) #return np.exp(x) + 2*np.sin(x) #return 1 - 2*np.cos(x) #return 100*x**99 # método de Newton # Parâmetros: # x - chute inicial # epson - tolerância para erro (quanto menor, melhor) def newton(x, epson): novo_x = np.random.random() erro = abs(x - novo_x) while erro >= epson: novo_x = x - f(x)/df(x) erro = abs(x - novo_x) print('x : %.10f ********** Erro: %.10f' %(novo_x, erro)) x = novo_x return x # Início do script x0 = float(input('Entre com o chute inicial (x0): ')) print('Raiz da função: %f' %newton(x0, 0.0000001)) # Plota gráfico da função eixo_x = np.linspace(-1, 1, 1000) # intervalo de plotagem eixo_y = f(eixo_x) plt.figure(1) plt.plot(eixo_x, eixo_y) plt.show() Método da secante Um problema com o método de Newton é que ele depende explicitamente da derivada da função f(x). Em alguns casos, ela pode ser difícil ou até mesmo impossível de calcular. Um outro método para encontrar raízes de funções que não requer derivadas é o método das secantes. Esse método pode ser pensado como uma aproximação do método de Newton utilizando a técnica de diferenças finitas para o computo numérico das derivadas. Iniciando pelos pontos x0 e x1 é possível construir uma linha entre (x0, f(x0)) e (x1, f(x1)), como indicado na figura acima. A equação dessa reta é dada por: y−f (x1)=m(x−x1) onde m= f (x1)−f (x0) x1−x0 (inclinação da reta) Assim, temos y= f (x1)−f (x0) x1−x0 (x−x1)+ f (x1) Para encontrar o ponto em que essa reta intercepta o eixo x, basta atribuir valor zero a y: f (x1)−f ( x0) x1−x0 (x−x1)+ f (x1)=0 Multiplicando ambos os lados por x1−x0 f (x1)−f ( x0) temos: x−x1+ f (x1) x1−x0 f ( x1)−f (x0) =0 Isolar x nos leva a: x=x1−f (x1) x1−x0 f (x1)−f (x0) Como o processo é iterativo e deve ser repetido por várias vezes, chega-se a seguinte relação de recorrência: xk=xk−1−f (xk−1) xk−1−xk−2 f (xk−1)−f ( xk−2) Ex55: Faça um programa que compute as raízes das mesmas funções do exercício anterior utilizando o método da secante. Utilize como critério de parada o erro absoluto entre duas estimativas: |xk−xk−1| . Se essa diferença for muito pequena, ou seja, menor que 10 -7, o método para de executar. import numpy as np import matplotlib.pyplot as plt # define função f(x) def f(x): return x**3 + x - 1 #return x - np.cos(x) #return np.exp(x) - 2*np.cos(x) #return x - 2*np.sin(x) #return x**100 # método da secante # Parâmetros: # x0, x1 - chutes iniciais # epson - tolerância para erro (quanto menor, melhor) def secante(x0, x1, epson): erro = abs(x0 - x1) while erro >= epson: novo_x = x1 - f(x1)*(x1 - x0)/(f(x1) - f(x0)) erro = abs(x1 - novo_x) print('x : %.10f ********** Erro: %.10f' %(novo_x, erro)) x0, x1 = x1, novo_x return novo_x # Início do script x0 = float(input('Entre com o primeiro chute inicial (x0): ')) x1 = float(input('Entre com o segundo chute inicial (x1): ')) print('Raiz da função: %f' %secante(x0, x1, 0.0000001)) # Plota gráfico da função eixo_x = np.linspace(-1, 1, 1000) # intervalo de plotagem eixo_y = f(eixo_x) plt.figure(1) plt.plot(eixo_x, eixo_y) plt.show() Para interessados em métodos numéricos, existem outros algoritmos para encontrar raízes de equações, como o método da bisseção e o método da posição falsa. Não iremos discuti-los neste curso, mas nem por isso são menos importantes. Integração numérica: A regra de Simpson A regra de Simpson é um método numérico que aproxima o valor de uma integral definida através de polinômios quadráticos (parábolas). É muito utilizado como uma forma computacional de se calcular a área sob uma curva. Primeiro, iremos derivar uma fórmula para calcular a área sob uma parábola definida pela equação y=ax2 +bx+c passando por 3 pontos: (-h, y0), (0, y1) e (h, y2), conforme ilustra a figura a seguir. A área sob a curva nada mais é que a integral definida da função y = f(x) de -h a h: # Define funções (forma alternativa: lambda functions) f = lambda x: np.sqrt((1 + x**3)) # de 1 a 4 g = lambda x: 1/np.sqrt((1 + x**4)) # de 0 a 2 p = lambda x: (1/(2*np.pi)**0.5)*np.exp(-0.5*x**2) # de 0 a 5 a = float(input('Entre com o limite inferior (a): ')) b = float(input('Entre com o limite superior (b): ')) n = int(input('Entre com o número de subintervalos (n): ')) print('A área sob a curva vale %f' %simpson(f, a, b, n)) Ordenação de dados Ser capaz de ordenar os elementos de um conjunto de dados é uma das tarefas básicas mais requisitadas por aplicações computacionais. Como exemplo, podemos citar a busca binária, um algoritmo de busca muito mais eficiente que a simples busca sequencial. Buscar elementos em conjuntos ordenados é bem mais rápido do que em conjuntos desordenados. Existem diversos algoritmos de ordenação, sendo alguns mais eficientes do que outros. Neste curso, iremos apresentar 3 deles: Bubble sort, Selection sort e Insertion sort. Bubble sort O algoritmo Bubble sort é uma das abordagens mais simplistas para a ordenação de dados. A ideia básica consiste em percorrer o vetor diversas vezes, em cada passagem fazendo flutuar para o topo da lista (posição mais a direita possível) o maior elemento da sequência. Esse padrão de movimentação lembra a forma como as bolhas em um tanque procuram seu próprio nível, e disso vem o nome do algoritmo (também conhecido como o método bolha) Embora no melhor caso esse algoritmo necessite de apenas n operações relevantes, onde n representa o número de elementos no vetor, no pior caso são feitas n2 operações. Portanto, diz-se que a complexidade do método é de ordem quadrática. Por essa razão, ele não é recomendado para programas que precisem de velocidade e operem com quantidade elevada de dados. A seguir veremos uma implementação em Python desse algoritmo. def BubbleSort(vetor): for i in range(len(vetor)-1, 0, -1): for j in range(i): if vetor[j] > vetor[j+1]: aux = vetor[j] vetor[j] = vetor[j+1] vetor[j+1] = aux Exemplo: mostre os passos necessários para a ordenação do seguinte vetor [5, 2, 13, 7, -3, 4, 15, 10, 1, 6] 1a passagem (levar maior elemento para última posição) [5, 2, 13, 7, -3, 4, 15, 10, 1, 6] [5, 2, 13, 7, -3, 4, 15, 10, 1, 6] [5, 2, 7, 13, -3, 4, 15, 10, 1, 6] [5, 2, 7, -3, 13, 4, 15, 10, 1, 6] [5, 2, 7, -3, 4, 13, 15, 10, 1, 6] [5, 2, 7, -3, 4, 13, 15, 10, 1, 6] [5, 2, 7, -3, 4, 13, 10, 15, 1, 6] [5, 2, 7, -3, 4, 13, 10, 1, 15, 6] [5, 2, 7, -3, 4, 13, 10, 1, 6, 15] 2a passagem (levar segundo maior para penúltima posição) [2, 5, 7, -3, 4, 13, 10, 1, 6, 15] [2, 5, 7, -3, 4, 13, 10, 1, 6, 15] [2, 5, -3, 7, 4, 13, 10, 1, 6, 15] [2, 5, -3, 4, 7, 13, 10, 1, 6, 15] [2, 5, -3, 4, 7, 13, 10, 1, 6, 15] [2, 5, -3, 4, 7, 10, 13, 1, 6, 15] [2, 5, -3, 4, 7, 10, 1, 13, 6, 15] [2, 5, -3, 4, 7, 10, 1, 6, 13, 15] 3a passagem (levar terceiro maior para antepenúltima posição) [2, 5, -3, 4, 7, 10, 1, 6, 13, 15] [2, -3, 5, 4, 7, 10, 1, 6, 13, 15] [2, -3, 4, 5, 7, 10, 1, 6, 13, 15] [2, -3, 4, 5, 7, 10, 1, 6, 13, 15] [2, -3, 4, 5, 7, 10, 1, 6, 13, 15] [2, -3, 4, 5, 7, 1, 10, 6, 13, 15] [2, -3, 4, 5, 7, 1, 6, 10, 13, 15] 4a passagem [-3, 2, 4, 5, 7, 1, 6, 10, 13, 15] [-3, 2, 4, 5, 7, 1, 6, 10, 13, 15] [-3, 2, 4, 5, 7, 1, 6, 10, 13, 15] [-3, 2, 4, 5, 7, 1, 6, 10, 13, 15] [-3, 2, 4, 5, 1, 7, 6, 10, 13, 15] [-3, 2, 4, 5, 1, 6, 7, 10, 13, 15] 5a passagem [-3, 2, 4, 5, 1, 6, 7, 10, 13, 15] [-3, 2, 4, 5, 1, 6, 7, 10, 13, 15] [-3, 2, 4, 5, 1, 6, 7, 10, 13, 15] [-3, 2, 4, 1, 5, 6, 7, 10, 13, 15] [-3, 2, 4, 1, 5, 6, 7, 10, 13, 15] 6a passagem [-3, 2, 4, 1, 5, 6, 7, 10, 13, 15] [-3, 2, 4, 1, 5, 6, 7, 10, 13, 15] [-3, 2, 1, 4, 5, 6, 7, 10, 13, 15] [-3, 2, 1, 4, 5, 6, 7, 10, 13, 15] 7a passagem [-3, 2, 1, 4, 5, 6, 7, 10, 13, 15] [-3, 1, 2, 4, 5, 6, 7, 10, 13, 15] [-3, 1, 2, 4, 5, 6, 7, 10, 13, 15] 8a passagem [-3, 1, 2, 4, 5, 6, 7, 10, 13, 15] [-3, 1, 2, 4, 5, 6, 7, 10, 13, 15] 9a passagem [-3, 1, 2, 4, 5, 6, 7, 10, 13, 15] Fim: garantia de que vetor está ordenado só é obtida após todos os passos. Selection sort A ordenação por seleção é um método baseado em se passar o menor valor do vetor para a primeira posição mais a esquerda disponível, depois o de segundo menor valor para a segunda posição e assim sucessivamente, com os n – 1 elementos restantes. Esse algoritmo compara a cada iteração um elemento com os demais, visando encontrar o menor. A complexidade desse algoritmo será sempre de ordem quadrática, isto é o número de operações realizadas depende do quadrado do tamanho do vetor de entrada. Algumas vantagens desse método são: é um algoritmo simples de ser implementado, não usa um vetor auxiliar e portanto ocupa pouca memória, é um dos mais velozes para vetores pequenos. Como desvantagens podemos citar o fato de que ele não é muito eficiente para grandes vetores. def SelectionSort(vetor): for i in range(len(vetor)): menor = i for k in range(i+1, len(vetor)): if vetor[k] < vetor[menor]: menor = k tmp = vetor[menor] vetor[menor] = vetor[i] vetor[i] = tmp Exemplo: mostre os passos necessários para a ordenação do seguinte vetor [5, 2, 13, 7, -3, 4, 15, 10, 1, 6] 1a passagem: [5, 2, 13, 7, -3, 4, 15, 10, 1, 6] → [-3, 2, 13, 7, 5, 4, 15, 10, 1, 6] 2a passagem: [-3, 2, 13, 7, 5, 4, 15, 10, 1, 6] → [-3, 1, 13, 7, 5, 4, 15, 10, 2, 6] 3a passagem: [-3, 1, 13, 7, 5, 4, 15, 10, 2, 6] → [-3, 1, 2, 7, 5, 4, 15, 10, 13, 6] d) 4o acesso: [-3, 2, 1, 4, 5, 6, 7, 8, 10, 13, 15] encontrou o elemento x = 8, PARE. Exercício: Utilizando um dos algoritmos de ordenação estudados, compute as estatísticas de ordem de um conjunto de 100 números inteiros gerados aleatoriamente e pertencentes ao intervalo [0, 99]. Mínimo: menor elemento do conjunto Q1: 1o quartil (elemento que divide o conjunto ordenado em 4 partes iguais: 1o elemento) Q2: 2o quartil ou mediana (elemento do meio no conjunto ordenado) Q3: 3o quartil (elemento que divide o conjunto ordenado em 4 partes iguais: 3o elemento) Máximo: maior elemento do conjunto Links interessantes: a) simulação dos algoritmos de ordenação na forma de danças https://www.youtube.com/user/AlgoRythmics b) animações dos algoritmos de ordenação https://www.toptal.com/developers/sorting-algorithms Regressão linear simples Regressão linear simples é um método estatístico que nos permite estudar e analisar relacionamentos entre duas variáveis aleatórias: - uma variável, denotada por x, denominada de variável independente ou exploratória; - outra variável, denotada por y, denominada de variável resposta ou dependente; Antes de proceder, devemos esclarecer que tipos de relacionamentos não nos interessa estudar na regressão linear: relacionamentos determinísticos ou funcionais. A figura a seguir ilustra um exemplo de relacionamento determinístico: a relação entre temperaturas em Celsius e Farenheit. Note que os pontos (x, y) observados caem diretamente sobre uma linha reta. Isso ocorre porque o relacionamento entre graus Celsius e Farenheit é dada por: F= 9 5 C+32 ou seja, se você conhece a temperatura em Celsius você pode usar essa equação para determinar a temperatura em Farenheit exatamente. Alguns outros exemplos de relações determinísticas incluem: - circunferência = 2π r - Lei de Ohm: U = Ri - velocidade = s / t Para cada uma dessas relações determinísticas existe uma função que descreve exatamente o relacionamento entre duas variáveis. Regressão linear não está interessada em estudar esses tipos de relações, mas sim relações estatísticas (não-determinísticas). O exemplo a seguir ilustra um relacionamento estatístico entre duas varáveis: a variável resposta y é a mortalidade devido ao câncer de pele (número de mortes para cada 10 milhões de pessoas) e a variável exploratória x é a latitude do centro de cada um dos 49 estados norte-americanos. O gráfico sugere um relacionamento negativo entre a latitude e a mortalidade devido ao câncer de pele, mas o relacionamento não é perfeito, ou seja, o gráfico exibe um comportamento aproximado, uma tendência, devido ao espalhamento e a incerteza presente nos dados. Alguns outros exemplos de relacionamentos estatísticos incluem: - Altura e peso - Álcool consumido e taxa de álcool no sangue - Capacidade pulmonar e anos de fumo Assim, o problema em questão consiste em, dado um conjunto de pontos observados (x i , y i) para i=1,.. , n , estimar o melhor relacionamento linear possível entre as duas variáveis. Em outras palavras, desejamos encontrar a reta que melhor se ajusta aos dados. O grau de ajuste é definido em termos dos erros entre os verdadeiros valores de y i e suas predições lineares ŷ i . O objetivo consiste em encontrar a reta y=α+β x que minimiza a soma dos resíduos ao quadrado, dado por Q(α ,β)=∑i=1 n ei 2 =∑i=1 n ( y i− ŷ i) 2 =∑i=1 n ( y i−α−β xi) 2 Derivando Q(α ,β) em relação a α e igualando o resultado a zero, tem-se: d Q(α ,β) d α =∑i=1 n ( y i−α−β x i)=0 Aplicando a distributiva: ∑i=1 n y i−nα−β∑i=1 n x i=0 Dividindo tudo por n, temos: ȳ−α−β x̄=0 Isolando α chega-se a: α= ȳ−β x̄ (I) Da mesma forma, derivando Q(α ,β) em relação a β nos leva a: d Q(α ,β) dβ =∑i=1 n ( y i−α−β x i)x i=0 Aplicando a distributiva: ∑i=1 n (xi y i−α x i−β x i 2 )=0 Separando o somatório nos leva a: ∑i=1 n x i y i−α∑i=1 n xi−β∑i=1 n x i 2 =0 Dividindo tudo por n temos: xy−α x−β x2 =0 (II) Substituindo a equação (I) na equação (II) temos: xy−( y−β x )x−β x2 =0 Aplicando a distributiva: A questão é: como evoluir uma configuração de forma a construir esse padrão? Que regras são aplicadas para definir quais células vivem ou morrem na próxima geração? A função de transição do autômato da figura é dada pela seguinte tabela. x t(i−1) x t(i) x t(i+1) x t+ 1(i) 0 0 0 0 0 0 1 1 0 1 0 0 0 1 1 0 1 0 0 1 1 0 1 1 1 1 0 0 1 1 1 0 Uma forma de resumir toda essa tabela é através da seguinte representação: O que essa regra nos diz pode ser sumarizado em 8 fatos: 1) sempre que a célula i for morta e a (i-1) e (i+1) também forem, a célula i permanecerá morta na próxima geração. 2) sempre que a célula i for morta, a (i-1) for morta e a (i+1) for viva, a célula i viverá na próxima geração. 3) sempre que a célula i for viva e a (i-1) e a (i+1) forem mortas, a célula i morrerá na próxima geração. 4) sempre que a célula i for viva, a (i-1) for morta e a (i+1) for viva, a célula i morrerá na próxima geração. 5) sempre que a célula i for morta, a (i-1) for viva e a (i+1) for morta, a célula i viverá na próxima geração. 6) sempre que a célula i for morta e ambas (i-1) e (i+1) forem vivas, a célula i viverá na próxima geração. 7) sempre que a célula i for viva, a (i-1) for viva e a (i+1) for morta, a célula i morrerá na próxima geração. 8) sempre que a célula i for viva e ambas (i-1) e (i+1) forem vivas, a célula i morrerá na próxima geração. Note que não existem mais combinações possíveis de 0’s e 1’s usando apenas 3 bits, pois conseguimos contar em binário de 0 a 7, o que resulta em 8 possibilidades. Essa regra tem um nome: é a regra 50, pois o número binário correspondente a última coluna da função de transição vale 00110010, que em binário é justamente o número 50. Sendo assim, quantas possíveis regras existem para um autômato celular elementar? Basta computar 28, que resulta em 256. Portanto, o número total de regras distintas é 256. Por essa razão dizemos que existem 256 autômatos celulares elementares distintos, um para cada regra. O interessante é estudar e simular o que acontece com cada um desses autômatos durante sua evolução. De acordo com Wolfram, existem 4 classes de regras para um autômato celular elementar: - Classe 1: Estado Homogêneo Todas as células chegarão num mesmo estado após um número finito de estados - Classe 2: Estável simples As células não possuirão todas o mesmo estado, mas eles se repetem com a evolução temporal - Classe 3: Padrão irregular Não possui padrão reconhecível - Classe 4: Estrutura complexa Estruturas complexas que evoluem imprevisivelmente As regras mais interessantes são as da classe 4, pois definem um sistema complexo com propriedades dinâmicas interessantes, sendo algumas delas capazes até de simular máquinas de Turing, que são modelos computacionais capazes de serem programadas para realizar diferentes tarefas computacionais. Um exemplo de regra com essa característica é a regra 110. (Referências: https://en.wikipedia.org/wiki/Rule_110, http://www.complex-systems.com/pdf/15-1-1.pdf) Exercício: Construa a função de transição do autômato celular elementar definido pela regra 30. Aplique a regra para evoluir a condição inicial idêntica a da figura da regra 50 (apenas uma célula viva) por 20 gerações. Repita o exercício mas agora para a regra 110. A seguir é apresentado um algoritmo para a simulação de autômatos celulares elementares. geração = vetor(N) (N é o número de células) nova_geração = vetor(N) evolução = matriz(MAX, N) (MAX é o número de gerações) Inicializar geração (setar configuração inicial) para i = 1 até MAX evolução[i,:] = geração # Percorre cada célula da geração atual para j = 1 até N Aplicar regra de transição na célula j, gerando nova_geração geração = nova_geração Plotar resultados Ex: Baseado no algoritmo anterior, implementar um script em Python que, dado uma regra (número de 0 a 255), evolua uma configuração inicial de tamanho N = 1000 até a geração 500. import numpy as np import matplotlib.pyplot as plt # converte um número inteiro para sua representação binária (0-255) def converte_binario(numero): binario = bin(numero) binario = binario[2:] if len(binario) < 8: zeros = [0]*(8-len(binario)) binario = zeros + list(binario) return list(binario) # Início do script MAX = 500 g = np.zeros(1000) ng = np.zeros(1000) regra = int(input('Entre com o número da regra: ')) codigo = converte_binario(regra) Um problema comum que afeta simulações computacionais do jogo da vida é o chamado problema de valor de contorno. Isso nada mais é que uma falha ao se definir a função de transição para células na borda do sistema. Para se evitar essa indefinição, considera-se que o reticulado é na verdade um toro. Isso significa que não existem bordas, uma vez que a borda da esquerda é ligada a borda da direita, assim como a inferior é ligada a superior. Ex: Implementar um script em Python que, dada uma configuração inicial, simule o jogo da Vida num tabuleiro de dimensões 100 x 100 por 200 gerações. import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation import time # cria inicialização: rpentomino nas coordenadas i, j def init_config(tabuleiro, padrao, i, j): linhas = padrao.shape[0] colunas = padrao.shape[1] tabuleiro[i:i+linhas, j:j+colunas] = padrao # Início do script inicio = time.time() MAX = 200 SIZE = 100 geracao = np.zeros((SIZE, SIZE)) nova_geracao = np.zeros((SIZE, SIZE)) # Cubo de dados em que cada fatia representa uma geração matriz_evolucao = np.zeros((SIZE, SIZE, MAX)) # Define geração inicial unbounded = np.array([[1, 1, 1, 0, 1], [1, 0, 0, 0, 0], [0, 0, 0, 1, 1], [0, 1, 1, 0, 1], [1, 0, 1, 0, 1]]) glider = np.array([[1, 0, 0], [0, 1, 1], [1, 1, 0]]) r_pentomino = np.array([[0, 1, 1], [1, 1, 0], [0, 1, 0]]) diehard = np.array([[0, 0, 0, 0, 0, 0, 1, 0], [1, 1, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 1, 1, 1]]) acorn = np.array([[0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0], [1, 1, 0, 0, 1, 1, 1]]) init_config(geracao, r_pentomino, SIZE//2, SIZE//2) # Laço principal (atualiza gerações) for k in range(MAX): print('Processando geração %d...' %k) matriz_evolucao[:,:,k] = geracao # Laço principal: atualiza as gerações for i in range(SIZE): for j in range(SIZE): vivos = geracao[i-1, j-1] + geracao[i-1, j] + \ geracao[i-1, (j+1)%SIZE] + geracao[i, j-1] + \ geracao[i, (j+1)%SIZE] + geracao[(i+1)%SIZE, j-1] + \ geracao[(i+1)%SIZE, j] + geracao[(i+1)%SIZE, (j+1)%SIZE] if (geracao[i,j] == 1): if (vivos == 2 or vivos == 3): nova_geracao[i,j] = 1 else: nova_geracao[i,j] = 0 else: if (vivos == 3): nova_geracao[i,j] = 1 geracao = nova_geracao.copy() fim = time.time() print('Tempo gasto na simulação: %.2f s' %(fim-inicio)) # Gera animação da evolução do sistema fig = plt.figure(1) plt.axis('off') lista = [] for i in range(MAX): im = plt.imshow(matriz_evolucao[:,:,i], cmap='gray') lista.append([im]) ani = animation.ArtistAnimation(fig, lista, interval=100, blit=True, repeat_delay=1000) plt.show() Dilema do prisioneiro O Dilema dos Prisioneiros é um jogo muito famoso que representa bem o dilema entre cooperar e trair. Resumidamente, a estória é a seguinte. Dois suspeitos, A e B, são presos pela polícia. A polícia não tem provas suficientes para os condenar, então separa os prisioneiros em salas diferentes e oferece a ambos o mesmo acordo: 1. Se um dos prisioneiros confessar (trair o outro) e o outro permanecer em silêncio, o que confessou sai livre enquanto o cúmplice silencioso cumpre 10 anos. 2. Se ambos ficarem em silêncio (colaborarem um com ou outro), a polícia só pode condená-los a 1 ano cada um. 3. Se ambos confessarem (traírem o comparsa), cada um leva 5 anos de cadeia. Cada prisioneiro faz a decisão sem saber a escolha do outro - eles não podem conversar. Como o prisioneiro vai reagir? Existe algum decisão racional a tomar? Qual seria a sua decisão? Matriz de ganhos Uma forma esquemática para mostrar uma interação humana, ou seja, um jogo, é através de uma "matriz de resultados". Embora o enunciado do problema seja simples e intuitivo para entender de forma verbal, a representação gráfica oferece uma grande ajuda para visualizar o cenário de forma completa e entender as opções e implicações para cada jogador. Nesta figura você visualiza as duas opções de cada prisioneiro e o resultado de cada combinação de ação. Para cada célula, os valores vermelhos a direita referem-se ao Prisioneiro A; os azuis a esquerda referem-se ao Prisioneiro B. Estão descritos quantos anos cada prisioneiro ficará na cadeia. Neste cenário, quando menor o valor da pena, melhor para o prisioneiro. Os prisioneiros não podem combinar a decisão (estão em salas isoladas e sem comunicação) e devem escolher simultaneamente. Cada jogador quer ficar preso o menor tempo possível, ou seja, maximizar seu resultado individual. Qual a melhor decisão? Considerando os incentivos deste jogo (os valores das penas para cada combinação de decisões na matriz), existe uma única decisão racional a tomar: trair. A explicação é a seguinte: Imagine que você é o prisioneiro A. Assim, você raciocina nas duas hipóteses: - Suponha que o Prisioneiro B escolha Colaborar. Então, se você escolher Colaborar, leva 1 ano de prisão. Se escolher Trair, sai livre. Neste caso, Trair é a melhor opção. - Suponha que o Prisioneiro B escolha Trair. Então, se você escolher Colaborar, leva 10 anos de prisão. Se escolher Trair, fica com 5 anos. Neste caso, Trair é a melhor opção. Perceba que Trair é a melhor opção em ambos os casos. Em outras palavras, Trair é a melhor opção independente da decisão do Prisoneiro B. Se ambos colaborarem (manterem o preço original), os dois ganham $50 por dia. Se um deles abaixar o preço, recebe $60, enquanto o que mantém recebe apenas $30. Já se ambos reduzirem o preço, o resultado para cada um será $40, pois significa abaixar o preço sem aumentar o volume de clientes. De acordo com a metodologia de análise no Dilema dos Prisioneiros, reduzir-reduzir é o ponto de equilíbrio ($40, $40), pois abaixar o preço é a estratégia dominante em cada um, resultando em valor pior se comparado àquele inicial. Eles caíram na armadilha, e muitos chamam essas situações de dilema social — o interesse individual e a análise estritamente matemática e racional induzem a resultados piores do que opções que consideram o interesse coletivo. Como já foi mencionado, é difícil sair dessa armadilha — quem vai arriscar a colaborar (manter o preço), se há chance de o outro trair (reduzir o preço) e ganhar sozinho? Para o leitor interessado, recomendamos acessar o link do livro “Estratégias de Decisão” em: http://estrategiasdedecisao.com/dilema-dos-prisioneiros/ Dilema do prisioneiro iterativo O Dilema do Prisioneiro Iterativo (DPI) é uma referência bem conhecida para o estudo dos comportamentos de longo prazo de agentes racionais, como a cooperação que pode surgir entre agentes egoístas e independentes que precisam coexistir a longo prazo. Muitas estratégias bem conhecidas foram estudadas, formando a estratégia simples tit-for-tat (TFT), tornada famosa por Axelrod após seus torneios influentes, para outras mais envolvidas, como estratégias sem determinantes estudadas recentemente por Press e Dyson. Na prática, o Dilema do Prisioneiro Iterativo é uma repetição do dilema do prisioneiro com as seguintes restrições na matriz de ganhos: C T C (R, R) (S, T) T (T, S) (P, P) a) T > R > P > S: isso torna (T, T) a estratégia dominante no dilema do prisioneiro. b) 2R > T + S: isso faz (C, C) a melhor escolha para os jogadores a longo prazo. A seguir definimos algumas estratégias que podem ser adotadas no DPI. Estratégias 1. Colaboração incondicional: sempre colabora. 2. Traição incondicional: sempre trai. 3. Aleatório: 50% de chance para colaborar, 50% de chance para trair. 4. Colaboração com probabilidade P = Traição com probabilidade (1 - P) 5. Tit for Tat (TFT): coopera no 1o round e depois copia a ação anterior do oponente. 6. Suspicious TFT (STFT): trai no 1o round e depois copia a ação anterior do oponente. 7. Tit for Two Tats (TFTT): coopera sempre a menos que seja traído duas vezes em sequência. 8. Two Tits for Tats (TTFT): trai duas vezes seguidas após ser traído, caso contrário coopera. 9. Grim Trigger: Coopera, até que o oponente traia uma vez e então trai para o resto das jogadas. 10. Pavlov: Coopera se você e seu oponente tiveram estratégias iguais na jogada anterior, e trai se as jogadas anteriores foram diferentes. A seguir mostramos uma implementação em Python do DPI com algumas estratégias selecionadas. import numpy as np MAX = 100 ######################################################### # Definição das estratégias ######################################################### def always_cooperate(): return 0 def always_defect(): return 1 def random_player(): return np.random.randint(2) # Coopera na primeira escolha, depois copia a última escolha do oponente def tit_for_tat(p2, i): if i == 0: return 0 else: return p2 # Coopera até que adversário traia, e depois sempre trai def grim_trigger(p2): if p2 == 1: return 1 else: return 0 # Coopera se você e seu oponente tiveram estratégias iguais na jogada anterior e trai se as jogadas anteriores foram diferentes. def pavlov(p1, p2, i): if i == 0 or p1 == p2: return 0 else: return 1 ######################################################### # 0 = cooperar e 1 = trair # Definição das matrizes de ganho MA = np.array([[3,0],[5,1]]) MB = np.array([[3,5],[0,1]]) # Gera estratégias e computa os ganhos playerA = -1*np.ones(MAX, dtype='int') playerB = -1*np.ones(MAX, dtype='int') ganhoA = 0 ganhoB = 0 for i in range(MAX): playerA[i] = random_player() playerB[i] = tit_for_tat(playerA[i-1], i) ganhoA += MA[playerA[i], playerB[i]] ganhoB += MB[playerA[i], playerB[i]] print('Resultado') if ganhoA > ganhoB: print('Jogador A venceu') elif ganhoA < ganhoB: print('Jogador B venceu') else: print('Empate') print('Jogador A: %d ' %ganhoA) print('Jogador B: %d ' %ganhoB) print('Ganho total: %d' %(ganhoA+ganhoB)) Note que do ponto de vista individual, a estratégia always_defect(), vence qualquer outra estratégia. Porém, não é a melhor em termos de maximizar o ganho global. Por exemplo, se o jogador A adota a estratégia random_player() e o jogador adota a estratégia tit_for_tat(), em média o ganho global é superior aquele obtido quando um deles escolhe a estratégia always_defect(). Para maiores detalhes sobre estratégias para o DPI, o leitor interessado pode acessar: http://jasss.soc.surrey.ac.uk/20/4/12.html Recursão Dizemos que uma função é recursiva se ela é definida em termos dela mesma. Em matemática e computação uma classe de objetos ou métodos exibe um comportamento recursivo quando pode ser definido por duas propriedades: 1. Um caso base: condição de término da recursão em que o processo produz uma resposta. 2. Um passo recursivo: um conjunto de regras que reduz todos os outros casos ao caso base. A série de Fibonacci é um exemplo clássico de recursão, pois: F(0) = 0 (caso base 1) F(1) = 1 (caso base 2) Para todo n > 1, F(n) = F(n - 1) + F(n – 2) Em computação, algoritmos recursivos existem como uma alternativa a algoritmos iterativos, ou seja, métodos que utilizam explicitamente estruturas de repetição. Com a recursão, podemos eliminar iterações. A seguir veremos uma implementação em Python de uma função recursiva para o cálculo do fatorial de um inteiro arbitrário n. def fatorial(n): if n == 1: return 1 else: return n*fatorial(n-1) import numpy as np # particiona o vetor de entrada em menores e maiores que pivô def particiona(vetor, ini, fim): pivo = vetor[fim] i = ini # trasfere menores que pivô para a esquerda e maiores a direita for j in range(ini, fim): if vetor[j] <= pivo: vetor[i], vetor[j] = vetor[j], vetor[i] i += 1 vetor[i], vetor[fim] = vetor[fim], vetor[i] return i # retorna posição do pivô # separa os menores que pivô a esquerda e maiores a direita # faz o mesmo que a função particiona, mas de outra maneira def separa(vetor, ini, fim): c, i, j = vetor[ini], ini+1, fim while True: while i <= fim and vetor[i] <= c: i += 1 while c < vetor[j]: j -= 1 if i >= j: break vetor[i], vetor[j] = vetor[j], vetor[i] i += 1 j -= 1 vetor[ini] = vetor[j] vetor[j] = c return j # Chama recursivamente a função para ordenar metades def quicksort(vetor, ini, fim): if ini < fim: p = particiona(vetor, ini, fim) quicksort(vetor, ini, p-1) quicksort(vetor, p+1, fim) # Início do script lista = np.random.randint(1, 100, 20) print('Lista desordenada:') print(lista) print() quicksort(lista, 0, len(lista)-1) print('Lista ordenada:') print(lista) Exemplo: mostre os passos necessários para a ordenação do seguinte vetor [5, 2, 13, 7, -3, 4, 15, 10, 1, 6] 1o passo: Definir pivô = 6 (último elemento) 2o passo: Particionar vetor (menores a esquerda e maiores a direita) [5, 2, -3, 4, 1, 6, 13, 7, 15, 10] 3o passo: Aplicar 1 e 2 recursivamente para as metades a) 2 metades Metade 1: [5, 2, -3, 4, 1] → pivô = 1 [-3, 1, 5, 2, 4, 6, 13, 7, 15, 10] Metade 2: [13, 7, 15, 10] → pivô = 10 [-3, 1, 5, 2, 4, 6, 7, 10, 15, 13] b) 4 metades Note que a metade 1 possui um único elemento: [-3] → já está ordenada Metade 2: [5, 2, 4] → pivô = 4 [-3, 1, 2, 4, 5, 6, 7, 10, 15, 13] Note que a metade 3 possui apenas um único elemento: [7] → já está ordenadas Metade 4: [15, 13] → pivô = 13 [-3, 1, 2, 4, 5, 6, 7, 10, 13, 15] c) 4 metades: Note que cada uma das 4 metades restantes contém um único elemento e portanto já estão ordenadas. Fim: Assim, o vetor já está totalmente ordenado e o algoritmo para. Busca binária Vimos anteriormente que a busca binária requer uma lista ordenada de elementos para funcionar. Basicamente, a ideia consiste em acessar o elemento do meio da lista. Se ele for o que desejamos buscar, a busca se encerra. Caso contrário, se o que desejamos é menor que o elemento do meio, a busca é realizada na metade a esquerda. Senão, a busca é realizada na metade a direita. A seguir mostramos um script em Python que implementa a versão recursiva da busca binária. import random # Função recursiva def binary_search(lista, x, ini, fim): meio = ini + (fim - ini) // 2 if ini > fim: return -1 # elemento não encontrado elif lista[meio] == x: return meio elif lista[meio] > x: return binary_search(lista, x, ini, meio-1) else: return binary_search(lista, x, meio+1, fim) # Cria lista com valores aleatórios L = [] for i in range(100): L.append(random.randint(1, 100)) valor = int(input('Entre com o elemento a ser buscado: ')) L.sort() y = binary_search(L, valor, 0, len(L)-1) if y == -1: print('Elemento %d não encontrado.' %valor) else: print('Elemento %d está na posição %d' %(valor, y)) O Problema do Casamento Estável Objetivo: Dados como entrada Um conjunto M de n homens + n listas de preferências e Um conjunto W de mulheres + n listas de preferências Obter um casamento de cada homem de M com uma mulher de W de modo que ele seja estável. O conceito de estabilidade - Rankings / Listas de preferências: cada pessoa de um conjunto deve ter uma lista em que rankeia todas as pessoas do outro conjunto. ∀m∈M ∃r (m)=[w1 , w2 , ... ,wn] ∀ w∈W ∃r (w)=[m1, m2 ,... , mn] Definiremos um predicado ternário P(m, w, w’) para denotar que m prefere w a w’ , ou seja, w vem antes de w’ na lista r(m) Algoritmo de Gale-Shappley (conj. M dominante) Enquanto ∃mi livre (que ainda não tentou ∀ wi∈W ) Seja mi esse homem Seja wi a mulher mais bem rankeada em r(mi) (para a qual mi ainda não propôs) Se wi está livre (mi, wi) “engaged” (adiciona em S temporariamente) Senão // significa que ∃(m̄ , wi)∈S Se P(w i ,m̄ , mi) mi continua livre (vai continuar tentando) Senão // significa que P(w i ,mi , m̄) (mi, wi) “engaged” m̄ fica livre (vai tentar outras parceiras) Propriedades do algoritmo: i) Ponto de vista de mi∈M : a cada troca, sequencia de parceiras piora Para que está no conjunto dominante, sempre que houver uma troca será para pior ii) Ponto de vista de w i∈W : a cada troca, sequencia de parceiros melhora Para quem está conjunto passivo, sempre que houver uma troca será para melhorar Teorema: O emparelhamento S retornado pelo algoritmo de Gale-Shappley é estável. Prova: por contradição 1. Suponha que S retornado pelo algoritmo GS não é estável. Então, existe uma instabilidade (m, w) 2. Isso implica que P(m ,w , w ' )∧P(w , m ,m ' ) 3. Mas se m finalizou o algoritmo com w’, significa que a última proposta de m foi para w’ 4. Então, m propôs a w antes de w’ Isso implica que na lista de w, m’ deve vir antes de m, ou seja, P(w ,m ' , m) Caso contrário, w teria ficado com m’ 5. Assim, chegamos em P(w ,m ,m')∧P (w , m' , m) , que é falso sempre pois se um dos termos é verdade o outro não pode ser verdade também. Em outras palavras, não é possível para uma mulher m preferir m a m’ e m’ a m simultaneamente. 6. Portanto, a suposição inicial não é válida e a instabilidade não pode existir em S. Def: valid partner (vp) Dizemos que w é vp(m) se existe S estável tal que (m , w)∈S Def: best valid partner (bvp) Dizemos que w é bvp(m) se w é vp(m) e ∄w '≠w que seja vp(m) tal que P(m ,w ' , w) Teorema: Toda execução do algoritmo Gale-Shappley (com M dominante) resulta no conjunto S* ={(m , bvp(m)) ,∀m∈M } Esse resultado é de fundamental importância pois garante que não importa a ordem de escolha dos homens, o resultado final será sempre o mesmo. Esse fato do ponto de vista da teoria dos jogos significa que nesses tipos de sistemas não existe barganha, ou seja, não é possível barganhar e comprar o direito de escolher primeiro, pois o resultado será sempre o mesmo. Prova: por contradição 1. Suponha que S é um emparelhamento estável gerado pelo algoritmo GS em que m está emparelhado com laguém que não é sua bvp(m) = w 2. Então, m deve ter sido rejeitado por w = bvp(m), uma vez que ele começa a propor em ordem decrescente de preferência 3. Seja m o primeiro homem para o qual isso ocorre (ser rejeitado). Ele pode ter sido rejeitado por: a) m propôs a w, mas foi negado pois ela preferiu ficar com o atual m’ b) w rompeu com m por uma proposta melhor de um homem m’ Em qualquer caso, é certo que w está emparelhada a m’, o qual ela prefere, ou seja: P(w ,m ' , m) 4. Pela definição de bvp(m) existe um emparelhamento estável S’ que contém o par (m,w). 5. A pergunta é: Com quem m’ estaria emparelhado em S’? Com uma mulher w '≠w=bvp (m) 6. Note que m’ não pode ter sido rejeitado por ninguém quando se engajou com w, pois m foi o primeiro a ser rejeitado (de acordo com o passo 3). Como m’ propõe em ordem decrescente de preferência, então m’ prefere w a w’ (senão em S ele teria escolhido w’ e não w). Logo: P(m ' , w , w ') 7. Assim, como temos P(w ,m ' ,m)∧P (m' , w ,w ') e o par (m' , w)∉S ' , temos que (m’, w) define uma instabilidade em S’ (contradição pois S’ era suposto estável) Portanto, S deve obrigatoriamente associar todo homem m a sua bvp(m). Ex: Suponha que num site de relacionamentos existam as seguintes listas de preferências entre um grupo de rapazes e garotas. Man 1 2 3 4 5 A1 B2 A2 D2 E2 C2 B1 D2 B2 A2 C2 E2 C1 B2 E2 C2 D2 A2 D1 A2 D2 C2 B2 E2 E1 B2 D2 A2 E2 C2 Woman 1 2 3 4 5 A2 E1 A1 B1 D1 C1 B2 C1 B1 D1 A1 E1 C2 B1 C1 D1 E1 A1 D2 A1 E1 D1 C1 B1 E2 D1 B1 E1 C1 A1 Utilizando o algoritmo de Gale-Shapley encontre: a) um emparelhamento estável M sendo os rapazes o conjunto dominante. Mostre a sequencia de propostas até o emparelhamento estável. i m w engaged 1 A1 B2 yes 2 B1 D2 yes 3 C1 B2 yes 4 A1 A2 yes 5 D1 A2 no 6 D1 D2 yes 7 B1 B2 no 8 B1 A2 no 9 B1 C2 yes 10 E1 B2 no 11 E1 D2 yes 12 D1 C2 no 13 D1 B2 no 14 D1 E2 yes Vamos simular várias iterações do método em Python para analisar o comportamento do tamanho da população em função do tempo t. O script em Python a seguir mostra uma implementação computacional do modelo utilizando 100 iterações. import numpy as np import matplotlib.pyplot as plt # Número de iterações para atingir equilíbrio MAX = 1000 r = float(input('Entre com a constante r: ')) x = float(input('Entre com x0: ')) population = [x] for i in range(1, MAX): x = r*x*(1 - x) population.append(x) print('População no longo prazo: ', population[-1]) # Plota gráfico da população pelo tempo eixox = list(range(MAX)) plt.figure(1) plt.plot(eixox, population) #plt.axis([-1, 100, 0, 1]) plt.show() Execute o script e veja o que acontece para as entradas a seguir: a) r = 1 e x0 = 0.4 (extinção) b) r = 2 e x0 = 0.4 (equilíbrio em 50%) c) r = 2.4 e x0 = 0.6 (pequena oscilação, mas atinge equilíbrio em 58%) d) r = 3 e x0 = 0.4 (não há equilíbrio, população oscila, mas em torno de uma média) e) r = 4 e x0 = 0.4 (comportamento caótico, totalmente imprevisível) Em seguida, iremos estudar o que acontece com a população de equilíbrio conforme variamos o valor do parâmetro r. A ideia é que no eixo x iremos plotar os possíveis valores de r e no eixo y iremos plotar a população de equilíbrio para aquele valor de r específico. Iremos considerar que a população do equilíbrio é obtida depois de 1000 iterações. O script em Python a seguir mostra a implementação computacional dessa análise. import matplotlib.pyplot as plt import numpy as np # Cria um vetor com todos os possíveis valores de r R = np.linspace(0.5, 4, 20000) m = 0.5 # Inicializa os eixos x e y vazios X = [] Y = [] # Loop principal (iterar para todo r em R) for r in R: # Adiciona r no eixo x X.append(r) # Escolhe um valor aleatório entre 0 e 1 x = np.random.random() # Gera população de equilíbrio for l in range(1000): x=r*x*(1-x) Y.append(x) # Plota o gráfico sem utilizar retas ligando os pontos plt.plot(X, Y, ls='', marker=',') plt.show() O gráfico plotado pelo script acima é conhecido como bifurcation map. Esse fenômeno da bifurcação ocorre como uma manifestação do comportamento caótico da população de equilíbrio para valores de r maiores que 3. Na prática, o que temos é que para um valor de r = 3.49999, a população de equilíbrio é muito diferente daquela obtida para r = 3.50000 por exemplo. Pequenas perturbações no parâmetro r causa um efeito devastador na população de equilíbrio. Esse é o lema da teoria do caos, que pode ser parafraseado pela célebre sentença: o simples bater de asas de uma borboleta pode levar ao surgimento de um gigantesco furação, conhecido também como o efeito borboleta. Uma das propriedades do caos é que é possível encontrar ordem em comportamentos caóticos. Por exemplo, a seguir iremos desenvolver um script em Python para plotar uma sequencia de populações, começando de uma população inicial arbitrária e utilizando o valor de r = 3.99. import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np r = 3.99 x = np.random.random() # a população é x minúsculo! X = [x] # a lista é X maiúsculo! for i in range(1000): x = r*x*(1 - x) X.append(x) # Plota o gráfico da sequência plt.figure(1) plt.plot(X) plt.show() A = X[:len(X)-2] B = X[1:len(X)-1] C = X[2:] #Plota atrator em 3D fig = plt.figure(3) ax = fig.add_subplot(111, projection='3d') ax.plot(A, B, C, '.', c='red') plt.show() Note que o gráfico mostrado na figura 1 parece o de uma sequência totalmente aleatória. Na sequência, iremos plotar cada subsequência (xn , xn+1 , xn+2) como um ponto no R3. Na prática, isso significa que no eixo X iremos plotar a sequência original, no eixo Y iremos plotar a sequência deslocada de uma unidade e no eixo Z iremos plotar a sequência deslocada de duas unidades. Qual será o gráfico formado? Se de fato a sequência for completamente aleatória, nenhum padrão deverá ser observado, apenas pontos dispersos aleatoriamente pelo espaço. Mas, surpreendentemente, temos a formação do seguinte padrão, conhecido como o atrator do modelo.

1 / 88

Toggle sidebar

Documentos relacionados