Przeszukiwanie grafu w głąb bez iteracji

0

Mniej-więcej rozumiem, jak działa przeszukiwanie w głąb (wszerz jeszcze przede mną, jak coś). Jednak jedyne implementacje tego algorytmu, jakie znalazłem (w pseudokodzie), uwzględniają skorzystanie z iteracji po wszystkich nieodwiedzonych wierzchołkach. Jak natomiast mógłby wyglądać pseudokod przeszukiwania grafu w głąb bez iteracji?

Napisałem coś takiego (pseudokod), ale po przepisaniu na JavaScript Node.js mówi mi, że następuje przepełnienie stosu: :/

Pseudokod

DFS (pred, getNeighbors, curVertex, unvisitedNeighbors, path)
	if pred(curVertex)
	then return curVertex
	else
		if not empty(unvisitedNeighbors)
		then DFS(
			pred,
			unvisitedNeighbors[0],
			concat(
				relativeComplement(
					getNeighbors(unvisitedNeighbors[0]),
					slice(unvisitedNeighbors, 1, len(unvisitedNeighbors))
				),
				slice(unvisitedNeighbors, 1, len(unvisitedNeighbors))
			),
			concat(unvisitedNeighbors[0], path)
		)
		else
			if not empty(path)
			then DFS(
				pred,
				path[0],
				slice(unvisitedNeighbors, 1, len(unvisitedNeighbors)),
				slice(path, 1, len(path))
			)
			else return nil

Przykładowe wywołanie:

DFS(vt -> equal(vt, 4), /* tu jakaś funkcja pobierania sąsiednich wierzchołków */, 0, [1, 2, 3], [])

W pseudokodzie istotne jest dla mnie znalezienie kompromisu między jak najmniejszą liczbą operacji pomocnicznych (tj. np. slice, empty), zmiennych (tj. parametrów funkcji) i przejrzystością kodu.


UPDATE: Znalazłem i poprawiłem jeden błąd: lista path powinna być pusta przy wywołaniu. Jeśli nie będzie, to początkowy wierzchołek dodany będzie podwójnie. Nie wiem, czy ta poprawka ma jakiekolwiek przełożenie na działanie algorytmu – nadal pojawia się przepełnienie stosu.


PS. Jeszcze przed powyższą poprawką poprawiłem inny błąd, niezwiązany bezpośrednio z algorytmem. Funkcja pobierania sąsiednich wierzchołków dodawała dany wierzchołek do jego własnych sąsiadów.


UPDATE2: Poprawiłem drugi błąd w algorytmie. W rekurencyjnym wywoływaniu DFS podczas przechodzenia do nowego wierzchołka było zwykłe łączenie listy jego sąsiednich wierzchołków z listą nieodwiedzonych do tej pory wierzchołków. Skutkowało to tym, że jeśli była pętla np. (1, 2), (2, 1), to przechodząc z wierzchołka 1 do wierzchołka 2 wierzchołek 1 występował zaczynał występować na liście dwukrotnie.

...I właśnie pisząc powyższy akapit uświadomiłem sobie, że jest w moich danych testowych (tj. w grafie) cykl. Zobaczę, jak sobie radzi algorytm bez niego. Docelowo cykle chciałbym zachować – choćby dlatego, by algorytm mógł działać na grafach nieskierowanych.


PS2. Dla formalności: nadal, po drugiej poprawce, jest przepełnienie stosu.


UPDATE3: Ech, bez cykli testy przechodzą. Niezależnie od tego wrzucam implementację w JS – może rzeczywiście, jak wzmiankował niżej @tsz, jest gdzieś błąd w niej samej. Wrzucam też testy.

Implementacja:

// JavaScript

exports.dfs
	= (pred, getNbVtsFn, curVt, unvNbVts, path) =>
		pred(curVt)
			? curVt
			// Check whether you can move forwards
			: unvNbVts.length !== 0
				// Move forward
				? exports.dfs(
					pred,
					getNbVtsFn,
					unvNbVts[0],
					getNbVtsFn(unvNbVts[0])
						.filter(
							vt => !unvNbVts.slice(1).find(v => vt.id === v.id)
						).concat(unvNbVts.slice(1)),
					[curVt].concat(path)
				)
				// Check whether you can move backwards
				: path.length !== 0
					// Move backwards
					? exports.dfs(
						pred,
						getNbVtsFn,
						path[0],
						unvNbVts.slice(1),
						path.slice(1)
					)
					// No move possible === all vertices visited
					: undefined;

Testy (dla czytelności pominąłem komentarze JSDoc):

// JavaScript

const getNbVts
	= (gr, vt) =>
		gr.vts.filter(
			v =>
				vt.nbVtIDs.includes(v.id)
				&& v.id !== vt.id
		);

const mockGraph = {
	vts: [
		{ id: 0, nbVtIDs: [1, 2] },
		{ id: 1, nbVtIDs: [3] },
		{ id: 2, nbVtIDs: [3] },
		{ id: 3, nbVtIDs: [] }
	]
};

const root = mockGraph.vts.find(v => v.id === 0);

exports.testCases = [
	new TestCase(
		"dfs",
		dfs,
		[
			v => v.id === 0,
			v => getNbVts(mockGraph, v),
			root,
			getNbVts(mockGraph, root),
			[]
		],
		res => res.id === 0
	),
	new TestCase(
		"dfs",
		dfs,
		[
			v => v.id === 1,
			v => getNbVts(mockGraph, v),
			root,
			getNbVts(mockGraph, root),
			[]
		],
		res => res.id === 1
	),
	new TestCase(
		"dfs",
		dfs,
		[
			v => v.id === 2,
			v => getNbVts(mockGraph, v),
			root,
			getNbVts(mockGraph, root),
			[]
		],
		res => res.id === 2
	),
	new TestCase(
		"dfs",
		dfs,
		[
			v => v.id === 3,
			v => getNbVts(mockGraph, v),
			root,
			getNbVts(mockGraph, root),
			[]
		],
		res => res.id === 3
	)
];

UPDATE4: Przepisałem testy na prostsze. Teraz wierzchołki są tożsame z ich identyfikatorami (tj. są reprezentowane przez typ number).


UPDATE5: Co do tej nowej, wzmiankowanej powyżej wersji testów (i oczywiście samej implementacji) – nie wiem: czy zamieszczać? I tak już dużo kodu tu jest.


UPDATE6: W pierwszym PS napisałem, że poprawiłem funkcję pobierania sąsiednich wierzchołków. Wyglądało to tak, że dla danego wierzchołka uniemożliwiłem jej ustawianie jego samego jako jego sąsiedniego wierzchołka. Teraz sobie jednak uświadomiłem, że jeśli chcę obsługiwać cykle (a chcę, jak wyżej napisałem), funkcja ta musi móc dodawać dany wierzchołek do jego własnych sąsiadów.


UPDATE7: Doszedłem do wniosku, że nie będę poprawiać wersji kodu w pierwszym poście, tylko będę zamieszczać nowe wersje sukcesywnie w kolejnych postach. Dzięki temu będzie będzie bardziej przejrzyście, mam nadzieję.

1

Przepełnienie stosu wskazuje na zapętlenie się rekursji - pewnie nie oznaczasz odwiedzonych węzłów, zdebuguj to.

Implementacja w innych językach: https://www.geeksforgeeks.org/depth-first-search-or-dfs-for-a-graph/

0

@Charles_Ray: dzięki za wskazówkę. Być może tak jest. Jeszcze będę debugować.

0

Aktualizacja: znalazłem i poprawiłem jeden błąd (opis w pierwszym poście).

0

Zrob d...pa debuging.
Jak nic to nie da to zawsze pozostaje sposob najprostszy - zamien rekursje na iteracje + stos.

0

@vpiotr: dzięki za wskazówkę. Debugowałbym przez wypisywanie do konsoli, bardzo chętnie; w zasadzie lubię ten sposób bardziej niż debugowanie z pomocą IDE… W moim przypadku jest to jednak mało przydatne, ponieważ implementację w JS napisałem w dość, hm, zwarty sposób… Bez zbędnych szczegółów: całe ciało funkcji to wyrażenie. Najprostsze wydaje się więc debugowanie z pomocą IDE. Ale jestem w trakcie. Może mały błąd po małym błędzie poprawię całość.


PS. Wspominasz jeszcze o iteracji i stosie. Właśnie iteracji, jak napisałem w pierwszym poście, chciałbym uniknąć. ;)

1

Wrzuciłbyś kod w JS. Twój program może się rozwalać na czymś zupełnie specyficznym dla języka nawet jeśli pseudokod jest poprawny.

0

Aktualizacja: znalazłem i poprawiłem jeden błąd (opis w pierwszym poście).

@tsz: masz rację. Chciałem uniknąć debugowania implementacji w JS, ponieważ ważniejsze jest dla mnie teoretyczne rozpoznanie. Z implementacją poprawnego algorytmu zakładam (zakładałem?), że sobie poradzę. Ale może to i racja. Zobaczę, czy nie widzę jeszcze jakichś oczywistych bugów, i wrzucę.

0

Aktualizacja: dodałem do pierwszego postu implementację w JS oraz jej testy.

1

Iterację można przerobić na rekurencję ogonową https://www.w3schools.com/code/tryit.asp?filename=GGVSLHX7Y2MJ

<!DOCTYPE html>
<html>
<body>

<div id="result1"></div>
<div id="result2"></div>

<script>
var arr1 = [];
for (var i = 0; i < 10; i++) {
  arr1[i] = i;
}
document.getElementById("result1").innerHTML = arr1.toString();

var arr2 = [];
function go(i) {
  if (i < 10) {
    arr2[i] = i;
    go(i + 1);
  }
}
go(0);
document.getElementById("result2").innerHTML = arr2.toString();
</script>

</body>
</html>
0

@Wibowit: dzięki za wskazówkę. Na razie nie wiem jeszcze, jak mógłbym to wykorzystać. Być może się da, kto wie?

Na razie zresztą próbuję debugowania. Wciąż odkrywam nowe błędy, więc wciąż nie mogę nawet raz przejść całego algorytmu. :P

2

Jeśli wersja iteracyjna wygląda tak:

function dfs(parent) {
  for (var node = startingNode(parent); nodeIsValid(node); node = nextNode(parent, node)) {
    if (notVisited(node)) {
      dfs(node)
    }
  }
}

To wersja z rekurencją ogonową będzie taka:

function dfs(parent) {
  function go(node) {
    if (nodeIsValid(node)) {
      if (notVisited(node)) {
        dfs(node)
      }
    }
    go(nextNode(parent, node))
  }
  go(startingNode(parent))
}

Ogólny schemat zamiany iteracji na rekurencję ogonową jest taki:
Wejściowa postać:

for (var elem = start(); condition(); elem = nextElem()) {
  action(elem)
}

Wyjściowa postać:

function go(elem) {
  if (condition()) {
    action(elem)
    go(nextElem())
  }
}
go(start())
0

Aktualizacja: przepisałem testy na prostsze. Teraz wierzchołki są tożsame z ich ID (reprezentowane przez typ number). Nie wiem, czy zamieszczać tę nową wersję? Nie chciałbym wprowadzać zamieszania; i tak już dużo kodu w tym wątku jak dla mnie.


PS. Oczywiście też implementację samego algorytmu zmieniłem, by używała tych nowych wierzchołków.

0

Doszedłem do wniosku, że nie będę poprawiać wersji kodu w pierwszym poście, tylko będę zamieszczać nowe wersje sukcesywnie w kolejnych postach. Dzięki temu będzie będzie bardziej przejrzyście, mam nadzieję.

Poprawiłem kolejny bug: przedtem przy przechodzeniu do następnego nieodwiedzonego wierzchołka zbiór wierzchołków nieodwiedzonych był powiększany o wierzchołek, z którego algorytm przyszedł (czyli u mnie path[0]). Według tego, co myślę, powinno być natomiast tak, że ten wierzchołek powinien nie występować w tym zbiorze – został przecież odwiedzony. Dodałem zatem jeszcze jedno wywołanie funkcji pomocniczej relativeComplement. Obecna więc wersja pseudokodu wygląda tak (przeklejam z pliku JS, więc są gwiazki "komentarzowe"; już je zostawię, jeśli nikt nie ma nic przeciwko, z uwagi na wygodę):

Pseudokod

/**
 * 	DFS (pred, getNeighbors, curVertex, unvisitedNeighbors, path)
 * 		if pred(curVertex)
 * 		then return curVertex
 * 		else
 * 			if not empty(unvisitedNeighbors)
 * 			then DFS(
 * 				pred,
 * 				unvisitedNeighbors[0],
 * 				concat(
 * 					relativeComplement(
 * 						relativeComplement(
 * 							getNeighbors(unvisitedNeighbors[0]),
 * 							slice(
 * 								unvisitedNeighbors, 1, len(unvisitedNeighbors)
 * 							)
 * 						),
 * 						{ path[0] }
 * 					),
 * 					slice(unvisitedNeighbors, 1, len(unvisitedNeighbors))
 * 				),
 * 				concat(unvisitedNeighbors[0], path)
 * 			)
 * 			else
 * 				if not empty(path)
 * 				then DFS(
 * 					pred,
 * 					path[0],
 * 					slice(unvisitedNeighbors, 1, len(unvisitedNeighbors)),
 * 					slice(path, 1, len(path))
 * 				)
 * 				else return nil
 * 	
 * 	DFS(vt -> equal(vt, 4), ..., 0, [1, 2, 3], [])
 */
0

Źle napisałem – nie path[0], a curVertex (bo w momencie filtrowania (tj. u mnie – obliczania różnicy zbiorów) – w danej iteracji – path jeszcze nie zawiera curVertex; dopiero w następnej iteracji). Poprawiona wersja:

Pseudokod

/**
 * 	DFS (pred, getNeighbors, curVertex, unvisitedNeighbors, path)
 * 		if pred(curVertex)
 * 		then return curVertex
 * 		else
 * 			if not empty(unvisitedNeighbors)
 * 			then DFS(
 * 				pred,
 * 				unvisitedNeighbors[0],
 * 				concat(
 * 					relativeComplement(
 * 						relativeComplement(
 * 							getNeighbors(unvisitedNeighbors[0]),
 * 							slice(
 * 								unvisitedNeighbors, 1, len(unvisitedNeighbors)
 * 							)
 * 						),
 * 						{ curVertex }
 * 					),
 * 					slice(unvisitedNeighbors, 1, len(unvisitedNeighbors))
 * 				),
 * 				concat(unvisitedNeighbors[0], path)
 * 			)
 * 			else
 * 				if not empty(path)
 * 				then DFS(
 * 					pred,
 * 					path[0],
 * 					slice(unvisitedNeighbors, 1, len(unvisitedNeighbors)),
 * 					slice(path, 1, len(path))
 * 				)
 * 				else return nil
 * 	
 * 	DFS(vt -> equal(vt, 4), ..., 0, [1, 2, 3], [])
 */
0

Hej, chyba rozwiązałem... Testy przechodzą. No, ale to proste testy.

Co zrobiłem? Usunąłem calą "sekcję" cofania się. Bo przecież, tak rozumuję, gdy lista nieodwiedzonych wierzchołków będzie pusta, to znaczy, że już nie ma po co cofać się – wszystko w drodze powrotnej będzie odwiedzone. W związku z tym usunąłem też zmienną path – już nie była potrzebna.

Poniżej cały kod, także kod zaktualizowanych testów:

Pseudokod:

Pseudokod

/**
 * 	DFS (pred, getNeighbors, curVertex, unvisitedNeighbors)
 * 		if pred(curVertex)
 * 		then return curVertex
 * 		else
 * 			if not empty(unvisitedNeighbors)
 * 			then DFS(
 * 				pred,
 * 				unvisitedNeighbors[0],
 * 				concat(
 * 					relativeComplement(
 * 						relativeComplement(
 * 							getNeighbors(unvisitedNeighbors[0]),
 * 							slice(
 * 								unvisitedNeighbors, 1, len(unvisitedNeighbors)
 * 							)
 * 						),
 * 						{ curVertex }
 * 					),
 * 					slice(unvisitedNeighbors, 1, len(unvisitedNeighbors))
 * 				)
 * 			)
 * 			else return nil
 * 	
 * 	DFS(vt -> equal(vt, 4), ..., 0, [1, 2, 3])
 */

Implementacja w JS:

// JavaScript

exports.dfs
	= (pred, getNbVtsFn, curVt, unvNbVts) =>
		pred(curVt)
			? curVt
			// Check whether you can move forwards
			: unvNbVts.length !== 0
				// Move forward
				? exports.dfs(
					pred,
					getNbVtsFn,
					unvNbVts[0],
					getNbVtsFn(unvNbVts[0])
						.filter(
							vt =>
								!unvNbVts.slice(1)
									.includes(vt)
						).concat(
							unvNbVts.slice(1)
						).filter(vt => vt !== curVt)
				)
				: undefined;

Testy:

// JavaScript

const mockGraphWithCycles
	= new Map(
		[
			[0, [1, 2]],
			[1, [0, 3]],
			[2, [0, 3]],
			[3, [1, 2]]
		]
	);

const mockGraphWithoutCycles
	= new Map(
		[
			[0, [1, 2]],
			[1, [3]],
			[2, [3]],
			[3, []]
		]
	);

const getNbVts = (gr, vt) => gr.get(vt);

// To be able to easily switch between graph variants
const mockGraph = mockGraphWithCycles;

const initialVt = 0;

exports.testCases = [
	new TestCase(
		"dfs",
		dfs,
		[
			v => v === 0,
			v => getNbVts(mockGraph, v),
			initialVt,
			getNbVts(mockGraph, initialVt)
		],
		res => res === 0
	),
	new TestCase(
		"dfs",
		dfs,
		[
			v => v === 1,
			v => getNbVts(mockGraph, v),
			initialVt,
			getNbVts(mockGraph, initialVt)
		],
		res => res === 1
	),
	new TestCase(
		"dfs",
		dfs,
		[
			v => v === 2,
			v => getNbVts(mockGraph, v),
			initialVt,
			getNbVts(mockGraph, initialVt)
		],
		res => res === 2
	),
	new TestCase(
		"dfs",
		dfs,
		[
			v => v === 3,
			v => getNbVts(mockGraph, v),
			initialVt,
			getNbVts(mockGraph, initialVt)
		],
		res => res === 3
	)
];

Zobaczę jeszcze bardziej skomplikowane testy – a jak one przejdą, to zobaczę wreszcie Twój kod, @Wibowit. :P

0

Niestety, testy bardziej skomplikowane nie przechodzą. Więc debugowanie od początku. Dla jasności, graf testowy:

// JavaScript

const complexGraphWithCycles
	= new Map(
		[
			[0, [0, 1, 2, 3, 4, 5, 6]],
			[1, [0, 2, 3, 4, 5, 6]],
			[2, [0, 3, 4, 5, 6]],
			[3, [0, 4, 5, 6]],
			[4, [0, 5, 6]],
			[5, [0, 6]],
			[6, [0]]
		]
	);

PS. I przypadek testowy, dla którego testy nie przechodzą (dla pozostałych przechodzą):

// JavaScript

exports.testCases = [
	..., // Pozostałe testy
	new TestCase(
		"dfs",
		dfs,
		[
			v => v === 6,
			v => getNbVts(mockGraph, v),
			initialVt,
			getNbVts(mockGraph, initialVt)
		],
		res => res === 6
	)
];
0

Powyższy test już przechodzi. Zrobiłem parę zmian. Nie wiem dokładnie, jakie są poszczególne różnice między tą wersją a poprzednią, więc zamieszczę po prostu wynikowy kod.

Implementacja w JS:

// JavaScript

exports.dfs
	= (pred, getNbVtsFn, curVt, unvNbVts, vVts) =>
		pred(curVt)
			? curVt
			// Check whether you can move forwards
			: unvNbVts.length !== 0
				// Move forward
				? exports.dfs(
					pred,
					getNbVtsFn,
					unvNbVts[0],
					// Get next vt's nbVts
					getNbVtsFn(unvNbVts[0])
						// Filter out already unvisited vts to avoid duplicates
						.filter(
							vt =>
								!unvNbVts.slice(1)
									.includes(vt)
						)
						// Presumptively, filter out vt itself from its nbVts
						//	(avoid the loop)
						.filter(vt => vt !== unvNbVts[0])
						// Add next vt's nbVts to unvNbVts
						.concat(
							// Remove the next vt from unvisited vts
							unvNbVts.slice(1)
						)
						// Filter out all already visited vts
						.filter(vt => !vVts.includes(vt)),
					// Add the current vt to visited vts
					[curVt].concat(vVts)
				)
				: undefined;

PS. Różnicą jest na pewno, że w tej wersji dodałem listę odwiedzonych wierzchołków (vVts) (poprawiłem vNbVts na vVts, bo to pierwsze nie miało semantycznie sensu oczywiście ;) ).

0

Wrzucam wersję dzisiejszą pseudokodu i kodu. Jeśli ktoś miały ochotę, może ocenić, czy nie ma bugów.

@Wibowit, zacząłem analizować Twój kod. Wydaje się dość bliski mojemu ostatecznemu, dzisiejszemu (nie wczorajszemu) rezultatowi. Powiedz mi: czy mogę traktować funkcję nodeIsValid jako sprawdzenie, czy są jeszcze wierzchołki do przeglądania? W sensie: wierzchołek jest "invalid", jeśli nie istnieje = jest np. undefined. Jeśli tak, to moim zdaniem logika jest taka sama jak u mnie.

Możesz ocenić sam:

Implementacja w pseudokodzie bliższym językowi naturalnemu:

/**
 * 	Pseudocode of DFS
 * 	
 * 	1. Check whether the current vertex is the goal.
 * 		1.1. If true:
 * 			1.1.1. Return the current vertex.
 * 		1.2. If false:
 * 			1.2.1. Add the current vertex to the list of visited vertices.
 * 			1.2.2. Get the list of neighbor vertices of the current vertex.
 * 			1.2.3. Exclude already existing unvisited vertices from the list
 * 				of neighbor vertices of the current vertex.
 * 			1.2.4. Exclude already visited vertices from the list of neighbor
 * 				vertices of the current vertex.
 * 			1.2.5. Add the list of neighbor vertices of the current vertex
 * 				to the list of unvisited vertices.
 * 			1.2.6. Check whether the list of unvisited vertices is empty. // Poprawka: "empty" oczywiście, nie "not empty"
 * 				1.2.6.1. If true:
 * 					1.2.6.1.1. Exit unsuccessfully.
 * 				1.2.6.2. If false:
 * 					1.2.6.2.1. Set the first unvisited vertex as the new one
 * 						to visit.
 * 					1.2.6.2.2. Remove the first vertex from the list
 * 						of unvisited vertices.
 * 					1.2.6.2.3. Repeat from the point 1.
 */

Implementacja w pseudokodzie bliższym językowi programowania:

/**
 * 	Pseudocode
 * 
 * 	DFS (vt, vVts, unvVts)
 * 		if pred(vt)
 * 		then return vt
 * 		else
 * 			newVVts <- concat(vt, vVts)
 * 			nbVts <- getNbVts(vt)
 * 			uniqueNbVts <- relativeComplement(nbVts, unvVts)
 * 			unvUniqueNbVts <- relativeComplement(uniqueNbVts, vVts)
 * 			newUnvVts <- concat(unvUniqueNbVts, unvVts)
 * 
 * 			if empty(newUnvVts)
 * 			then return nil
 * 			else
 * 				newVt <- newUnvVts[0]
 * 				newUnvVts <- slice(newUnvVts, 1, len(newUnvVts))
 * 				return DFS(newVt, newVVts, newUnvVts)
 */

Implementacja w JavaScripcie:

// JavaScript

const dfs
	= function (pred, getNbVtsFn, curVt, curVVts, curUnvVts) {
		if (pred(curVt)) {
			return curVt;
		} else {
			const newVVts = [curVt].concat(curVVts);
			const curVtNbVts = getNbVtsFn(curVt);
			const uniqueCurVtNbVts
				= curVtNbVts.filter(vt => !curUnvVts.includes(vt));
			const unvUniqueCurVtNbVts
				= uniqueCurVtNbVts.filter(vt => !curVVts.includes(vt));
			let newUnvVts = unvUniqueCurVtNbVts.concat(curUnvVts);
			if (newUnvVts.length === 0) {
				return undefined;
			} else {
				const newVt = newUnvVts[0];
				newUnvVts = newUnvVts.slice(1);

				return dfs(pred, getNbVtsFn, newVt, newVVts, newUnvVts);
			}
		}
	};

UPDATE: Poprawiłem błąd (zaznaczyłem linię) – "empty" oczywiście, nie "not empty".

2

Zaklepałem wersję w Javce:

import java.util.*;

public class RecursiveDfs {
    public static void main(String[] args) {
        Integer[][] complexGraphWithCycles = new Integer[][]{
                new Integer[]{0, 1, 2, 3, 4, 5, 6},
                new Integer[]{0, 2, 3, 4, 5, 6},
                new Integer[]{0, 3, 4, 5, 6},
                new Integer[]{0, 4, 5, 6},
                new Integer[]{0, 5, 6},
                new Integer[]{0, 6},
                new Integer[]{0}

        };
        System.out.println("DFS");
        for (int start = 0; start <= 6; start++) {
            for (int end = 0; end <= 6; end++) {
                graphSearch(complexGraphWithCycles, start, end, true);
            }
        }
        System.out.println("BFS");
        for (int start = 0; start <= 6; start++) {
            for (int end = 0; end <= 6; end++) {
                graphSearch(complexGraphWithCycles, start, end, false);
            }
        }
    }

    static void graphSearch(Integer[][] graph, int start, int end, boolean depthFirst) {
        System.out.println("run(start=" + start + ",end=" + end + ",type=" + (depthFirst ? "DFS" : "BFS") + ")");
        Deque<Deque<Integer>> toVisit = new ArrayDeque<>();
        Set<Integer> visited = new HashSet<>();
        Deque<Integer> path = new ArrayDeque<>();
        class Looper {
            void recurse() {
                if (toVisit.isEmpty()) {
                    System.out.println("No path from " + start + " to " + end + "!");
                    return;
                }
                // extract node
                Integer node = (depthFirst ? toVisit.getLast() : toVisit.getFirst()).removeFirst();
                if (depthFirst) {
                    if (toVisit.getLast().isEmpty()) {
                        toVisit.removeLast();
                    }
                } else {
                    if (toVisit.getFirst().isEmpty()) {
                        toVisit.removeFirst();
                    }
                }
                // skip visited nodes
                if (visited.contains(node)) {
                    recurse();
                    return;
                }
                visited.add(node);
                // add node's children
                toVisit.addLast(new ArrayDeque<>(Arrays.asList(graph[node])));
                // print path if end reached or continue with next node otherwise
                path.addLast(node);
                if (node.equals(end)) {
                    System.out.println("Found node! Path: " + path);
                } else {
                    recurse();
                }
                // this removes current node
                path.removeLast();
            }
        }
        toVisit.add(new ArrayDeque<>(Collections.singletonList(start)));
        new Looper().recurse();
    }
}

Nie ma żadnej pętli w metodzie graphSearch. Złożoność jest taka jak algorytmu iteracyjnego z tym, że głębokość stosu może być znacznie większa w zależności od grafu. Dodałem wypisywanie ścieżki (to akurat psuje rekurencję, która nie jest już ogonowa, bo jest path.removeLast() na końcu).

0

@Wibowit, by lepiej zrozumieć, przepisałem Twoją implementację na pseudokod. Możesz sprawdzić, czy nie ma błędów? Jak coś, to pytaj.

complexGraphWithCycles <- [
	[0, 1, 2, 3, 4, 5, 6],
	[0, 2, 3, 4, 5, 6],
	[0, 3, 4, 5, 6],
	[0, 4, 5, 6],
	[0, 5, 6],
	[0, 6],
	[0]
]

write("DFS")
for start from 0 to 6
	for end from 0 to 6
		GRAPH_SEARCH(complexGraphWithCycles, start, end, true)

write("BFS")
for start from 0 to 6
	for end from 0 to 6
		GRAPH_SEARCH(complexGraphWithCycles, start, end, false)

GRAPH_SEARCH(graph, start, end, depthFirst)
	write(
		"run(start="
		+ start
		+ ",end="
		+ end
		+ ",type="
		+ (depthFirst ? "DFS" : "BFS")
		+ ")"
	)
	
	toVisit <- []
	visited <- []
	path <- []

	RECURSE ()
		if empty(toVisit)
		then
			write("No path from " + start + " to " + end + "!")
			return

		// extract node
		if equal(depthFirst, true)
		then node <- removeFirst(
			last(toVisit)
		)
		else node <- removeFirst(
			first(toVisit)
		)
		
		if equal(depthFirst, true)
		then
			if empty(
				last(toVisit)
			)
			then removeLast(toVisit)
		else
			if empty(
				first(toVisit)
			)
			then removeFirst(toVisit)

		// skip visited nodes
		if contains(visited, node)
		then
			RECURSE()
			return

		concat(node, visited)
		// add node's children
		concat(toVisit, graph[node])
		// print path if end reached or continue with next node otherwise
		addLast(path, node)
		
		if equal(node, end)
		then write("Found node! Path: " + path)
		else RECURSE()

		// this removes current node
		removeLast(path);

	concat(
		[
			start
		]
		toVisit
	)

	RECURSE()

UPDATE: Żeby uniknąć operatora ?:, u siebie lokalnie zmieniłem jeszcze jego użycie na poniże linijki:

if equal(depthFirst, true)
then write("run(start=" + start + ",end=" + end + ",type=BFS)")
else write("run(start=" + start + ",end=" + end + ",type=DFS)")

Zmian tutaj wprowadzać nie będę, żeby nie robić zamieszania.


UPDATE2: Poprawiłem bug w powyższym pseudokodzie: usunąłem niepotrzebne cudzysłowy ze stringu do wypisania.


PS: Chciałbym od razu zapytać, czy operacja addLast jest odwrotna do operacji add? Tzn. addLast dodaje na koniec, a add na początek? Bo tak to zapisałem w pseudokodzie. Ale może błędnie?

0

Przeorganizaowałem pseudokod, by był bardziej intuicyjny w stosunku do konwencji przyjętych przeze mnie w pseudokodzie. Całość wygląda teraz tak:

complexGraphWithCycles <- [
	[0, 1, 2, 3, 4, 5, 6],
	[0, 2, 3, 4, 5, 6],
	[0, 3, 4, 5, 6],
	[0, 4, 5, 6],
	[0, 5, 6],
	[0, 6],
	[0]
]

write("DFS")
for start from 0 to 6
	for end from 0 to 6
		GRAPH_SEARCH(complexGraphWithCycles, start, end, true)

write("BFS")
for start from 0 to 6
	for end from 0 to 6
		GRAPH_SEARCH(complexGraphWithCycles, start, end, false)

GRAPH_SEARCH(graph, start, end, depthFirst)
	if equal(depthFirst, true)
	then write("run(start=" + start + ",end=" + end + ",type=" + DFS + ")")
	else write("run(start=" + start + ",end=" + end + ",type=" + BFS + ")")
	
	toVisit <- []
	visited <- []
	path <- []

	concat(
		[
			start
		]
		toVisit
	)

	RECURSE()

RECURSE ()
	if empty(toVisit)
	then
		write("No path from " + start + " to " + end + "!")
		return

	// extract node
	if equal(depthFirst, true)
	then node <- removeFirst(
		last(toVisit)
	)
	else node <- removeFirst(
		first(toVisit)
	)
	
	if equal(depthFirst, true)
	then
		if empty(
			last(toVisit)
		)
		then removeLast(toVisit)
	else
		if empty(
			first(toVisit)
		)
		then removeFirst(toVisit)

	// skip visited nodes
	if contains(visited, node)
	then
		RECURSE()
		return

	concat(node, visited)
	// add node's children
	concat(toVisit, graph[node])
	// print path if end reached or continue with next node otherwise
	addLast(path, node)
	
	if equal(node, end)
	then write("Found node! Path: " + path)
	else RECURSE()

	// this removes current node
	removeLast(path);
0

Przeorganizowałem kod pseudokodu, by był bardziej funkcyjny (choć teraz, o ile rozumiem operacje Javy, funkcja RECURSE modyfikuje swoje argumenty), a ponadto miał więcej sensu. Wynik:

complexGraphWithCycles <- [
	[0, 1, 2, 3, 4, 5, 6],
	[0, 2, 3, 4, 5, 6],
	[0, 3, 4, 5, 6],
	[0, 4, 5, 6],
	[0, 5, 6],
	[0, 6],
	[0]
]

write("DFS")
for start from 0 to 6
	for end from 0 to 6
		GRAPH_SEARCH(complexGraphWithCycles, start, end, true)

write("BFS")
for start from 0 to 6
	for end from 0 to 6
		GRAPH_SEARCH(complexGraphWithCycles, start, end, false)

GRAPH_SEARCH(graph, start, end, depthFirst)
	if equal(depthFirst, true)
	then write("run(start=" + start + ",end=" + end + ",type=DFS)")
	else write("run(start=" + start + ",end=" + end + ",type=BFS)")

	RECURSE(graph, [start], [], [], depthFirst, start, end)

RECURSE (graph, toVisit, visited, path, depthFirst, start, end)
	if empty(toVisit)
	then
		write("No path from " + start + " to " + end + "!")
		return

	// extract node
	if equal(depthFirst, true)
	then node <- removeFirst(
		last(toVisit)
	)
	else node <- removeFirst(
		first(toVisit)
	)
	
	if equal(depthFirst, true)
	then
		if empty(
			last(toVisit)
		)
		then removeLast(toVisit)
	else
		if empty(
			first(toVisit)
		)
		then removeFirst(toVisit)

	// skip visited nodes
	if contains(visited, node)
	then
		RECURSE(graph, toVisit, visited, path, depthFirst, start, end)
		return

	concat(node, visited)
	// add node's children
	concat(toVisit, graph[node])
	// print path if end reached or continue with next node otherwise
	addLast(path, node)
	
	if equal(node, end)
	then write("Found node! Path: " + path)
	else RECURSE(graph, toVisit, visited, path, depthFirst, start, end)

	// this removes current node
	removeLast(path);
0

Pousuwałem, pozmieniałem. Teraz kod wydaje mi się bardziej przejrzysty. Może go zrozumiem. Wynik:

// equal(depthFirst, true) dla przeszukiwania w głąb
// equal(depthFirst, false) dla przeszukiwania wszerz
// Co powinna zawierać tablica toVisit? Czy tablicę z wierzchołkiem startowym?
// Czy tablica path powinna być pusta?
RECURSE (graph, toVisit, visited, path, depthFirst, end)
	if empty(toVisit)
	then
		write("End, fail")
		return false

	if equal(depthFirst, true)
	then
		node <- first(
			last(toVisit)
		)
		subarr(
			last(toVisit),
			1,
			len(
				last(toVisit)
			)
		)
	else
		node <- first(
			last(toVisit)
		)
		subarr(
			first(toVisit),
			1,
			len(
				first(toVisit)
			)
		)
	
	if equal(depthFirst, true)
	then
		if empty(
			last(toVisit)
		)
		then subarr(
			toVisit,
			0,
			len(toVisit) - 1
		)
	else
		if empty(
			first(toVisit)
		)
		then subarr(
			toVisit,
			1,
			len(toVisit)
		)

	// skip visited nodes
	if contains(visited, node)
	then
		RECURSE(graph, toVisit, visited, path, depthFirst, end)
		return

	concat(node, visited)
	// add node's children
	concat(toVisit, graph[node])
	// print path if end reached or continue with next node otherwise
	concat(path, node)
	
	if equal(node, end)
	then write("Found node! Path: " + path)
	else RECURSE(graph, toVisit, visited, path, depthFirst, end)

	// this removes current node
	subarr(
		path,
		0,
		len(path)
	);
0

Poprawiłem jeden błąd dwa błędy (oba off-by-one) i parę innych rzeczy zmieniłem: Wynik:

// equal(depthFirst, true) dla przeszukiwania w głąb
// equal(depthFirst, false) dla przeszukiwania wszerz
// Co powinna zawierać tablica toVisit? Czy tablicę z wierzchołkiem startowym?
// Czy tablica path powinna być pusta?
RECURSE (graph, toVisit, visited, path, depthFirst, end)
	if empty(toVisit)
	then
		write("End, fail")
		return false

	if equal(depthFirst, true)
	then
		node <- first(
			last(toVisit)
		)
		last(toVisit) <- subarr(
			last(toVisit),
			1,
			len(
				last(toVisit)
			)
		)
	else
		node <- first(
			last(toVisit)
		)
		first(toVisit) <- subarr(
			first(toVisit),
			1,
			len(
				first(toVisit)
			)
		)
	
	if equal(depthFirst, true)
	then
		if empty(
			last(toVisit)
		)
		then toVisit <- subarr(
			toVisit,
			0,
			len(toVisit) - 2
		)
	else
		if empty(
			first(toVisit)
		)
		then toVisit <- subarr(
			toVisit,
			1,
			len(toVisit)
		)

	// skip visited nodes
	if contains(visited, node)
	then
		RECURSE(graph, toVisit, visited, path, depthFirst, end)
		return

	concat(node, visited)
	// add node's children
	concat(toVisit, graph[node])
	// print path if end reached or continue with next node otherwise
	concat(path, node)
	
	if equal(node, end)
	then write("Found node! Path: " + path)
	else RECURSE(graph, toVisit, visited, path, depthFirst, end)

	// this removes current node
	path <- subarr(
		path,
		0,
		len(path) - 2
	);
0

@Wibowit: cały czas na Ciebie czekam. :)


Aktualizacja: ponieważ doszedłem do takiego rozumienia algorytmu przeszukiwania w głąb, że wydaje mi się, że mi ono na razie wystarczy, to chciałbym zamknąć wątek. Aczkolwiek jeśli ktoś w przyszłości miałby jakieś uwagi (np. że kod jest niepoprawny), chętnie przeczytam. Obecną wersję pseudokodu (w rozumieniu tego wątku – ostateczną) załączam poniżej.

Pseudocode

DEPTH_FIRST_SEARCH (vt, vVts, unvVts)
	if isGoal(vt)
	then return vt
	else
		newUnvVts <- concat(
			relativeComplement(
				relativeComplement(
					nbVts(vt),
					unvVts
				),
				vVts
			),
			unvVts
		)

		if empty(newUnvVts)
		then return nil
		else return DEPTH_FIRST_SEARCH(
			first(newUnvVts),
			concat(vt, vVts),
			subarr(
				newUnvVts,
				1,
				len(newUnvVts) - 1
			)
		)
0

Ten mój poprzedni algorytm jest chyba popsuty (n.p. ścieżki w BFSie są źle wypisywane). Chciałem upiec wiele pieczeni na jednym ogniu i się trochę zemściło ;)

Pominąłem wypisywanie ścieżek i użyłem niemutowalnych struktur danych (z structural sharing dla dobrej złożoności) w Scali:

import scala.annotation.tailrec

object NoIterationDfs {
  def main(args: Array[String]): Unit = {
    val complexGraphWithCycles = Array(
      List(0, 1, 2, 3, 4, 5, 6),
      List(0, 2, 3, 4, 5, 6),
      List(0, 3, 4, 5, 6),
      List(0, 4, 5, 6),
      List(0, 5, 6),
      List(0, 6),
      List(0)
    )
    for (source <- 0 to 6; target <- 0 to 6) {
      dfs(complexGraphWithCycles, source, target)
    }
  }

  def dfs(graph: Array[List[Int]], source: Int, target: Int): Unit = {
    @tailrec
    def go(nodesToVisit: List[Int], visited: Set[Int]): Unit = {
      nodesToVisit match { // deconstruction to head and tail takes O(1) on List
        case node :: _ if node == target =>
          println(s"Found path from $source to $target!")
        case node :: restNodes if !visited.contains(node) =>
          // list1 ::: list2 does O(list1.size) prepends each taking O(1) time
          go(graph(node) ::: restNodes, visited + node)
        case _ :: restNodes => // skip visited nodes
          go(restNodes, visited)
        case Nil =>
          println(s"No path from $source to $target!")
      }
    }
    go(List(source), Set.empty)
    println()
  }
}

Całość ma złożoność O((E + V) lg V):

  • w linijce go(graph(node) ::: restNodes, ...) mamy powiększanie argumentu nodesToVisit. Jest to uruchamiane maksymalnie raz dla każdego wierzchołka, a przy uruchamianiu trwa dla danego wierzchołka wprost proporcjonalnie do liczby krawędzi wychodzących. Stąd O(E).
  • dla każdego wierzchołka z nodesToVisit (czyli O(E) przypadków) mamy uruchamianie visited.contains(node), a ponadto dla każdego nieodwiedzonego (czyli O(V) przypadków) mamy uruchamianie visited + node (tworzenie nowego zbioru). Jeśli zbiór jest oparty o drzewa zrównoważone to operacje contains i + zajmują czas logarytmiczny, a więc sumarycznie mamy O(E lg V + V lg V) = O((E + V) lg V).

1 użytkowników online, w tym zalogowanych: 0, gości: 1