From 297404676e6691e2eb666fa473e8123dd21d395d Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Thu, 5 Nov 2026 14:25:25 +0800 Subject: [PATCH] fix(dsa): handle graph and integer boundaries Bound C graph path reconstruction and reject unrepresentable graph results. Correct self-loop counts and integer-safe searching. Use an indexed heap for TypeScript graph paths and update regression coverage and documentation for the revised contracts. --- README.md | 10 +- REMEDIATION.md | 8 +- by-language/c/algorithms/searching.c | 8 +- .../c/data-structures/graph_algorithms.c | 110 ++++++++++---- .../c/data-structures/graph_algorithms.h | 19 ++- by-language/c/data-structures/graph_core.c | 19 ++- by-language/c/data-structures/graph_core.h | 4 +- by-language/c/tests/test_graph.c | 124 ++++++++++++++++ by-language/c/tests/test_searching.c | 23 +++ by-language/go/datastructures/graph_core.go | 8 +- by-language/go/datastructures/graph_test.go | 12 ++ .../java/dsa/datastructures/GraphCore.java | 9 +- .../dsa/datastructures/GraphCoreTest.java | 9 ++ .../src/algorithms/graph-algorithms.ts | 138 +++++++++++++----- .../src/data-structures/graph-core.ts | 6 +- .../tests/algorithms/graph-algorithms.test.ts | 36 +++++ .../tests/data-structures/graph-core.test.ts | 3 + by-topic/graphs/README.md | 10 +- by-topic/searching/README.md | 3 + 19 files changed, 460 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index a2b7aae..8d6beb6 100644 --- a/README.md +++ b/README.md @@ -213,10 +213,12 @@ bun test ## Correctness and API contracts The [remediation notes](REMEDIATION.md) record the boundary cases covered by -the regression suites and the commands used to verify them. Two path APIs now -require an explicit bound: C `shortest_path_reconstruct` takes the output array -capacity, and Rust `reconstruct_path` takes the source vertex. Callers of those -APIs should update their arguments before using the revised implementations. +the regression suites and the commands used to verify them. C +`shortest_path_reconstruct` and `graph_floyd_warshall_path` take the output +array capacity; Rust `reconstruct_path` takes the source vertex. Callers of +those APIs should update their arguments before using the revised +implementations. C graph weights reserve `GRAPH_INF` (`INT_MAX`), and graph +algorithms reject unrepresentable path distances or MST totals. ### Git Hooks (Optional) ```bash diff --git a/REMEDIATION.md b/REMEDIATION.md index d23d79f..df14a10 100644 --- a/REMEDIATION.md +++ b/REMEDIATION.md @@ -11,6 +11,10 @@ possible input or deployment has been audited. | Area | Current contract | |------|------------------| | C shortest paths | `shortest_path_reconstruct` takes `path_capacity` in elements and returns `false` for an insufficient buffer, invalid destination, unreachable path, or negative-cycle result. | +| C Floyd-Warshall paths | `graph_floyd_warshall_path` also takes `path_capacity` in elements and leaves the output array untouched when it is too small. A path from a vertex to itself contains that one vertex. Use the next-hop matrix only after `graph_floyd_warshall` returns `true`. | +| C graph weights | `GRAPH_INF` (`INT_MAX`) is reserved and `graph_add_edge` rejects it. Dijkstra and Bellman-Ford return `NULL`, and Floyd-Warshall returns `false`, when a computed distance cannot fit below `GRAPH_INF`. Kruskal and Prim return `NULL` if a total MST weight overflows `int`. | +| Undirected graph edge counts | Go, Java, and TypeScript count each self-loop once, alongside ordinary edges. | +| TypeScript graph paths | Dijkstra and Prim use an indexed binary min-heap, and unweighted BFS reconstructs its final path from parent links. | | C segment tree | Both constructors return `NULL` when `4 * n` integer slots cannot fit in `size_t`; callers must check the result before querying or updating. | | C B-tree | `btree_create` returns `NULL` when the minimum degree is below 2 or too large for signed arithmetic or node allocations. | | Rust LRU cache | Eviction removes the map entry by node identity before freeing the node, including for keys whose clones compare differently. The map is a `BTreeMap`; eviction scans it, so `put` is O(n) in the worst case. | @@ -35,8 +39,8 @@ possible input or deployment has been audited. trie enumeration, search byte counters, graph path buffers, counting/radix sort, hash tombstones, and B-tree root changes. - Graph algorithms: absent destinations, negative vertex labels, self-loops, - parallel edges, object-valued vertices, and path reconstruction from a - nonzero source. + parallel edges, object-valued vertices, integer-range boundaries, bounded + path reconstruction, and path reconstruction from a nonzero source. - String algorithms: delimiters occurring in input, Unicode boundaries, and palindrome sentinels. - Data structures: lazy segment-tree logical length, skip-list live length, diff --git a/by-language/c/algorithms/searching.c b/by-language/c/algorithms/searching.c index 01a45b2..b4fbe85 100644 --- a/by-language/c/algorithms/searching.c +++ b/by-language/c/algorithms/searching.c @@ -475,7 +475,9 @@ int kth_missing(const int *arr, size_t n, int k) { /* Helper for sorting used by three_sum */ static int compare_ints(const void *a, const void *b) { - return (*(int *)a - *(int *)b); + int left = *(const int *)a; + int right = *(const int *)b; + return (left > right) - (left < right); } bool two_sum_sorted(const int *arr, size_t n, int target, size_t *i_out, size_t *j_out) { @@ -485,7 +487,7 @@ bool two_sum_sorted(const int *arr, size_t n, int target, size_t *i_out, size_t size_t right = n - 1; while (left < right) { - int sum = arr[left] + arr[right]; + long long sum = (long long)arr[left] + arr[right]; if (sum == target) { if (i_out) *i_out = left; @@ -516,7 +518,7 @@ size_t three_sum(int *arr, size_t n, int target, int (*triplets)[3], size_t max_ size_t right = n - 1; while (left < right && count < max_triplets) { - int sum = arr[i] + arr[left] + arr[right]; + long long sum = (long long)arr[i] + arr[left] + arr[right]; if (sum == target) { triplets[count][0] = arr[i]; diff --git a/by-language/c/data-structures/graph_algorithms.c b/by-language/c/data-structures/graph_algorithms.c index 5e5a2f2..31a179a 100644 --- a/by-language/c/data-structures/graph_algorithms.c +++ b/by-language/c/data-structures/graph_algorithms.c @@ -191,7 +191,17 @@ static bool uf_union(UnionFind *uf, int x, int y) { /* ============== Edge Comparison for Kruskal ============== */ static int edge_compare(const void *a, const void *b) { - return ((EdgeItem *)a)->weight - ((EdgeItem *)b)->weight; + int left = ((const EdgeItem *)a)->weight; + int right = ((const EdgeItem *)b)->weight; + return (left > right) - (left < right); +} + +/* GRAPH_INF is reserved for unreachable paths. A distance must fit below it. */ +static bool add_distance(int left, int right, int *sum_out) { + long long sum = (long long)left + right; + if (sum < INT_MIN || sum >= GRAPH_INF) return false; + *sum_out = (int)sum; + return true; } /* ============== Result Allocation Helpers ============== */ @@ -377,7 +387,12 @@ ShortestPathResult *graph_dijkstra(const Graph *g, int source) { for (Edge *e = g->adj_list[u]; e != NULL; e = e->next) { int v = e->dest; if (minpq_contains(pq, v)) { - int new_dist = result->distance[u] + e->weight; + int new_dist; + if (!add_distance(result->distance[u], e->weight, &new_dist)) { + minpq_destroy(pq); + shortest_path_result_free(result); + return NULL; + } if (new_dist < result->distance[v]) { result->distance[v] = new_dist; result->parent[v] = u; @@ -411,7 +426,11 @@ ShortestPathResult *graph_bellman_ford(const Graph *g, int source) { for (Edge *e = g->adj_list[u]; e != NULL; e = e->next) { int v = e->dest; - int new_dist = result->distance[u] + e->weight; + int new_dist; + if (!add_distance(result->distance[u], e->weight, &new_dist)) { + shortest_path_result_free(result); + return NULL; + } if (new_dist < result->distance[v]) { result->distance[v] = new_dist; result->parent[v] = u; @@ -426,7 +445,12 @@ ShortestPathResult *graph_bellman_ford(const Graph *g, int source) { for (Edge *e = g->adj_list[u]; e != NULL; e = e->next) { int v = e->dest; - if (result->distance[u] + e->weight < result->distance[v]) { + int new_dist; + if (!add_distance(result->distance[u], e->weight, &new_dist)) { + shortest_path_result_free(result); + return NULL; + } + if (new_dist < result->distance[v]) { result->has_negative_cycle = true; return result; } @@ -497,9 +521,11 @@ bool graph_floyd_warshall(const Graph *g, int **dist, int **next) { /* Set distances from adjacency list */ for (int u = 0; u < n; u++) { for (Edge *e = g->adj_list[u]; e != NULL; e = e->next) { - dist[u][e->dest] = e->weight; - if (next != NULL) { - next[u][e->dest] = e->dest; + if (e->weight < dist[u][e->dest]) { + dist[u][e->dest] = e->weight; + if (next != NULL) { + next[u][e->dest] = e->dest; + } } } } @@ -509,7 +535,10 @@ bool graph_floyd_warshall(const Graph *g, int **dist, int **next) { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (dist[i][k] != GRAPH_INF && dist[k][j] != GRAPH_INF) { - int new_dist = dist[i][k] + dist[k][j]; + int new_dist; + if (!add_distance(dist[i][k], dist[k][j], &new_dist)) { + return false; + } if (new_dist < dist[i][j]) { dist[i][j] = new_dist; if (next != NULL) { @@ -532,32 +561,45 @@ bool graph_floyd_warshall(const Graph *g, int **dist, int **next) { } bool graph_floyd_warshall_path(int **next, int n, int src, int dest, - int *path_out, int *path_len_out) { + int *path_out, size_t path_capacity, + int *path_len_out) { + if (path_len_out != NULL) *path_len_out = 0; if (next == NULL || path_out == NULL || path_len_out == NULL) { return false; } - if (src < 0 || src >= n || dest < 0 || dest >= n) { + if (n <= 0 || src < 0 || src >= n || dest < 0 || dest >= n) { return false; } + if (src == dest) { + if (path_capacity == 0) return false; + path_out[0] = src; + *path_len_out = 1; + return true; + } if (next[src][dest] == -1) { - *path_len_out = 0; return false; /* No path */ } - int len = 0; + /* Validate the entire chain and capacity before writing to path_out. */ + size_t len = 1; int current = src; - path_out[len++] = current; - while (current != dest) { current = next[current][dest]; - if (current == -1 || len >= n) { - *path_len_out = 0; + if (current < 0 || current >= n || len >= (size_t)n) { return false; } - path_out[len++] = current; + len++; } + if (len > path_capacity) return false; - *path_len_out = len; + current = src; + path_out[0] = current; + for (size_t i = 1; i < len; i++) { + current = next[current][dest]; + path_out[i] = current; + } + + *path_len_out = (int)len; return true; } @@ -707,6 +749,7 @@ MSTResult *graph_mst_kruskal(const Graph *g) { result->edges = NULL; result->num_edges = 0; result->total_weight = 0; + long long total_weight = 0; /* Count edges and create edge array */ int edge_count = graph_edge_count(g); @@ -771,13 +814,18 @@ MSTResult *graph_mst_kruskal(const Graph *g) { result->edges[result->num_edges][0] = u; result->edges[result->num_edges][1] = v; result->edges[result->num_edges][2] = edges[i].weight; - result->total_weight += edges[i].weight; + total_weight += edges[i].weight; result->num_edges++; } } free(edges); uf_destroy(uf); + if (total_weight < INT_MIN || total_weight > INT_MAX) { + mst_result_free(result); + return NULL; + } + result->total_weight = (int)total_weight; return result; } @@ -793,6 +841,7 @@ MSTResult *graph_mst_prim(const Graph *g) { result->edges = malloc((n - 1) * sizeof(int *)); result->num_edges = 0; result->total_weight = 0; + long long total_weight = 0; if (result->edges == NULL) { free(result); @@ -833,13 +882,19 @@ MSTResult *graph_mst_prim(const Graph *g) { /* Add edge to MST */ if (parent[u] != -1) { result->edges[result->num_edges] = malloc(3 * sizeof(int)); - if (result->edges[result->num_edges] != NULL) { - result->edges[result->num_edges][0] = parent[u]; - result->edges[result->num_edges][1] = u; - result->edges[result->num_edges][2] = key[u]; - result->total_weight += key[u]; - result->num_edges++; + if (result->edges[result->num_edges] == NULL) { + free(key); + free(parent); + free(in_mst); + minpq_destroy(pq); + mst_result_free(result); + return NULL; } + result->edges[result->num_edges][0] = parent[u]; + result->edges[result->num_edges][1] = u; + result->edges[result->num_edges][2] = key[u]; + total_weight += key[u]; + result->num_edges++; } for (Edge *e = g->adj_list[u]; e != NULL; e = e->next) { @@ -858,6 +913,11 @@ MSTResult *graph_mst_prim(const Graph *g) { free(parent); free(in_mst); minpq_destroy(pq); + if (total_weight < INT_MIN || total_weight > INT_MAX) { + mst_result_free(result); + return NULL; + } + result->total_weight = (int)total_weight; return result; } diff --git a/by-language/c/data-structures/graph_algorithms.h b/by-language/c/data-structures/graph_algorithms.h index 770c6fb..48b9afa 100644 --- a/by-language/c/data-structures/graph_algorithms.h +++ b/by-language/c/data-structures/graph_algorithms.h @@ -69,7 +69,8 @@ TraversalResult *graph_dfs_full(const Graph *g); * Requires non-negative edge weights. * @param g Graph * @param source Starting vertex - * @return Shortest path result (caller must free) + * @return Shortest path result (caller must free), or NULL if a computed + * distance cannot be represented below GRAPH_INF */ ShortestPathResult *graph_dijkstra(const Graph *g, int source); @@ -78,7 +79,8 @@ ShortestPathResult *graph_dijkstra(const Graph *g, int source); * Handles negative edge weights, detects negative cycles. * @param g Graph * @param source Starting vertex - * @return Shortest path result (caller must free) + * @return Shortest path result (caller must free), or NULL if a computed + * distance cannot be represented below GRAPH_INF */ ShortestPathResult *graph_bellman_ford(const Graph *g, int source); @@ -88,7 +90,7 @@ ShortestPathResult *graph_bellman_ford(const Graph *g, int source); * @param g Graph * @param dist_out 2D array to store distances (caller allocates V*V ints) * @param next_out 2D array for path reconstruction (caller allocates V*V ints, can be NULL) - * @return true on success, false if negative cycle detected + * @return true on success, false for a negative cycle or unrepresentable distance */ bool graph_floyd_warshall(const Graph *g, int **dist_out, int **next_out); @@ -99,12 +101,15 @@ bool graph_floyd_warshall(const Graph *g, int **dist_out, int **next_out); * @param src Source vertex * @param dest Destination vertex * @param path_out Array to store path (caller allocates) + * @param path_capacity Number of int elements in path_out * @param path_len_out Pointer to store path length * @return true if a path fits; false for an invalid or unreachable destination, - * a negative-cycle result, or insufficient output capacity + * a malformed next-hop chain, or insufficient output capacity. Use only + * after graph_floyd_warshall returns true. */ bool graph_floyd_warshall_path(int **next, int n, int src, int dest, - int *path_out, int *path_len_out); + int *path_out, size_t path_capacity, + int *path_len_out); /** * Reconstruct path from source to destination. @@ -141,7 +146,7 @@ TopoSortResult *graph_topo_sort_dfs(const Graph *g); * Kruskal's algorithm for MST. * Uses Union-Find for cycle detection. * @param g Graph (should be undirected) - * @return MST result (caller must free) + * @return MST result (caller must free), or NULL if the total weight overflows int */ MSTResult *graph_mst_kruskal(const Graph *g); @@ -149,7 +154,7 @@ MSTResult *graph_mst_kruskal(const Graph *g); * Prim's algorithm for MST. * Uses min-heap for efficient implementation. * @param g Graph (should be undirected) - * @return MST result (caller must free) + * @return MST result (caller must free), or NULL if the total weight overflows int */ MSTResult *graph_mst_prim(const Graph *g); diff --git a/by-language/c/data-structures/graph_core.c b/by-language/c/data-structures/graph_core.c index 69672c3..706cc55 100644 --- a/by-language/c/data-structures/graph_core.c +++ b/by-language/c/data-structures/graph_core.c @@ -45,24 +45,29 @@ void graph_destroy(Graph *g) { bool graph_add_edge(Graph *g, int src, int dest, int weight) { if (g == NULL || src < 0 || src >= g->num_vertices || - dest < 0 || dest >= g->num_vertices) { + dest < 0 || dest >= g->num_vertices || weight == GRAPH_INF) { return false; } - /* Add edge src -> dest */ Edge *e = malloc(sizeof(Edge)); if (e == NULL) return false; + Edge *e2 = NULL; + if (!g->directed) { + e2 = malloc(sizeof(Edge)); + if (e2 == NULL) { + free(e); + return false; + } + } + + /* Allocate both directions before changing the graph. */ e->dest = dest; e->weight = weight; e->next = g->adj_list[src]; g->adj_list[src] = e; - /* If undirected, add reverse edge */ - if (!g->directed) { - Edge *e2 = malloc(sizeof(Edge)); - if (e2 == NULL) return false; - + if (e2 != NULL) { e2->dest = src; e2->weight = weight; e2->next = g->adj_list[dest]; diff --git a/by-language/c/data-structures/graph_core.h b/by-language/c/data-structures/graph_core.h index a3b7273..040dbf0 100644 --- a/by-language/c/data-structures/graph_core.h +++ b/by-language/c/data-structures/graph_core.h @@ -91,8 +91,8 @@ void graph_destroy(Graph *g); * @param g Graph * @param src Source vertex * @param dest Destination vertex - * @param weight Edge weight (use 1 for unweighted) - * @return true on success + * @param weight Edge weight (use 1 for unweighted); GRAPH_INF is reserved + * @return true on success, false for an invalid edge or allocation failure */ bool graph_add_edge(Graph *g, int src, int dest, int weight); diff --git a/by-language/c/tests/test_graph.c b/by-language/c/tests/test_graph.c index 81cf8ba..5e307dd 100644 --- a/by-language/c/tests/test_graph.c +++ b/by-language/c/tests/test_graph.c @@ -4,6 +4,7 @@ #include "test_framework.h" #include "../data-structures/graph.h" +#include #include /* ============== Graph Creation Tests ============== */ @@ -186,6 +187,73 @@ TEST(dijkstra_path_reconstruction) { graph_destroy(g); } +TEST(graph_rejects_reserved_weight) { + Graph *g = graph_create(2, false); + ASSERT_NOT_NULL(g); + ASSERT_FALSE(graph_add_edge(g, 0, 1, GRAPH_INF)); + ASSERT_EQ(0, graph_edge_count(g)); + graph_destroy(g); +} + +TEST(shortest_paths_reject_unrepresentable_distance) { + Graph *g = graph_create(3, true); + ASSERT_NOT_NULL(g); + ASSERT_TRUE(graph_add_edge(g, 0, 1, INT_MAX - 1)); + ASSERT_TRUE(graph_add_edge(g, 1, 2, 2)); + ASSERT_EQ(NULL, graph_dijkstra(g, 0)); + ASSERT_EQ(NULL, graph_bellman_ford(g, 0)); + + int dist_data[3][3], next_data[3][3]; + int *dist[] = {dist_data[0], dist_data[1], dist_data[2]}; + int *next[] = {next_data[0], next_data[1], next_data[2]}; + ASSERT_FALSE(graph_floyd_warshall(g, dist, next)); + graph_destroy(g); + + g = graph_create(3, true); + ASSERT_NOT_NULL(g); + graph_add_edge(g, 0, 1, INT_MIN); + graph_add_edge(g, 1, 2, -1); + ASSERT_EQ(NULL, graph_bellman_ford(g, 0)); + ASSERT_FALSE(graph_floyd_warshall(g, dist, next)); + graph_destroy(g); +} + +TEST(floyd_warshall_parallel_edges_and_path_capacity) { + Graph *g = graph_create(3, true); + ASSERT_NOT_NULL(g); + graph_add_edge(g, 0, 1, 10); + graph_add_edge(g, 0, 1, 1); + graph_add_edge(g, 0, 0, 5); + graph_add_edge(g, 1, 2, 2); + + int dist_data[3][3], next_data[3][3]; + int *dist[] = {dist_data[0], dist_data[1], dist_data[2]}; + int *next[] = {next_data[0], next_data[1], next_data[2]}; + ASSERT_TRUE(graph_floyd_warshall(g, dist, next)); + ASSERT_EQ(0, dist[0][0]); + ASSERT_EQ(1, dist[0][1]); + ASSERT_EQ(3, dist[0][2]); + + int short_path[2] = {-7, -7}; + int path_len = -1; + ASSERT_FALSE(graph_floyd_warshall_path(next, 3, 0, 2, + short_path, 2, &path_len)); + ASSERT_EQ(0, path_len); + ASSERT_EQ(-7, short_path[0]); + ASSERT_EQ(-7, short_path[1]); + + int path[3]; + ASSERT_TRUE(graph_floyd_warshall_path(next, 3, 0, 2, path, 3, &path_len)); + ASSERT_EQ(3, path_len); + ASSERT_EQ(0, path[0]); + ASSERT_EQ(1, path[1]); + ASSERT_EQ(2, path[2]); + ASSERT_TRUE(graph_floyd_warshall_path(next, 3, 0, 0, path, 1, &path_len)); + ASSERT_EQ(1, path_len); + ASSERT_EQ(0, path[0]); + graph_destroy(g); +} + /* ============== Bellman-Ford Tests ============== */ TEST(bellman_ford_simple) { @@ -358,6 +426,57 @@ TEST(mst_kruskal_self_loop) { graph_destroy(g); } +TEST(mst_integer_extremes) { + Graph *g = graph_create(3, false); + ASSERT_NOT_NULL(g); + graph_add_edge(g, 0, 1, INT_MIN); + graph_add_edge(g, 0, 2, 0); + graph_add_edge(g, 1, 2, 1); + MSTResult *kruskal = graph_mst_kruskal(g); + MSTResult *prim = graph_mst_prim(g); + ASSERT_NOT_NULL(kruskal); + ASSERT_NOT_NULL(prim); + ASSERT_EQ(INT_MIN, kruskal->total_weight); + ASSERT_EQ(INT_MIN, prim->total_weight); + mst_result_free(kruskal); + mst_result_free(prim); + graph_destroy(g); + + /* The intermediate sum is below INT_MIN, but the final MST fits. */ + g = graph_create(4, false); + ASSERT_NOT_NULL(g); + graph_add_edge(g, 0, 1, INT_MIN); + graph_add_edge(g, 1, 2, -1); + graph_add_edge(g, 2, 3, INT_MAX - 1); + kruskal = graph_mst_kruskal(g); + prim = graph_mst_prim(g); + ASSERT_NOT_NULL(kruskal); + ASSERT_NOT_NULL(prim); + ASSERT_EQ(-3, kruskal->total_weight); + ASSERT_EQ(-3, prim->total_weight); + mst_result_free(kruskal); + mst_result_free(prim); + graph_destroy(g); +} + +TEST(mst_rejects_unrepresentable_total) { + Graph *g = graph_create(3, false); + ASSERT_NOT_NULL(g); + graph_add_edge(g, 0, 1, INT_MAX - 1); + graph_add_edge(g, 1, 2, 2); + ASSERT_EQ(NULL, graph_mst_kruskal(g)); + ASSERT_EQ(NULL, graph_mst_prim(g)); + graph_destroy(g); + + g = graph_create(3, false); + ASSERT_NOT_NULL(g); + graph_add_edge(g, 0, 1, INT_MIN); + graph_add_edge(g, 1, 2, -1); + ASSERT_EQ(NULL, graph_mst_kruskal(g)); + ASSERT_EQ(NULL, graph_mst_prim(g)); + graph_destroy(g); +} + TEST(mst_prim_simple) { Graph *g = graph_create(4, false); graph_add_edge(g, 0, 1, 10); @@ -587,6 +706,9 @@ int main(void) { RUN_TEST(dijkstra_simple); RUN_TEST(dijkstra_unreachable); RUN_TEST(dijkstra_path_reconstruction); + RUN_TEST(graph_rejects_reserved_weight); + RUN_TEST(shortest_paths_reject_unrepresentable_distance); + RUN_TEST(floyd_warshall_parallel_edges_and_path_capacity); /* Bellman-Ford tests */ RUN_TEST(bellman_ford_simple); @@ -602,6 +724,8 @@ int main(void) { /* MST tests */ RUN_TEST(mst_kruskal_simple); RUN_TEST(mst_kruskal_self_loop); + RUN_TEST(mst_integer_extremes); + RUN_TEST(mst_rejects_unrepresentable_total); RUN_TEST(mst_prim_simple); RUN_TEST(mst_complete_graph); diff --git a/by-language/c/tests/test_searching.c b/by-language/c/tests/test_searching.c index d33efe3..3c531a5 100644 --- a/by-language/c/tests/test_searching.c +++ b/by-language/c/tests/test_searching.c @@ -4,6 +4,7 @@ #include "test_framework.h" #include "../algorithms/searching.h" +#include #include /* ============== Linear Search Tests ============== */ @@ -378,6 +379,26 @@ TEST(three_sum_basic) { ASSERT_TRUE(count >= 1); /* At least one triplet */ } +TEST(two_sum_sorted_integer_extremes) { + int no_pair[] = {INT_MAX, INT_MAX}; + ASSERT_FALSE(two_sum_sorted(no_pair, 2, -2, NULL, NULL)); + + int pair[] = {INT_MIN, INT_MAX}; + size_t i = 2, j = 2; + ASSERT_TRUE(two_sum_sorted(pair, 2, -1, &i, &j)); + ASSERT_EQ(0, i); + ASSERT_EQ(1, j); +} + +TEST(three_sum_integer_extremes) { + int arr[] = {0, INT_MIN, INT_MAX}; + int triplets[1][3]; + ASSERT_EQ(1, three_sum(arr, 3, -1, triplets, 1)); + ASSERT_EQ(INT_MIN, triplets[0][0]); + ASSERT_EQ(0, triplets[0][1]); + ASSERT_EQ(INT_MAX, triplets[0][2]); +} + TEST(remove_duplicates_basic) { int arr[] = {1, 1, 2, 2, 2, 3, 4, 4, 5}; size_t new_len = remove_duplicates(arr, 9); @@ -609,7 +630,9 @@ int main(void) { /* Two Pointers */ RUN_TEST(two_sum_sorted_found); RUN_TEST(two_sum_sorted_not_found); + RUN_TEST(two_sum_sorted_integer_extremes); RUN_TEST(three_sum_basic); + RUN_TEST(three_sum_integer_extremes); RUN_TEST(remove_duplicates_basic); RUN_TEST(move_zeros_basic); RUN_TEST(max_water_container_basic); diff --git a/by-language/go/datastructures/graph_core.go b/by-language/go/datastructures/graph_core.go index 6c91e8d..ac6f01a 100644 --- a/by-language/go/datastructures/graph_core.go +++ b/by-language/go/datastructures/graph_core.go @@ -21,11 +21,15 @@ func (g *GraphCore) NumVertices() int { return len(g.adj) } func (g *GraphCore) EdgeCount() int { count := 0 - for _, neighbors := range g.adj { + loops := 0 + for vertex, neighbors := range g.adj { count += len(neighbors) + if _, ok := neighbors[vertex]; ok { + loops++ + } } if !g.directed { - count /= 2 + count = (count + loops) / 2 } return count } diff --git a/by-language/go/datastructures/graph_test.go b/by-language/go/datastructures/graph_test.go index e1a18ed..7f9e7bc 100644 --- a/by-language/go/datastructures/graph_test.go +++ b/by-language/go/datastructures/graph_test.go @@ -123,6 +123,18 @@ func TestGraphCoreDirected(t *testing.T) { } } +func TestGraphCoreUndirectedSelfLoopCount(t *testing.T) { + g := NewGraphCore(false) + g.AddEdge(1, 1, 1) + if got := g.EdgeCount(); got != 1 { + t.Fatalf("one self-loop: got %d edges, want 1", got) + } + g.AddEdge(1, 2, 1) + if got := g.EdgeCount(); got != 2 { + t.Fatalf("self-loop plus ordinary edge: got %d edges, want 2", got) + } +} + func TestGraphCoreRemove(t *testing.T) { g := NewGraphCore(false) g.AddEdge(0, 1, 1) diff --git a/by-language/java/src/main/java/dsa/datastructures/GraphCore.java b/by-language/java/src/main/java/dsa/datastructures/GraphCore.java index 8823c9f..a47276e 100644 --- a/by-language/java/src/main/java/dsa/datastructures/GraphCore.java +++ b/by-language/java/src/main/java/dsa/datastructures/GraphCore.java @@ -229,11 +229,16 @@ public int numVertices() { */ public int numEdges() { int count = 0; - for (Map neighbors : adjacencyList.values()) { + int loops = 0; + for (Map.Entry> entry : adjacencyList.entrySet()) { + Map neighbors = entry.getValue(); count += neighbors.size(); + if (neighbors.containsKey(entry.getKey())) { + loops++; + } } if (!directed) { - count /= 2; + count = (count + loops) / 2; } return count; } diff --git a/by-language/java/src/test/java/dsa/datastructures/GraphCoreTest.java b/by-language/java/src/test/java/dsa/datastructures/GraphCoreTest.java index f47936d..f428ada 100644 --- a/by-language/java/src/test/java/dsa/datastructures/GraphCoreTest.java +++ b/by-language/java/src/test/java/dsa/datastructures/GraphCoreTest.java @@ -43,6 +43,15 @@ void addEdgeUndirected() { assertEquals(2, g.numVertices()); } + @Test + void countUndirectedSelfLoops() { + GraphCore g = new GraphCore(false); + g.addEdge("A", "A", 1.0); + assertEquals(1, g.numEdges()); + g.addEdge("A", "B", 2.0); + assertEquals(2, g.numEdges()); + } + @Test void addEdgeDirected() { GraphCore g = new GraphCore(true); diff --git a/by-language/typescript/src/algorithms/graph-algorithms.ts b/by-language/typescript/src/algorithms/graph-algorithms.ts index 2f9da2d..3feda0f 100644 --- a/by-language/typescript/src/algorithms/graph-algorithms.ts +++ b/by-language/typescript/src/algorithms/graph-algorithms.ts @@ -21,6 +21,74 @@ import { GraphCore } from "../data-structures/graph-core"; +/** Min-priority queue with one entry per vertex and O(log V) decreases. */ +class IndexedMinQueue { + private items: { vertex: T; priority: number }[] = []; + private positions = new Map(); + + get size(): number { + return this.items.length; + } + + upsert(vertex: T, priority: number): boolean { + const position = this.positions.get(vertex); + if (position !== undefined) { + if (priority >= this.items[position].priority) return false; + this.items[position].priority = priority; + this.siftUp(position); + return true; + } + + const index = this.items.length; + this.items.push({ vertex, priority }); + this.positions.set(vertex, index); + this.siftUp(index); + return true; + } + + pop(): { vertex: T; priority: number } { + const minimum = this.items[0]; + const last = this.items.pop()!; + this.positions.delete(minimum.vertex); + if (this.items.length > 0) { + this.items[0] = last; + this.positions.set(last.vertex, 0); + this.siftDown(0); + } + return minimum; + } + + private swap(left: number, right: number): void { + [this.items[left], this.items[right]] = [this.items[right], this.items[left]]; + this.positions.set(this.items[left].vertex, left); + this.positions.set(this.items[right].vertex, right); + } + + private siftUp(index: number): void { + while (index > 0) { + const parent = Math.floor((index - 1) / 2); + if (this.items[parent].priority <= this.items[index].priority) break; + this.swap(parent, index); + index = parent; + } + } + + private siftDown(index: number): void { + while (2 * index + 1 < this.items.length) { + const left = 2 * index + 1; + const right = left + 1; + let smallest = left; + if (right < this.items.length && + this.items[right].priority < this.items[left].priority) { + smallest = right; + } + if (this.items[index].priority <= this.items[smallest].priority) break; + this.swap(index, smallest); + index = smallest; + } + } +} + // ========================================================================= // GRAPH TRAVERSAL // ========================================================================= @@ -108,20 +176,11 @@ export function dijkstra(graph: GraphCore, start: T): Map { const visited = new Set(); - // Simple priority queue using array (adequate for correctness; - // production code would use a proper min-heap) - const pq: [number, T][] = [[0, start]]; + const pq = new IndexedMinQueue(); + pq.upsert(start, 0); - while (pq.length > 0) { - // Find minimum distance entry - let minIdx = 0; - for (let i = 1; i < pq.length; i++) { - if (pq[i][0] < pq[minIdx][0]) { - minIdx = i; - } - } - const [dist, vertex] = pq[minIdx]; - pq.splice(minIdx, 1); + while (pq.size > 0) { + const { priority: dist, vertex } = pq.pop(); if (visited.has(vertex)) continue; visited.add(vertex); @@ -132,7 +191,7 @@ export function dijkstra(graph: GraphCore, start: T): Map { const newDist = dist + weight; if (newDist < distances.get(neighbor)!) { distances.set(neighbor, newDist); - pq.push([newDist, neighbor]); + pq.upsert(neighbor, newDist); } } } @@ -207,20 +266,27 @@ export function shortestPathBfs( const visited = new Set(); visited.add(start); - const queue: [T, T[]][] = [[start, [start]]]; + const parent = new Map(); + const queue: T[] = [start]; let front = 0; while (front < queue.length) { - const [vertex, path] = queue[front++]; + const vertex = queue[front++]; for (const neighbor of graph.neighbors(vertex)) { + if (visited.has(neighbor)) continue; + visited.add(neighbor); + parent.set(neighbor, vertex); if (neighbor === end) { - return [...path, neighbor]; - } - if (!visited.has(neighbor)) { - visited.add(neighbor); - queue.push([neighbor, [...path, neighbor]]); + const path = [end]; + let current = end; + while (current !== start) { + current = parent.get(current)!; + path.push(current); + } + return path.reverse(); } + queue.push(neighbor); } } @@ -400,32 +466,24 @@ export function primMST( const mstEdges: [T, T, number][] = []; let totalWeight = 0; const visited = new Set(); - visited.add(start); - - // Priority queue: [weight, from, to] - let edgesPq: [number, T, T][] = []; - for (const [neighbor, weight] of graph.neighborsWeighted(start)) { - edgesPq.push([weight, start, neighbor]); - } - edgesPq.sort((a, b) => a[0] - b[0]); - - while (edgesPq.length > 0 && visited.size < graph.size) { - // Extract min - const [weight, u, v] = edgesPq.shift()!; + const parent = new Map(); + const pq = new IndexedMinQueue(); + pq.upsert(start, 0); - if (visited.has(v)) continue; + while (pq.size > 0) { + const { vertex: v, priority: weight } = pq.pop(); visited.add(v); - mstEdges.push([u, v, weight]); - totalWeight += weight; + if (parent.has(v)) { + mstEdges.push([parent.get(v)!, v, weight]); + totalWeight += weight; + } - // Add edges from newly added vertex for (const [neighbor, edgeWeight] of graph.neighborsWeighted(v)) { - if (!visited.has(neighbor)) { - edgesPq.push([edgeWeight, v, neighbor]); + if (!visited.has(neighbor) && pq.upsert(neighbor, edgeWeight)) { + parent.set(neighbor, v); } } - edgesPq.sort((a, b) => a[0] - b[0]); } return { edges: mstEdges, totalWeight }; diff --git a/by-language/typescript/src/data-structures/graph-core.ts b/by-language/typescript/src/data-structures/graph-core.ts index a6062af..388c2eb 100644 --- a/by-language/typescript/src/data-structures/graph-core.ts +++ b/by-language/typescript/src/data-structures/graph-core.ts @@ -38,10 +38,12 @@ export class GraphCore { get edgeCount(): number { let count = 0; - for (const neighbors of this._adj.values()) { + let loops = 0; + for (const [vertex, neighbors] of this._adj) { count += neighbors.size; + if (neighbors.has(vertex)) loops++; } - return this._directed ? count : Math.floor(count / 2); + return this._directed ? count : (count + loops) / 2; } get size(): number { diff --git a/by-language/typescript/tests/algorithms/graph-algorithms.test.ts b/by-language/typescript/tests/algorithms/graph-algorithms.test.ts index 6b94718..8048729 100644 --- a/by-language/typescript/tests/algorithms/graph-algorithms.test.ts +++ b/by-language/typescript/tests/algorithms/graph-algorithms.test.ts @@ -234,6 +234,20 @@ describe("Graph Algorithms", () => { const distances = dijkstra(g, 1); expect(distances.get(1)).toBe(0); }); + + test("decreases priorities for object vertices", () => { + const a = {}, b = {}, c = {}, d = {}; + const g = new GraphCore(true); + g.addEdge(a, b, 9); + g.addEdge(a, c, 1); + g.addEdge(a, d, 8); + g.addEdge(c, b, 2); + g.addEdge(b, d, 1); + + const distances = dijkstra(g, a); + expect(distances.get(b)).toBe(3); + expect(distances.get(d)).toBe(4); + }); }); // ======================================================================= @@ -311,6 +325,14 @@ describe("Graph Algorithms", () => { expect(path![path!.length - 1]).toBe(3); }); + test("reconstructs a path through object vertices", () => { + const a = {}, b = {}, c = {}; + const g = new GraphCore(true); + g.addEdge(a, b); + g.addEdge(b, c); + expect(shortestPathBfs(g, a, c)).toEqual([a, b, c]); + }); + test("same start and end", () => { const g = new GraphCore(); g.addVertex(1); @@ -617,6 +639,20 @@ describe("Graph Algorithms", () => { expect(edges.length).toBe(1); expect(totalWeight).toBe(5); }); + + test("decreases the selected edge for object vertices", () => { + const a = {}, b = {}, c = {}, d = {}; + const g = new GraphCore(); + g.addEdge(a, b, 9); + g.addEdge(a, c, 1); + g.addEdge(c, b, 2); + g.addEdge(b, d, 1); + g.addEdge(c, d, 10); + + const result = primMST(g); + expect(result.edges.length).toBe(3); + expect(result.totalWeight).toBe(4); + }); }); // ======================================================================= diff --git a/by-language/typescript/tests/data-structures/graph-core.test.ts b/by-language/typescript/tests/data-structures/graph-core.test.ts index 4f3b223..2ee6e08 100644 --- a/by-language/typescript/tests/data-structures/graph-core.test.ts +++ b/by-language/typescript/tests/data-structures/graph-core.test.ts @@ -261,6 +261,9 @@ describe("GraphCore", () => { g.addEdge(1, 1); expect(g.hasEdge(1, 1)).toBe(true); expect(g.size).toBe(1); + expect(g.edgeCount).toBe(1); + g.addEdge(1, 2); + expect(g.edgeCount).toBe(2); }); }); diff --git a/by-topic/graphs/README.md b/by-topic/graphs/README.md index 97b6d10..c095635 100644 --- a/by-topic/graphs/README.md +++ b/by-topic/graphs/README.md @@ -162,9 +162,13 @@ vertex objects have no ordering. The Python graph copy preserves isolated vertices, and undirected self-loops count as one edge. For path reconstruction, C callers must pass the output array capacity to -`shortest_path_reconstruct`. Rust callers must pass the source to -`reconstruct_path(parent, source, target)`. Reject a Bellman-Ford result with -a negative cycle before reconstructing a path. +`shortest_path_reconstruct` and `graph_floyd_warshall_path`. Only use a C +Floyd-Warshall next-hop matrix after `graph_floyd_warshall` returns `true`. +C graph weights reserve `GRAPH_INF` (`INT_MAX`), and its shortest-path and MST +algorithms reject results outside their documented integer ranges. Go, Java, +and TypeScript count an undirected self-loop as one edge. Rust callers must +pass the source to `reconstruct_path(parent, source, target)`. Reject a +Bellman-Ford result with a negative cycle before reconstructing a path. ### Topological Sort (Kahn's Algorithm) ```python diff --git a/by-topic/searching/README.md b/by-topic/searching/README.md index fe9517d..b5dbf25 100644 --- a/by-topic/searching/README.md +++ b/by-topic/searching/README.md @@ -102,6 +102,9 @@ def two_sum_sorted(arr, target): return [] ``` +The C two-sum and three-sum implementations use wider intermediate sums so +inputs near `INT_MIN` and `INT_MAX` do not overflow during comparisons. + ### Same Direction (Fast/Slow) ```python # Remove duplicates in-place