INF1631 - Estruturas Discretas - 99.1

Aulas

4/3 - Introdução

objetivos do curso, "outline"

9/3 - A Estrutura dos Naturais

11/3 - A Estrutura de Listas

13/3 - A Estrutura de Listas (laboratório)

18/3 - Indução Forte

23/3 - Laboratório

  1. Considere a função abaixo:
          (define f
            (lambda (x n)
              (if (zero? n)
                  1
                  (if (even? n)
                      (f (* x x) (quotient n 2))
                      (* x (f (* x x) (quotient n 2)))))))
    
    1. O que calcula essa função?
    2. Prove sua resposta ao item anterior.
    3. No cálculo de (f x n), quantas vezes a função f é chamada?
    1. Escreva uma função que receba um número x e uma lista de números l, e retorne uma lista com os elementos de l que são menores que x.
    2. Idem ao item anterior, mas com os elementos maiores ou iguais a x.
    3. Escreva uma função para ordenar uma lista usando o algoritmo Quicksort. Nesse algoritmo, escolhemos um pivot (o primeiro elemento da lista, por exemplo), separamos a lista original em duas, uma com os elementos menores que o pivot e a outra com os elementos maiores ou iguais, ordenamos essas duas listas, e juntamos os resultados.
  2. Escreva uma função que retorne a lista de fatores primos de um número natural. Prove que sua função está correta.

25/3 - O problema das Oito Rainhas

30/3 - O problema das Oito Rainhas (lab.)

  1. Implemente um algoritmo que liste todas as soluções válidas para a problema das oito rainhas.
  2. Modifique seu algoritmo de modo que ele imprima apenas uma solução. (Dica: faça sua função retornar um booleano indicando se alguma solução foi achada.)
  3. Modifique seu algoritmo de modo que ele retorne uma única lista com todas as soluções válidas.
(define lim 8)

(define (ataca l1 c1 l2 c2)
  (or (= c1 c2)
      (= (- l1 c1) (- l2 c2))
      (= (+ l1 c1) (+ l2 c2))))
  
(define (livre l c p)
  (if (null? p) 
      #t   
      (and (not (ataca l c (length p) (car p)))
           (livre l c (cdr p)))))
  
(define (q p i)
  (if (= (length p) lim)
      (begin (display p) (newline) #t)
      (if (> i lim)
          #f       
          (or (and (livre (+ (length p) 1) i p)
                   (q (cons i p) 1))
              (q p (+ i 1))))))

06/04 - Subconjuntos (lab.)

A função abaixo enumera todos os subconjuntos de um dado conjunto (representado por uma lista l com seus elementos):
(define subs
  (lambda (l a)
    (if (null? l)
        (begin (display a) (newline))
        (begin (subs (cdr l) a)
               (subs (cdr l) (cons (car l) a))))))
  1. Como funciona essa função? (Note que a é um acumulador de resultados parciais...)
  2. Baseado na estrutura de subs escreva a função subs#, que retorna o número de elementos impressos em uma chamada (subs l '()). Simplifique ao máximo sua função (retire argumentos não utilizados, etc).
  3. Que função matemática subs# calcula? Prove sua resposta.
  4. Modifique subs de modo que ela receba um parâmetro adicional n, e imprima somente os subconjuntos de l com no máximo n elementos.
    (define subsn
      (lambda (l n a)
        (if (or (null? l) (zero? n))
            (begin (display a) (newline))
            (begin (subsn (cdr l) n a)
                   (subsn (cdr l) (- n 1) (cons (car l) a))))))
    
  5. Reescreva subs# para essa nova função.

08/04 - Combinações

13/04 - Combinações com repetições

  1. Modifique o algoritmo comb (ver acima) para que ele enumere todas as combinações com repetições de n elementos de uma lista l. (Chame a nova função de comb*.)
  2. Quantas combinações com repetições de m elementos podemos fazer sobre uma lista com n elementos? Se não souber, continue...
  3. Considere a função abaixo:
    (define (f l)
      (if (null? l)
          l
          (cons (+ (car l) (length l) -1) (f (cdr l)))))
    
  4. Modifique sua função comb* para que ela aplique f sobre os resultados antes de mostrá-los. (O que a função faz agora?)
  5. Seja l uma lista não crescente de m números entre 1 e n, e l1 = (f l).
    1. Qual o comprimento de l1?
    2. Quais os valores máximo e mínimo que podem ocorrer em l1?
    3. Mostre que l1 é uma lista decrescente (e portanto não possui valores repetidos).
  6. Escreva uma função g que seja a inversa de f. Prove que (f (g l)) = l e (g (f l)) = l.
  7. Seja l uma combinação de m números entre 1 e n, listados em ordem decrescente, e l1 = (g l).
    1. Qual o comprimento de l1?
    2. Quais os valores máximo e mínimo que podem ocorrer em l1?
  8. Quantas combinações com repetições de m elementos podemos fazer sobre uma lista com n elementos?

15/04 - Problemas combinatórios

20/04 - Problemas combinatórios (exercícios)

22/04 - Problemas de Bolas e Urnas

27/04 - Bolas indistintas em Urnas indistintas

Nesse laboratório, vamos escrever uma função para enumerar todas as maneiras de colocarmos b bolas indistintas em u urnas indistintas.

Podemos repartir as soluções em dois grupos: as soluções que deixam ao menos uma urna vazia e as que não deixam nenhuma urna vazia. No primeiro caso, o problema se resume a distribuir as b bolas nas u-1 urnas ainda disponíveis. No segundo caso, para garantir que nenhuma urna está vazia, precisamos colocar pelo menos uma bola em cada urna, utilizando portanto u bolas; em seguida, podemos distribuir livremente as b-u bolas restantes por todas as u urnas.

Como as bolas são indistintas, podemos representar cada solução por uma lista de inteiros de comprimento u, que indica quantas bolas cada urna contém. A ação de colocarmos uma bola em cada urna, então, se traduz em incrementarmos os elementos da lista. Para implementar essa ação, escreva uma função auxiliar que receba uma lista l e um número u, e retorne l com os u primeiros números incrementados de 1.

Agora, escreva a função dist que gera as distribuições. Siga a estrutura geral usada nas outras funções de enumeração que já escrevemos (em especial, use um acumulador para os resultados). Procure identificar cuidadosamente os casos base. O caso geral pode ser resolvido seguindo-se a estratégia recursiva delineada acima.

Finalmente, modifique sua função dist para escrever dist#, que retorna o número de distribuições.

04/05 - Problemas combinatórios (exercícios)

11/05 - Grafos: o básico

Micro-revisão de arrays (ou vetores) em Scheme: arrays em Scheme são estruturas dinâmicas, que devem ser explicitamente criadas antes de serem utilizadas. Arrays são indexados por inteiros entre 0 (o primeiro elemento de um array) até n-1, onde n é o tamanho do array.

Agora, o exercício é construir um conversor de grafos. Sua função deve receber uma lista com dois elementos: um número n e uma lista de pares de números entre 0 e n-1; por exemplo, (5 ((2 3) (3 4) (2 4) (0 1) (0 2))). A função então deve retornar um array com n elementos, cada um deles contendo uma lista de números, com a seguinte característica: um número y deve constar da lista de números na posição x do array se e somente se o par (x y) estiver na lista de pares do argumento. Por exemplo, se a função for chamada com a lista mostrada acima, retornaria algo como #((2 1) () (4 3) (4) ()) (a ordem dos números dentro de cada lista não é relevante). Por exemplo, por causa da presença do par (2 4) na entrada, a lista na posição 2 do array, (4 3), contém o número 4.

Dica: faça uma função auxiliar, recursiva, que receba a lista de pares e o array, e insira os números nas listas adequadas. Faça outra função, principal, que crie o array e use essa função auxiliar para preenchê-lo.

(define (fill g l)
  (if (not (null? l))
    (let ((x (caar l)) (y (cadar l)))
      (vector-set! g x (cons y (vector-ref g x)))
      (fill g (cdr l)))))

(define (converte g1)
  (let ((g (make-vector (car g1) '())))
     (fill g (cadr g1))
     g))

Modifique agora sua função para um par (x y) na lista de entrada gerar um y na lista da posição x do array *e* um x na lista da posição y. Por exemplo, a lista de exemplo geraria como saída o array #((2 1) (0) (0 4 3) (4 2) (2 3)).

13/05 - Grafos - Introdução

(capítulo 3 do livro)

18/05 - Grafos - Percorrimento em Profundidade

Vários algoritmos sobre grafos, como achar um caminho entre dois vértices ou descobrir se um grafo é conexo, podem ser implementados sobre uma técnica chamada de Percorrimento em Profundidade (ou "siga seu nariz").

A implementação (recursiva) desse algoritmo é bastante simples: temos uma função básica, visita, que recebe um grafo G (representado por um vetor de listas de adjacências) e um nó N, e então chama visita recursivamente para todos os nós vizinhos de N (isto é, aqueles nós para os quais exista um arco saindo diretamente de N).

A implementação "ingênua" feita acima tem um grave problema: se o grafo contiver ciclos, sua função poderá entrar em loop (por que?). Além disso, ela pode visitar várias vezes um mesmo nó. Uma maneira de solucionarmos isso é adicionarmos mais um parâmetro à nossa função: um array que indica, para cada nó, se ele já foi visitado. Assim, quando chegamos em um nó pela segunda vez (posição no array já "marcada"), não precisamos visitar seus vizinhos novamente.

20/05 - Busca em Largura e Busca em Profundidade

25/05 - Busca em Largura e Busca em Profundidade

08/06 - Pequenas operações sobre Grafos

10/06 - Árvores

Uma árvore (dirigida) é um grafo dirigido, com um nó especial denominado raiz, tal que existe um único caminho da raiz para cada nó do grafo.

Lemas, teoremas e corolários: árvores são grafos conexos e acíclicos. A raiz tem sempre grau de entrada 0, e os outros nós grau de entrada 1. Uma árvore com n vértices tem exatamente n-1 arestas.

Uma árvore não dirigida é um grafo não dirigido conexo e acíclico.

Lemas, teoremas e corolários: um grafo não dirigido é uma árvore se e somente se existe alguma maneira de atribuir direções aos arcos que o transforme em uma árvore dirigida. Dada uma árvore não dirigida com n nós, existem exatamente n maneiras diferentes de atribuirmos direções aos arcos para transformá-la em uma árvore dirigida, correspondendo a escolhermos cada um dos nós como raiz.

Uma árvore geradora de um grafo não dirigido G é um sub-grafo de G que (1) seja uma árvore e que (2) contenha todos os nós de G.

Algoritmo para achar uma árvore geradora de custo mínimo para um dado grafo (não dirigido com custos).

15/06 - Coloração de Grafos

Colorir um grafo consiste em atribuirmos uma cor (representada por um número natural) à cada nó do grafo, de modo que nós vizinhos nunca tenham cores iguais. O problema da coloração mínima consiste em colorirmos um grafo com o menor número possível de cores. Esse número é chamado de número cromático do grafo.

Nosso trabalho é construirmos um algoritmo para colorir um grafo (não necessariamente com a coloração mínima). O algoritmo é uma modificação do algoritmo básico de busca em profundidade (cola): quando visitamos um nó ainda não marcado, marcamos o nó com uma cor (isto é, colocamos o número representando a cor no array de marcas...). A cor escolhida para o nó será sempre a menor cor livre, isso é, ainda não ocupada por algum vizinho.

Observe que o problema da coloração se refere a grafos não dirigidos (só estamos interessados se nós são ou não são vizinhos). Por isso, a lista de adjacências de cada nó deve incluir todos os vizinhos desse nó. Um grafo como (4 ((0 1) (3 2) (0 3) (2 1))) é representado como #((1 3) (0 2) (1 3) (2 0)).

As duas funções abaixo podem facilitar bastante o trabalho:

Para terminar: você é capaz de construir um grafo para o qual esse algoritmo não constrói a coloração mínima?

17/06 - Árvores Geradoras

22/06 - Árvores Geradoras de Custo Mínimo

O objetivo é construirmos um programa que, dado um grafo conexo, retorne uma lista dos arcos que compõe a árvore geradora de custo mínimo para o grafo. O algoritmo usado é o que foi apresentado na aula passada.

Arcos serão representados por listas com três elementos: (origem destino custo). Para simplificar a manipulação de arcos, definimos as funções auxiliares abaixo:

(define origem car)
(define destino cadr)
(define custo caddr)

Para representarmos o conjunto de nós "dentro", vamos usar um array de booleanos indexado pelos nós: um valor #t indica que o nó pertence ao conjunto.

(define (pertence? no conjunto) (vector-ref conjunto no))
(define (insere no conjunto) (vector-set! conjunto no #t))

Grafos serão representados por um array que, para cada nó, contém a lista de todos os arcos incidentes no nó. Isso implica uma certa redundância no grafo (pois cada arco já contém o seu nó origem), mas simplifica o programa. Como exemplo, o grafo

(4 ((0 1 10) (3 1 1) (2 0 5)))
será representado como
#(((0 1 10) (0 2 5)) ((1 0 10) (1 3 1)) ((2 0 5)) ((3 1 1)))
.