177 lines
7.1 KiB
C
177 lines
7.1 KiB
C
#include <c_EulerianCycle.h>
|
|
|
|
/* ------------------------------------------------------------------------------------------------------------------ */
|
|
/* */
|
|
|
|
|
|
// Helper struct to uniquely represent and filter out undirected edge pairings during the trail walk
|
|
typedef struct {
|
|
c_VertexId_t v;
|
|
c_VertexId_t w;
|
|
c_bool_t is_used;
|
|
} c_EdgeRef_t;
|
|
|
|
/* ------------------------------------------------------------------------------------------------------------------ */
|
|
/* */
|
|
|
|
c_err_t c_EulerianCycle_Init(c_EulerianCycle_t* self, const c_Graph_t* G, c_Allocator_t* allocator) {
|
|
if (!self || !G ) return C_ERR_PARAM;
|
|
|
|
self->allocator = allocator?*allocator:c_DefaultAllocator;
|
|
self->has_cycle = C_FALSE;
|
|
|
|
// Initialize our results storage list container component
|
|
if (c_VertexIdList_Init(&self->cycle, 0, &self->allocator) != C_SUCCESS) {
|
|
return C_ERR_NOMEM;
|
|
}
|
|
|
|
if (G->V == 0) return C_SUCCESS;
|
|
|
|
// Condition 1: Check parity of vertex degrees (All active vertices must be even)
|
|
c_VertexId_t start_vertex = G->V; // Use G->V as an unassigned sentinel flag
|
|
for (c_VertexId_t v = 0; v < G->V; v++) {
|
|
c_AdjList_t* list = c_Graph_GetAdjList((c_Graph_t*)G, v);
|
|
c_size_t degree = list ? list->size : 0;
|
|
|
|
if (degree % 2 != 0) {
|
|
return C_SUCCESS; // Odd degree breaks Eulerian cycle condition immediately
|
|
}
|
|
if (degree > 0 && start_vertex == G->V) {
|
|
start_vertex = v; // Locate the first non-isolated node to start traversal
|
|
}
|
|
}
|
|
|
|
// Handle trivial baseline case where a graph has vertices but zero edges
|
|
if (G->E == 0) {
|
|
self->has_cycle = C_TRUE;
|
|
if (c_VertexIdList_Append(&self->cycle, 0) != C_SUCCESS) {
|
|
c_VertexIdList_Destroy(&self->cycle);
|
|
return C_ERR_NOMEM;
|
|
}
|
|
return C_SUCCESS;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Hierholzer's Algorithm Optimized Preparation */
|
|
|
|
// Tracks our processing cursor index position inside each vertex's adjacency array
|
|
c_size_t* adj_cursor = (c_size_t*)c_Allocator_Alloc(&self->allocator, G->V * sizeof(c_size_t));
|
|
|
|
// Since each undirected edge is stored twice (v->w and w->v), we can allocate a tracking bitmask
|
|
// array of size G->V. Each vertex tracks which neighbors it has already visited using a local bit field or boolean flag array.
|
|
// To keep it simple, clean, and cache-friendly, we allocate a flat array tracking visited states for all edges globally.
|
|
// Total directional edge slots in the graph = 2 * G->E. We can assign an overall visited flag to each unique undirected pair.
|
|
// To find the twin reverse edge instantly without deep loops, we can track a mirrored boolean array parallel to each adj_list entry.
|
|
|
|
c_bool_t** edge_visited = (c_bool_t**)c_Allocator_Alloc(&self->allocator, G->V * sizeof(c_bool_t*));
|
|
|
|
if (!adj_cursor || !edge_visited) {
|
|
goto free_temp_memory;
|
|
}
|
|
|
|
memset(adj_cursor, 0, G->V * sizeof(c_size_t));
|
|
memset(edge_visited, 0, G->V * sizeof(c_bool_t*));
|
|
|
|
// Allocate sub-arrays matching the exact size layout of each adjacency list track
|
|
for (c_VertexId_t i = 0; i < G->V; i++) {
|
|
c_AdjList_t* list = c_Graph_GetAdjList((c_Graph_t*)G, i);
|
|
c_size_t sz = list ? list->size : 0;
|
|
if (sz > 0) {
|
|
edge_visited[i] = (c_bool_t*)c_Allocator_Alloc(&self->allocator, sz * sizeof(c_bool_t));
|
|
if (!edge_visited[i]) goto free_temp_memory;
|
|
memset(edge_visited[i], 0, sz * sizeof(c_bool_t));
|
|
}
|
|
}
|
|
|
|
// Setup Hierholzer's explicit LIFO stack and temporary path components
|
|
c_VertexIdList_t stack;
|
|
c_VertexIdList_t reversed_route;
|
|
|
|
if (c_VertexIdList_Init(&stack, 16, &self->allocator) != C_SUCCESS) goto free_temp_memory;
|
|
if (c_VertexIdList_Init(&reversed_route, 16, &self->allocator) != C_SUCCESS) {
|
|
c_VertexIdList_Destroy(&stack);
|
|
goto free_temp_memory;
|
|
}
|
|
|
|
// Push the first valid non-isolated starting vertex onto the processing stack
|
|
c_VertexIdList_Append(&stack, (c_uint_t)start_vertex);
|
|
|
|
while (stack.size > 0) {
|
|
c_VertexId_t v = (c_VertexId_t)stack.array[stack.size - 1];
|
|
c_AdjList_t* list = c_Graph_GetAdjList((c_Graph_t*)G, v);
|
|
|
|
// Check if the current vertex has any unvisited outgoing edges left
|
|
if (list && adj_cursor[v] < list->size) {
|
|
c_size_t idx = adj_cursor[v]++; // Advance cursor to consume edge slot
|
|
|
|
if (!edge_visited[v][idx]) {
|
|
c_VertexId_t w = (c_VertexId_t)list->array[idx];
|
|
|
|
// Mark edge v -> w as burned
|
|
edge_visited[v][idx] = C_TRUE;
|
|
|
|
// UNDIRECTED INVARIANT MATCH: Burn the twin matching reverse edge w -> v immediately.
|
|
// We do a small targeted scan on w's clean local array to find the match, maximizing cache line usage.
|
|
c_AdjList_t* twin_list = c_Graph_GetAdjList((c_Graph_t*)G, w);
|
|
if (twin_list) {
|
|
for (c_size_t j = 0; j < twin_list->size; j++) {
|
|
if (twin_list->array[j] == v && !edge_visited[w][j]) {
|
|
edge_visited[w][j] = C_TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Push neighbor target node onto stack to step forward into the cycle loop path
|
|
c_VertexIdList_Append(&stack, (c_uint_t)w);
|
|
}
|
|
} else {
|
|
// Vertex has no unvisited edges left: pop from stack and append to our route timeline
|
|
c_VertexIdList_Append(&reversed_route, (c_uint_t)v);
|
|
stack.size--;
|
|
}
|
|
}
|
|
|
|
// Condition 2: Validate graph connectedness.
|
|
// An Eulerian cycle must consume every single edge in the entire graph exactly once.
|
|
// Therefore, the total vertices recorded along the path itinerary trail must equal G->E + 1.
|
|
if (reversed_route.size == G->E + 1) {
|
|
self->has_cycle = C_TRUE;
|
|
|
|
// Reverse elements from the stack output to rebuild a proper forward chronological itinerary path (source -> target)
|
|
for (c_size_t i = reversed_route.size; i > 0; i--) {
|
|
c_uint_t val;
|
|
c_VertexIdList_Get(&reversed_route, i - 1, &val);
|
|
c_VertexIdList_Append(&self->cycle, val);
|
|
}
|
|
}
|
|
|
|
c_VertexIdList_Destroy(&stack);
|
|
c_VertexIdList_Destroy(&reversed_route);
|
|
|
|
free_temp_memory:
|
|
if (adj_cursor) c_Allocator_Free(&self->allocator, adj_cursor);
|
|
if (edge_visited) {
|
|
for (c_VertexId_t i = 0; i < G->V; i++) {
|
|
if (edge_visited[i]) c_Allocator_Free(&self->allocator, edge_visited[i]);
|
|
}
|
|
c_Allocator_Free(&self->allocator, edge_visited);
|
|
}
|
|
return C_SUCCESS;
|
|
}
|
|
|
|
void c_EulerianCycle_Destroy(c_EulerianCycle_t* self) {
|
|
if (!self) return;
|
|
c_VertexIdList_Destroy(&self->cycle);
|
|
self->has_cycle = C_FALSE;
|
|
}
|
|
|
|
c_bool_t c_EulerianCycle_HasCycle(const c_EulerianCycle_t* self) {
|
|
return self ? self->has_cycle : C_FALSE;
|
|
}
|
|
|
|
const c_VertexIdList_t* c_EulerianCycle_Path(const c_EulerianCycle_t* self) {
|
|
return self ? &self->cycle : NULL;
|
|
}
|
|
|