All-Pairs Shortest Paths
Objectivo
Familiarizar os alunos com a sincronização e comunicação entre
processos em arquitecturas multiprocessador, através da utilização de
spinlocks e do mapeamento de ficheiros em memória partilhada.
Descrição
O problema que se pretende resolver consiste em encontrar o conjunto
de todos os caminhos mais curtos entre pares de nós de um determinado
grafo. Este problema é conhecido na literatura por all-pairs
shortest paths.
A ideia é a seguinte: dado um grafo dirigido G = (V,E) em que V é o
conjunto de nós e E é o conjunto de ligações entre os nós, pretende-se
determinar para todos os pares de nós (vi,vj) o tamanho do menor
caminho que começa em vi e termina em vj. Um caminho não é nada mais
do que uma sequência de ligações, em que o nó de destino e o nó de
origem de duas ligações consecutivas da sequência são o mesmo. Uma
ligação pode ser vista como um tuplo da forma < vi, vj, t > em que
vi é o nó de origem, vj é o nó de destino e t é o tamanho da ligação
entre os nós. A soma dos tamanhos das ligações de um caminho definem o
tamanho do caminho.
Implementação
Um grafo dirigido com V nós pode ser representado por uma matriz D1 de
dimensão NxN (N = 6 no exemplo que se segue) em que cada elemento
(i,j) da matriz corresponde ao tamanho da ligação que liga o nó vi ao
nó vj. A não existência de qualquer ligação entre um determinado par
de nós pode ser representado pelo valor zero.

A solução do problema pode ser igualmente representada por uma matriz
Df de dimensão NxN em que cada elemento (i,j) da matriz corresponde ao
menor tamanho dos caminhos que começam em vi e terminam em vj.

O algoritmo de Floyd-Warshall permite obter a matriz Df a partir da
matriz D1 utilizando uma forma adaptada de multiplicação de matrizes,
em que as operações de multiplicação e soma são substituídas pelas
operações de soma e mínimo, respectivamente. Utilizando essa forma
adaptada de multiplicação podemos obter a matriz Df construindo
sucessivamente matrizes D2 = D1xD1, D4 = D2xD2, D8 = D4xD4, ..., Df =
DgxDg em que f >= N e g < N.
A multiplicação de matrizes com a mesma dimensão N x N pode ser
eficientemente implementada em ambientes de memória partilhada
particionando a matriz em P submatrizes de dimensões M x N, em que P é
o número de processos disponíveis e M = N / P, e atribuindo o cálculo
de cada submatriz a cada um dos P processos. Note que esta partição
apenas pode ser aplicada se P dividir N.

O cálculo de cada matriz Di+1 do algoritmo de Floyd-Warshall é feito
iterativamente sendo cada processo responsável pelo cálculo da
submatriz respectiva. No final de cada passo i, os processos
sincronizam entre si, actualizam a matriz Di com os resultados obtidos
nas suas submatrizes e iniciam o passo i+1 a partir da nova matriz
Di+1.
Input e Output
A aplicação a desenvolver deverá aceitar como entrada de dados um
ficheiro com a descrição do grafo a considerar. Essa descrição deverá
seguir o seguinte padrão: a primeira linha indica o inteiro N que
representa o número de nós do grafo, enquanto que as seguintes N
linhas indicam a matriz que representa as ligações entre todos os
pares de nós do grafo. Segue-se o ficheiro que descreve o grafo do
exemplo apresentado:
6
0 2 0 5 0 0
0 0 0 0 0 0
0 2 0 0 0 5
0 0 0 0 1 0
3 9 3 0 0 0
0 0 0 0 1 0
Se a dimensão da matriz não for compatível com o número de processos
indicados então deverá ser apresentada uma mensagem de erro e a
execução deve ser cancelada.
Como resultado a aplicação deverá escrever as N linhas da matriz que
representa o menor tamanho dos caminhos entre todos os pares de nós do
grafo. Para o grafo do exemplo, o resultado a obter seria o seguinte:
0 2 9 5 6 14
0 0 0 0 0 0
9 2 0 14 6 5
4 6 4 0 1 9
3 5 3 8 0 8
4 6 4 9 1 0
O ficheiro de entrada de dados deverá ser lido do standard
input e o resultado deverá ser escrito para o standard
output (carregue aqui para obter um
conjunto de exemplos de teste).
Resultados
A aplicação a desenvolver deverá utilizar spinlocks (carregue aqui para obter o ficheiro .h com as macros de
locking) para sincronizar as operações de escrita sobre as matrizes Di
e o mapeamento de ficheiros em memória partilhada para representar
essas matrizes (carregue aqui para obter o
esqueleto do código da aplicação).