diff --git a/tests/browser/graph-clone-identity.test.js b/tests/browser/graph-clone-identity.test.js new file mode 100644 index 000000000..9adb0300f --- /dev/null +++ b/tests/browser/graph-clone-identity.test.js @@ -0,0 +1,251 @@ +import { after, before, describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { execFile, execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { + generateHarness, + parseCompilerResults, +} from "../../web/compiler-explorer.js"; +import { launchChromium, startStaticServer } from "./source.js"; + +const message = + "Returned graph must not contain distinct nodes with the same ID."; +const spec = { + kind: "function", + entry: "replicateTopology", + paramNames: ["node"], + paramTypes: ["Node"], + returnType: "Node", + argTypes: ["graphNode"], + outputType: "graphNode", + checker: "exact", + cases: [ + { + label: "triangle", + input: [ + [ + [2, 3], + [1, 3], + [1, 2], + ], + ], + expected: [ + [2, 3], + [1, 3], + [1, 2], + ], + }, + ], +}; + +// Both duplicate placements are reachable through a later edge, after the +// legitimate object with that ID has already been visited. +function candidate( + language, + duplicate, + invalidId, + invalidRoot, + nullNeighbor, + originalNeighbors, +) { + const id = duplicate || 1; + if (language === "javascript") + return `function replicateTopology(node) { + const a = new Node(1), b = new Node(2), c = new Node(3); + a.neighbors = [c, b]; b.neighbors = [c, a]; c.neighbors = [b, a]; + ${duplicate ? `const d = new Node(${id}, ${id === 1 ? "a" : "b"}.neighbors.slice()); ${id === 1 ? "b.neighbors = [c, d]" : "c.neighbors = [d, a]"};` : ""} + return a; + }`; + if (language === "python") + return `def replicateTopology(node): + a, b, c = Node(1), Node(2), Node(3) + a.neighbors, b.neighbors, c.neighbors = [c, b], [c, a], [b, a] + ${duplicate ? `d = Node(${id})\n d.neighbors = list(${id === 1 ? "a" : "b"}.neighbors)\n ${id === 1 ? "b.neighbors = [c, d]" : "c.neighbors = [d, a]"}` : ""} + return a +`; + if (language === "cpp") + return `class Solution { public: Node* replicateTopology(Node* node) { + auto* a = new Node(1); auto* b = new Node(2); auto* c = new Node(3); + a->neighbors = {c, b}; b->neighbors = {c, a}; c->neighbors = {b, a}; + ${duplicate ? `auto* d = new Node(${id}); d->neighbors = ${id === 1 ? "a" : "b"}->neighbors; ${id === 1 ? "b->neighbors = {c, d}" : "c->neighbors = {d, a}"};` : ""} + return a; + }};`; + if (language === "java") + return `class Solution { public Node replicateTopology(Node node) { + Node a = new Node(1), b = new Node(2), c = new Node(3); + a.neighbors.addAll(Arrays.asList(c, b)); b.neighbors.addAll(Arrays.asList(c, a)); c.neighbors.addAll(Arrays.asList(b, a)); + ${duplicate ? `Node d = new Node(${id}); d.neighbors.addAll(${id === 1 ? "a" : "b"}.neighbors); ${id === 1 ? "b.neighbors.set(1, d)" : "c.neighbors.set(0, d)"};` : ""} + return a; + }}`; + return `struct Node* replicateTopology(struct Node* node) { + struct Node* a = calloc(1, sizeof(*a)); struct Node* b = calloc(1, sizeof(*b)); struct Node* c = calloc(1, sizeof(*c)); + a->val = 1; b->val = 2; c->val = 3; + a->numNeighbors = b->numNeighbors = c->numNeighbors = 2; + a->neighbors = calloc(2, sizeof(*a->neighbors)); b->neighbors = calloc(2, sizeof(*b->neighbors)); c->neighbors = calloc(2, sizeof(*c->neighbors)); + a->neighbors[0] = b; a->neighbors[1] = c; b->neighbors[0] = a; b->neighbors[1] = c; c->neighbors[0] = a; c->neighbors[1] = b; + ${duplicate ? `struct Node* d = calloc(1, sizeof(*d)); *d = *${id === 1 ? "a" : "b"}; ${id === 1 ? "b->neighbors[0]" : "c->neighbors[1]"} = d;` : ""} + ${ + invalidId === undefined + ? "" + : `struct Node* extra = calloc(1, sizeof(*extra)); extra->val = ${invalidId}; + ${invalidRoot ? `struct Node* root = calloc(1, sizeof(*root)); root->val = ${invalidId}; root->numNeighbors = 2; root->neighbors = calloc(2, sizeof(*root->neighbors)); root->neighbors[0] = a; root->neighbors[1] = extra; return root;` : "a->neighbors = realloc(a->neighbors, 3 * sizeof(*a->neighbors)); a->neighbors[2] = extra; a->numNeighbors = 3;"}` + } + ${nullNeighbor ? "a->neighbors[0] = NULL;" : ""} + ${originalNeighbors ? "b->neighbors = node->neighbors[0]->neighbors;" : ""} + return a; + }`; +} + +let browser, server, base; +before(async () => { + browser = await launchChromium(); + if (!browser) return; + ({ server, base } = await startStaticServer({ + handle: (_request, response, url) => { + if (url.pathname !== "/judges/graph-clone-identity.json") return false; + response.setHeader("content-type", "application/json"); + response.end(JSON.stringify(spec)); + return true; + }, + })); +}); +after(async () => { + await browser?.close(); + if (server) await new Promise((resolve) => server.close(resolve)); +}); + +for (const language of ["javascript", "python"]) { + for (const duplicate of [0, 1, 2]) { + test(`${language} graph clone ${duplicate ? `rejects duplicate ID ${duplicate}` : "accepts shared nodes and reordered neighbors"}`, async (t) => { + if (!browser) return t.skip("playwright chromium unavailable"); + const page = await browser.newPage(); + try { + await page.goto(`${base}/observer.html`); + const summary = await page.evaluate( + async ({ language, code }) => { + const { runBrowserTests } = await import("/runners.js"); + return runBrowserTests("graph-clone-identity", code, language); + }, + { language, code: candidate(language, duplicate) }, + ); + assert.equal(summary.setupError, undefined); + assert.equal(summary.total, 1); + assert.equal(summary.cases.length, 1); + assert.equal(summary.passed, duplicate ? 0 : 1); + if (duplicate) + assert.match( + summary.cases[0].error, + /distinct nodes with the same ID/, + ); + } finally { + await page.close(); + } + }); + } +} + +const run = promisify(execFile); +describe("graph clone toolchain harnesses", { concurrency: true }, () => { + for (const [language, compiler, flags, extension] of [ + ["c", "cc", ["-std=c17"], "c"], + ["cpp", "g++", ["-std=c++20"], "cpp"], + ["java", "javac", [], "java"], + ]) { + let available = true; + try { + execFileSync(compiler, [language === "java" ? "-version" : "--version"], { + stdio: "ignore", + }); + if (language === "java") + execFileSync("java", ["-version"], { stdio: "ignore" }); + } catch { + available = false; + } + const scenarios = [0, 1, 2].map((duplicate) => ({ duplicate })); + if (language === "c") + for (const invalidId of [0, 1024]) + for (const invalidRoot of [true, false]) + scenarios.push({ duplicate: 0, invalidId, invalidRoot }); + if (language === "c") scenarios.push({ duplicate: 0, nullNeighbor: true }); + if (language === "c") + scenarios.push({ duplicate: 0, originalNeighbors: true }); + for (const { + duplicate, + invalidId, + invalidRoot, + nullNeighbor, + originalNeighbors, + } of scenarios) { + test( + `${language} graph clone ${originalNeighbors ? "reports input nodes before duplicate IDs" : nullNeighbor ? "rejects a NULL neighbor" : invalidId !== undefined ? `rejects out-of-range ${invalidRoot ? "root" : "neighbor"} ID ${invalidId}` : duplicate ? `rejects duplicate ID ${duplicate}` : language === "c" ? "accepts shared nodes" : "accepts shared nodes and reordered neighbors"}`, + { + skip: available + ? false + : language === "java" + ? "javac/java are not available" + : `${compiler} is not available`, + }, + async () => { + const dir = mkdtempSync(join(tmpdir(), "codetrial-graph-")); + try { + const source = join( + dir, + language === "java" ? "Main.java" : `graph.${extension}`, + ); + const binary = join(dir, "graph"); + writeFileSync( + source, + generateHarness( + language, + spec, + candidate( + language, + duplicate, + invalidId, + invalidRoot, + nullNeighbor, + originalNeighbors, + ), + ), + ); + await run( + compiler, + language === "java" ? [source] : [...flags, source, "-o", binary], + ); + const output = + language === "java" + ? await run("java", ["-cp", dir, "Main"]) + : await run(binary); + const results = parseCompilerResults(output.stdout).results; + assert.equal(results.length, 1); + if (originalNeighbors) + assert.equal( + results[0].error ?? results[0].actual, + "Returned graph must be a deep copy.", + ); + else if (nullNeighbor) + assert.equal( + results[0].error ?? results[0].actual, + "Returned graph has a NULL neighbor.", + ); + else if (invalidId !== undefined) + assert.equal( + results[0].error ?? results[0].actual, + "Returned graph node IDs must be between 1 and 1023.", + ); + else if (duplicate) + assert.ok( + (results[0].error ?? results[0].actual).endsWith(message), + ); + else assert.deepEqual(results[0].actual, spec.cases[0].expected); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, + ); + } + } +}); diff --git a/web/compiler-explorer.js b/web/compiler-explorer.js index 0aa7957e9..e242dc066 100644 --- a/web/compiler-explorer.js +++ b/web/compiler-explorer.js @@ -401,7 +401,7 @@ static void json_list(FILE* out, struct ListNode* node) { for (struct ListNode * static void json_tree(FILE* out, struct TreeNode* root) { if (!root) { fputs("[]", out); return; } struct TreeNode* queue[4096]; const char* values[4096]; char numbers[4096][32]; int head = 0, tail = 0, count = 0; queue[tail++] = root; while (head < tail && tail < 4096) { struct TreeNode* node = queue[head++]; if (!node) { values[count++] = "null"; continue; } snprintf(numbers[count], sizeof(numbers[count]), "%d", node->val); values[count] = numbers[count]; count++; queue[tail++] = node->left; queue[tail++] = node->right; } while (count > 0 && strcmp(values[count - 1], "null") == 0) count--; fputc('[', out); for (int i = 0; i < count; i++) { if (i) fputc(',', out); fputs(values[i], out); } fputc(']', out); } static void json_random_list(FILE* out, struct Node* node) { struct Node* nodes[10000]; int count = 0; for (struct Node* cur = node; cur && count < 10000; cur = cur->next) { if (isOriginalRandomNode(cur)) { json_string(out, "Returned list must be a deep copy."); return; } nodes[count++] = cur; } fputc('[', out); for (int i = 0; i < count; i++) { if (i) fputc(',', out); int random = -1; for (int j = 0; j < count; j++) if (nodes[i]->random == nodes[j]) random = j; fprintf(out, "[%d,", nodes[i]->val); if (nodes[i]->random && random < 0) { json_string(out, "Returned random pointer must stay inside the copied list."); fputc(']', out); continue; } if (random < 0) fputs("null", out); else fprintf(out, "%d", random); fputc(']', out); } fputc(']', out); } static int compare_graph_neighbor_ids(const void* a, const void* b) { int left = *(const int*)a; int right = *(const int*)b; return (left > right) - (left < right); } -static void json_graph(FILE* out, struct Node* node) { if (!node) { fputs("[]", out); return; } struct Node* nodes[1024]; bool seen[1024] = {0}; int count = 0; nodes[count++] = node; for (int i = 0; i < count && count < 1024; i++) { struct Node* cur = nodes[i]; if (isOriginalGraphNode(cur)) { json_string(out, "Returned graph must be a deep copy."); return; } if (cur->val > 0 && cur->val < 1024) seen[cur->val] = true; for (int j = 0; j < cur->numNeighbors; j++) { struct Node* next = cur->neighbors[j]; if (!next) { json_string(out, "Returned graph has a NULL neighbor."); return; } if (next->val > 0 && next->val < 1024 && !seen[next->val]) { seen[next->val] = true; nodes[count++] = next; } } } fputc('[', out); for (int value = 1, printed = 0; value < 1024; value++) { struct Node* cur = NULL; for (int i = 0; i < count; i++) if (nodes[i]->val == value) cur = nodes[i]; if (!cur) continue; if (printed++) fputc(',', out); int degree = cur->numNeighbors; int* neighborIds = NULL; if (degree > 0) { neighborIds = malloc((size_t)degree * sizeof(*neighborIds)); if (!neighborIds) exit(EXIT_FAILURE); for (int j = 0; j < degree; j++) neighborIds[j] = cur->neighbors[j]->val; qsort(neighborIds, (size_t)degree, sizeof(*neighborIds), compare_graph_neighbor_ids); } json_int_array(out, neighborIds, degree); free(neighborIds); } fputc(']', out); } +static void json_graph(FILE* out, struct Node* node) { if (!node) { fputs("[]", out); return; } struct Node* nodes[1024]; struct Node* seen[1024] = {0}; int count = 0; nodes[count++] = node; for (int i = 0; i < count && count < 1024; i++) { struct Node* cur = nodes[i]; if (isOriginalGraphNode(cur)) { json_string(out, "Returned graph must be a deep copy."); return; } if (cur->val <= 0 || cur->val >= 1024) { json_string(out, "Returned graph node IDs must be between 1 and 1023."); return; } seen[cur->val] = cur; for (int j = 0; j < cur->numNeighbors; j++) { struct Node* next = cur->neighbors[j]; if (!next) { json_string(out, "Returned graph has a NULL neighbor."); return; } if (next->val <= 0 || next->val >= 1024) { json_string(out, "Returned graph node IDs must be between 1 and 1023."); return; } if (isOriginalGraphNode(next)) { json_string(out, "Returned graph must be a deep copy."); return; } if (seen[next->val] && seen[next->val] != next) { json_string(out, "Returned graph must not contain distinct nodes with the same ID."); return; } if (!seen[next->val]) { seen[next->val] = next; nodes[count++] = next; } } } fputc('[', out); for (int value = 1, printed = 0; value < 1024; value++) { struct Node* cur = NULL; for (int i = 0; i < count; i++) if (nodes[i]->val == value) cur = nodes[i]; if (!cur) continue; if (printed++) fputc(',', out); int degree = cur->numNeighbors; int* neighborIds = NULL; if (degree > 0) { neighborIds = malloc((size_t)degree * sizeof(*neighborIds)); if (!neighborIds) exit(EXIT_FAILURE); for (int j = 0; j < degree; j++) neighborIds[j] = cur->neighbors[j]->val; qsort(neighborIds, (size_t)degree, sizeof(*neighborIds), compare_graph_neighbor_ids); } json_int_array(out, neighborIds, degree); free(neighborIds); } fputc(']', out); } static void json_next_tree(FILE* out, struct Node* root) { if (!root) { fputs("[]", out); return; } struct Node* queue[4096]; int head = 0, tail = 0, firstLevel = 1; queue[tail++] = root; fputc('[', out); while (head < tail) { int end = tail; if (!firstLevel) fputc(',', out); firstLevel = 0; fputc('[', out); for (int i = head; i < end; i++) { if (i > head) fputc(',', out); fprintf(out, "%d", queue[i]->val); if (queue[i]->next != (i + 1 < end ? queue[i + 1] : NULL)) { fputs("]", out); fputs("]", out); return; } if (queue[i]->left) queue[tail++] = queue[i]->left; if (queue[i]->right) queue[tail++] = queue[i]->right; } fputc(']', out); head = end; } fputc(']', out); } static void json_quad_tree_visit(FILE* out, struct Node* node, int* printed) { if (!node) return; if ((*printed)++) fputc(',', out); fprintf(out, "[%d,%d]", node->isLeaf ? 1 : 0, node->isLeaf ? (node->val ? 1 : 0) : 1); if (!node->isLeaf) { json_quad_tree_visit(out, node->topLeft, printed); json_quad_tree_visit(out, node->topRight, printed); json_quad_tree_visit(out, node->bottomLeft, printed); json_quad_tree_visit(out, node->bottomRight, printed); } } static void json_quad_tree(FILE* out, struct Node* root) { int printed = 0; fputc('[', out); json_quad_tree_visit(out, root, &printed); fputc(']', out); } @@ -562,7 +562,7 @@ TreeNode* findTreeNode(TreeNode* root, int value) { queue q; if (root Node* randomList(vector>> items) { vector nodes; for (auto item : items) nodes.push_back(new Node(item.first)); originalRandomNodes = set(nodes.begin(), nodes.end()); for (size_t i = 0; i < nodes.size(); ++i) { nodes[i]->next = i + 1 < nodes.size() ? nodes[i + 1] : nullptr; if (items[i].second) nodes[i]->random = nodes[*items[i].second]; } return nodes.empty() ? nullptr : nodes[0]; } string randomListToJson(Node* node) { vector nodes; map index; set seen; for (auto* cur = node; cur && !seen.count(cur); cur = cur->next) { if (originalRandomNodes.count(cur)) throw runtime_error("Returned list must be a deep copy."); seen.insert(cur); index[cur] = nodes.size(); nodes.push_back(cur); } string out = "["; for (size_t i = 0; i < nodes.size(); ++i) { if (i) out += ","; if (nodes[i]->random && !index.count(nodes[i]->random)) throw runtime_error("Returned random pointer must stay inside the copied list."); out += "[" + to_string(nodes[i]->val) + "," + (nodes[i]->random ? to_string(index[nodes[i]->random]) : string("null")) + "]"; } return out + "]"; } Node* graphNode(vector> adjacency) { if (adjacency.empty()) { originalGraphNodes.clear(); return nullptr; } vector nodes; for (size_t i = 0; i < adjacency.size(); ++i) nodes.push_back(new Node(i + 1)); originalGraphNodes = set(nodes.begin(), nodes.end()); for (size_t i = 0; i < adjacency.size(); ++i) for (int value : adjacency[i]) nodes[i]->neighbors.push_back(nodes[value - 1]); return nodes[0]; } -string graphToJson(Node* node) { if (!node) return "[]"; map byValue; queue q; q.push(node); while (!q.empty()) { auto* cur = q.front(); q.pop(); if (!cur) continue; if (originalGraphNodes.count(cur)) throw runtime_error("Returned graph must be a deep copy."); if (byValue.count(cur->val)) continue; byValue[cur->val] = cur; for (auto* next : cur->neighbors) { if (!next) throw runtime_error("Returned graph has a NULL neighbor."); q.push(next); } } string out = "["; bool first = true; for (auto& [value, cur] : byValue) { if (!first) out += ","; first = false; vector neighbors; for (auto* next : cur->neighbors) neighbors.push_back(next->val); sort(neighbors.begin(), neighbors.end()); out += toJson(neighbors); } return out + "]"; } +string graphToJson(Node* node) { if (!node) return "[]"; map byValue; queue q; q.push(node); while (!q.empty()) { auto* cur = q.front(); q.pop(); if (!cur) continue; if (originalGraphNodes.count(cur)) throw runtime_error("Returned graph must be a deep copy."); if (byValue.count(cur->val)) { if (byValue.at(cur->val) != cur) throw runtime_error("Returned graph must not contain distinct nodes with the same ID."); continue; } byValue[cur->val] = cur; for (auto* next : cur->neighbors) { if (!next) throw runtime_error("Returned graph has a NULL neighbor."); q.push(next); } } string out = "["; bool first = true; for (auto& [value, cur] : byValue) { if (!first) out += ","; first = false; vector neighbors; for (auto* next : cur->neighbors) neighbors.push_back(next->val); sort(neighbors.begin(), neighbors.end()); out += toJson(neighbors); } return out + "]"; } Node* nextTree(vector> values) { if (values.empty() || !values[0]) return nullptr; auto* root = new Node(*values[0]); queue q; q.push(root); size_t i = 1; while (!q.empty() && i < values.size()) { auto* node = q.front(); q.pop(); if (i < values.size() && values[i]) { node->left = new Node(*values[i]); q.push(node->left); } ++i; if (i < values.size() && values[i]) { node->right = new Node(*values[i]); q.push(node->right); } ++i; } return root; } string nextTreeToJson(Node* root) { vector levels; queue q; if (root) q.push(root); while (!q.empty()) { size_t count = q.size(); vector level; for (size_t i = 0; i < count; ++i) { auto* node = q.front(); q.pop(); level.push_back(node); if (node->left) q.push(node->left); if (node->right) q.push(node->right); } vector row; for (size_t i = 0; i < level.size(); ++i) { if (level[i]->next != (i + 1 < level.size() ? level[i + 1] : nullptr)) throw runtime_error("next pointers do not match the tree level order."); row.push_back(level[i]->val); } levels.push_back(toJson(row)); } string out = "["; for (size_t i = 0; i < levels.size(); ++i) { if (i) out += ","; out += levels[i]; } return out + "]"; } string quadTreeToJson(Node* root) { vector rows; function visit = [&](Node* node) { if (!node) return; rows.push_back("[" + to_string(node->isLeaf ? 1 : 0) + "," + to_string(node->isLeaf ? (node->val ? 1 : 0) : 1) + "]"); if (!node->isLeaf) { visit(node->topLeft); visit(node->topRight); visit(node->bottomLeft); visit(node->bottomRight); } }; visit(root); string out = "["; for (size_t i = 0; i < rows.size(); ++i) { if (i) out += ","; out += rows[i]; } return out + "]"; }`; @@ -731,7 +731,7 @@ class Main { static Node randomList(int[][] items) { ArrayList nodes = new ArrayList<>(); for (int[] item : items) nodes.add(new Node(item[0])); originalRandomNodes = new HashSet<>(nodes); for (int i = 0; i < nodes.size(); i++) { nodes.get(i).next = i + 1 < nodes.size() ? nodes.get(i + 1) : null; if (items[i][1] >= 0) nodes.get(i).random = nodes.get(items[i][1]); } return nodes.isEmpty() ? null : nodes.get(0); } static String randomListJson(Node node) { ArrayList nodes = new ArrayList<>(); HashMap index = new HashMap<>(); HashSet seen = new HashSet<>(); for (Node cur = node; cur != null && !seen.contains(cur); cur = cur.next) { if (originalRandomNodes.contains(cur)) throw new IllegalArgumentException("Returned list must be a deep copy."); seen.add(cur); index.put(cur, nodes.size()); nodes.add(cur); } StringJoiner out = new StringJoiner(",", "[", "]"); for (Node cur : nodes) { if (cur.random != null && !index.containsKey(cur.random)) throw new IllegalArgumentException("Returned random pointer must stay inside the copied list."); out.add("[" + cur.val + "," + (cur.random == null ? "null" : index.get(cur.random)) + "]"); } return out.toString(); } static Node graphNode(int[][] adjacency) { if (adjacency.length == 0) { originalGraphNodes = new HashSet<>(); return null; } ArrayList nodes = new ArrayList<>(); for (int i = 0; i < adjacency.length; i++) nodes.add(new Node(i + 1)); originalGraphNodes = new HashSet<>(nodes); for (int i = 0; i < adjacency.length; i++) for (int value : adjacency[i]) nodes.get(i).neighbors.add(nodes.get(value - 1)); return nodes.get(0); } - static String graphJson(Node node) { if (node == null) return "[]"; TreeMap byValue = new TreeMap<>(); ArrayDeque q = new ArrayDeque<>(); q.add(node); while (!q.isEmpty()) { Node cur = q.remove(); if (cur == null) continue; if (originalGraphNodes.contains(cur)) throw new IllegalArgumentException("Returned graph must be a deep copy."); if (byValue.containsKey(cur.val)) continue; byValue.put(cur.val, cur); for (Node next : cur.neighbors) q.add(next); } StringJoiner out = new StringJoiner(",", "[", "]"); for (Node cur : byValue.values()) { ArrayList row = new ArrayList<>(); for (Node next : cur.neighbors) row.add(next.val); Collections.sort(row); out.add(json(row)); } return out.toString(); } + static String graphJson(Node node) { if (node == null) return "[]"; TreeMap byValue = new TreeMap<>(); ArrayDeque q = new ArrayDeque<>(); q.add(node); while (!q.isEmpty()) { Node cur = q.remove(); if (cur == null) continue; if (originalGraphNodes.contains(cur)) throw new IllegalArgumentException("Returned graph must be a deep copy."); if (byValue.containsKey(cur.val)) { if (byValue.get(cur.val) != cur) throw new IllegalArgumentException("Returned graph must not contain distinct nodes with the same ID."); continue; } byValue.put(cur.val, cur); for (Node next : cur.neighbors) q.add(next); } StringJoiner out = new StringJoiner(",", "[", "]"); for (Node cur : byValue.values()) { ArrayList row = new ArrayList<>(); for (Node next : cur.neighbors) row.add(next.val); Collections.sort(row); out.add(json(row)); } return out.toString(); } static Node nextTree(Integer[] values) { if (values.length == 0 || values[0] == null) return null; Node root = new Node(values[0]); ArrayDeque q = new ArrayDeque<>(); q.add(root); int i = 1; while (!q.isEmpty() && i < values.length) { Node node = q.remove(); if (i < values.length && values[i] != null) { node.left = new Node(values[i]); q.add(node.left); } i++; if (i < values.length && values[i] != null) { node.right = new Node(values[i]); q.add(node.right); } i++; } return root; } static String nextTreeJson(Node root) { ArrayList levels = new ArrayList<>(); ArrayDeque q = new ArrayDeque<>(); if (root != null) q.add(root); while (!q.isEmpty()) { int count = q.size(); ArrayList level = new ArrayList<>(); for (int i = 0; i < count; i++) { Node node = q.remove(); level.add(node); if (node.left != null) q.add(node.left); if (node.right != null) q.add(node.right); } ArrayList row = new ArrayList<>(); for (int i = 0; i < level.size(); i++) { if (level.get(i).next != (i + 1 < level.size() ? level.get(i + 1) : null)) throw new IllegalArgumentException("next pointers do not match the tree level order."); row.add(level.get(i).val); } levels.add(json(row)); } return "[" + String.join(",", levels) + "]"; } static String quadTreeJson(Node root) { ArrayList rows = new ArrayList<>(); quadTreeVisit(root, rows); return "[" + String.join(",", rows) + "]"; } diff --git a/web/runners.js b/web/runners.js index 8bd938499..7bbba3193 100644 --- a/web/runners.js +++ b/web/runners.js @@ -706,7 +706,10 @@ const JS_RUNNER_SOURCE = ` for (const current of queue) { if (!current) continue; if (originalGraphNodes.has(current)) throw new TypeError("Returned graph must be a deep copy."); - if (byValue.has(current.val)) continue; + if (byValue.has(current.val)) { + if (byValue.get(current.val) !== current) throw new TypeError("Returned graph must not contain distinct nodes with the same ID."); + continue; + } byValue.set(current.val, current); for (const neighbor of current.neighbors || []) queue.push(neighbor); } @@ -1033,6 +1036,8 @@ def _graph_to_adjacency(node): if id(current) in _original_graph_nodes: raise TypeError("Returned graph must be a deep copy.") if current.val in by_value: + if by_value[current.val] is not current: + raise TypeError("Returned graph must not contain distinct nodes with the same ID.") continue by_value[current.val] = current queue.extend(getattr(current, "neighbors", []) or [])