|
0 |
3 |
1 |
2 |
9 |
|
7 |
3 |
4 |
9 |
9 |
|
1 |
7 |
5 |
5 |
3 |
|
2 |
3 |
4 |
2 |
5 |
Your task is to find the minimum cost value to go from the top-left corner to the bottom-right corner of a given number maze of size NxM where 1 <= N, M <= 999. Note that the solution for the given example is 24.
The input file contains several mazes. The first input line contains a positive integer defining the number of mazes that follow. Each maze is defined by: one line with the number of rows, N; one line with the number of columns, M; and N lines, one per each row of the maze, containing the maze numbers separated by spaces.
For each maze, output one line with the required minimum value.
2 4 5 0 3 1 2 9 7 3 4 9 9 1 7 5 5 3 2 3 4 2 5 1 6 0 1 2 3 4 5
24 15
| 1 | 9 | 9 |
| 1 | 9 | 9 |
| 1 | 1 | 1 |
1: (1,1)Cada iteração do algoritmo começa então por pegar na posição com menor custo acumulado; neste caso é a casa (1,1) que é a única no map. Então removemos esta entrada e inserimos as que estão à volta, com os custos actualizados. O map fica então:
2: (2,1) 10: (1,2)Voltamos a pegar na casa com menor custo, (2,1), e fazemos a expansão.
3: (3,1) 10: (1,2) 11: (2,2)A posição (1,2) continua inalterada porque não lhe mexemos, apenas removemos a melhor, (2,1) e introduzimos as que estão à sua volta. A seguir fica:
4: (3,2) 10: (1,2) 11: (2,2)Na próxima iteração, é importante notar que não vai haver uma nova expansão para a casa (2,2) (já está marcada). Isto porque se já está marcada, quer dizer que já lá chegámos pelo melhor caminho possível. De facto, a melhor maneira de chegar à casa (2,2) é com custo 11. Na próxima iteração chegamos ao fim:
5: (3,3) 10: (1,2) 11: (2,2)Como já chegámos à casa final (3,3), o custo acumulado é a solução procurada: 5. Isto porque sempre que entramos numa casa, entrámos lá vindos pelo melhor caminho (menos custoso) para lá chegar. Vamos analisar um exemplo mais elaborado, e ver os passos seguidos pelo algoritmo. Consideremos agora a matriz
| 1 | 2 | 0 |
| 1 | 2 | 9 |
| 2 | 9 | 0 |
1: (1,1)E a seguir passa para as casas à volta:
2: (2,1) 3: (1,2)Agora vamos para a casa (2,1) que já fica fora do caminho óptimo; mas vamos ver o que acontece.
3: (1,2) 4: (2,2) 4: (3,1)Como os novos caminhos são mais custosos, vamos voltar à casa (1,2) embora parecesse um caminho pior no passo anterior.
3: (1,3) 4: (2,2) 4: (3,1)Mas agora aparece um 9, o que vai fazer o custo aumentar muito e o algoritmo vai optar pelas outras alternativas.
4: (2,2) 4: (3,1) 12: (2,3)Em caso de empate, qualquer uma serve. Note-se que ao chegar à casa (3,2) tanto faz vir de (3,1) ou (2,3) porque os custos acumulados são iguais.
4: (3,1) 12: (2,3) 13: (3,2)Agora vamos continuar da casa (3,1) mas não podemos avançar, porque as casas à sua volta já foram visitadas! Por isso, esta alternativa é simplesmente eliminada, sem adicionar novos caminhos ao map.
12: (2,3) 13: (3,2)Como se vê, não foi inserida nenhuma solução nova. Agora pegamos na casa (2,3) e chegamos ao fim, com o custo 12. O código seguinte é da autoria do Hugo Santos, com algumas modificações mínimas. Um input de uma matriz 1000x1000 para testar (2MB! Comprimido 8 KB): 2004nov.txt.gz. Código em C++:
#include <iostream>
#include <map>
struct Point {
int x, y;
};
using namespace std;
#define maze(x,y) mazeptr[x + y*cols]
#define stepped(x,y) steppedptr[x + y*cols]
int *mazeptr;
bool *steppedptr;
int rows, cols;
multimap<int, Point> cost_map;
multimap<int, Point>::iterator cost_map_iter;
int dirs[4][2] = {
{ -1, 0 }, // left
{ 0, -1 }, // up
{ 1, 0 }, // right
{ 0, 1 } // down
};
bool valid(Point &p)
{
return (p.x >= 0 && p.x < cols && p.y >= 0 && p.y < rows);
}
int cost()
{
int i;
int total;
Point cur, p;
cur.x = cur.y = 0;
total = maze(cur.x, cur.y);
cost_map.insert(pair<int, Point>(total, cur));
for (;;) {
cost_map_iter = cost_map.begin();
cur = cost_map_iter->second;
total = cost_map_iter->first;
for (i = 0; i < 4; i++) {
p.x = cur.x + dirs[i][0];
p.y = cur.y + dirs[i][1];
if (p.x == cols - 1 && p.y == rows - 1)
return total + maze(p.x, p.y);
if (valid(p) && !stepped(p.x, p.y)) {
cost_map.insert(pair<int, Point>(total + maze(p.x, p.y), p));
}
}
cost_map.erase(cost_map_iter);
stepped(cur.x, cur.y) = true;
}
}
int main(int argc, char *argv[])
{
int num_mazes, i, j;
cin >> num_mazes;
while (num_mazes--) {
cin >> rows >> cols;
mazeptr = new int[rows * cols];
steppedptr = new bool[rows * cols];
for (i = 0; i < rows; i++)
for (j = 0; j < cols; j++) {
cin >> maze(j, i);
stepped(j,i) = false;
}
cout << cost() << endl;
delete mazeptr;
delete steppedptr;
}
return 0;
}
Comecemos por analisar a função main. O enunciado diz que o primeiro número no input é o número de problemas a resolver, e temos um ciclo que executa esse número de vezes (o ciclo while). Depois, criamos uma matriz que vai ter os custos e outra que diz se uma casa já foi visitada ou não. Continuamos com a leitura do input, inserindo o custo da casa respectiva na matriz e assinalando-a como não vizitada (valor lógico "false"). A função cost calcula o resultado que é depois enviado para a saída.
As matrizes usadas são representadas por um array; para indexar uma posição (x,y), usamos a posição (x + y*cols) do array. Para o código ficar mais claro, foram definidas as macros maze(x,y) e stepped(x,y), que indexam cada uma um dos arrays. A matriz dirs contém os incrementos que são feitos nas coordenadas (x,y) quando se anda para cima, baixo, esquerda ou direita. A função valid verifica se um ponto é válido, isto é, se as suas coordenadas são válidas (ficam dentro da matriz). O cost_map é a estrutura central: é onde são mapeados os custos em posições. É usado um multimap porque podem haver várias casas com o mesmo custo (num map normal, cada elemento tem de ter uma chave de indexação única; como a chave aqui são os custos e podem haver várias casas com o mesmo custo, usamos um multimap que já permite essas situações).
Finalmente a função cost. Começamos por assumir que estamos na casa (1,1) (os índices em C começam em 0, por isso é a casa (0,0)), e inserimos o custo dessa casa associado a ela no mapa. A seguir entramos num ciclo infinito, que só acaba quando entrarmos na casa final (infinito que acaba... ya). Em cada passo do ciclo, começamos por ver qual é a casa que está no início do mapa. Para isso usamos um iterator, e ele aponta para uma estrutura do tipo pair; o campo "first" é a chave e o campo "second" é o valor associado. O ponto fica na variável "cur" e o custo na "total". O ciclo "for" que se segue vai executar 4 vezes, tentado saltar para as 4 casas à volta da casa actual. O ponto "p" é esse novo ponto temporário. Depois temos 2 testes: o primeiro, vê se a casa nova é a final; se sim, a função retorna o custo total mais o custo da casa final. O outro teste verifica se o ponto é válido e se ainda não foi visitado, e nesse caso inserimos no mapa os novos dados: o custo total mais o custo desse novo ponto associado a esse ponto. A inserção no mapa garante que este novo elemento vai ficar na casa correcta, de modo que no próximo passo do algoritmo o primeiro elemento é o com menor custo. Esta operação é de complexidade O(log n). Finalmente, depois de fazer a expansão para as casas à volta, removemos a casa visitada do mapa e marcamo-la como visitada para que futuros passos do algoritmo não voltem lá.
Complicado? :) Não é, o código é que talvez não se compreenda à primeira. Convém perceber a ideia geral do algoritmo, e saber como se usa um multimap e iterators (STL...). É boa prática tentar resolver este problema do início, ou seja, re-implementar a solução.