#include /* ------------------------------------------------------------------------------------------------------------------ */ /* */ // 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; }