Escolar Documentos
Profissional Documentos
Cultura Documentos
alicecamarques@gmail.com
juliana.silvestresilva@hotmail.com
1. Questão 1
Considerando um grafo não direcionado simples, podemos afirmar que um ciclo consiste
em um caminho fechado sem vértices repetidos. Dessa maneira, a primeira questão deste
trabalho visa resolver o problema de se enumerar todos os ciclos existentes em um grafo.
Para isso, foram utilizadas duas abordagens, sendo uma delas baseada na permutação os
vértices do grafo e a outra baseada em caminhamento.
Ambas as resoluções foram implementadas na linguagem de programação Java.
Foi utilizada como base a classe Grafo, a qual armazena informações como a matriz de
adjacência e a lista de adjacência do grafo em questão, bem como a quantidade de vértices
e um vetor com todos os vértices do grafo.
O(n + m)
2. Questão 2
Em discussões sobre desenvolvimento de algoritmos, a escolha da estratégia para re-
solver um problema é fundamental, visto que existem diversos métodos com difer-
entes aplicações e complexidadades. As estratégias abordadas nessa questão, branch-
and-bound e backtracking são relativamente parecidas em conceito, porém possuem
implementações distintas, o que resulta na preferência de uma à outra em determinadas
circunstâncias.
2.1. Branch-and-Bound
Branch-and-bound é um método enumerativo para algoritmos, no qual a solução é obtida
por meio do particionamento do espaço de soluções, ou seja, o problema original é repeti-
damente decomposto em subproblemas menores até que uma solução seja obtida. Isso
permite identificar tal solução como a melhor possı́vel ou ótima dentre todas as soluções
viáveis para o problema. Portanto, o algoritmo baseia-se na idéia de desenvolver uma
enumeração inteligente das soluções candidatas à solução ótima inteira de um problema.
Esse método geralmente é aplicado em problemas de otimização discreta e de otimização
combinatória, como por exemplo problemas de programação inteira, de fluxo de rede, e
de associação de tarefas.
No problema de associação de tarefas, há N tarefas e N trabalhadores. Qualquer
uma das tarefas disponı́veis pode ser atribuı́do a qualquer trabalhador com a condição de
que, se uma tarefa for atribuı́da a um trabalhador, os outros trabalhadores não poderão
aceitar essa tarefa especı́fica. Além disso, cada tarefa tem algum custo associado a ela, o
qual difere de um trabalhador para outro. O objetivo principal é completar todas as tarefas
atribuindo uma tarefa a cada trabalhador, de tal forma que a soma do custo de todas as
tarefas seja minimizada.
Para a resolução deste problema, foi implementado um código na linguagem de
programação C++. A lógica utilizada foi que, para cada trabalhador, a tarefa com custo
mı́nimo da lista de tarefas não atribuı́das é escolhida. Como a primeira parte da técnica
branch-and-bound pede que o algoritmo trate as possı́veis soluções como se estivessem
em uma estrutura de árvore, onde os nós da árvore são utilizados como parte de possı́veis
soluções, foi implementada uma estrutura de Nó. Essa estrutura contém o pai do nó atual,
o custo para nós ancestrais, o custo menos promissor, a identificação do trabalhador e da
tarefa e um array com as informações sobre as tarefas disponı́veis. Também foi criada a
função novoNo(), que aloca um novo nó da árvore de busca. A função calcularCusto()
calcula o custo menos promissor do nó após o trabalhador X ser atribuı́do à tarefa Y. Para
isso, é utilizado um array que armazena o custo das tarefas, passado por parâmetro, e é
criado um outro array para armazenar as tarefas indisponı́veis, o qual vai sendo preenchido
à medida que é feita uma verificação da associação entre os trabalhadores, as tarefas e os
respectivos custos. A função imprimirAssociacao() imprime na tela as associações entre
os trabalhadores e as tarefas e, por fim, há a função acharCustoMinimo(), que encontra
o custo mı́nimo usando a estratégia de branch-and-bound. Para isso, encontra-se um nó
ativo com menor custo, adiciona-se os filhos à fila de prioridade de nós ativos criada nessa
mesma função e, finalmente, ele é excluido da fila. Na função principal, cria-se a matriz
de custo com os trabalhadores e as tarefas, e a função acharCustoMı́nimo() é chamada.
Para testar a implementação, foram feitos testes com quatro matrizes de custo diferentes,
os quais obtiveram sucesso.
2.2. Backtracking
Backtracking é uma estratégia refinada do algoritmo de busca por força bruta que resolve
problemas incremental e recursivamente (tentativa e erro), removendo as soluções parciais
que falham em ajudar na solução para o problema. Ou seja, boa parte das soluções podem
ser eliminadas sem serem explicitamente examinadas.
Esse algoritmo pode ser utilizado em problemas cuja solução pode ser definida
a partir de uma seqüência de decisões e em problemas que podem ser modelados por
uma árvore que representa todas as possı́veis seqüências de decisão. Dessa forma, ele
é aplicável na solução de vários problemas conhecidos, como o problema do caixeiro
viajante, das N rainhas, da geração de permutações e problemas de labirinto.
O problema das N rainhas consiste em dispor N rainhas sobre um tabuleiro de
xadrez de tal modo que elas não se ataquem. No jogo de xadrez, as rainhas podem se
atacar ou se movimentar na horizontal, na vertical e também ao longo das diagonais.
Sendo assim, o objetivo é dispor as N rainhas de modo que elas não compartilhem linhas,
colunas e nem diagonais.
Para a resolução deste problema, foi implementado um código na linguagem de
programação C++. Foram criados três funções: mostrarTabuleiro(), seguro() e executar().
A função mostrarTabuleiro() imprime o tabuleiro na tela, representado por uma matriz,
com todas as rainhas posicionadas. A função seguro() verifica se é seguro colocar a rainha
em uma determinada coluna. Para isso, ela verifica se ocorre ataque na linha, na coluna, na
diagonal principal e na diagonal secundária. E a função executar() é uma função recursivo
que utiliza o backtracking para gerar as soluções. Ela começa pela primeira coluna e
considera as N posições disponı́veis. Para cada uma das posições da primeira coluna, ela
considera as posições da segunda coluna que são válidas. Repete-se esse processo até
preencher as N colunas do tabuleiro. Se chegar à ultima coluna e for possı́vel colocar
as N rainhas, então há uma solução para o problema. Se chegar a alguma coluna onde
não haja nenhuma posição válida para colocar uma rainha, retorna-se à coluna anterior,
procurando pela próxima posição válida. Dessa forma, aplicando esta ideia repetidamente
às colunas restantes, no final, obtém-se o conjunto de todas as soluções possı́veis. Para
testar a implementação, foram feitos testes com 4 e 8 rainhas, os quais obtiveram sucesso.
2.3. Comparação entre Estratégias de Desenvolvimento de Algoritmos
• Assim como o backtracking, a abordagem gulosa também pode não alcançar ime-
diatamente o resultado global ótimo, ao contrário do branch-and-bound.
• Os algoritmos de backtracking e de branch-and-bound costumam ter
implementações recursivas, enquanto os algoritmos gulosos tentam buscar a mel-
hor escolha de forma iterativa.
• Algoritmos gulosos costumam ter resultados mais rápidos do que técnicas de
branch-and-bound para problemas com menos entradas, mas, à medida que a en-
trada cresce, o mesmo pode ajhcabar sendo custoso como o branch-and-bound.