diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..0091edf --- /dev/null +++ b/.clang-format @@ -0,0 +1,12 @@ +# clang-format config for the C++ solution snippets. +# These are bare LeetCode editor bodies (no includes), so formatting is the only +# whole-file check that's meaningful; the syntax gate handles correctness. +BasedOnStyle: Google +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 100 +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +SortIncludes: false diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..98d08e6 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,15 @@ +# CodeRabbit config — lightweight, archive-appropriate. +language: en-US +reviews: + profile: chill + request_changes_workflow: false + high_level_summary: true + poem: false + review_status: true + path_filters: + - "!solutions/**" # don't nitpick historical submission snippets + - "scripts/**" + - ".github/**" + - "*.md" +chat: + auto_reply: true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..93f5a46 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Keep LF for scripts and config so CI bash/Python behave identically on all OSes. +* text=auto eol=lf +*.png binary +*.jpg binary +*.svg text eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..6698e40 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [aliammari1] diff --git a/.github/ISSUE_TEMPLATE/solution_issue.md b/.github/ISSUE_TEMPLATE/solution_issue.md new file mode 100644 index 0000000..39631a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/solution_issue.md @@ -0,0 +1,15 @@ +--- +name: Solution issue +about: Report an incorrect solution or suggest an improvement +title: "[slug] short description" +labels: ["solution"] +--- + +**Problem slug:** + +**Language:** + +**What's wrong / what could be better?** + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c7b7067 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +version: 2 +# Archive repo: the only moving dependencies are GitHub Actions and the docs +# build tooling (requirements-docs.txt). Dependabot is the single dependency +# bot here — a Renovate config was removed to avoid duplicate update PRs. +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + commit-message: + prefix: "ci" + labels: ["dependencies"] + groups: + github-actions: + patterns: ["*"] + + - package-ecosystem: pip + directory: "/" + schedule: + interval: weekly + commit-message: + prefix: "build" + labels: ["dependencies"] diff --git a/.github/workflows/ai-explain.yml b/.github/workflows/ai-explain.yml new file mode 100644 index 0000000..fb424b6 --- /dev/null +++ b/.github/workflows/ai-explain.yml @@ -0,0 +1,62 @@ +name: AI solution explanations + +on: + workflow_dispatch: + inputs: + limit: + description: "Max number of slugs to process (0 = all)" + default: "20" + force: + description: "Overwrite existing EXPLANATION.md" + type: boolean + default: false + schedule: + # Weekly top-up of any newly-added Accepted solutions lacking an explanation. + - cron: "0 5 * * 1" + +permissions: + contents: write + pull-requests: write + +jobs: + explain: + name: Generate EXPLANATION.md with Claude + runs-on: ubuntu-latest + # Gated: the job only runs when ANTHROPIC_API_KEY is configured. + if: ${{ vars.ENABLE_AI == 'true' || github.event_name == 'workflow_dispatch' }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Anthropic SDK + run: pip install "anthropic>=0.40" + + - name: Generate explanations (with WA/RE diff hook) + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + AI_MODEL: ${{ vars.AI_MODEL || 'claude-sonnet-4-6' }} + run: | + # Script is a no-op when ANTHROPIC_API_KEY is unset, so this is safe + # to run on PRs / forks without the secret. + ARGS="--tag --limit ${{ github.event.inputs.limit || '20' }}" + if [ "${{ github.event.inputs.force }}" = "true" ]; then ARGS="$ARGS --force"; fi + python scripts/ai_explain.py $ARGS + + - name: Open PR with new explanations + if: ${{ env.HAS_KEY == 'true' }} + env: + HAS_KEY: ${{ secrets.ANTHROPIC_API_KEY != '' }} + uses: peter-evans/create-pull-request@v6 + with: + commit-message: "docs: add AI-generated solution explanations" + title: "docs: AI solution explanations" + body: | + Auto-generated `EXPLANATION.md` files (approach, complexity, and a + diff vs the recorded Wrong-Answer / Runtime-Error attempts). + Review before merging. + branch: ai/explanations + add-paths: | + solutions/**/EXPLANATION.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..13cda78 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,109 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + progress: + name: Progress table is current + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + # Fails if README's PROGRESS block or docs/progress.json drifted from + # solutions/ — keeps the 173-vs-542 stats honest and reconciled. + - name: Check generated stats + run: python scripts/generate_progress.py --check + + cpp-syntax: + name: C++ preamble-injection syntax gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install g++ + run: sudo apt-get update && sudo apt-get install -y g++ + # Injects `#include ` + `using namespace std;` then runs + # g++ -fsyntax-only. Real errors fail the build (no `|| echo`). + - name: Syntax-check all Accepted C++ solutions + run: python scripts/compile_check.py --cxx g++ --std c++20 + + clang-format: + name: clang-format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: clang-format check (quoted globs) + run: | + # Paths contain spaces and commas — NUL-delimit and quote. + mapfile -d '' files < <(find solutions -type f -name 'Solution.cpp' -print0) + if [ ${#files[@]} -eq 0 ]; then echo "no C++ files"; exit 0; fi + clang-format --dry-run --Werror "${files[@]}" + + cpp-linter: + name: cpp-linter + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cpp-linter/cpp-linter-action@v2 + id: linter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + style: file + tidy-checks: "-*" # format-only; bare snippets have no compile DB + files-changed-only: false + extensions: cpp + - name: Fail on lint findings + if: steps.linter.outputs.checks-failed > 0 + run: exit 1 + + sql: + name: sqlfluff (Oracle + MySQL) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install sqlfluff + run: pip install "sqlfluff==3.3.0" + - name: Lint Oracle SQL (quoted globs) + run: | + mapfile -d '' files < <(find solutions -type f -name '*.oraclesql' -print0) + if [ ${#files[@]} -eq 0 ]; then echo "no Oracle SQL"; exit 0; fi + sqlfluff lint --dialect oracle "${files[@]}" + - name: Lint MySQL (quoted globs) + run: | + mapfile -d '' files < <(find solutions -type f -name '*.mysql' -print0) + if [ ${#files[@]} -eq 0 ]; then echo "no MySQL"; exit 0; fi + sqlfluff lint --dialect mysql "${files[@]}" + + docs: + name: mkdocs build (strict) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install docs deps + run: pip install -r requirements-docs.txt + - name: Generate pages + run: python scripts/generate_progress.py --docs + - name: Build + run: mkdocs build --strict diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml new file mode 100644 index 0000000..6bba6b5 --- /dev/null +++ b/.github/workflows/claude-review.yml @@ -0,0 +1,29 @@ +name: Claude PR review + +on: + pull_request: + types: [opened, synchronize] + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + review: + name: "@claude review" + runs-on: ubuntu-latest + # Gated on the secret: skips cleanly when ANTHROPIC_API_KEY is absent. + if: ${{ github.event_name == 'pull_request' || contains(github.event.comment.body, '@claude') }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Claude Code Action + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # Configurable; defaults to Sonnet for cost on an archive repo. + model: ${{ vars.AI_MODEL || 'claude-sonnet-4-6' }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..9eddf48 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,56 @@ +name: Deploy docs (Cloudflare Pages) + +on: + push: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docs-deploy + cancel-in-progress: true + +jobs: + deploy: + name: Build & deploy to Cloudflare Pages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install docs deps + run: pip install -r requirements-docs.txt + + - name: Generate per-slug pages + run: python scripts/generate_progress.py --docs + + - name: Build site + run: mkdocs build --strict + + # Gated: only deploys when the Cloudflare secrets are configured. On forks + # / PRs without them this whole step is skipped, so the workflow stays + # green without ever needing a CF account during development. + - name: Check Cloudflare secrets + id: cf + run: | + if [ -n "${{ secrets.CLOUDFLARE_API_TOKEN }}" ] && [ -n "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "::notice::Cloudflare secrets not set — skipping deploy (build verified only)." + fi + + - name: Deploy to Cloudflare Pages + if: steps.cf.outputs.enabled == 'true' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy site --project-name=leetcode-problems diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af6485f --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# mkdocs build output +/site/ + +# Generated docs pages (rebuilt by scripts/generate_progress.py --docs in CI; +# not committed to avoid churn). Only docs/progress.json is committed so the +# README's dynamic badges resolve without needing a docs build. +/docs/problems/ +/docs/index.md +/docs/tags.md +/docs/lessons.md + +# Python +__pycache__/ +*.pyc +.venv/ +venv/ + +# OS +.DS_Store +Thumbs.db + +# Internal growth/marketing notes (not for public) +GROWTH.md +BANNER.md +SUBMITTING_TO_AWESOME.md +launch-kit.md +star-strategy.md +modernization-backlog.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..15ba879 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,33 @@ +# Mirrors the CI gates locally. Install: pip install pre-commit && pre-commit install +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: end-of-file-fixer + exclude: '^solutions/' + - id: trailing-whitespace + exclude: '^solutions/' + - id: check-yaml + + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.5 + hooks: + - id: clang-format + files: '^solutions/.*Solution\.cpp$' + + - repo: https://github.com/sqlfluff/sqlfluff + rev: 3.3.0 + hooks: + - id: sqlfluff-lint + name: sqlfluff (oracle) + files: '\.oraclesql$' + args: ["--dialect", "oracle"] + + - repo: local + hooks: + - id: progress-table + name: progress table up to date + entry: python scripts/generate_progress.py --check + language: system + pass_filenames: false + files: '^solutions/|^scripts/generate_progress\.py$' diff --git a/.sqlfluff b/.sqlfluff new file mode 100644 index 0000000..48eb5f2 --- /dev/null +++ b/.sqlfluff @@ -0,0 +1,20 @@ +# sqlfluff config. The default dialect is oracle (most DB solutions are +# .oraclesql); the MySQL files are linted with `--dialect mysql` overriding this +# in the dedicated CI step. Lint-only — these are LeetCode editor snippets, not +# a managed schema, so we keep the rule set lenient and skip auto-fix in CI. +[sqlfluff] +dialect = oracle +templater = raw +max_line_length = 120 +# The files open with a `// https://...` comment line that is not valid SQL in +# either dialect; ignore parsing/templating noise from that and focus on style. +ignore = parsing,templating + +[sqlfluff:indentation] +tab_space_size = 4 + +[sqlfluff:rules:capitalisation.keywords] +capitalisation_policy = consistent + +[sqlfluff:rules:capitalisation.identifiers] +extended_capitalisation_policy = consistent diff --git a/LICENSE b/LICENSE index ff9dda0..c08a69d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,45 +1,36 @@ -# COMMERCIAL USE LICENSE - -Copyright (c) 2023-2025 Ali Ammari - -## PERMISSIONS - -You are free to: -- Use, modify, and distribute this code for personal and non-commercial purposes -- Study and learn from this code -- Fork and contribute improvements -- Use in educational settings - -## RESTRICTIONS - -Commercial use is NOT permitted without explicit written permission. - -Commercial use includes: -- Selling or distributing this code or derived products -- Incorporating into paid products or services -- Using to provide commercial services -- Using in profit-generating activities - -## COMMERCIAL USE REQUEST - -For commercial use, contact: ammari.ali.0001@gmail.com - -Subject: "Commercial Use Request - Leetcode_problems" - -Include: -- Your name and organization -- Description of intended use -- Implementation timeline - -## INTELLECTUAL PROPERTY NOTICE - -This project uses third-party assets and libraries. The author does NOT own these assets. Each component retains its original license and copyright. - -Users are responsible for: -- Reviewing all third-party licenses -- Ensuring compliance -- Obtaining separate licenses for commercial use of third-party components if required - -## DISCLAIMER - -THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. +MIT License + +SPDX-License-Identifier: MIT + +Copyright (c) 2023-2026 Ali Ammari + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +NOTE ON LEETCODE INTELLECTUAL PROPERTY + +The MIT license above applies only to the original solution source code authored +by the repository owner. LeetCode problem statements, test data, problem titles, +problem numbers, and the LeetCode name and trademarks are the intellectual +property of LeetCode (leetcode.com) and are NOT covered by this license. Problem +slugs referenced in this archive (e.g. via the `// https://leetcode.com/problems/` +comments) link back to the original problems on leetcode.com, which remain +subject to LeetCode's own Terms of Service. This repository reproduces only the +author's own submitted solutions, not LeetCode's problem content. diff --git a/README.md b/README.md index a701cf4..5b8c773 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,34 @@ -# Leetcode Problems +# Every LeetCode problem I solved — including my real Wrong-Answer / TLE submissions and the lesson from each -A comprehensive collection of LeetCode problem solutions organized by individual problem directories. This repository serves as a personal coding practice journal and tracks submission history across multiple attempts and languages. + + + +[![Solved](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Faliammari1%2FLeetcode_problems%2Fmain%2Fdocs%2Fprogress.json&query=%24.stats.unique_slugs&label=problems&color=brightgreen)](docs/progress.json) +[![Submissions](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Faliammari1%2FLeetcode_problems%2Fmain%2Fdocs%2Fprogress.json&query=%24.stats.total_files&label=submissions&color=blue)](docs/progress.json) +[![Accepted](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Faliammari1%2FLeetcode_problems%2Fmain%2Fdocs%2Fprogress.json&query=%24.stats.accepted_pct&suffix=%25&label=accepted&color=success)](docs/progress.json) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) + +Not another clean-solutions dump. This archive of **173 problems / 542 files** +keeps the **real submission history** — every Wrong Answer, Runtime Error, Time +Limit Exceeded, and Compile Error attempt that came *before* the accepted +solution, each timestamped — across C++, Java, Oracle SQL, and MySQL. You can +read the *progression* of how each problem got solved, and the bug each failed +attempt tripped on. + +### 🔎 [Browse all 173 problems (searchable) →](https://leetcode-problems.pages.dev) + +Per-problem pages show the **verdict history** (Accepted → Wrong Answer → TLE → +Runtime Error, timestamped) with every language variant in tabs. See also the +[**Lessons / common mistakes**](https://leetcode-problems.pages.dev/lessons/) +index — the 89 problems I did *not* get on the first try. + +> **⭐ If preserving the failed attempts is useful to you, star the repo** — it +> helps others find an honest, mistakes-and-all study archive. + +> **Coming in Wave 2:** the **`leetcode-solutions` MCP** — a bundled, offline +> MCP server exposing all 173 problems *with their wrong-answer history* to any +> MCP-capable assistant, no session cookie required. *"The MCP that remembers +> what NOT to do."* ## 🚀 Features @@ -12,29 +40,61 @@ A comprehensive collection of LeetCode problem solutions organized by individual ## 📊 Progress Statistics -| Language | Files | Percentage | -|-------------|-------|------------| -| C++ | 496 | 72.7% | -| Java | 18 | 2.6% | -| Oracle SQL | 26 | 3.8% | -| MySQL | 2 | 0.3% | -| **Total Problems** | **173** | **100%** | +> These numbers are generated from a single source of truth — +> `scripts/generate_progress.py` walks `solutions/` and reconciles **unique +> problems** vs **total submission files** (the README previously contradicted +> itself on this). CI fails if this block is stale. + + + +- **Unique problems (slugs):** 173 +- **Total submission files:** 542 (every saved attempt across verdicts and timestamps) +- **Accepted problems:** 160 (92.5% of unique problems) + +### Language coverage -*Last updated: 2025-06-26* +| Language | Files | Problems | % of files | +|----------|-------|----------|-----------| +| C++ | 496 | 146 | 91.5% | +| Java | 18 | 9 | 3.3% | +| Oracle SQL | 26 | 22 | 4.8% | +| MySQL | 2 | 2 | 0.4% | +| **Total** | **542** | **173** | **100%** | + +### Submission verdicts + +| Verdict | Files | +|---------|-------| +| Accepted | 277 | +| Wrong Answer | 156 | +| Time Limit Exceeded | 57 | +| Runtime Error | 47 | +| Compile Error | 5 | + +*Generated by `scripts/generate_progress.py`. Do not edit this block by hand.* + + ## 🛠️ Technologies Used -- **Primary Languages**: C++ (72.7%), Java (2.6%) -- **Database Languages**: Oracle SQL, MySQL +- **Primary Languages**: C++ (the bulk of the archive), Java +- **Database Languages**: **Oracle SQL** (`.oraclesql`) and MySQL (`.mysql`). + Note: LeetCode's SQL-50 study plan is MySQL-graded, but most database + solutions here were written and saved as Oracle PL/SQL, so they are linted + with `sqlfluff --dialect oracle` (and the MySQL files with `--dialect mysql`). - **Tools**: VS Code, LeetCode Platform Integration - **Platforms**: LeetCode +See the live counts in [Progress Statistics](#-progress-statistics) above. + ## 📁 Repository Structure ``` Leetcode_problems/ -├── assets/ -│ └── repo_image_Leetcode_problems.png +├── assets/ # banner / social-preview assets (see BANNER.md) +├── docs/ # mkdocs-material site (generated per-slug pages) +├── scripts/ +│ └── generate_progress.py # single source of truth for stats + docs ├── solutions/ │ ├── add-binary/ │ │ └── Accepted/ @@ -54,7 +114,7 @@ Leetcode_problems/ │ │ └── Accepted/ │ │ └── [timestamp]/ │ │ └── Solution.oraclesql -│ └── [173 total problem directories] +│ └── [one directory per problem slug] ├── LICENSE └── README.md ``` @@ -67,28 +127,41 @@ Each problem follows this consistent structure: - **Timestamp Directories**: Each submission saved with exact timestamp - **Solution Files**: Named by language (e.g., `Solution.cpp`, `Solution.java`, `Solution.oraclesql`) -## 🎯 Problem Categories - -Based on the 173 solved problems, this repository covers a wide range of topics: - -### Data Structures -- **Arrays**: Two Sum, Best Time to Buy and Sell Stock, Build Array from Permutation -- **Strings**: Valid Anagram, Longest Common Prefix, Reverse String -- **Linked Lists**: Reverse Linked List, Merge Two Sorted Lists, Middle of the Linked List -- **Trees**: Binary Tree Traversals, Maximum Depth of Binary Tree, Same Tree -- **Hash Tables**: Contains Duplicate, Single Number, Valid Anagram -- **Stacks**: Valid Parentheses, Min Stack, Baseball Game - -### Algorithms -- **Math Problems**: Fibonacci Number, Power calculations, Palindrome Number -- **Binary Search**: Binary Search, First Bad Version, Search Insert Position -- **Two Pointers**: Two Sum II, Reverse String, Valid Palindrome -- **Simulation**: Fizz Buzz, Robot Return to Origin, Pascal's Triangle - -### Database Problems (SQL) -- **Joins**: Combine Two Tables, Replace Employee ID -- **Aggregation**: Average Salary, Daily Leads and Partners -- **Filtering**: Big Countries, Not Boring Movies, Recyclable Products +## 🧭 Pattern index + +The full, auto-generated index by language/pattern lives on the docs site +([Index by Language](https://leetcode-problems.pages.dev/tags/)). The 14 core +patterns this archive covers: + +| Pattern | Example problems (slug) | +|---------|-------------------------| +| Arrays | `two-sum`, `best-time-to-buy-and-sell-stock`, `build-array-from-permutation` | +| Two Pointers | `two-sum-ii-input-array-is-sorted`, `palindrome-linked-list`, `backspace-string-compare` | +| Sliding Window | `best-time-to-buy-and-sell-stock`, `max-consecutive-ones` | +| Binary Search | `binary-search`, `search-insert-position`, `first-bad-version` | +| Hashing | `contains-duplicate`, `valid-anagram`, `single-number` | +| Stack | `valid-parentheses`, `baseball-game`, `min-stack` | +| Linked List | `reverse-linked-list`, `merge-two-sorted-lists`, `middle-of-the-linked-list` | +| Trees | `binary-tree-inorder-traversal`, `symmetric-tree`, `validate-binary-search-tree` | +| BFS / DFS | `binary-tree-level-order-traversal`, `binary-tree-right-side-view` | +| Divide & Conquer | `median-of-two-sorted-arrays`, `merge-two-sorted-lists` | +| Greedy | `can-place-flowers`, `can-make-arithmetic-progression-from-sequence` | +| Dynamic Programming | `climbing-stairs`, `fibonacci-number` | +| Math | `palindrome-number`, `convert-the-temperature`, `add-binary` | +| SQL | `combine-two-tables`, `big-countries`, `article-views-i` | + +### 🗄️ SQL-50 / Oracle track + +Database solutions follow LeetCode's **SQL-50** study plan. A quirk worth +calling out: SQL-50 is **MySQL-graded**, but most queries here were authored and +saved as **Oracle PL/SQL** (`.oraclesql`, 26 files / 22 problems) with a couple +of MySQL files (`.mysql`). They are linted accordingly — +`sqlfluff --dialect oracle` for the Oracle files and `--dialect mysql` for the +MySQL ones (see [`.sqlfluff`](.sqlfluff)). Representative problems: joins +(`combine-two-tables`, `replace-employee-id-with-the-unique-identifier`), +aggregation (`average-salary-excluding-the-minimum-and-maximum-salary`, +`daily-leads-and-partners`), filtering (`big-countries`, `not-boring-movies`, +`recyclable-and-low-fat-products`). ## 🔍 Example Solutions @@ -129,11 +202,16 @@ class Solution { ``` ### SQL Solution Format + +SQL solutions carry the same `//` first-line link comment as the C++/Java files +(the metadata hook the generator parses), followed by the LeetCode editor's own +prompt comment: + ```sql --- https://leetcode.com/problems/combine-two-tables -SELECT firstName, lastName, city, state -FROM person p -LEFT JOIN address a ON p.personId = a.personId; +// https://leetcode.com/problems/article-views-i + +/* Write your PL/SQL query statement below */ +select distinct author_id as "id" from views where author_id = viewer_id order by author_id; ``` ## 📝 Submission Status Categories @@ -182,6 +260,76 @@ LEFT JOIN address a ON p.personId = a.personId; -- Copy and paste SQL from Solution.oraclesql into your database client ``` +## 🧪 Tooling & CI + +Because the saved snippets are bare LeetCode editor bodies with **no +`#include` / `import`**, a naive `g++ Solution.cpp` would compile nothing. CI +therefore uses dedicated gates (all path globbing is **quoted**, since verdict +and timestamp directories contain spaces and commas): + +- **C++ preamble-injection syntax gate** (`scripts/compile_check.py`): for each + Accepted `Solution.cpp`, prepend `#include ` + `using + namespace std;` into a temp file, then run + `g++ -std=c++20 -fsyntax-only -Wall` and fail on real errors. +- **clang-format** (`.clang-format`) + **cpp-linter** on C++ files. +- **sqlfluff** lint: `--dialect oracle` for `.oraclesql`, `--dialect mysql` for + `.mysql`. +- **Progress check**: `generate_progress.py --check` fails CI if the README + stats block or `docs/progress.json` drift from `solutions/`. + +Run the gates locally: + +```bash +python scripts/generate_progress.py --check +python scripts/compile_check.py # needs g++ +clang-format --dry-run --Werror $(git ls-files 'solutions/**/Solution.cpp') +sqlfluff lint --dialect oracle $(git ls-files 'solutions/**/*.oraclesql') +``` + +## 📖 Documentation site (live demo) + +> **Live demo:** https://leetcode-problems.pages.dev + +An [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) site is +generated from `solutions/` — **one page per problem slug** showing its full +**verdict history** (Accepted → Wrong Answer → Runtime Error → TLE, each +timestamped) with every language variant in content tabs, plus an index by +language. This is the showcase for the archive's unique angle: you can read the +*progression* of attempts, not just the final answer. + +Build / preview it locally: + +```bash +pip install -r requirements-docs.txt +python scripts/generate_progress.py --docs # writes docs/problems/*.md, docs/index.md, docs/tags.md +mkdocs build --strict # verifies all 173 pages +mkdocs serve # http://127.0.0.1:8000 +``` + +**Deploy (Cloudflare Pages).** Push to `main` triggers +`.github/workflows/docs.yml`, which regenerates the per-slug pages, runs +`mkdocs build --strict`, and deploys `site/` to a Cloudflare Pages project named +`leetcode-problems`. The deploy step is **gated** — it only runs when the +`CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` repository secrets are set, so +forks and PRs still get a green build without any Cloudflare account. To wire up +your own: + +1. Create a Cloudflare Pages project (Direct Upload) named `leetcode-problems`. +2. Add `CLOUDFLARE_API_TOKEN` (Pages:Edit) and `CLOUDFLARE_ACCOUNT_ID` as repo + secrets. +3. Push to `main` — the workflow builds and deploys automatically. + +## 🤖 AI tooling + +A workflow (`.github/workflows/ai-explain.yml`, gated on `ANTHROPIC_API_KEY`) +runs `scripts/ai_explain.py`. For each Accepted solution it asks Claude +(`claude-sonnet-4-6`, configurable) to write an `EXPLANATION.md` covering the +approach, complexity, **and a diff/explanation versus the sibling Wrong-Answer / +Runtime-Error attempts** — the bug that the earlier submission tripped on. The +docs generator folds these explanations into each problem page. The same script +can auto-tag difficulty/pattern. CodeRabbit / claude-code-action handle PR +review. + ## 🤝 Contributing Contributions are welcome! Here's how you can help: @@ -203,7 +351,9 @@ Contributions are welcome! Here's how you can help: ## 📈 Study Plan -### Current Progress: 173 Problems Solved +### Current Progress + +See the live, auto-generated counts in [Progress Statistics](#-progress-statistics). #### Language Distribution - **C++ Focus**: Master data structures and algorithms implementation @@ -222,7 +372,7 @@ Connect with me on LeetCode: [aliammari1](https://leetcode.com/aliammari1/) ## 🎯 Goals -- [x] Solve 100+ problems (173/100+ ✅) +- [x] Solve 100+ problems ✅ - [x] Master C++ implementation - [x] Learn Java fundamentals - [x] Practice SQL queries @@ -247,7 +397,14 @@ Connect with me on LeetCode: [aliammari1](https://leetcode.com/aliammari1/) ## 📄 License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +The original solution source code in this repository is licensed under the **MIT +License** (`SPDX-License-Identifier: MIT`) — see [LICENSE](LICENSE). + +> **LeetCode IP:** the MIT license covers only the author's own solution code. +> LeetCode problem statements, test data, titles, numbers, and trademarks belong +> to LeetCode (leetcode.com) and are **not** covered by this license. The slug +> links in each file point back to the original problems, which remain subject to +> LeetCode's Terms of Service. ## 🙏 Acknowledgments @@ -263,4 +420,12 @@ Project Link: [https://github.com/aliammari1/Leetcode_problems](https://github.c --- +## 🔗 Related projects + +- **[HackerRank solution archive](https://github.com/aliammari1/Hackerrank_problems)** — the companion archive, same honest, history-preserving format. +- **[All my projects (profile hub)](https://github.com/aliammari1)** — apps, tools, and games. +- **[LeetCode profile](https://leetcode.com/aliammari1/)** + +--- + *Happy Coding! 🚀* \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..085cfdf --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +This repository is an archive of personal LeetCode solutions — it ships no +running service, no dependencies that execute at runtime, and stores no secrets. +The practical security surface is the CI/automation tooling. + +## Reporting + +If you find a problem in the tooling (a workflow that could leak a token, a +script with a command-injection issue, etc.), please open a +[GitHub Security Advisory](https://github.com/aliammari1/Leetcode_problems/security/advisories/new) +or email the maintainer. Please do **not** open a public issue for a +security-relevant report. + +## Scope + +- In scope: `scripts/`, `.github/workflows/`, CI configuration. +- Out of scope: the correctness of individual solution snippets (those are a + historical record, including deliberately-kept failed attempts). diff --git a/docs/progress.json b/docs/progress.json new file mode 100644 index 0000000..3f21862 --- /dev/null +++ b/docs/progress.json @@ -0,0 +1,60 @@ +{ + "schemaVersion": 1, + "label": "solved", + "message": "173 problems", + "color": "brightgreen", + "stats": { + "unique_slugs": 173, + "total_files": 542, + "accepted_slugs": 160, + "accepted_pct": 92.5, + "languages": [ + { + "language": "C++", + "files": 496, + "slugs": 146, + "pct_files": 91.5 + }, + { + "language": "Java", + "files": 18, + "slugs": 9, + "pct_files": 3.3 + }, + { + "language": "Oracle SQL", + "files": 26, + "slugs": 22, + "pct_files": 4.8 + }, + { + "language": "MySQL", + "files": 2, + "slugs": 2, + "pct_files": 0.4 + } + ], + "verdicts": [ + { + "verdict": "Accepted", + "files": 277 + }, + { + "verdict": "Wrong Answer", + "files": 156 + }, + { + "verdict": "Time Limit Exceeded", + "files": 57 + }, + { + "verdict": "Runtime Error", + "files": 47 + }, + { + "verdict": "Compile Error", + "files": 5 + } + ] + } +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..683d8a8 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,70 @@ +site_name: LeetCode Solutions Archive +site_description: >- + A personal LeetCode archive with real submission history — including the wrong + attempts before each accepted solution — across C++, Java, and Oracle/MySQL SQL. +site_author: Ali Ammari +repo_url: https://github.com/aliammari1/Leetcode_problems +repo_name: aliammari1/Leetcode_problems +docs_dir: docs +copyright: >- + Solution code © Ali Ammari (MIT). LeetCode problem statements and trademarks + are the property of LeetCode. + +theme: + name: material + palette: + - scheme: slate + primary: black + accent: amber + toggle: + icon: material/weather-sunny + name: Switch to light mode + - scheme: default + primary: black + accent: amber + toggle: + icon: material/weather-night + name: Switch to dark mode + features: + - content.code.copy + - content.tabs.link + - navigation.tabs + - navigation.top + - navigation.instant + - search.highlight + - search.suggest + icon: + repo: fontawesome/brands/github + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + +plugins: + - search + # Note: the per-slug pages under docs/problems/ are GENERATED fresh by + # scripts/generate_progress.py --docs and are not committed, so a + # git-revision-date plugin would warn on every page ("no git logs") and fail + # --strict. We deliberately omit it; the build date is shown by the theme. + +nav: + - Home: index.md + - All Problems: problems/index.md + - Lessons / common mistakes: lessons.md + - Index by Language: tags.md + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/aliammari1 + - icon: simple/leetcode + link: https://leetcode.com/aliammari1/ diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..db96278 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,5 @@ +# Pinned docs toolchain. mkdocs-material entered maintenance mode in late 2025; +# pin a known-good release rather than tracking latest. +mkdocs-material==9.5.49 +mkdocs==1.6.1 +pymdown-extensions==10.14.3 diff --git a/scripts/ai_explain.py b/scripts/ai_explain.py new file mode 100644 index 0000000..8b42d0f --- /dev/null +++ b/scripts/ai_explain.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Generate per-solution AI explanations with Claude. + +For each Accepted solution this writes an ``EXPLANATION.md`` next to it covering: + +1. the approach and intuition, +2. time / space complexity, +3. **a diff/explanation versus the sibling Wrong-Answer / Runtime-Error / TLE + attempts for the same problem** — what the earlier submission got wrong and + how the accepted one fixes it. That comparison against the real failed + attempts is the unique hook of this archive. + +Optionally (``--tag``) it also emits a one-line difficulty + pattern tag block, +which the docs generator can surface as tags. + +The output feeds the mkdocs page generator (`generate_progress.py --docs`), +which embeds any `EXPLANATION.md` it finds on the problem page. + +Gated on ``ANTHROPIC_API_KEY``: if the key is unset the script prints a notice +and exits 0, so the workflow is a no-op on forks/PRs without the secret. Uses +the official Anthropic Python SDK (``pip install anthropic``). + +Usage:: + + python scripts/ai_explain.py # all Accepted lacking EXPLANATION.md + python scripts/ai_explain.py --slug two-sum # one problem + python scripts/ai_explain.py --force --limit 20 + python scripts/ai_explain.py --model claude-sonnet-4-6 +""" + +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SOLUTIONS = ROOT / "solutions" +DEFAULT_MODEL = "claude-sonnet-4-6" + +SYSTEM_PROMPT = ( + "You are a precise competitive-programming reviewer. You write tight, " + "technically accurate explanations of LeetCode solutions for a " + "documentation site. Use Markdown. Do not restate the full problem; assume " + "the reader can follow the linked problem. Be concrete about complexity and " + "about the specific bug in any failed attempt." +) + + +def read_solution(d: Path) -> str | None: + for sol in sorted(d.glob("Solution.*")): + if sol.is_file(): + return sol.read_text(encoding="utf-8", errors="replace") + return None + + +def collect_attempts(slug_dir: Path) -> dict[str, list[str]]: + """Return {verdict: [code, ...]} across all timestamp dirs for a slug.""" + out: dict[str, list[str]] = {} + for verdict_dir in sorted(p for p in slug_dir.iterdir() if p.is_dir()): + for ts_dir in sorted(p for p in verdict_dir.iterdir() if p.is_dir()): + code = read_solution(ts_dir) + if code: + out.setdefault(verdict_dir.name, []).append(code) + return out + + +def build_prompt(slug: str, accepted_code: str, attempts: dict[str, list[str]], tag: bool) -> str: + parts = [ + f"Problem: https://leetcode.com/problems/{slug}/", + "", + "ACCEPTED solution:", + "```", + accepted_code.strip(), + "```", + ] + failed = {v: c for v, c in attempts.items() if v != "Accepted"} + if failed: + parts += ["", "Earlier FAILED attempts on the same problem:"] + for verdict, codes in failed.items(): + for code in codes[:2]: # cap to keep the prompt bounded + parts += [f"", f"[{verdict}]", "```", code.strip(), "```"] + parts += [ + "", + "Write EXPLANATION.md with these sections:", + "## Approach — the intuition and the algorithm in a few sentences.", + "## Complexity — time and space, with a one-line justification.", + ] + if failed: + parts.append( + "## What the earlier attempts got wrong — for each failed verdict " + "above, name the specific bug (off-by-one, wrong data structure, " + "missing edge case, etc.) and how the accepted version fixes it." + ) + else: + parts.append( + "## Notes — any edge cases or pitfalls worth flagging (there are no " + "recorded failed attempts for this problem)." + ) + if tag: + parts.append( + "Finally add a line exactly like `Tags: difficulty=, " + "pattern=` based on the solution." + ) + return "\n".join(parts) + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--model", default=os.environ.get("AI_MODEL", DEFAULT_MODEL)) + ap.add_argument("--slug", help="only this slug") + ap.add_argument("--limit", type=int, default=0, help="cap number of slugs processed") + ap.add_argument("--force", action="store_true", help="overwrite existing EXPLANATION.md") + ap.add_argument("--tag", action="store_true", help="also emit difficulty/pattern tags") + args = ap.parse_args() + + api_key = os.environ.get("ANTHROPIC_API_KEY") + if not api_key: + print("ANTHROPIC_API_KEY not set — skipping AI explanations (no-op).") + return 0 + + try: + import anthropic + except ImportError: + print("::error::`anthropic` package not installed (pip install anthropic).", file=sys.stderr) + return 2 + + client = anthropic.Anthropic(api_key=api_key) + + slug_dirs = sorted(p for p in SOLUTIONS.iterdir() if p.is_dir()) + if args.slug: + slug_dirs = [p for p in slug_dirs if p.name == args.slug] + + processed = 0 + for slug_dir in slug_dirs: + if args.limit and processed >= args.limit: + break + accepted_dir = slug_dir / "Accepted" + if not accepted_dir.is_dir(): + continue + # Pick the most recent accepted attempt that has a solution file. + accepted_code = None + target_dir = None + for ts_dir in sorted((p for p in accepted_dir.iterdir() if p.is_dir()), reverse=True): + code = read_solution(ts_dir) + if code: + accepted_code, target_dir = code, ts_dir + break + if not accepted_code or target_dir is None: + continue + out_path = target_dir / "EXPLANATION.md" + if out_path.exists() and not args.force: + continue + + attempts = collect_attempts(slug_dir) + prompt = build_prompt(slug_dir.name, accepted_code, attempts, args.tag) + + try: + # Adaptive thinking + streaming per Claude API guidance. + with client.messages.stream( + model=args.model, + max_tokens=2000, + system=SYSTEM_PROMPT, + thinking={"type": "adaptive"}, + messages=[{"role": "user", "content": prompt}], + ) as stream: + msg = stream.get_final_message() + except Exception as exc: # noqa: BLE001 - surface and continue + print(f"::warning::{slug_dir.name}: API error: {exc}", file=sys.stderr) + continue + + text = "".join(b.text for b in msg.content if b.type == "text").strip() + if not text: + print(f"::warning::{slug_dir.name}: empty response", file=sys.stderr) + continue + + header = ( + f"\n\n" + ) + out_path.write_text(header + text + "\n", encoding="utf-8") + print(f"wrote {out_path.relative_to(ROOT)}") + processed += 1 + + print(f"done: {processed} explanation(s) generated.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/compile_check.py b/scripts/compile_check.py new file mode 100644 index 0000000..926d09c --- /dev/null +++ b/scripts/compile_check.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Preamble-injection C++ syntax gate. + +The saved snippets are bare LeetCode editor bodies: a `class Solution { ... }` +(or a free function) with **no `#include` and no `using namespace std;`**. A +plain `g++ Solution.cpp` would fail to even parse `string`/`vector`/`reverse`, +so the repo's old CI "compiled" nothing and passed via `|| echo`. + +This gate fixes that. For each Accepted `Solution.cpp` it writes a temp file: + + #include + using namespace std; + + +then runs `g++ -std=c++20 -fsyntax-only -Wall` and fails on real compiler +errors (warnings do not fail the build). It is a *syntax* gate — it does not +link or run, because these are method bodies with no `main()` and no judge +harness. + +Only Accepted snippets are checked by default: Wrong-Answer / Runtime-Error / +TLE attempts are kept deliberately as part of the submission history and may +legitimately not even parse (Compile Error verdicts certainly won't). + +All filesystem access uses pathlib, so the spaces+commas in verdict/timestamp +directory names need no shell quoting here. (The README/CI shell one-liners that +glob these paths quote them; this script is the portable path.) + +Usage:: + + python scripts/compile_check.py # all Accepted .cpp + python scripts/compile_check.py --all-verdicts # every .cpp (expect failures) + python scripts/compile_check.py --cxx g++-13 --std c++20 +""" + +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SOLUTIONS = ROOT / "solutions" +PREAMBLE = "#include \nusing namespace std;\n" + + +def iter_cpp(all_verdicts: bool): + """Yield Solution.cpp paths. By default only those under an Accepted dir.""" + for slug_dir in sorted(p for p in SOLUTIONS.iterdir() if p.is_dir()): + for verdict_dir in sorted(p for p in slug_dir.iterdir() if p.is_dir()): + if not all_verdicts and verdict_dir.name != "Accepted": + continue + for sol in verdict_dir.rglob("Solution.cpp"): + yield sol + + +def check_one(cxx: str, std: str, src: Path) -> tuple[bool, str]: + body = src.read_text(encoding="utf-8", errors="replace") + with tempfile.NamedTemporaryFile( + "w", suffix=".cpp", delete=False, encoding="utf-8" + ) as tmp: + tmp.write(PREAMBLE) + tmp.write(body) + tmp_path = Path(tmp.name) + try: + proc = subprocess.run( + [cxx, f"-std={std}", "-fsyntax-only", "-Wall", str(tmp_path)], + capture_output=True, + text=True, + ) + return proc.returncode == 0, proc.stderr + finally: + tmp_path.unlink(missing_ok=True) + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--cxx", default="g++", help="C++ compiler (default: g++)") + ap.add_argument("--std", default="c++20", help="C++ standard (default: c++20)") + ap.add_argument( + "--all-verdicts", + action="store_true", + help="check every Solution.cpp, not just Accepted (expect failures)", + ) + args = ap.parse_args() + + if shutil.which(args.cxx) is None: + print( + f"::error::compiler '{args.cxx}' not found on PATH; cannot run the " + "C++ syntax gate.", + file=sys.stderr, + ) + return 2 + + failures: list[tuple[Path, str]] = [] + total = 0 + for src in iter_cpp(args.all_verdicts): + total += 1 + ok, stderr = check_one(args.cxx, args.std, src) + rel = src.relative_to(ROOT) + if ok: + print(f"PASS {rel}") + else: + print(f"FAIL {rel}") + failures.append((rel, stderr)) + + print(f"\nChecked {total} file(s); {len(failures)} failed.") + if failures: + print("\n--- failures ---", file=sys.stderr) + for rel, stderr in failures: + print(f"\n### {rel}", file=sys.stderr) + print(stderr.strip(), file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/generate_progress.py b/scripts/generate_progress.py new file mode 100644 index 0000000..a756eb0 --- /dev/null +++ b/scripts/generate_progress.py @@ -0,0 +1,543 @@ +#!/usr/bin/env python3 +"""Single source of truth for this archive's statistics and docs. + +Walks ``solutions////Solution.``, reconciling +the two numbers the README used to contradict each other on: + +* **unique slugs** -- one directory per LeetCode problem +* **submission files** -- every saved attempt, across verdicts/timestamps + +It is the only place these counts are computed. From the parsed model it +regenerates: + +* ``docs/progress.json`` -- machine-readable stats (feeds dynamic badges) +* the ```` block in ``README.md`` +* ``docs/`` mkdocs-material pages (one per slug + indexes), when ``--docs`` + +Layout note: verdict and timestamp directory names contain spaces **and** +commas (e.g. ``Wrong Answer/8-8-2023, 12_20_23 AM``). Everything here uses +``pathlib`` so no shell quoting is involved; the CI globs that *do* touch the +shell quote every path (see ``.github/workflows``). + +Usage:: + + python scripts/generate_progress.py # regenerate stats + README + python scripts/generate_progress.py --docs # also (re)build docs/ pages + python scripts/generate_progress.py --check # exit 1 if anything is stale +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from collections import Counter, defaultdict +from dataclasses import dataclass, field +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SOLUTIONS = ROOT / "solutions" +DOCS = ROOT / "docs" +README = ROOT / "README.md" +PROGRESS_JSON = DOCS / "progress.json" +NUMBERS_JSON = Path(__file__).resolve().parent / "problem_numbers.json" + + +def load_numbers() -> dict[str, int]: + """Slug -> canonical LeetCode problem number (for exact-match SEO titles). + + Optional file; missing slugs simply omit the number prefix. + """ + if not NUMBERS_JSON.is_file(): + return {} + raw = json.loads(NUMBERS_JSON.read_text(encoding="utf-8")) + return {k: v for k, v in raw.items() if not k.startswith("_")} + + +NUMBERS = load_numbers() + +# Maps file extension -> (display language, mkdocs content-tab fence language). +EXT_LANG = { + ".cpp": ("C++", "cpp"), + ".java": ("Java", "java"), + ".oraclesql": ("Oracle SQL", "sql"), + ".mysql": ("MySQL", "sql"), +} + +# First-line metadata hook present in every solution file, e.g. +# // https://leetcode.com/problems/two-sum +SLUG_RE = re.compile(r"leetcode\.com/problems/([a-z0-9-]+)", re.IGNORECASE) + +VERDICT_ORDER = [ + "Accepted", + "Wrong Answer", + "Time Limit Exceeded", + "Runtime Error", + "Compile Error", +] + +PROGRESS_START = "" +PROGRESS_END = "" + + +@dataclass +class Submission: + slug: str + verdict: str + timestamp: str + ext: str + path: Path + + @property + def language(self) -> str: + return EXT_LANG.get(self.ext, (self.ext.lstrip("."), "text"))[0] + + +@dataclass +class Problem: + slug: str + submissions: list[Submission] = field(default_factory=list) + + @property + def url(self) -> str: + return f"https://leetcode.com/problems/{self.slug}/" + + @property + def title(self) -> str: + return self.slug.replace("-", " ").title() + + @property + def number(self) -> int | None: + return NUMBERS.get(self.slug) + + @property + def seo_title(self) -> str: + """Exact-match page title for search: '<#>. — Solution'. + + Falls back to ' — Solution' when the number is unknown. + """ + if self.number is not None: + return f"{self.number}. {self.title} — Solution" + return f"{self.title} — Solution" + + @property + def accepted(self) -> bool: + return any(s.verdict == "Accepted" for s in self.submissions) + + @property + def languages(self) -> list[str]: + seen: list[str] = [] + for s in self.submissions: + if s.language not in seen: + seen.append(s.language) + return seen + + +def first_line_slug(path: Path) -> str | None: + """Return the slug from the file's first-line comment, if present.""" + try: + with path.open("r", encoding="utf-8", errors="replace") as fh: + for _ in range(5): # slug is on line 1, but tolerate a stray BOM/blank + line = fh.readline() + if not line: + break + m = SLUG_RE.search(line) + if m: + return m.group(1).lower() + except OSError: + return None + return None + + +def scan() -> dict[str, Problem]: + """Walk solutions/ and build the slug -> Problem map. + + Slug is taken from the first-line comment where available and falls back to + the directory name (they agree across the whole archive today, but the + comment is the authoritative metadata hook the README documents). + """ + problems: dict[str, Problem] = {} + if not SOLUTIONS.is_dir(): + return problems + + for slug_dir in sorted(p for p in SOLUTIONS.iterdir() if p.is_dir()): + dir_slug = slug_dir.name + for verdict_dir in sorted(p for p in slug_dir.iterdir() if p.is_dir()): + verdict = verdict_dir.name + for ts_dir in sorted(p for p in verdict_dir.iterdir() if p.is_dir()): + timestamp = ts_dir.name + for sol in sorted(ts_dir.iterdir()): + if not sol.is_file() or sol.suffix not in EXT_LANG: + continue + slug = first_line_slug(sol) or dir_slug + problems.setdefault(slug, Problem(slug)) + problems[slug].submissions.append( + Submission(slug, verdict, timestamp, sol.suffix, sol) + ) + return problems + + +def compute_stats(problems: dict[str, Problem]) -> dict: + lang_files: Counter[str] = Counter() + verdict_files: Counter[str] = Counter() + lang_slugs: dict[str, set[str]] = defaultdict(set) + total_files = 0 + accepted_slugs = 0 + + for slug, prob in problems.items(): + if prob.accepted: + accepted_slugs += 1 + for s in prob.submissions: + total_files += 1 + lang_files[s.language] += 1 + verdict_files[s.verdict] += 1 + lang_slugs[s.language].add(slug) + + unique_slugs = len(problems) + languages = [] + for lang in ("C++", "Java", "Oracle SQL", "MySQL"): + if lang_files.get(lang): + languages.append( + { + "language": lang, + "files": lang_files[lang], + "slugs": len(lang_slugs[lang]), + "pct_files": round(100 * lang_files[lang] / total_files, 1) + if total_files + else 0.0, + } + ) + + verdicts = [ + {"verdict": v, "files": verdict_files[v]} + for v in VERDICT_ORDER + if verdict_files.get(v) + ] + + accepted_pct = round(100 * accepted_slugs / unique_slugs, 1) if unique_slugs else 0.0 + + return { + "unique_slugs": unique_slugs, + "total_files": total_files, + "accepted_slugs": accepted_slugs, + "accepted_pct": accepted_pct, + "languages": languages, + "verdicts": verdicts, + } + + +def progress_json(stats: dict) -> dict: + """Shape for docs/progress.json -- also a shields.io endpoint badge source.""" + return { + "schemaVersion": 1, + "label": "solved", + "message": f"{stats['unique_slugs']} problems", + "color": "brightgreen", + "stats": stats, + } + + +def render_progress_block(stats: dict) -> str: + lines = [ + PROGRESS_START, + "", + f"- **Unique problems (slugs):** {stats['unique_slugs']}", + f"- **Total submission files:** {stats['total_files']} " + "(every saved attempt across verdicts and timestamps)", + f"- **Accepted problems:** {stats['accepted_slugs']} " + f"({stats['accepted_pct']}% of unique problems)", + "", + "### Language coverage", + "", + "| Language | Files | Problems | % of files |", + "|----------|-------|----------|-----------|", + ] + for lang in stats["languages"]: + lines.append( + f"| {lang['language']} | {lang['files']} | {lang['slugs']} | {lang['pct_files']}% |" + ) + lines.append( + f"| **Total** | **{stats['total_files']}** | **{stats['unique_slugs']}** | **100%** |" + ) + lines += ["", "### Submission verdicts", "", "| Verdict | Files |", "|---------|-------|"] + for v in stats["verdicts"]: + lines.append(f"| {v['verdict']} | {v['files']} |") + lines += [ + "", + f"*Generated by `scripts/generate_progress.py`. Do not edit this block by hand.*", + "", + PROGRESS_END, + ] + return "\n".join(lines) + + +def update_readme(stats: dict) -> str: + text = README.read_text(encoding="utf-8") + block = render_progress_block(stats) + if PROGRESS_START in text and PROGRESS_END in text: + new = re.sub( + re.escape(PROGRESS_START) + r".*?" + re.escape(PROGRESS_END), + block, + text, + flags=re.DOTALL, + ) + else: + # Insert the block right after the "## Progress Statistics" heading. + marker = "## 📊 Progress Statistics" + if marker in text: + head, tail = text.split(marker, 1) + # Drop the old static table up to the next "##" heading. + rest = re.sub(r".*?(\n## )", r"\1", tail, count=1, flags=re.DOTALL) + new = f"{head}{marker}\n\n{block}\n{rest}" + else: + new = text + "\n\n## 📊 Progress Statistics\n\n" + block + "\n" + return new + + +# --------------------------------------------------------------------------- # +# Docs generation (mkdocs-material) +# --------------------------------------------------------------------------- # + +def slugify_lang(lang: str) -> str: + return lang.lower().replace("+", "p").replace(" ", "-") + + +def build_docs(problems: dict[str, Problem], stats: dict) -> None: + pages = DOCS / "problems" + pages.mkdir(parents=True, exist_ok=True) + # Clear stale generated pages. + for old in pages.glob("*.md"): + old.unlink() + + for slug, prob in sorted(problems.items()): + write_problem_page(pages / f"{slug}.md", prob) + + write_problems_index(pages / "index.md", problems) + write_tags_index(DOCS / "tags.md", problems) + write_lessons_index(DOCS / "lessons.md", problems) + write_docs_home(DOCS / "index.md", stats, problems) + + +# Verdicts that are "wrong attempts" worth a lesson callout. +WRONG_VERDICTS = ("Wrong Answer", "Time Limit Exceeded", "Runtime Error", "Compile Error") + +# Cross-link footer (Related projects + profile hub) appended to every page. +RELATED_FOOTER = "\n".join( + [ + "---", + "", + "**Related:** " + "[HackerRank solution archive](https://github.com/aliammari1/Hackerrank_problems) · " + "[All my projects (profile hub)](https://github.com/aliammari1) · " + "[LeetCode profile](https://leetcode.com/aliammari1/)", + ] +) + + +def write_problem_page(path: Path, prob: Problem) -> None: + lines = [ + f"# {prob.seo_title}", + "", + f"[:material-open-in-new: Open on LeetCode]({prob.url}){{ .md-button }}", + "", + ] + # AI explanation, if the AI workflow produced one for any accepted attempt. + for s in prob.submissions: + expl = s.path.parent / "EXPLANATION.md" + if s.verdict == "Accepted" and expl.is_file(): + lines += ["## Explanation", "", expl.read_text(encoding="utf-8").strip(), ""] + break + + lines += ["## Verdict history", "", "| Verdict | Language | Submitted |", "|---------|----------|-----------|"] + for s in sorted(prob.submissions, key=lambda x: (VERDICT_ORDER.index(x.verdict) if x.verdict in VERDICT_ORDER else 99, x.timestamp)): + lines.append(f"| {s.verdict} | {s.language} | {s.timestamp} |") + lines.append("") + + # Group solutions into content tabs by language, accepted first. + lines += ["## Solutions", ""] + by_lang: dict[str, list[Submission]] = defaultdict(list) + for s in prob.submissions: + by_lang[s.language].append(s) + for lang, subs in by_lang.items(): + fence = EXT_LANG.get(subs[0].ext, ("", "text"))[1] + subs_sorted = sorted(subs, key=lambda x: (x.verdict != "Accepted", x.timestamp)) + lines.append(f'=== "{lang}"') + lines.append("") + for s in subs_sorted: + code = s.path.read_text(encoding="utf-8", errors="replace").rstrip() + lines.append(f" **{s.verdict}** — {s.timestamp}") + lines.append("") + lines.append(f" ```{fence}") + for codeline in code.splitlines(): + lines.append(f" {codeline}") + lines.append(" ```") + lines.append("") + lines.append(RELATED_FOOTER) + lines.append("") + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def write_problems_index(path: Path, problems: dict[str, Problem]) -> None: + lines = ["# All Problems", "", "| # | Problem | Status | Languages |", "|---|---------|--------|-----------|"] + for slug, prob in sorted(problems.items()): + status = "✅ Accepted" if prob.accepted else "⏳ Attempted" + langs = ", ".join(prob.languages) + num = str(prob.number) if prob.number is not None else "—" + lines.append(f"| {num} | [{prob.title}]({slug}.md) | {status} | {langs} |") + lines.append("") + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def write_lessons_index(path: Path, problems: dict[str, Problem]) -> None: + """Index of problems that have real failed attempts before the accept. + + This is the archive's signature page: the slugs where there is a documented + Wrong Answer / TLE / Runtime Error / Compile Error to learn from. + """ + rows: list[tuple[Problem, Counter[str]]] = [] + for prob in problems.values(): + wrong: Counter[str] = Counter() + for s in prob.submissions: + if s.verdict in WRONG_VERDICTS: + wrong[s.verdict] += 1 + if wrong: + rows.append((prob, wrong)) + + rows.sort(key=lambda r: (-sum(r[1].values()), r[0].slug)) + total_wrong = sum(sum(w.values()) for _, w in rows) + + lines = [ + "# Lessons / common mistakes", + "", + "The point of this archive: **every problem below was *not* solved on the", + "first try.** These pages keep the failed submissions — Wrong Answer, Time", + "Limit Exceeded, Runtime Error, Compile Error — next to the accepted one, so", + "you can read *what went wrong and why* instead of only a clean final answer.", + "", + f"- **Problems with at least one failed attempt:** {len(rows)}", + f"- **Total failed submissions preserved:** {total_wrong}", + "", + "| # | Problem | Failed attempts (by verdict) |", + "|---|---------|------------------------------|", + ] + for prob, wrong in rows: + num = str(prob.number) if prob.number is not None else "—" + detail = ", ".join(f"{v}: {wrong[v]}" for v in VERDICT_ORDER if wrong.get(v)) + lines.append(f"| {num} | [{prob.title}](problems/{prob.slug}.md) | {detail} |") + lines.append("") + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def write_tags_index(path: Path, problems: dict[str, Problem]) -> None: + """Index by language; AI pattern/difficulty tags slot in here when present.""" + by_lang: dict[str, list[Problem]] = defaultdict(list) + for prob in problems.values(): + for lang in prob.languages: + by_lang[lang].append(prob) + lines = ["# Index by Language", ""] + for lang in sorted(by_lang): + lines.append(f"## {lang}") + lines.append("") + for prob in sorted(by_lang[lang], key=lambda p: p.slug): + lines.append(f"- [{prob.title}](problems/{prob.slug}.md)") + lines.append("") + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def write_docs_home(path: Path, stats: dict, problems: dict[str, Problem]) -> None: + n = stats["unique_slugs"] + lines = [ + "# LeetCode Solutions Archive", + "", + "Every LeetCode problem I solved — **including my real Wrong-Answer / TLE", + "submissions and the lesson from each.** Not a clean-solutions dump: each page", + "keeps the failed attempts next to the accepted one so you can read the", + "*progression*, not just the final answer.", + "", + f"[:material-magnify: Browse all {n} problems (searchable)]" + "(problems/index.md){ .md-button .md-button--primary }", + "[:material-lightbulb-on: Lessons / common mistakes](lessons.md){ .md-button }", + "", + "> Use the search box (top right) to jump straight to any problem by number or", + "> name.", + "", + "## At a glance", + "", + f"- **Unique problems:** {stats['unique_slugs']}", + f"- **Total submissions:** {stats['total_files']}", + f"- **Accepted:** {stats['accepted_slugs']} ({stats['accepted_pct']}%)", + "", + "| Language | Files | Problems |", + "|----------|-------|----------|", + ] + for lang in stats["languages"]: + lines.append(f"| {lang['language']} | {lang['files']} | {lang['slugs']} |") + lines += [ + "", + "## Coming soon: the `leetcode-solutions` MCP (Wave 2)", + "", + "A bundled, offline **MCP server** that exposes this archive — 173 problems", + "*with their real wrong-answer history* — to any MCP-capable assistant. No", + "session cookie required (rivals need one). *\"The MCP that remembers what NOT", + "to do.\"*", + "", + RELATED_FOOTER, + "", + ] + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +# --------------------------------------------------------------------------- # + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--docs", action="store_true", help="also (re)build docs/ pages") + ap.add_argument( + "--check", + action="store_true", + help="verify README/progress.json are up to date; exit 1 if stale", + ) + args = ap.parse_args() + + problems = scan() + stats = compute_stats(problems) + pj = progress_json(stats) + new_readme = update_readme(stats) + pj_text = json.dumps(pj, indent=2) + "\n" + + print( + f"slugs={stats['unique_slugs']} files={stats['total_files']} " + f"accepted={stats['accepted_slugs']} ({stats['accepted_pct']}%)" + ) + for lang in stats["languages"]: + print(f" {lang['language']:<12} files={lang['files']:<4} slugs={lang['slugs']}") + + if args.check: + stale = [] + current_readme = README.read_text(encoding="utf-8") + if current_readme != new_readme: + stale.append("README.md progress block") + if not PROGRESS_JSON.is_file() or PROGRESS_JSON.read_text(encoding="utf-8") != pj_text: + stale.append("docs/progress.json") + if stale: + print("STALE (run scripts/generate_progress.py): " + ", ".join(stale), file=sys.stderr) + return 1 + print("OK: generated artifacts are up to date.") + return 0 + + DOCS.mkdir(parents=True, exist_ok=True) + PROGRESS_JSON.write_text(pj_text, encoding="utf-8") + README.write_text(new_readme, encoding="utf-8") + print(f"wrote {PROGRESS_JSON.relative_to(ROOT)} and updated README.md") + + if args.docs: + build_docs(problems, stats) + print(f"wrote docs/ pages for {len(problems)} problems") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/problem_numbers.json b/scripts/problem_numbers.json new file mode 100644 index 0000000..74ae9bc --- /dev/null +++ b/scripts/problem_numbers.json @@ -0,0 +1,176 @@ +{ + "_comment": "Canonical LeetCode problem numbers per slug. Used by generate_progress.py to build exact-match SEO page titles ('<#>. — Solution'). Slugs absent here fall back to ' — Solution'. Keep alphabetical by slug.", + "add-binary": 67, + "add-two-integers": 2235, + "add-two-numbers": 2, + "array-partition": 561, + "article-views-i": 1148, + "average-salary-excluding-the-minimum-and-maximum-salary": 1484, + "average-time-of-process-per-machine": 1661, + "backspace-string-compare": 844, + "baseball-game": 682, + "best-time-to-buy-and-sell-stock": 121, + "best-time-to-buy-and-sell-stock-ii": 122, + "big-countries": 595, + "binary-search": 704, + "binary-search-tree-iterator": 173, + "binary-tree-inorder-traversal": 94, + "binary-tree-level-order-traversal": 102, + "binary-tree-postorder-traversal": 145, + "binary-tree-preorder-traversal": 144, + "binary-tree-right-side-view": 199, + "build-array-from-permutation": 1920, + "can-make-arithmetic-progression-from-sequence": 1502, + "can-place-flowers": 605, + "check-if-a-string-is-an-acronym-of-words": 2828, + "check-if-n-and-its-double-exist": 1346, + "climbing-stairs": 70, + "combine-two-tables": 175, + "concatenation-of-array": 1929, + "contains-duplicate": 217, + "convert-binary-number-in-a-linked-list-to-integer": 1290, + "convert-the-temperature": 2469, + "copy-list-with-random-pointer": 138, + "count-odd-numbers-in-an-interval-range": 1523, + "count-pairs-whose-sum-is-less-than-target": 2824, + "customers-who-never-order": 183, + "customer-who-visited-but-did-not-make-any-transactions": 1581, + "daily-leads-and-partners": 1693, + "daily-temperatures": 739, + "delete-duplicate-emails": 196, + "delete-the-middle-node-of-a-linked-list": 2095, + "design-circular-queue": 622, + "design-linked-list": 707, + "determine-the-minimum-sum-of-a-k-avoiding-array": 2829, + "diagonal-traverse": 498, + "duplicate-emails": 182, + "duplicate-zeros": 1089, + "employee-bonus": 577, + "employees-earning-more-than-their-managers": 181, + "fibonacci-number": 509, + "find-all-numbers-disappeared-in-an-array": 448, + "find-customer-referee": 584, + "find-numbers-with-even-number-of-digits": 1295, + "find-pivot-index": 724, + "find-the-difference": 389, + "find-the-index-of-the-first-occurrence-in-a-string": 28, + "find-total-time-spent-by-each-employee": 1741, + "first-bad-version": 278, + "fizz-buzz": 412, + "flatten-a-multilevel-doubly-linked-list": 430, + "game-play-analysis-i": 511, + "greatest-common-divisor-of-strings": 1071, + "group-anagrams": 49, + "guess-number-higher-or-lower": 374, + "height-checker": 1051, + "html-entity-parser": 1410, + "increasing-triplet-subsequence": 334, + "insert-greatest-common-divisors-in-linked-list": 2807, + "insert-into-a-binary-search-tree": 701, + "intersection-of-two-linked-lists": 160, + "invalid-tweets": 1683, + "isomorphic-strings": 205, + "is-subsequence": 392, + "kids-with-the-greatest-number-of-candies": 1431, + "k-th-symbol-in-grammar": 779, + "largest-number-at-least-twice-of-others": 747, + "largest-perimeter-triangle": 976, + "length-of-last-word": 58, + "linked-list-cycle": 141, + "linked-list-cycle-ii": 142, + "longest-common-prefix": 14, + "majority-element": 169, + "managers-with-at-least-5-direct-reports": 570, + "matrix-diagonal-sum": 1572, + "max-consecutive-ones": 485, + "maximum-average-subarray-i": 643, + "maximum-depth-of-binary-tree": 104, + "maximum-depth-of-n-ary-tree": 559, + "maximum-twin-sum-of-a-linked-list": 2130, + "max-pair-sum-in-an-array": 2815, + "median-of-two-sorted-arrays": 4, + "merge-nodes-in-between-zeros": 2181, + "merge-sorted-array": 88, + "merge-strings-alternately": 1768, + "merge-two-sorted-lists": 21, + "middle-of-the-linked-list": 876, + "minimum-size-subarray-sum": 209, + "min-stack": 155, + "monotonic-array": 896, + "move-zeroes": 283, + "n-ary-tree-level-order-traversal": 429, + "n-ary-tree-postorder-traversal": 590, + "n-ary-tree-preorder-traversal": 589, + "not-boring-movies": 620, + "number-of-1-bits": 191, + "number-of-employees-who-met-the-target": 2890, + "number-of-good-pairs": 1512, + "number-of-steps-to-reduce-a-number-to-zero": 1342, + "number-of-unique-subjects-taught-by-each-teacher": 2356, + "odd-even-linked-list": 328, + "palindrome-linked-list": 234, + "palindrome-number": 9, + "pascals-triangle": 118, + "pascals-triangle-ii": 119, + "path-sum": 112, + "plus-one": 66, + "populating-next-right-pointers-in-each-node": 116, + "populating-next-right-pointers-in-each-node-ii": 117, + "power-of-four": 342, + "power-of-three": 326, + "power-of-two": 231, + "powx-n": 50, + "product-of-array-except-self": 238, + "product-sales-analysis-i": 1068, + "recyclable-and-low-fat-products": 1757, + "remove-duplicates-from-sorted-array": 26, + "remove-duplicates-from-sorted-array-ii": 80, + "remove-duplicates-from-sorted-list": 83, + "remove-duplicates-from-sorted-list-ii": 82, + "remove-element": 27, + "remove-linked-list-elements": 203, + "remove-nth-node-from-end-of-list": 19, + "repeated-substring-pattern": 459, + "replace-elements-with-greatest-element-on-right-side": 1299, + "replace-employee-id-with-the-unique-identifier": 1378, + "reverse-integer": 7, + "reverse-linked-list": 206, + "reverse-string": 344, + "reverse-vowels-of-a-string": 345, + "reverse-words-in-a-string": 151, + "reverse-words-in-a-string-iii": 557, + "richest-customer-wealth": 1672, + "rising-temperature": 197, + "robot-return-to-origin": 657, + "roman-to-integer": 13, + "rotate-array": 189, + "rotate-list": 61, + "running-sum-of-1d-array": 1480, + "same-tree": 100, + "score-of-a-string": 3110, + "search-a-2d-matrix": 74, + "search-in-a-binary-search-tree": 700, + "search-in-rotated-sorted-array": 33, + "search-insert-position": 35, + "sign-of-the-product-of-an-array": 1822, + "single-number": 136, + "sort-array-by-parity": 905, + "sort-characters-by-frequency": 451, + "spiral-matrix": 54, + "sqrtx": 69, + "squares-of-a-sorted-array": 977, + "string-to-integer-atoi": 8, + "students-and-examinations": 1280, + "subtract-the-product-and-sum-of-digits-of-an-integer": 1281, + "swap-nodes-in-pairs": 24, + "symmetric-tree": 101, + "third-maximum-number": 414, + "to-lower-case": 709, + "two-sum": 1, + "two-sum-ii-input-array-is-sorted": 167, + "unique-number-of-occurrences": 1207, + "valid-anagram": 242, + "valid-mountain-array": 941, + "valid-parentheses": 20, + "validate-binary-search-tree": 98 +}