diff --git a/app/(protected)/admin/contests/actions.ts b/app/(protected)/admin/contests/actions.ts index f8e9e73..52be354 100644 --- a/app/(protected)/admin/contests/actions.ts +++ b/app/(protected)/admin/contests/actions.ts @@ -219,6 +219,8 @@ export async function finalizeContest(formData: FormData) { userId: true, contestProblemId: true, verdict: true, + passedCount: true, + totalCount: true, createdAt: true, }, }, @@ -274,6 +276,7 @@ export async function finalizeContest(formData: FormData) { userId, rank: standing?.rank ?? participantIds.size, solvedCount: standing?.solvedCount ?? 0, + score: standing?.score ?? 0, penalty: standing?.penalty ?? 0, ratingBefore, ratingAfter: change.newRating, diff --git a/app/(protected)/admin/problems/actions.ts b/app/(protected)/admin/problems/actions.ts index cf6dc39..f15111c 100644 --- a/app/(protected)/admin/problems/actions.ts +++ b/app/(protected)/admin/problems/actions.ts @@ -40,7 +40,10 @@ export async function runReferenceSolution(slug: string, language?: string): Pro select: { language: true, code: true }, }, timeLimitMs: true, - testCases: { orderBy: { order: "asc" }, select: { input: true, expectedOutput: true } }, + testCases: { + orderBy: { order: "asc" }, + select: { input: true, expectedOutput: true }, + }, }, }); diff --git a/app/contests/[slug]/page.tsx b/app/contests/[slug]/page.tsx index dd8e0e5..17a1eb6 100644 --- a/app/contests/[slug]/page.tsx +++ b/app/contests/[slug]/page.tsx @@ -36,6 +36,8 @@ export default async function ContestDetailPage({ userId: true, contestProblemId: true, verdict: true, + passedCount: true, + totalCount: true, createdAt: true, }, }, @@ -192,8 +194,11 @@ export default async function ContestDetailPage({

Standings

- Solved · Time taken + Points · Solved · Time taken
+

+ Each problem is worth 100 points. Only your best submission per problem counts. +

{standings.length > 0 ? (
{standings.map((entry) => ( @@ -201,13 +206,14 @@ export default async function ContestDetailPage({ #{entry.rank} {entry.name} - {entry.solvedCount} · {entry.penalty} min + {entry.score}/{contest.problems.length * 100} · {entry.solvedCount} solved ·{" "} + {entry.penalty} min
))} ) : ( -
No accepted submissions yet.
+
No scored submissions yet.
)}
diff --git a/components/practice/problem-workspace.tsx b/components/practice/problem-workspace.tsx index 2371991..bb6099a 100644 --- a/components/practice/problem-workspace.tsx +++ b/components/practice/problem-workspace.tsx @@ -63,6 +63,11 @@ const starterCode: Record = { }; function SubmissionMeta({ submission }: Readonly<{ submission: Submission }>) { + const score = + submission.totalCount > 0 + ? Math.round((submission.passedCount * 100) / submission.totalCount) + : 0; + return ( <> @@ -72,6 +77,7 @@ function SubmissionMeta({ submission }: Readonly<{ submission: Submission }>) { {submission.passedCount}/{submission.totalCount} tests + {submission.totalCount > 0 ? {score}/100 points : null} {submission.runtimeMs ?? 0}ms {submission.note ? {submission.note} : null} diff --git a/lib/contest.ts b/lib/contest.ts index 9a48e9c..1f8f8da 100644 --- a/lib/contest.ts +++ b/lib/contest.ts @@ -59,14 +59,17 @@ export type ContestSubmissionRow = { userId: string; contestProblemId: string; verdict: SubmissionVerdict; + passedCount: number; + totalCount: number; createdAt: Date; }; export type StandingRow = { userId: string; + score: number; solvedCount: number; penalty: number; - lastAcAt: Date | null; + lastScoredAt: Date | null; rank: number; }; @@ -192,6 +195,12 @@ function minutesFromStart(startsAt: Date, at: Date) { return Math.max(0, Math.floor((at.getTime() - startsAt.getTime()) / 60_000)); } +function submissionScore(submission: ContestSubmissionRow) { + return submission.totalCount > 0 + ? Math.round((submission.passedCount * 100) / submission.totalCount) + : 0; +} + export function computeStandings( submissions: ContestSubmissionRow[], startsAt: Date, @@ -206,36 +215,54 @@ export function computeStandings( byUserProblem.set(key, bucket); } - const stats = new Map(); + const stats = new Map< + string, + { score: number; solvedCount: number; penalty: number; lastScoredAt: Date | null } + >(); for (const [key, attempts] of Array.from(byUserProblem.entries())) { const userId = key.split(":")[0]!; const ordered = [...attempts].sort( (left, right) => left.createdAt.getTime() - right.createdAt.getTime(), ); - const acceptedIndex = ordered.findIndex( - (attempt) => attempt.verdict === SubmissionVerdict.ACCEPTED, - ); + let bestIndex = -1; + let bestScore = 0; + for (let index = 0; index < ordered.length; index += 1) { + const attempt = ordered[index]!; + const score = submissionScore(attempt); + if (score > bestScore) { + bestScore = score; + bestIndex = index; + } + } - if (acceptedIndex < 0) { + if (bestIndex < 0) { continue; } - const accepted = ordered[acceptedIndex]!; + const bestAttempt = ordered[bestIndex]!; const wrongBefore = ordered - .slice(0, acceptedIndex) + .slice(0, bestIndex) .filter((attempt) => attempt.verdict !== SubmissionVerdict.PENDING).length; const problemPenalty = - minutesFromStart(startTimesByUser.get(userId) ?? startsAt, accepted.createdAt) + + minutesFromStart(startTimesByUser.get(userId) ?? startsAt, bestAttempt.createdAt) + wrongBefore * CONTEST_WRONG_PENALTY_MINUTES; - const current = stats.get(userId) ?? { solvedCount: 0, penalty: 0, lastAcAt: null }; - current.solvedCount += 1; + const current = stats.get(userId) ?? { + score: 0, + solvedCount: 0, + penalty: 0, + lastScoredAt: null, + }; + current.score += bestScore; + if (bestAttempt.verdict === SubmissionVerdict.ACCEPTED) { + current.solvedCount += 1; + } current.penalty += problemPenalty; - current.lastAcAt = - !current.lastAcAt || accepted.createdAt > current.lastAcAt - ? accepted.createdAt - : current.lastAcAt; + current.lastScoredAt = + !current.lastScoredAt || bestAttempt.createdAt > current.lastScoredAt + ? bestAttempt.createdAt + : current.lastScoredAt; stats.set(userId, current); } @@ -243,16 +270,18 @@ export function computeStandings( .map(([userId, row]) => ({ userId, ...row })) .sort( (left, right) => + right.score - left.score || right.solvedCount - left.solvedCount || left.penalty - right.penalty || - (left.lastAcAt?.getTime() ?? 0) - (right.lastAcAt?.getTime() ?? 0), + (left.lastScoredAt?.getTime() ?? 0) - (right.lastScoredAt?.getTime() ?? 0), ); return ranked.map((row, index) => ({ userId: row.userId, + score: row.score, solvedCount: row.solvedCount, penalty: row.penalty, - lastAcAt: row.lastAcAt, + lastScoredAt: row.lastScoredAt, rank: index + 1, })); } diff --git a/lib/judge/core.ts b/lib/judge/core.ts index cf949ff..0d5277d 100644 --- a/lib/judge/core.ts +++ b/lib/judge/core.ts @@ -56,6 +56,8 @@ export async function judgeSubmission({ }): Promise { let passedCount = 0; let runtimeMs = 0; + let verdict: SubmissionVerdict = SubmissionVerdict.ACCEPTED; + let failureMessage: string | null = null; for (const testCase of testCases) { const result = await executor({ language, code, stdin: testCase.input, timeLimitMs }); @@ -75,40 +77,33 @@ export async function judgeSubmission({ } if (result.timedOut || result.signal === "SIGKILL" || result.signal === "SIGTERM") { - return { - verdict: SubmissionVerdict.TLE, - passedCount, - totalCount: testCases.length, - runtimeMs, - }; + verdict = SubmissionVerdict.TLE; + continue; } if (result.exitCode !== 0) { - return { - verdict: SubmissionVerdict.RUNTIME_ERROR, - passedCount, - totalCount: testCases.length, - runtimeMs, - failureMessage: runtimeFailureMessage(result, testCase.isSample), - }; + if (verdict !== SubmissionVerdict.TLE) { + verdict = SubmissionVerdict.RUNTIME_ERROR; + } + failureMessage ??= runtimeFailureMessage(result, testCase.isSample); + continue; } if (normalizeOutput(result.stdout) !== normalizeOutput(testCase.expectedOutput)) { - return { - verdict: SubmissionVerdict.WRONG_ANSWER, - passedCount, - totalCount: testCases.length, - runtimeMs, - }; + if (verdict === SubmissionVerdict.ACCEPTED) { + verdict = SubmissionVerdict.WRONG_ANSWER; + } + continue; } passedCount += 1; } return { - verdict: SubmissionVerdict.ACCEPTED, + verdict, passedCount, totalCount: testCases.length, runtimeMs, + failureMessage, }; } diff --git a/prisma/hireup-problem-set.js b/prisma/hireup-problem-set.js index 859eee3..ba32bc7 100644 --- a/prisma/hireup-problem-set.js +++ b/prisma/hireup-problem-set.js @@ -1,12 +1,7 @@ // Problems for the "HireUp Online Assessment" — the first round of the HireUp -// mock-hiring event. Original problems written in the style of recent (last ~2 -// years) Uber / Amazon / Google online assessments, customized so they are not -// verbatim copies of any company's proprietary question. -// -// Both are HARD and target the tier top companies actually test: A is a 2D grid -// DP carrying an extra "skip budget" state; B is a shortest-path problem solved -// with Dijkstra over an augmented (node, upgrades-used) state. Seeded unpublished -// so they only appear inside the OA contest. +// mock-hiring event. The pair escalates from a prefix-imbalance observation to +// weighted tree rerooting, giving candidates an approachable entry point and a +// stronger second problem within the assessment window. // // Reference solutions live in scripts/reference-solutions/.{py,cpp} and are // attached by prisma/seed.mjs. Stress-test outputs below are closed-form and @@ -14,79 +9,66 @@ const HIREUP_OA_SLUG = "hireup-oa"; -function onesGrid(rows, cols, k) { - const row = `${new Array(cols).fill(1).join(" ")}\n`; - return `${rows} ${cols} ${k}\n${row.repeat(rows)}`; +function repeatValues(count, value) { + return `${new Array(count).fill(value).join(" ")}\n`; } -// Closed form for an all-ones R x C grid: a monotone path visits R + C - 1 cells; -// the start and end must be paid, so at most (R + C - 3) interior cells can be -// skipped. Cost = (cells) - min(k, interior). -function onesGridAnswer(rows, cols, k) { - const cells = rows + cols - 1; - const interior = Math.max(0, cells - 2); - return cells - Math.min(k, interior); +function sequenceLine(count, mapper) { + return `${Array.from({ length: count }, (_, index) => mapper(index)).join(" ")}\n`; } -function chainGraph(nodes, edges, k, weight) { - const lines = [`${nodes} ${edges} ${k}`]; - for (let i = 1; i <= edges; i += 1) { - lines.push(`${i} ${i + 1} ${weight}`); +function weightedChain(nodes, weight) { + const lines = [String(nodes)]; + for (let node = 1; node < nodes; node += 1) { + lines.push(`${node - 1} ${node} ${weight}`); } return `${lines.join("\n")}\n`; } -// --- Problem A: Grid Delivery With Skips -------------------------------------- -// Amazon/Google-flavored grid DP. The classic "minimum path sum" becomes a 3D DP -// once you can waive the cost of up to K cells: dp[i][j][k] = best cost to reach -// (i, j) having used k waivers, transitioning from the top/left and optionally -// skipping the current (non-corner) cell. -const gridDeliverySkips = { - slug: "grid-delivery-skips", - title: "Grid Delivery With Skips", +// --- Problem A: Driver Rebalancing Across City Corridors --------------------- +const driverRebalancing = { + slug: "driver-rebalancing", + title: "Driver Rebalancing Across City Corridors", statement: - "A delivery robot must cross an R x C grid of positive tolls. It starts at the top-left cell (0, 0), ends at the bottom-right cell (R-1, C-1), and may move only one cell right or one cell down at a time. Normally it pays the toll printed on every cell it visits.\n\nThe robot carries K skip passes. Using a pass on a cell waives that cell's toll entirely. It may use at most K passes over the whole trip, at most one per cell, but it can never skip the start cell (0, 0) or the end cell (R-1, C-1) — those tolls are always paid.\n\nPrint the minimum total toll the robot must pay to get from the start to the end.\n\nInput format:\n- First line: R C K\n- Next R lines: C integers each, the grid tolls row by row\n\nPrint a single integer. The answer can exceed a 32-bit integer.", - constraints: "1 <= R, C <= 200\n0 <= K <= 30\n1 <= toll[i][j] <= 10^9", - tags: ["Dynamic Programming", "Matrix", "Grid", "Amazon", "Google", "HireUp"], - difficulty: "HARD", - timeLimitMs: 4000, + "A city has N zones arranged in a straight line and numbered 1 through N. Zone i currently has current[i] available drivers, while target[i] is the desired number of drivers in that zone.\n\nIn one move, you may move one driver from a zone to either adjacent zone. Moving a driver across one zone boundary costs 1. A driver may cross several boundaries through several moves.\n\nThe total number of current drivers equals the total target, so it is always possible to reach the target distribution. Find the minimum total movement cost.\n\nInput format:\n- First line: N\n- Second line: N integers current[1], current[2], ..., current[N]\n- Third line: N integers target[1], target[2], ..., target[N]\n\nPrint a single integer: the minimum total movement cost. The answer can exceed a 32-bit integer.", + constraints: + "1 <= N <= 2 * 10^5\n0 <= current[i], target[i] <= 10^9\nsum(current) = sum(target) <= 10^13", + tags: ["Array", "Prefix Sum", "Greedy", "Uber", "HireUp"], + difficulty: "MEDIUM", + timeLimitMs: 2000, samples: [ - { input: "2 2 1\n1 2\n3 4\n", expectedOutput: "5\n" }, - { input: "2 2 0\n1 2\n3 4\n", expectedOutput: "7\n" }, + { input: "4\n0 3 0 2\n1 1 2 1\n", expectedOutput: "3\n" }, + { input: "5\n10 0 0 0 0\n0 0 0 0 10\n", expectedOutput: "40\n" }, ], hidden: [ - { input: "1 1 0\n5\n", expectedOutput: "5\n" }, - { input: "3 3 0\n1 1 1\n1 1 1\n1 1 1\n", expectedOutput: "5\n" }, - { input: "3 3 2\n1 1 1\n1 1 1\n1 1 1\n", expectedOutput: "3\n" }, - { input: "2 3 1\n1 5 1\n1 5 9\n", expectedOutput: "11\n" }, + { input: "1\n7\n7\n", expectedOutput: "0\n" }, + { input: "5\n1 2 3 4 5\n1 2 3 4 5\n", expectedOutput: "0\n" }, + { input: "6\n5 0 4 0 0 3\n0 3 0 4 2 3\n", expectedOutput: "15\n" }, + { input: "3\n0 0 1000000000\n1000000000 0 0\n", expectedOutput: "2000000000\n" }, ], }; -// --- Problem B: Network Upgrade Routing -------------------------------------- -// Google/Uber-flavored shortest path. From node 1 to node n in a directed -// weighted graph, you may zero out the weight of at most K edges. Dijkstra over -// (node, upgrades-used) states finds the cheapest route; report -1 if node n is -// unreachable. -const networkUpgradeRouting = { - slug: "network-upgrade-routing", - title: "Network Upgrade Routing", +// --- Problem B: Best Dispatch Hub --------------------------------------------- +const bestDispatchHub = { + slug: "best-dispatch-hub", + title: "Best Dispatch Hub", statement: - "A backbone network has n routers numbered 1..n and m one-way links. Link i sends traffic from router u to router v with latency w. You must route a stream from router 1 to router n along a sequence of links, paying the sum of the latencies you use.\n\nYou hold K upgrade credits. Spending a credit on a link makes its latency 0 for this route. You may spend at most K credits in total, at most one per link.\n\nPrint the minimum total latency of a route from router 1 to router n, or -1 if no route exists.\n\nInput format:\n- First line: n m K\n- Next m lines: u v w describing a directed link u -> v with latency w\n\nPrint a single integer. The answer can exceed a 32-bit integer.", - constraints: "1 <= n <= 10^5\n0 <= m <= 2*10^5\n0 <= K <= 10\n1 <= u, v <= n\n1 <= w <= 10^9", - tags: ["Graph", "Shortest Path", "Dijkstra", "Uber", "Google", "HireUp"], + "Uber operates in a city whose road network forms a tree. There are N intersections numbered 0 through N-1 and exactly N-1 bidirectional roads. Every intersection is reachable from every other intersection.\n\nEach road connects intersections u and v and has a positive travel time w. If a driver dispatch hub is placed at intersection r, its dispatch cost is the sum of the shortest travel times from r to all N intersections, including a travel time of 0 from r to itself.\n\nFor every possible hub location r, compute its dispatch cost.\n\nInput format:\n- First line: N\n- Next N-1 lines: three integers u, v, and w describing a bidirectional road\n\nPrint N space-separated integers. The value at index r must be the dispatch cost when the hub is placed at intersection r. Use 64-bit arithmetic.", + constraints: "1 <= N <= 5 * 10^4\n0 <= u, v < N\n1 <= w <= 10^6", + tags: ["Tree", "Dynamic Programming", "Rerooting", "DFS", "Uber", "HireUp"], difficulty: "HARD", - timeLimitMs: 4000, + timeLimitMs: 3000, samples: [ - { input: "4 4 1\n1 2 5\n2 4 5\n1 3 1\n3 4 1\n", expectedOutput: "1\n" }, - { input: "4 4 0\n1 2 5\n2 4 5\n1 3 1\n3 4 1\n", expectedOutput: "2\n" }, + { input: "4\n0 1 1\n0 2 1\n2 3 1\n", expectedOutput: "4 6 4 6\n" }, + { input: "2\n0 1 7\n", expectedOutput: "7 7\n" }, ], hidden: [ - { input: "3 1 2\n1 2 4\n", expectedOutput: "-1\n" }, - { input: "1 0 0\n", expectedOutput: "0\n" }, - { input: "3 2 2\n1 2 7\n2 3 9\n", expectedOutput: "0\n" }, + { input: "1\n", expectedOutput: "0\n" }, + { input: "3\n0 1 2\n1 2 3\n", expectedOutput: "7 5 8\n" }, + { input: "5\n0 1 1\n0 2 2\n0 3 3\n0 4 4\n", expectedOutput: "10 13 16 19 22\n" }, { - input: "5 5 1\n1 2 10\n2 5 10\n1 3 3\n3 4 3\n4 5 3\n", - expectedOutput: "6\n", + input: "4\n0 1 1000000\n1 2 1000000\n2 3 1000000\n", + expectedOutput: "6000000 4000000 4000000 6000000\n", }, ], }; @@ -94,45 +76,58 @@ const networkUpgradeRouting = { // Efficiency / stress tests. Inputs are large; expected outputs are closed-form // and independently verified against the reference solutions. function buildHireupStressTests() { - // A: all-ones grids at the size limit exercise the O(R * C * K) DP. - const aFull = { - input: onesGrid(200, 200, 0), - expectedOutput: `${onesGridAnswer(200, 200, 0)}\n`, + const bigN = 200_000; + // Best Dispatch Hub prints one cost per intersection, so its answer grows with + // N. Cap it well under the judge's 2 MB output limit (lib/judge/piston.ts) — + // otherwise a correct solution would have its output truncated and be marked + // wrong. At 50k nodes an O(N^2) solution still times out comfortably. + const treeN = 50_000; + + // A: moving one large group from the first zone to the last makes every driver + // cross every boundary. + const aLongTransfer = { + input: `${bigN}\n100000000 ${repeatValues(bigN - 1, 0)}${repeatValues(bigN - 1, 0).trim()} 100000000\n`, + expectedOutput: `${(bigN - 1) * 100_000_000}\n`, }; - const aFullSkips = { - input: onesGrid(200, 200, 30), - expectedOutput: `${onesGridAnswer(200, 200, 30)}\n`, + + // A: each adjacent pair starts with its driver in the left zone and needs it + // in the right zone, so exactly half the boundaries carry one driver. + const aAlternating = { + input: `${bigN}\n${sequenceLine(bigN, (index) => (index % 2 === 0 ? 1 : 0))}${sequenceLine(bigN, (index) => (index % 2 === 0 ? 0 : 1))}`, + expectedOutput: `${bigN / 2}\n`, }; - const aMidSkips = { - input: onesGrid(150, 150, 30), - expectedOutput: `${onesGridAnswer(150, 150, 30)}\n`, + + const aAlreadyBalanced = { + input: `${bigN}\n${repeatValues(bigN, 1000000)}${repeatValues(bigN, 1000000)}`, + expectedOutput: "0\n", }; - // B: long chains 1 -> 2 -> ... force an efficient Dijkstra over the augmented - // state; a chain of e edges with weight w and k free edges costs (e - k) * w. - const bigNodes = 50_000; - const weight = 1_000_000_000; + // B: a long chain forces linear-time traversal and reroot propagation. For a + // node r, the sum is 1 + ... + r plus 1 + ... + (N-1-r). const bChain = { - input: chainGraph(bigNodes, bigNodes - 1, 0, weight), - expectedOutput: `${(bigNodes - 1) * weight}\n`, - }; - const bChainFree = { - input: chainGraph(bigNodes, bigNodes - 1, 10, weight), - expectedOutput: `${(bigNodes - 1 - 10) * weight}\n`, + input: weightedChain(treeN, 1), + expectedOutput: sequenceLine(treeN, (root) => { + const left = (root * (root + 1)) / 2; + const rightNodes = treeN - 1 - root; + const right = (rightNodes * (rightNodes + 1)) / 2; + return left + right; + }), }; - // Chain that stops one short of node n -> node n is unreachable -> -1. - const bUnreachable = { - input: chainGraph(bigNodes, bigNodes - 2, 10, weight), - expectedOutput: "-1\n", + + // B: in a unit-weight star the center costs N-1 and every leaf costs 2N-3. + const starEdges = Array.from({ length: treeN - 1 }, (_, index) => `0 ${index + 1} 1`).join("\n"); + const bStar = { + input: `${treeN}\n${starEdges}\n`, + expectedOutput: `${treeN - 1} ${repeatValues(treeN - 1, 2 * treeN - 3)}`, }; return { - "grid-delivery-skips": [aFull, aFullSkips, aMidSkips], - "network-upgrade-routing": [bChain, bChainFree, bUnreachable], + "driver-rebalancing": [aLongTransfer, aAlternating, aAlreadyBalanced], + "best-dispatch-hub": [bChain, bStar], }; } -const hireupProblems = [gridDeliverySkips, networkUpgradeRouting]; +const hireupProblems = [driverRebalancing, bestDispatchHub]; module.exports = { HIREUP_OA_SLUG, diff --git a/prisma/migrations/20260801090000_add_contest_partial_credit/migration.sql b/prisma/migrations/20260801090000_add_contest_partial_credit/migration.sql new file mode 100644 index 0000000..e6ab56a --- /dev/null +++ b/prisma/migrations/20260801090000_add_contest_partial_credit/migration.sql @@ -0,0 +1,20 @@ +-- Weighted test cases and partial-credit contest standings. +ALTER TABLE "TestCase" ADD COLUMN "points" INTEGER NOT NULL DEFAULT 1; + +ALTER TABLE "Submission" +ADD COLUMN "earnedPoints" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "possiblePoints" INTEGER NOT NULL DEFAULT 0; + +UPDATE "Submission" +SET "earnedPoints" = "passedCount", "possiblePoints" = "totalCount"; + +ALTER TABLE "ContestSubmission" +ADD COLUMN "earnedPoints" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "possiblePoints" INTEGER NOT NULL DEFAULT 0; + +UPDATE "ContestSubmission" +SET "earnedPoints" = "passedCount", "possiblePoints" = "totalCount"; + +ALTER TABLE "ContestParticipant" ADD COLUMN "score" INTEGER NOT NULL DEFAULT 0; + +UPDATE "ContestParticipant" SET "score" = "solvedCount" * 100; \ No newline at end of file diff --git a/prisma/migrations/20260801120000_simplify_contest_partial_credit/migration.sql b/prisma/migrations/20260801120000_simplify_contest_partial_credit/migration.sql new file mode 100644 index 0000000..a9c40ef --- /dev/null +++ b/prisma/migrations/20260801120000_simplify_contest_partial_credit/migration.sql @@ -0,0 +1,10 @@ +-- All contests use equal credit per test case, so only pass/total counts are needed. +ALTER TABLE "TestCase" DROP COLUMN "points"; + +ALTER TABLE "Submission" +DROP COLUMN "earnedPoints", +DROP COLUMN "possiblePoints"; + +ALTER TABLE "ContestSubmission" +DROP COLUMN "earnedPoints", +DROP COLUMN "possiblePoints"; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c920b32..8b159a3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -565,6 +565,7 @@ model ContestParticipant { userId String rank Int solvedCount Int + score Int @default(0) penalty Int ratingBefore Int ratingAfter Int diff --git a/prisma/seed.mjs b/prisma/seed.mjs index b5feb02..a812afd 100644 --- a/prisma/seed.mjs +++ b/prisma/seed.mjs @@ -1105,7 +1105,7 @@ async function main() { hireupProblemIds.push(savedProblem.id); } - // 1 August 8:00 PM IST (14:30 UTC), one-hour window, 60-minute personal timer. + // 1 August 8:00 PM IST (14:30 UTC), 90-minute window and personal timer. // Seeded PUBLISHED directly (never through the admin publish action) so it is // surfaced only inside the HireUp hub and is not mirrored onto the Events tab. // `status` is owned by the app after seeding, so it is only set on create. @@ -1114,19 +1114,19 @@ async function main() { update: { title: "HireUp Online Assessment", description: - "Online assessment for the HireUp mock hiring. Solve the problems within your 60-minute timer.", + "Online assessment for the HireUp mock hiring. Solve the problems within your 90-minute timer.", startsAt: new Date("2026-08-01T14:30:00.000Z"), - endsAt: new Date("2026-08-01T15:30:00.000Z"), - durationMinutes: 60, + endsAt: new Date("2026-08-01T16:00:00.000Z"), + durationMinutes: 90, }, create: { slug: HIREUP_OA_SLUG, title: "HireUp Online Assessment", description: - "Online assessment for the HireUp mock hiring. Solve the problems within your 60-minute timer.", + "Online assessment for the HireUp mock hiring. Solve the problems within your 90-minute timer.", startsAt: new Date("2026-08-01T14:30:00.000Z"), - endsAt: new Date("2026-08-01T15:30:00.000Z"), - durationMinutes: 60, + endsAt: new Date("2026-08-01T16:00:00.000Z"), + durationMinutes: 90, status: "PUBLISHED", }, select: { id: true }, diff --git a/scripts/reference-solutions/best-dispatch-hub.cpp b/scripts/reference-solutions/best-dispatch-hub.cpp new file mode 100644 index 0000000..2d9ca03 --- /dev/null +++ b/scripts/reference-solutions/best-dispatch-hub.cpp @@ -0,0 +1,59 @@ +#include +#include +#include +using namespace std; + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + int intersections; + cin >> intersections; + vector>> graph(intersections); + for (int edge = 0; edge + 1 < intersections; ++edge) { + int first, second; + long long weight; + cin >> first >> second >> weight; + graph[first].push_back({second, weight}); + graph[second].push_back({first, weight}); + } + + vector parent(intersections, -1); + vector subtreeSize(intersections, 1); + vector parentWeight(intersections, 0); + vector distance(intersections, 0); + vector order = {0}; + for (size_t index = 0; index < order.size(); ++index) { + int node = order[index]; + for (auto [neighbor, weight] : graph[node]) { + if (neighbor == parent[node]) continue; + parent[neighbor] = node; + parentWeight[neighbor] = weight; + distance[neighbor] = distance[node] + weight; + order.push_back(neighbor); + } + } + + long long rootCost = 0; + for (long long value : distance) rootCost += value; + for (int index = intersections - 1; index > 0; --index) { + int node = order[index]; + subtreeSize[parent[node]] += subtreeSize[node]; + } + + vector cost(intersections, 0); + cost[0] = rootCost; + for (size_t index = 1; index < order.size(); ++index) { + int node = order[index]; + long long outside = intersections - subtreeSize[node]; + cost[node] = + cost[parent[node]] + (outside - subtreeSize[node]) * parentWeight[node]; + } + + for (int node = 0; node < intersections; ++node) { + if (node > 0) cout << ' '; + cout << cost[node]; + } + cout << '\n'; + return 0; +} \ No newline at end of file diff --git a/scripts/reference-solutions/best-dispatch-hub.py b/scripts/reference-solutions/best-dispatch-hub.py new file mode 100644 index 0000000..f21e78e --- /dev/null +++ b/scripts/reference-solutions/best-dispatch-hub.py @@ -0,0 +1,39 @@ +import sys + + +def main(): + data = list(map(int, sys.stdin.buffer.read().split())) + intersections = data[0] + graph = [[] for _ in range(intersections)] + for index in range(1, len(data), 3): + first, second, weight = data[index : index + 3] + graph[first].append((second, weight)) + graph[second].append((first, weight)) + + parent = [-1] * intersections + parent_weight = [0] * intersections + distance = [0] * intersections + order = [0] + for node in order: + for neighbor, weight in graph[node]: + if neighbor == parent[node]: + continue + parent[neighbor] = node + parent_weight[neighbor] = weight + distance[neighbor] = distance[node] + weight + order.append(neighbor) + + subtree_size = [1] * intersections + for node in reversed(order[1:]): + subtree_size[parent[node]] += subtree_size[node] + + cost = [0] * intersections + cost[0] = sum(distance) + for node in order[1:]: + outside = intersections - subtree_size[node] + cost[node] = cost[parent[node]] + (outside - subtree_size[node]) * parent_weight[node] + + print(*cost) + + +main() \ No newline at end of file diff --git a/scripts/reference-solutions/driver-rebalancing.cpp b/scripts/reference-solutions/driver-rebalancing.cpp new file mode 100644 index 0000000..6667dbd --- /dev/null +++ b/scripts/reference-solutions/driver-rebalancing.cpp @@ -0,0 +1,26 @@ +#include +#include +#include +using namespace std; + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + int zones; + cin >> zones; + vector current(zones); + vector target(zones); + for (long long& drivers : current) cin >> drivers; + for (long long& drivers : target) cin >> drivers; + + long long imbalance = 0; + long long cost = 0; + for (int index = 0; index + 1 < zones; ++index) { + imbalance += current[index] - target[index]; + cost += llabs(imbalance); + } + + cout << cost << '\n'; + return 0; +} \ No newline at end of file diff --git a/scripts/reference-solutions/driver-rebalancing.py b/scripts/reference-solutions/driver-rebalancing.py new file mode 100644 index 0000000..1675707 --- /dev/null +++ b/scripts/reference-solutions/driver-rebalancing.py @@ -0,0 +1,19 @@ +import sys + + +def main(): + data = list(map(int, sys.stdin.buffer.read().split())) + zones = data[0] + current = data[1 : zones + 1] + target = data[zones + 1 : 2 * zones + 1] + + imbalance = 0 + cost = 0 + for index in range(zones - 1): + imbalance += current[index] - target[index] + cost += abs(imbalance) + + print(cost) + + +main() \ No newline at end of file diff --git a/scripts/simulate-contest-ratings.ts b/scripts/simulate-contest-ratings.ts index 342ce4d..76d01aa 100644 --- a/scripts/simulate-contest-ratings.ts +++ b/scripts/simulate-contest-ratings.ts @@ -36,7 +36,15 @@ function sub( verdict: SubmissionVerdict, minutes: number, ): ContestSubmissionRow { - return { userId, contestProblemId, verdict, createdAt: at(minutes) }; + const accepted = verdict === SubmissionVerdict.ACCEPTED; + return { + userId, + contestProblemId, + verdict, + passedCount: accepted ? 1 : 0, + totalCount: 1, + createdAt: at(minutes), + }; } /** Scripted contest: Alice solves both (with a WA on B), Bob solves A fast, Cara solves B after WA, Dan DNF. */ diff --git a/scripts/validate-problems.mjs b/scripts/validate-problems.mjs index 39d5767..6b03c17 100644 --- a/scripts/validate-problems.mjs +++ b/scripts/validate-problems.mjs @@ -131,6 +131,10 @@ try { input: test.input, encoding: "utf8", timeout: Math.max(5000, problem.timeLimitMs + 3000), + // Node defaults to 1 MB, which silently truncates large answers and + // reports them as mismatches. Stay above the judge's own output limit + // so oversized answers surface as real diffs instead of buffer errors. + maxBuffer: 8 * 1024 * 1024, }); if (run.status !== 0) { diff --git a/tests/unit/lib/contest.test.ts b/tests/unit/lib/contest.test.ts index e9b0e20..de8a0cd 100644 --- a/tests/unit/lib/contest.test.ts +++ b/tests/unit/lib/contest.test.ts @@ -17,8 +17,17 @@ function submission( contestProblemId: string, verdict: SubmissionVerdict, createdAt: Date, + passedCount = verdict === SubmissionVerdict.ACCEPTED ? 1 : 0, + totalCount = 1, ) { - return { userId, contestProblemId, verdict, createdAt }; + return { + userId, + contestProblemId, + verdict, + passedCount, + totalCount, + createdAt, + }; } describe("computeStandings", () => { @@ -59,6 +68,43 @@ describe("computeStandings", () => { expect(standings).toEqual([]); }); + it("uses only the best partial score for each problem", () => { + const standings = computeStandings( + [ + submission( + "alice", + "p1", + SubmissionVerdict.WRONG_ANSWER, + new Date("2026-07-01T10:05:00.000Z"), + 3, + 10, + ), + submission( + "alice", + "p1", + SubmissionVerdict.WRONG_ANSWER, + new Date("2026-07-01T10:20:00.000Z"), + 7, + 10, + ), + submission( + "bob", + "p1", + SubmissionVerdict.WRONG_ANSWER, + new Date("2026-07-01T10:10:00.000Z"), + 6, + 10, + ), + ], + startsAt, + ); + + expect(standings).toEqual([ + expect.objectContaining({ userId: "alice", score: 70, solvedCount: 0, rank: 1 }), + expect.objectContaining({ userId: "bob", score: 60, solvedCount: 0, rank: 2 }), + ]); + }); + it("uses participant start times for demo contest penalties", () => { const standings = computeStandings( [ diff --git a/tests/unit/lib/judge.test.ts b/tests/unit/lib/judge.test.ts index 5bc16a4..8e96a89 100644 --- a/tests/unit/lib/judge.test.ts +++ b/tests/unit/lib/judge.test.ts @@ -45,12 +45,14 @@ describe("judgeSubmission", () => { passedCount: 2, totalCount: 2, runtimeMs: 6, + failureMessage: null, }); }); - it("returns wrong answer on the first mismatched output", async () => { + it("continues after a wrong answer and counts later passing tests", async () => { + let execution = 0; const executor: CodeExecutor = async () => ({ - stdout: "0\n", + stdout: execution++ === 0 ? "0\n" : "6\n", stderr: "", exitCode: 0, signal: null, @@ -65,7 +67,11 @@ describe("judgeSubmission", () => { timeLimitMs: 2000, }); - expect(result).toMatchObject({ verdict: SubmissionVerdict.WRONG_ANSWER, passedCount: 0 }); + expect(result).toMatchObject({ + verdict: SubmissionVerdict.WRONG_ANSWER, + passedCount: 1, + totalCount: 2, + }); }); it("maps compile errors before output comparison", async () => {