feat: add initial frontend and backend dependencies

This commit is contained in:
hissin 2025-11-20 14:28:41 +08:00
commit 3dedaf0899
41 changed files with 4412 additions and 0 deletions

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Python
__pycache__/
*.py[cod]
*$py.class
.venv/
venv/
ENV/
env/
.env
*.egg-info/
.coverage
htmlcov/
.pytest_cache/
# Node.js
node_modules/
dist/
build/
.npm
.eslintcache
# OS / IDE
.DS_Store
.idea/
.vscode/
*.swp
*.swo
thumbs.db

42
Dockerfile Normal file
View File

@ -0,0 +1,42 @@
# Multi-stage Dockerfile for Algorithm Platform
# Stage 1: Build Frontend
FROM node:18-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build
# Stage 2: Setup Backend
FROM python:3.11-slim AS backend
WORKDIR /app/backend
COPY backend/requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ ./
# Stage 3: Final - Nginx + Backend
FROM nginx:alpine
WORKDIR /app
# Install Python and dependencies in final stage
RUN apk add --no-cache python3 py3-pip
# Copy backend
COPY --from=backend /app/backend /app/backend
COPY --from=backend /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
# Copy frontend build to Nginx
COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html
# Copy Nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Expose ports
EXPOSE 80
# Start script
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
CMD ["/docker-entrypoint.sh"]

49
README.md Normal file
View File

@ -0,0 +1,49 @@
# Algorithm Test Case Generation and Verification Platform
## Project Structure
- `backend/`: FastAPI Python Backend
- `frontend/`: React Vite Frontend
## Prerequisites
- Python 3.8+
- Node.js 16+
## Setup & Run
### Quick Start with Docker (Recommended)
```bash
docker-compose up --build
```
Then access:
- **Frontend**: http://localhost
- **Backend API**: http://localhost:8000
- **API Docs**: http://localhost:8000/docs
### Manual Setup
### 1. Backend
```bash
cd backend
# Optional: Create virtual env
# python -m venv venv
# source venv/bin/activate
pip install -r requirements.txt
uvicorn main:app --reload
```
Backend will run at `http://localhost:8000`
### 2. Frontend
```bash
cd frontend
npm install
npm run dev
```
Frontend will run at `http://localhost:5173`
## Features Implemented
1. **Maximum Subarray Sum**:
- Brute Force ($O(N^2)$)
- Divide & Conquer ($O(N \log N)$)
- Kadane's Algorithm ($O(N)$)
- Automatic Random Case Generation
- Performance Benchmarking & Visualization

View File

@ -0,0 +1,28 @@
from abc import ABC, abstractmethod
from typing import Any, Dict, List
class BaseProblem(ABC):
"""
Abstract base class for all algorithm problems.
"""
@abstractmethod
def generate_case(self, **kwargs) -> Dict[str, Any]:
"""
Generate a random test case based on parameters.
"""
pass
@abstractmethod
def solve(self, input_data: Any, algorithm: str) -> Any:
"""
Solve the problem using the specified algorithm.
"""
pass
@abstractmethod
def verify(self, input_data: Any, result: Any) -> bool:
"""
Verify the correctness of the result (optional, or compare with standard answer).
"""
pass

View File

@ -0,0 +1,17 @@
import time
import functools
from typing import Tuple, Any
def time_execution(func):
"""
Decorator to measure execution time of a function.
Returns a tuple (result, execution_time_in_seconds).
"""
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Tuple[Any, float]:
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
execution_time = end_time - start_time
return result, execution_time
return wrapper

26
backend/main.py Normal file
View File

@ -0,0 +1,26 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routers import max_subarray_router
app = FastAPI(title="Algorithm Platform API")
# Allow CORS for frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, replace with specific frontend origin
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def read_root():
return {"message": "Welcome to Algorithm Verification Platform API"}
app.include_router(max_subarray_router.router)
from routers import interval_scheduling_router
app.include_router(interval_scheduling_router.router)
from routers import n_queens_router
app.include_router(n_queens_router.router)
from routers import knapsack_router
app.include_router(knapsack_router.router)

View File

View File

@ -0,0 +1,17 @@
import random
from typing import List, Tuple
def generate_interval_scheduling_case(size: int = 10, min_time: int = 0, max_time: int = 100) -> List[Tuple[int, int]]:
"""
Generates a list of random intervals (start, end).
"""
intervals = []
for _ in range(size):
start = random.randint(min_time, max_time - 1)
# Ensure end > start
duration = random.randint(1, (max_time - start) // 2 + 1 if (max_time - start) > 2 else 1)
end = min(start + duration, max_time)
if end <= start:
end = start + 1
intervals.append((start, end))
return intervals

View File

@ -0,0 +1,39 @@
from typing import Any, Dict, List
from common.base_problem import BaseProblem
from problems.interval_scheduling.generator import generate_interval_scheduling_case
from problems.interval_scheduling.solvers import solve_greedy, solve_brute_force
from common.timer_utils import time_execution
class IntervalSchedulingProblem(BaseProblem):
def generate_case(self, size: int = 10, min_time: int = 0, max_time: int = 100) -> Dict[str, Any]:
intervals = generate_interval_scheduling_case(size, min_time, max_time)
return {"intervals": intervals}
def solve(self, input_data: Dict[str, Any], algorithm: str) -> Dict[str, Any]:
intervals = input_data.get("intervals", [])
# Ensure intervals are tuples
intervals = [tuple(x) for x in intervals]
result = None
duration = 0.0
if algorithm == "greedy":
result, duration = time_execution(solve_greedy)(intervals)
elif algorithm == "brute_force":
result, duration = time_execution(solve_brute_force)(intervals)
else:
raise ValueError(f"Unknown algorithm: {algorithm}")
return {
"algorithm": algorithm,
"result_count": len(result),
"result_intervals": result,
"time_seconds": duration
}
def verify(self, input_data: Any, result: Any) -> bool:
# Use Greedy as the source of truth (it is optimal for this problem)
intervals = input_data.get("intervals", [])
intervals = [tuple(x) for x in intervals]
truth, _ = time_execution(solve_greedy)(intervals)
return result.get("result_count") == len(truth)

View File

@ -0,0 +1,60 @@
from typing import List, Tuple
def solve_greedy(intervals: List[Tuple[int, int]]) -> List[Tuple[int, int]]:
"""
Greedy algorithm: Sort by end time, pick non-overlapping.
O(N log N)
"""
if not intervals:
return []
# Sort by end time
sorted_intervals = sorted(intervals, key=lambda x: x[1])
selected = []
last_end_time = -float('inf')
for start, end in sorted_intervals:
if start >= last_end_time:
selected.append((start, end))
last_end_time = end
return selected
def solve_brute_force(intervals: List[Tuple[int, int]]) -> List[Tuple[int, int]]:
"""
Brute Force algorithm: Try all combinations.
O(2^N) - Only for small inputs!
"""
n = len(intervals)
max_count = 0
best_subset = []
# Helper to check if a subset of intervals is valid (non-overlapping)
def is_valid(subset):
sorted_subset = sorted(subset, key=lambda x: x[1])
last_end = -float('inf')
for start, end in sorted_subset:
if start < last_end:
return False
last_end = end
return True
# Iterate through all 2^N subsets (using bitmask)
# Limit N to avoid hanging
if n > 20:
# Fallback or error for safety, though we handle this in router usually
return solve_greedy(intervals) # Cheating slightly for safety if called directly with large N
for i in range(1 << n):
subset = []
for j in range(n):
if (i >> j) & 1:
subset.append(intervals[j])
if is_valid(subset):
if len(subset) > max_count:
max_count = len(subset)
best_subset = subset
return best_subset

View File

@ -0,0 +1,21 @@
import random
from typing import List, Dict, Any
def generate_knapsack_case(num_items: int = 10, max_weight: int = 50, max_value: int = 100, capacity_ratio: float = 0.5) -> Dict[str, Any]:
"""
Generates a case for 0/1 Knapsack.
"""
items = []
total_weight = 0
for i in range(num_items):
w = random.randint(1, max_weight)
v = random.randint(1, max_value)
items.append({"id": i, "weight": w, "value": v})
total_weight += w
capacity = int(total_weight * capacity_ratio)
return {
"items": items,
"capacity": capacity
}

View File

@ -0,0 +1,37 @@
from typing import Any, Dict, List
from common.base_problem import BaseProblem
from problems.knapsack.generator import generate_knapsack_case
from problems.knapsack.solvers import solve_dp, solve_backtracking
from common.timer_utils import time_execution
class KnapsackProblem(BaseProblem):
def generate_case(self, num_items: int = 10, max_weight: int = 50, max_value: int = 100) -> Dict[str, Any]:
return generate_knapsack_case(num_items, max_weight, max_value)
def solve(self, input_data: Dict[str, Any], algorithm: str) -> Dict[str, Any]:
items = input_data.get("items", [])
capacity = input_data.get("capacity", 0)
result = None
duration = 0.0
if algorithm == "dp":
result, duration = time_execution(solve_dp)(items, capacity)
elif algorithm == "backtracking":
result, duration = time_execution(solve_backtracking)(items, capacity)
else:
raise ValueError(f"Unknown algorithm: {algorithm}")
return {
"algorithm": algorithm,
"max_value": result["max_value"],
"selected_items": result["selected_items"],
"time_seconds": duration
}
def verify(self, input_data: Any, result: Any) -> bool:
# Use DP as source of truth
items = input_data.get("items", [])
capacity = input_data.get("capacity", 0)
truth, _ = time_execution(solve_dp)(items, capacity)
return result["max_value"] == truth["max_value"]

View File

@ -0,0 +1,73 @@
from typing import List, Dict, Any, Tuple
def solve_dp(items: List[Dict[str, int]], capacity: int) -> Dict[str, Any]:
"""
Dynamic Programming solution for 0/1 Knapsack.
O(N * W)
"""
n = len(items)
# dp[i][w] = max value using first i items with capacity w
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
item = items[i-1]
w, v = item["weight"], item["value"]
for j in range(capacity + 1):
if w <= j:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v)
else:
dp[i][j] = dp[i-1][j]
max_value = dp[n][capacity]
# Backtrack to find selected items
selected_indices = []
w = capacity
for i in range(n, 0, -1):
if dp[i][w] != dp[i-1][w]:
item = items[i-1]
selected_indices.append(item["id"])
w -= item["weight"]
return {
"max_value": max_value,
"selected_items": selected_indices
}
def solve_backtracking(items: List[Dict[str, int]], capacity: int) -> Dict[str, Any]:
"""
Backtracking solution for 0/1 Knapsack.
O(2^N)
"""
n = len(items)
max_val = 0
best_selection = []
def backtrack(index, current_weight, current_value, selection):
nonlocal max_val, best_selection
if current_weight > capacity:
return
if current_value > max_val:
max_val = current_value
best_selection = selection[:]
if index == n:
return
# Include current item
item = items[index]
if current_weight + item["weight"] <= capacity:
selection.append(item["id"])
backtrack(index + 1, current_weight + item["weight"], current_value + item["value"], selection)
selection.pop()
# Exclude current item
backtrack(index + 1, current_weight, current_value, selection)
backtrack(0, 0, 0, [])
return {
"max_value": max_val,
"selected_items": best_selection
}

View File

@ -0,0 +1,8 @@
import random
from typing import List, Dict
def generate_max_subarray_case(size: int = 100, min_val: int = -100, max_val: int = 100) -> List[int]:
"""
Generate a list of random integers.
"""
return [random.randint(min_val, max_val) for _ in range(size)]

View File

@ -0,0 +1,35 @@
from typing import Any, Dict, List
from common.base_problem import BaseProblem
from problems.max_subarray.generator import generate_max_subarray_case
from problems.max_subarray.solvers import solve_brute_force, solve_divide_conquer, solve_kadane
from common.timer_utils import time_execution
class MaxSubarrayProblem(BaseProblem):
def generate_case(self, size: int = 100, min_val: int = -100, max_val: int = 100) -> Dict[str, Any]:
arr = generate_max_subarray_case(size, min_val, max_val)
return {"array": arr}
def solve(self, input_data: Dict[str, Any], algorithm: str) -> Dict[str, Any]:
arr = input_data.get("array", [])
result = None
duration = 0.0
if algorithm == "brute_force":
result, duration = time_execution(solve_brute_force)(arr)
elif algorithm == "divide_conquer":
result, duration = time_execution(solve_divide_conquer)(arr)
elif algorithm == "kadane":
result, duration = time_execution(solve_kadane)(arr)
else:
raise ValueError(f"Unknown algorithm: {algorithm}")
return {
"algorithm": algorithm,
"result": result,
"time_seconds": duration
}
def verify(self, input_data: Any, result: Any) -> bool:
# Use Kadane as the source of truth
truth, _ = time_execution(solve_kadane)(input_data.get("array", []))
return result == truth

View File

@ -0,0 +1,61 @@
from typing import List, Tuple
# 1. Brute Force O(N^2)
def solve_brute_force(arr: List[int]) -> int:
max_sum = float('-inf')
n = len(arr)
for i in range(n):
current_sum = 0
for j in range(i, n):
current_sum += arr[j]
if current_sum > max_sum:
max_sum = current_sum
return max_sum
# 2. Divide and Conquer O(N log N)
def solve_divide_conquer(arr: List[int]) -> int:
def max_crossing_sum(arr, low, mid, high):
left_sum = float('-inf')
curr_sum = 0
for i in range(mid, low - 1, -1):
curr_sum += arr[i]
if curr_sum > left_sum:
left_sum = curr_sum
right_sum = float('-inf')
curr_sum = 0
for i in range(mid + 1, high + 1):
curr_sum += arr[i]
if curr_sum > right_sum:
right_sum = curr_sum
return left_sum + right_sum
def solve_recursive(arr, low, high):
if low == high:
return arr[low]
mid = (low + high) // 2
return max(
solve_recursive(arr, low, mid),
solve_recursive(arr, mid + 1, high),
max_crossing_sum(arr, low, mid, high)
)
if not arr:
return 0
return solve_recursive(arr, 0, len(arr) - 1)
# 3. Dynamic Programming (Kadane's Algorithm) O(N)
def solve_kadane(arr: List[int]) -> int:
if not arr:
return 0
max_so_far = arr[0]
curr_max = arr[0]
for i in range(1, len(arr)):
curr_max = max(arr[i], curr_max + arr[i])
max_so_far = max(max_so_far, curr_max)
return max_so_far

View File

@ -0,0 +1,8 @@
from typing import Dict, Any
def generate_n_queens_case(n: int = 8) -> Dict[str, int]:
"""
Generates a case for N-Queens.
Actually just returns N since the problem is defined by the board size.
"""
return {"n": n}

View File

@ -0,0 +1,48 @@
from typing import Any, Dict, List
from common.base_problem import BaseProblem
from problems.n_queens.generator import generate_n_queens_case
from problems.n_queens.solvers import solve_backtracking
from common.timer_utils import time_execution
class NQueensProblem(BaseProblem):
def generate_case(self, n: int = 8) -> Dict[str, Any]:
return generate_n_queens_case(n)
def solve(self, input_data: Dict[str, Any], algorithm: str) -> Dict[str, Any]:
n = input_data.get("n", 8)
result = None
duration = 0.0
if algorithm == "backtracking":
# For visualization, we usually just want one solution,
# but for benchmarking maybe we want to count all?
# Let's default to finding ONE solution for "solve" to be fast enough for UI
# If N is small (< 12), we can find all.
find_all = n < 10
result, duration = time_execution(solve_backtracking)(n, find_all)
else:
raise ValueError(f"Unknown algorithm: {algorithm}")
return {
"algorithm": algorithm,
"solution_count": len(result),
"first_solution": result[0] if result else None,
"time_seconds": duration
}
def verify(self, input_data: Any, result: Any) -> bool:
# Verification is tricky without a ground truth.
# We can check if the returned solution is valid.
sol = result.get("first_solution")
if not sol:
return True # No solution found (could be valid for some N, though N>=4 always has sol)
n = len(sol)
for r in range(n):
c = sol[r]
# Check conflicts
for prev_r in range(r):
prev_c = sol[prev_r]
if c == prev_c or abs(r - prev_r) == abs(c - prev_c):
return False
return True

View File

@ -0,0 +1,38 @@
from typing import List, Tuple, Optional
def solve_backtracking(n: int, find_all: bool = False) -> List[List[int]]:
"""
Backtracking algorithm for N-Queens.
Returns a list of solutions. Each solution is a list of column indices for each row.
"""
solutions = []
board = [-1] * n # board[row] = col
cols = [False] * n
diag1 = [False] * (2 * n - 1) # row + col
diag2 = [False] * (2 * n - 1) # row - col + (n - 1)
def backtrack(row: int):
if row == n:
solutions.append(board[:])
return True
for col in range(n):
if not cols[col] and not diag1[row + col] and not diag2[row - col + n - 1]:
board[row] = col
cols[col] = True
diag1[row + col] = True
diag2[row - col + n - 1] = True
if backtrack(row + 1):
if not find_all:
return True
# Backtrack
cols[col] = False
diag1[row + col] = False
diag2[row - col + n - 1] = False
return False
backtrack(0)
return solutions

5
backend/requirements.txt Normal file
View File

@ -0,0 +1,5 @@
fastapi
uvicorn
matplotlib
pandas
numpy

View File

View File

@ -0,0 +1,75 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict, Any, Tuple
from problems.interval_scheduling.problem import IntervalSchedulingProblem
router = APIRouter(
prefix="/interval_scheduling",
tags=["interval_scheduling"],
responses={404: {"description": "Not found"}},
)
problem_solver = IntervalSchedulingProblem()
class GenerateRequest(BaseModel):
size: int = 10
min_time: int = 0
max_time: int = 100
class SolveRequest(BaseModel):
intervals: List[Tuple[int, int]]
algorithms: List[str] = ["greedy", "brute_force"]
class BenchmarkRequest(BaseModel):
sizes: List[int] = [5, 10, 15, 20]
algorithms: List[str] = ["greedy", "brute_force"]
@router.post("/generate")
async def generate_case(req: GenerateRequest):
return problem_solver.generate_case(size=req.size, min_time=req.min_time, max_time=req.max_time)
@router.post("/solve")
async def solve_case(req: SolveRequest):
input_data = {"intervals": req.intervals}
results = []
for algo in req.algorithms:
try:
# Skip Brute Force for large inputs to avoid timeout
if len(req.intervals) > 22 and algo == "brute_force":
results.append({
"algorithm": algo,
"error": "Input too large for Brute Force (limit 22)",
"skipped": True
})
continue
res = problem_solver.solve(input_data, algo)
results.append(res)
except ValueError as e:
results.append({"algorithm": algo, "error": str(e)})
return results
@router.post("/benchmark")
async def benchmark(req: BenchmarkRequest):
benchmark_results = []
for size in req.sizes:
input_data = problem_solver.generate_case(size=size)
size_result = {"size": size, "algorithms": []}
for algo in req.algorithms:
try:
if size > 22 and algo == "brute_force":
size_result["algorithms"].append({
"algorithm": algo,
"time_seconds": None,
"skipped": True
})
continue
res = problem_solver.solve(input_data, algo)
size_result["algorithms"].append(res)
except Exception as e:
size_result["algorithms"].append({"algorithm": algo, "error": str(e)})
benchmark_results.append(size_result)
return benchmark_results

View File

@ -0,0 +1,76 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from problems.knapsack.problem import KnapsackProblem
router = APIRouter(
prefix="/knapsack",
tags=["knapsack"],
responses={404: {"description": "Not found"}},
)
problem_solver = KnapsackProblem()
class GenerateRequest(BaseModel):
num_items: int = 10
max_weight: int = 50
max_value: int = 100
class SolveRequest(BaseModel):
items: List[Dict[str, int]]
capacity: int
algorithms: List[str] = ["dp", "backtracking"]
class BenchmarkRequest(BaseModel):
sizes: List[int] = [10, 20, 30]
algorithms: List[str] = ["dp", "backtracking"]
@router.post("/generate")
async def generate_case(req: GenerateRequest):
return problem_solver.generate_case(num_items=req.num_items, max_weight=req.max_weight, max_value=req.max_value)
@router.post("/solve")
async def solve_case(req: SolveRequest):
input_data = {"items": req.items, "capacity": req.capacity}
results = []
for algo in req.algorithms:
try:
# Skip Backtracking for large inputs
if len(req.items) > 22 and algo == "backtracking":
results.append({
"algorithm": algo,
"error": "Input too large for Backtracking (limit 22)",
"skipped": True
})
continue
res = problem_solver.solve(input_data, algo)
results.append(res)
except ValueError as e:
results.append({"algorithm": algo, "error": str(e)})
return results
@router.post("/benchmark")
async def benchmark(req: BenchmarkRequest):
benchmark_results = []
for size in req.sizes:
input_data = problem_solver.generate_case(num_items=size)
size_result = {"size": size, "algorithms": []}
for algo in req.algorithms:
try:
if size > 22 and algo == "backtracking":
size_result["algorithms"].append({
"algorithm": algo,
"time_seconds": None,
"skipped": True
})
continue
res = problem_solver.solve(input_data, algo)
size_result["algorithms"].append(res)
except Exception as e:
size_result["algorithms"].append({"algorithm": algo, "error": str(e)})
benchmark_results.append(size_result)
return benchmark_results

View File

@ -0,0 +1,70 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from problems.max_subarray.problem import MaxSubarrayProblem
router = APIRouter(
prefix="/max_subarray",
tags=["max_subarray"],
responses={404: {"description": "Not found"}},
)
problem_solver = MaxSubarrayProblem()
class GenerateRequest(BaseModel):
size: int = 100
min_val: int = -100
max_val: int = 100
class SolveRequest(BaseModel):
array: List[int]
algorithms: List[str] = ["kadane", "divide_conquer", "brute_force"]
class BenchmarkRequest(BaseModel):
sizes: List[int] = [10, 50, 100, 500, 1000]
algorithms: List[str] = ["kadane", "divide_conquer", "brute_force"]
@router.post("/generate")
async def generate_case(req: GenerateRequest):
return problem_solver.generate_case(size=req.size, min_val=req.min_val, max_val=req.max_val)
@router.post("/solve")
async def solve_case(req: SolveRequest):
input_data = {"array": req.array}
results = []
for algo in req.algorithms:
try:
res = problem_solver.solve(input_data, algo)
results.append(res)
except ValueError as e:
results.append({"algorithm": algo, "error": str(e)})
return results
@router.post("/benchmark")
async def benchmark(req: BenchmarkRequest):
benchmark_results = []
for size in req.sizes:
# Generate a random case for this size
# To reduce noise, we might want to run multiple times and average,
# but for now single run per size is fine for demonstration
input_data = problem_solver.generate_case(size=size)
size_result = {"size": size, "algorithms": []}
for algo in req.algorithms:
try:
# Skip O(N^2) for very large inputs to avoid timeout
if size > 5000 and algo == "brute_force":
size_result["algorithms"].append({
"algorithm": algo,
"time_seconds": None,
"skipped": True
})
continue
res = problem_solver.solve(input_data, algo)
size_result["algorithms"].append(res)
except Exception as e:
size_result["algorithms"].append({"algorithm": algo, "error": str(e)})
benchmark_results.append(size_result)
return benchmark_results

View File

@ -0,0 +1,66 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from problems.n_queens.problem import NQueensProblem
router = APIRouter(
prefix="/n_queens",
tags=["n_queens"],
responses={404: {"description": "Not found"}},
)
problem_solver = NQueensProblem()
class GenerateRequest(BaseModel):
n: int = 8
class SolveRequest(BaseModel):
n: int
algorithms: List[str] = ["backtracking"]
class BenchmarkRequest(BaseModel):
sizes: List[int] = [4, 8, 10, 12]
algorithms: List[str] = ["backtracking"]
@router.post("/generate")
async def generate_case(req: GenerateRequest):
return problem_solver.generate_case(n=req.n)
@router.post("/solve")
async def solve_case(req: SolveRequest):
input_data = {"n": req.n}
results = []
for algo in req.algorithms:
try:
res = problem_solver.solve(input_data, algo)
results.append(res)
except ValueError as e:
results.append({"algorithm": algo, "error": str(e)})
return results
@router.post("/benchmark")
async def benchmark(req: BenchmarkRequest):
benchmark_results = []
for size in req.sizes:
input_data = {"n": size}
size_result = {"size": size, "algorithms": []}
for algo in req.algorithms:
try:
# Skip large N for backtracking if finding all solutions (though our solve defaults to one for large N)
# But for benchmark we might want to be careful.
if size > 14 and algo == "backtracking":
size_result["algorithms"].append({
"algorithm": algo,
"time_seconds": None,
"skipped": True
})
continue
res = problem_solver.solve(input_data, algo)
size_result["algorithms"].append(res)
except Exception as e:
size_result["algorithms"].append({"algorithm": algo, "error": str(e)})
benchmark_results.append(size_result)
return benchmark_results

58
backend/verify_all.py Normal file
View File

@ -0,0 +1,58 @@
import sys
import os
import random
# Add backend to path
sys.path.append(os.path.abspath("/Users/hissin/测试/25-cxsj-final/backend"))
from problems.max_subarray.problem import MaxSubarrayProblem
from problems.interval_scheduling.problem import IntervalSchedulingProblem
from problems.n_queens.problem import NQueensProblem
from problems.knapsack.problem import KnapsackProblem
def test_max_subarray():
print("\nTesting Max Subarray...")
prob = MaxSubarrayProblem()
case = prob.generate_case(size=10)
res = prob.solve(case, "kadane")
print(f"Max Subarray Result: {res['result']}")
assert prob.verify(case, res['result'])
print("Max Subarray Passed!")
def test_interval_scheduling():
print("\nTesting Interval Scheduling...")
prob = IntervalSchedulingProblem()
case = prob.generate_case(size=10)
res = prob.solve(case, "greedy")
print(f"Interval Scheduling Count: {res['result_count']}")
assert prob.verify(case, res)
print("Interval Scheduling Passed!")
def test_n_queens():
print("\nTesting N-Queens...")
prob = NQueensProblem()
case = prob.generate_case(n=8)
res = prob.solve(case, "backtracking")
print(f"N-Queens (N=8) Solutions: {res['solution_count']}")
assert res['solution_count'] == 92
print("N-Queens Passed!")
def test_knapsack():
print("\nTesting Knapsack...")
prob = KnapsackProblem()
case = prob.generate_case(num_items=10, max_weight=20, max_value=100)
res_dp = prob.solve(case, "dp")
res_back = prob.solve(case, "backtracking")
print(f"Knapsack DP Value: {res_dp['max_value']}")
print(f"Knapsack Backtracking Value: {res_back['max_value']}")
assert res_dp['max_value'] == res_back['max_value']
assert prob.verify(case, res_dp)
print("Knapsack Passed!")
if __name__ == "__main__":
test_max_subarray()
test_interval_scheduling()
test_n_queens()
test_knapsack()

61
backend/verify_backend.py Normal file
View File

@ -0,0 +1,61 @@
import sys
import os
# Add backend to path
sys.path.append(os.path.abspath("/Users/hissin/测试/25-cxsj-final/backend"))
from problems.interval_scheduling.generator import generate_interval_scheduling_case
from problems.interval_scheduling.solvers import solve_greedy, solve_brute_force
from problems.interval_scheduling.problem import IntervalSchedulingProblem
def test_generator():
print("Testing Generator...")
intervals = generate_interval_scheduling_case(size=10, min_time=0, max_time=100)
print(f"Generated {len(intervals)} intervals: {intervals}")
assert len(intervals) == 10
for start, end in intervals:
assert start < end
assert start >= 0
assert end <= 100
print("Generator Passed!")
def test_solvers():
print("\nTesting Solvers...")
# Known case
intervals = [(1, 3), (2, 4), (3, 5)]
# Greedy should pick (1,3) and (3,5) -> count 2
greedy_res = solve_greedy(intervals)
print(f"Greedy result: {greedy_res}")
assert len(greedy_res) == 2
brute_res = solve_brute_force(intervals)
print(f"Brute Force result: {brute_res}")
assert len(brute_res) == 2
# Complex case
intervals = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)]
greedy_res = solve_greedy(intervals)
brute_res = solve_brute_force(intervals)
print(f"Complex Greedy: {len(greedy_res)}")
print(f"Complex Brute: {len(brute_res)}")
assert len(greedy_res) == len(brute_res)
print("Solvers Passed!")
def test_problem_class():
print("\nTesting Problem Class...")
prob = IntervalSchedulingProblem()
case = prob.generate_case(size=5)
res_greedy = prob.solve(case, "greedy")
res_brute = prob.solve(case, "brute_force")
print(f"Class Greedy: {res_greedy}")
print(f"Class Brute: {res_brute}")
assert prob.verify(case, res_greedy)
assert res_greedy["result_count"] == res_brute["result_count"]
print("Problem Class Passed!")
if __name__ == "__main__":
test_generator()
test_solvers()
test_problem_class()

84
docs/DESIGN_DOCUMENT.md Normal file
View File

@ -0,0 +1,84 @@
# 算法测试用例生成与自我验证平台 - 设计文档
## 1. 项目概述
本项目旨在构建一个集“自动出题”、“多算法求解”、“性能分析”于一体的算法学习与验证平台。通过针对特定算法问题生成随机测试数据,并使用不同复杂度的算法进行求解,验证结果一致性并对比运行效率。项目包含一个现代化 Web 前端界面,用于交互式地配置参数与展示图表。
## 2. 系统架构设计
系统采用 **前后端分离** 架构:
### 2.1 核心模块
#### 后端 (Backend)
1. **Problem Interface (问题接口)**
* 定义问题的通用接口,包括 `generate_case()` (生成用例), `solve()` (求解), `verify()` (验证)。
2. **Solver Engine (求解引擎)**
* 针对每个问题实现多种算法策略暴力、分治、DP、贪心等
3. **Performance Analyzer (性能分析器)**
* 执行基准测试,验证正确性,统计运行时间。
4. **API Layer (API 层)**
* 基于 **FastAPI** 提供 RESTful 接口,处理前端的配置请求并返回计算结果。
#### 前端 (Frontend)
1. **Control Panel (控制面板)**
* 选择算法问题如“最大子数组和”、“0/1背包”
* 配置生成参数(数据规模、数值范围、稀疏度等)。
2. **Visualization (可视化展示)**
* 使用图表库展示不同算法的时间复杂度对比曲线。
* 展示具体的测试用例数据与解题结果(用于教学演示)。
### 2.2 技术栈
* **后端**: Python 3.x, FastAPI (Web框架), Matplotlib/Pandas (数据分析)
* **前端**: React, TailwindCSS (样式), Recharts (图表可视化), Axios (网络请求)
## 3. 选定算法问题
### 问题一:最大子数组和 (Maximum Subarray Sum)
**描述**: 给定一个整数数组,找到一个具有最大和的连续子数组。
**测试用例生成**:
* 随机生成长度为 $N$ 的整数数组。
* 数值范围可配置(包含正负数)。
**算法实现**:
1. **暴力枚举 (Brute Force)**: 双重循环枚举所有子数组,时间复杂度 $O(N^2)$。
2. **分治算法 (Divide and Conquer)**: 递归将数组分为左右两半,最大子数组可能在左、右或跨越中点,时间复杂度 $O(N \log N)$。
3. **动态规划 (Kadane's Algorithm)**: 线性扫描,时间复杂度 $O(N)$。
### 问题二0/1 背包问题 (0/1 Knapsack Problem)
**描述**: 给定一组物品每种物品都有自己的重量和价值在限定的总重量内选择其中若干个也即每种物品可以选0个或1个使得物品的总价值最高。
**测试用例生成**:
* 随机生成 $N$ 个物品,每个物品具有重量 $w$ 和价值 $v$。
* 随机生成背包容量 $C$。
**算法实现**:
1. **回溯法 (Backtracking)**: 递归枚举所有可能的选择情况,时间复杂度 $O(2^N)$。
2. **动态规划 (Dynamic Programming)**: 使用二维或一维数组记录状态,时间复杂度 $O(N \cdot C)$。
3. **贪心算法 (Greedy, 近似解/对比)**: 按价值密度排序选择注意0/1背包贪心法通常不能得到最优解可作为对比项展示“错误”或“近似”的算法或者用于对比部分背包问题。*注本项目中为了验证正确性主要对比回溯与DP贪心可作为错误示范分析。*
## 4. 目录结构规划
```
algorithm_platform/
├── README.md
├── DESIGN_DOCUMENT.md
├── backend/ # 后端代码
│ ├── main.py # FastAPI 入口
│ ├── common/ # 通用工具
│ │ ├── __init__.py
│ │ ├── base_problem.py
│ │ └── timer_utils.py
│ ├── problems/ # 算法实现模块
│ │ ├── __init__.py
│ │ ├── max_subarray/
│ │ └── knapsack/
│ └── routers/ # API 路由
└── frontend/ # 前端代码
├── src/
│ ├── components/ # UI 组件
│ ├── pages/ # 页面
│ └── services/ # API 调用
├── package.json
└── vite.config.js
```
## 5. 开发计划
1. **第一阶段**: 搭建后端核心骨架 (FastAPI + Base Classes)。
2. **第二阶段**: 实现“最大子数组和”算法逻辑与 API。
3. **第三阶段**: 搭建 React 前端框架,实现基础交互与图表组件。
4. **第四阶段**: 实现“0/1背包”算法逻辑并接入前端。
5. **第五阶段**: 优化 UI/UX完善性能对比的可视化报告。

14
frontend/index.html Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Algorithm Platform</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

30
frontend/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "algorithm-platform-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.6.0",
"lucide-react": "^0.292.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"recharts": "^2.9.0",
"clsx": "^2.0.0",
"tailwind-merge": "^2.0.0"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"vite": "^5.0.0"
}
}

2066
frontend/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

72
frontend/src/App.jsx Normal file
View File

@ -0,0 +1,72 @@
import React, { useState } from 'react';
import MaxSubarray from './components/MaxSubarray';
import IntervalScheduling from './components/IntervalScheduling';
import NQueens from './components/NQueens';
import Knapsack from './components/Knapsack';
function App() {
const [activeTab, setActiveTab] = useState('max_subarray');
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<span className="text-xl font-bold text-gray-800">算法验证平台</span>
<div className="ml-10 flex items-baseline space-x-4">
<button
onClick={() => setActiveTab('max_subarray')}
className={`px-3 py-2 rounded-md text-sm font-medium ${activeTab === 'max_subarray'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
最大子数组和
</button>
<button
onClick={() => setActiveTab('interval_scheduling')}
className={`px-3 py-2 rounded-md text-sm font-medium ${activeTab === 'interval_scheduling'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
区间调度
</button>
<button
onClick={() => setActiveTab('n_queens')}
className={`px-3 py-2 rounded-md text-sm font-medium ${activeTab === 'n_queens'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
N皇后
</button>
<button
onClick={() => setActiveTab('knapsack')}
className={`px-3 py-2 rounded-md text-sm font-medium ${activeTab === 'knapsack'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
0/1 背包
</button>
</div>
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
{activeTab === 'max_subarray' && <MaxSubarray />}
{activeTab === 'interval_scheduling' && <IntervalScheduling />}
{activeTab === 'n_queens' && <NQueens />}
{activeTab === 'knapsack' && <Knapsack />}
</div>
</main>
</div>
);
}
export default App;

103
frontend/src/api.js Normal file
View File

@ -0,0 +1,103 @@
import axios from 'axios';
const API_URL = 'http://localhost:8000';
export const generateCase = async (size, minVal, maxVal) => {
const response = await axios.post(`${API_URL}/max_subarray/generate`, {
size,
min_val: minVal,
max_val: maxVal
});
return response.data;
};
export const solveCase = async (array, algorithms) => {
const response = await axios.post(`${API_URL}/max_subarray/solve`, {
array,
algorithms
});
return response.data;
};
export const runBenchmark = async (sizes, algorithms) => {
const response = await axios.post(`${API_URL}/max_subarray/benchmark`, {
sizes,
algorithms
});
return response.data;
};
// Interval Scheduling API
export const generateIntervalCase = async (size, minTime, maxTime) => {
const response = await axios.post(`${API_URL}/interval_scheduling/generate`, {
size,
min_time: minTime,
max_time: maxTime
});
return response.data;
};
export const solveIntervalCase = async (intervals, algorithms) => {
const response = await axios.post(`${API_URL}/interval_scheduling/solve`, {
intervals,
algorithms
});
return response.data;
};
export const runIntervalBenchmark = async (sizes, algorithms) => {
const response = await axios.post(`${API_URL}/interval_scheduling/benchmark`, {
sizes,
algorithms
});
return response.data;
};
// N-Queens API
export const generateNQueensCase = async (n) => {
const response = await axios.post(`${API_URL}/n_queens/generate`, { n });
return response.data;
};
export const solveNQueensCase = async (n, algorithms) => {
const response = await axios.post(`${API_URL}/n_queens/solve`, {
n,
algorithms
});
return response.data;
};
export const runNQueensBenchmark = async (sizes, algorithms) => {
const response = await axios.post(`${API_URL}/n_queens/benchmark`, {
sizes,
algorithms
});
return response.data;
};
// Knapsack API
export const generateKnapsackCase = async (numItems, maxWeight, maxValue) => {
const response = await axios.post(`${API_URL}/knapsack/generate`, {
num_items: numItems,
max_weight: maxWeight,
max_value: maxValue
});
return response.data;
};
export const solveKnapsackCase = async (items, capacity, algorithms) => {
const response = await axios.post(`${API_URL}/knapsack/solve`, {
items,
capacity,
algorithms
});
return response.data;
};
export const runKnapsackBenchmark = async (sizes, algorithms) => {
const response = await axios.post(`${API_URL}/knapsack/benchmark`, {
sizes,
algorithms
});
return response.data;
};

View File

@ -0,0 +1,250 @@
import React, { useState } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { generateIntervalCase, solveIntervalCase, runIntervalBenchmark } from '../api';
const IntervalScheduling = () => {
const [params, setParams] = useState({ size: 10, min: 0, max: 100 });
const [currentCase, setCurrentCase] = useState(null);
const [solveResults, setSolveResults] = useState(null);
const [benchmarkResults, setBenchmarkResults] = useState(null);
const [loading, setLoading] = useState(false);
// Algorithm selection state
const [selectedAlgorithms, setSelectedAlgorithms] = useState({
greedy: true,
brute_force: true
});
const algorithmsList = [
{ id: 'greedy', name: '贪心算法 (Greedy) O(N log N)' },
{ id: 'brute_force', name: '暴力求解 (Brute Force) O(2^N)' }
];
const getSelectedAlgoKeys = () => {
return Object.keys(selectedAlgorithms).filter(k => selectedAlgorithms[k]);
};
const handleGenerate = async () => {
setLoading(true);
try {
const data = await generateIntervalCase(params.size, params.min, params.max);
setCurrentCase(data.intervals);
setSolveResults(null);
} catch (error) {
console.error("Error generating case:", error);
alert("生成测试用例失败");
}
setLoading(false);
};
const handleSolve = async () => {
if (!currentCase) return;
const algos = getSelectedAlgoKeys();
if (algos.length === 0) {
alert("请至少选择一种算法");
return;
}
setLoading(true);
try {
const results = await solveIntervalCase(currentCase, algos);
setSolveResults(results);
} catch (error) {
console.error("Error solving case:", error);
alert("求解失败");
}
setLoading(false);
};
const handleBenchmark = async () => {
const algos = getSelectedAlgoKeys();
if (algos.length === 0) {
alert("请至少选择一种算法");
return;
}
setLoading(true);
try {
// Smaller sizes for Brute Force safety
const sizes = [5, 10, 15, 20, 22];
const results = await runIntervalBenchmark(sizes, algos);
// Transform data for Recharts
const chartData = results.map(item => {
const point = { size: item.size };
item.algorithms.forEach(algo => {
if (algo.time_seconds !== undefined && algo.time_seconds !== null) {
point[algo.algorithm] = algo.time_seconds;
}
});
return point;
});
setBenchmarkResults(chartData);
} catch (error) {
console.error("Error running benchmark:", error);
alert("性能测试失败");
}
setLoading(false);
};
const toggleAlgorithm = (algoId) => {
setSelectedAlgorithms(prev => ({
...prev,
[algoId]: !prev[algoId]
}));
};
return (
<div className="p-6 space-y-8">
<h1 className="text-3xl font-bold text-gray-800">区间调度问题 (Interval Scheduling)</h1>
<p className="text-gray-600">目标在给定的一组区间中选择尽可能多的互不重叠的区间</p>
{/* Algorithm Selection */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">算法选择</h2>
<div className="flex flex-wrap gap-4">
{algorithmsList.map(algo => (
<label key={algo.id} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={selectedAlgorithms[algo.id]}
onChange={() => toggleAlgorithm(algo.id)}
className="form-checkbox h-5 w-5 text-blue-600"
/>
<span className="text-gray-700">{algo.name}</span>
</label>
))}
</div>
</div>
{/* Section 1: Generate & Solve */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">1. 生成测试用例</h2>
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">区间数量 (N)</label>
<input
type="number"
value={params.size}
onChange={(e) => setParams({ ...params, size: parseInt(e.target.value) })}
className="mt-1 block w-full border rounded-md p-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">最小时间</label>
<input
type="number"
value={params.min}
onChange={(e) => setParams({ ...params, min: parseInt(e.target.value) })}
className="mt-1 block w-full border rounded-md p-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">最大时间</label>
<input
type="number"
value={params.max}
onChange={(e) => setParams({ ...params, max: parseInt(e.target.value) })}
className="mt-1 block w-full border rounded-md p-2"
/>
</div>
</div>
<button
onClick={handleGenerate}
disabled={loading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
>
{loading ? '处理中...' : '生成测试用例'}
</button>
</div>
{currentCase && (
<div className="mt-4">
<h3 className="font-medium">已生成区间 (预览):</h3>
<div className="font-mono text-sm bg-gray-100 p-2 rounded break-all max-h-64 overflow-y-auto">
{currentCase.map((interval, idx) => (
<div key={idx} className="mb-1">
[{interval[0]}, {interval[1]}]
</div>
))}
</div>
<p className="text-xs text-gray-500 mt-1">总区间数量: {currentCase.length}</p>
</div>
)}
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">2. 求解与验证</h2>
<button
onClick={handleSolve}
disabled={!currentCase || loading}
className="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 disabled:bg-gray-400 mb-4"
>
运行选中算法
</button>
{solveResults && (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">算法</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">选中数量</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">耗时 ()</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{solveResults.map((res, idx) => (
<tr key={idx}>
<td className="px-4 py-2 text-sm font-medium text-gray-900">{res.algorithm}</td>
<td className="px-4 py-2 text-sm text-gray-500">{res.result_count}</td>
<td className="px-4 py-2 text-sm text-gray-500">{res.time_seconds?.toFixed(6)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* Section 2: Benchmark */}
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold">3. 性能对比测试</h2>
<button
onClick={handleBenchmark}
disabled={loading}
className="bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 disabled:bg-gray-400"
>
运行性能测试
</button>
</div>
{benchmarkResults && (
<div className="h-96 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={benchmarkResults}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="size" label={{ value: '区间数量 (N)', position: 'insideBottomRight', offset: -10 }} />
<YAxis label={{ value: '耗时 (秒)', angle: -90, position: 'insideLeft' }} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="greedy" stroke="#00ff00" name="贪心算法" />
<Line type="monotone" dataKey="brute_force" stroke="#ff0000" name="暴力求解" />
</LineChart>
</ResponsiveContainer>
</div>
)}
</div>
</div>
);
};
export default IntervalScheduling;

View File

@ -0,0 +1,261 @@
import React, { useState } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { generateKnapsackCase, solveKnapsackCase, runKnapsackBenchmark } from '../api';
const Knapsack = () => {
const [params, setParams] = useState({ num_items: 10, max_weight: 50, max_value: 100 });
const [currentCase, setCurrentCase] = useState(null);
const [solveResults, setSolveResults] = useState(null);
const [benchmarkResults, setBenchmarkResults] = useState(null);
const [loading, setLoading] = useState(false);
// Algorithm selection state
const [selectedAlgorithms, setSelectedAlgorithms] = useState({
dp: true,
backtracking: true
});
const algorithmsList = [
{ id: 'dp', name: '动态规划 (DP) O(NW)' },
{ id: 'backtracking', name: '回溯算法 (Backtracking) O(2^N)' }
];
const getSelectedAlgoKeys = () => {
return Object.keys(selectedAlgorithms).filter(k => selectedAlgorithms[k]);
};
const handleGenerate = async () => {
setLoading(true);
try {
const data = await generateKnapsackCase(params.num_items, params.max_weight, params.max_value);
setCurrentCase(data);
setSolveResults(null);
} catch (error) {
console.error("Error generating case:", error);
alert("生成测试用例失败");
}
setLoading(false);
};
const handleSolve = async () => {
if (!currentCase) return;
const algos = getSelectedAlgoKeys();
if (algos.length === 0) {
alert("请至少选择一种算法");
return;
}
setLoading(true);
try {
const results = await solveKnapsackCase(currentCase.items, currentCase.capacity, algos);
setSolveResults(results);
} catch (error) {
console.error("Error solving case:", error);
alert("求解失败");
}
setLoading(false);
};
const handleBenchmark = async () => {
const algos = getSelectedAlgoKeys();
if (algos.length === 0) {
alert("请至少选择一种算法");
return;
}
setLoading(true);
try {
const sizes = [10, 15, 20, 22, 25];
const results = await runKnapsackBenchmark(sizes, algos);
const chartData = results.map(item => {
const point = { size: item.size };
item.algorithms.forEach(algo => {
if (algo.time_seconds !== undefined && algo.time_seconds !== null) {
point[algo.algorithm] = algo.time_seconds;
}
});
return point;
});
setBenchmarkResults(chartData);
} catch (error) {
console.error("Error running benchmark:", error);
alert("性能测试失败");
}
setLoading(false);
};
const toggleAlgorithm = (algoId) => {
setSelectedAlgorithms(prev => ({
...prev,
[algoId]: !prev[algoId]
}));
};
return (
<div className="p-6 space-y-8">
<h1 className="text-3xl font-bold text-gray-800">0/1 背包问题 (0/1 Knapsack)</h1>
<p className="text-gray-600">目标在不超过背包容量的情况下选择物品使得总价值最大</p>
{/* Algorithm Selection */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">算法选择</h2>
<div className="flex flex-wrap gap-4">
{algorithmsList.map(algo => (
<label key={algo.id} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={selectedAlgorithms[algo.id]}
onChange={() => toggleAlgorithm(algo.id)}
className="form-checkbox h-5 w-5 text-blue-600"
/>
<span className="text-gray-700">{algo.name}</span>
</label>
))}
</div>
</div>
{/* Section 1: Generate & Solve */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">1. 生成测试用例</h2>
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">物品数量</label>
<input
type="number"
value={params.num_items}
onChange={(e) => setParams({ ...params, num_items: parseInt(e.target.value) })}
className="mt-1 block w-full border rounded-md p-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">最大重量</label>
<input
type="number"
value={params.max_weight}
onChange={(e) => setParams({ ...params, max_weight: parseInt(e.target.value) })}
className="mt-1 block w-full border rounded-md p-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">最大价值</label>
<input
type="number"
value={params.max_value}
onChange={(e) => setParams({ ...params, max_value: parseInt(e.target.value) })}
className="mt-1 block w-full border rounded-md p-2"
/>
</div>
</div>
<button
onClick={handleGenerate}
disabled={loading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
>
{loading ? '处理中...' : '生成测试用例'}
</button>
</div>
{currentCase && (
<div className="mt-4">
<h3 className="font-medium">背包容量: {currentCase.capacity}</h3>
<h3 className="font-medium mt-2">物品列表 (预览):</h3>
<div className="font-mono text-sm bg-gray-100 p-2 rounded break-all max-h-64 overflow-y-auto">
<table className="min-w-full text-xs">
<thead>
<tr>
<th className="text-left">ID</th>
<th className="text-left">重量</th>
<th className="text-left">价值</th>
</tr>
</thead>
<tbody>
{currentCase.items.map((item) => (
<tr key={item.id}>
<td>{item.id}</td>
<td>{item.weight}</td>
<td>{item.value}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">2. 求解与验证</h2>
<button
onClick={handleSolve}
disabled={!currentCase || loading}
className="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 disabled:bg-gray-400 mb-4"
>
运行选中算法
</button>
{solveResults && (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">算法</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">最大价值</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">耗时 ()</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{solveResults.map((res, idx) => (
<tr key={idx}>
<td className="px-4 py-2 text-sm font-medium text-gray-900">{res.algorithm}</td>
<td className="px-4 py-2 text-sm text-gray-500">{res.max_value}</td>
<td className="px-4 py-2 text-sm text-gray-500">{res.time_seconds?.toFixed(6)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* Section 2: Benchmark */}
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold">3. 性能对比测试</h2>
<button
onClick={handleBenchmark}
disabled={loading}
className="bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 disabled:bg-gray-400"
>
运行性能测试
</button>
</div>
{benchmarkResults && (
<div className="h-96 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={benchmarkResults}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="size" label={{ value: '物品数量 (N)', position: 'insideBottomRight', offset: -10 }} />
<YAxis label={{ value: '耗时 (秒)', angle: -90, position: 'insideLeft' }} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="dp" stroke="#00ff00" name="动态规划" />
<Line type="monotone" dataKey="backtracking" stroke="#ff0000" name="回溯算法" />
</LineChart>
</ResponsiveContainer>
</div>
)}
</div>
</div>
);
};
export default Knapsack;

View File

@ -0,0 +1,248 @@
import React, { useState } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { generateCase, solveCase, runBenchmark } from '../api';
const MaxSubarray = () => {
const [params, setParams] = useState({ size: 100, min: -100, max: 100 });
const [currentCase, setCurrentCase] = useState(null);
const [solveResults, setSolveResults] = useState(null);
const [benchmarkResults, setBenchmarkResults] = useState(null);
const [loading, setLoading] = useState(false);
// Algorithm selection state
const [selectedAlgorithms, setSelectedAlgorithms] = useState({
brute_force: true,
divide_conquer: true,
kadane: true
});
const algorithmsList = [
{ id: 'brute_force', name: '暴力求解 (Brute Force) O(N^2)' },
{ id: 'divide_conquer', name: '分治算法 (Divide & Conquer) O(N log N)' },
{ id: 'kadane', name: 'Kadane算法 (Kadane) O(N)' }
];
const getSelectedAlgoKeys = () => {
return Object.keys(selectedAlgorithms).filter(k => selectedAlgorithms[k]);
};
const handleGenerate = async () => {
setLoading(true);
try {
const data = await generateCase(params.size, params.min, params.max);
setCurrentCase(data.array);
setSolveResults(null);
} catch (error) {
console.error("Error generating case:", error);
alert("生成测试用例失败");
}
setLoading(false);
};
const handleSolve = async () => {
if (!currentCase) return;
const algos = getSelectedAlgoKeys();
if (algos.length === 0) {
alert("请至少选择一种算法");
return;
}
setLoading(true);
try {
const results = await solveCase(currentCase, algos);
setSolveResults(results);
} catch (error) {
console.error("Error solving case:", error);
alert("求解失败");
}
setLoading(false);
};
const handleBenchmark = async () => {
const algos = getSelectedAlgoKeys();
if (algos.length === 0) {
alert("请至少选择一种算法");
return;
}
setLoading(true);
try {
// Smaller sizes for quick demo
const sizes = [10, 50, 100, 200, 500, 1000, 2000];
const results = await runBenchmark(sizes, algos);
// Transform data for Recharts
const chartData = results.map(item => {
const point = { size: item.size };
item.algorithms.forEach(algo => {
if (algo.time_seconds !== undefined && algo.time_seconds !== null) {
point[algo.algorithm] = algo.time_seconds;
}
});
return point;
});
setBenchmarkResults(chartData);
} catch (error) {
console.error("Error running benchmark:", error);
alert("性能测试失败");
}
setLoading(false);
};
const toggleAlgorithm = (algoId) => {
setSelectedAlgorithms(prev => ({
...prev,
[algoId]: !prev[algoId]
}));
};
return (
<div className="p-6 space-y-8">
<h1 className="text-3xl font-bold text-gray-800">最大子数组和问题 (Maximum Subarray Sum)</h1>
{/* Algorithm Selection */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">算法选择</h2>
<div className="flex flex-wrap gap-4">
{algorithmsList.map(algo => (
<label key={algo.id} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={selectedAlgorithms[algo.id]}
onChange={() => toggleAlgorithm(algo.id)}
className="form-checkbox h-5 w-5 text-blue-600"
/>
<span className="text-gray-700">{algo.name}</span>
</label>
))}
</div>
</div>
{/* Section 1: Generate & Solve */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">1. 生成测试用例</h2>
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">数据规模 (N)</label>
<input
type="number"
value={params.size}
onChange={(e) => setParams({ ...params, size: parseInt(e.target.value) })}
className="mt-1 block w-full border rounded-md p-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">最小值</label>
<input
type="number"
value={params.min}
onChange={(e) => setParams({ ...params, min: parseInt(e.target.value) })}
className="mt-1 block w-full border rounded-md p-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">最大值</label>
<input
type="number"
value={params.max}
onChange={(e) => setParams({ ...params, max: parseInt(e.target.value) })}
className="mt-1 block w-full border rounded-md p-2"
/>
</div>
</div>
<button
onClick={handleGenerate}
disabled={loading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
>
{loading ? '处理中...' : '生成测试用例'}
</button>
</div>
{currentCase && (
<div className="mt-4">
<h3 className="font-medium">已生成数组 (预览):</h3>
<p className="font-mono text-sm bg-gray-100 p-2 rounded break-all max-h-32 overflow-y-auto">
[{currentCase.slice(0, 50).join(', ')}{currentCase.length > 50 ? '...' : ''}]
</p>
<p className="text-xs text-gray-500 mt-1">总元素数量: {currentCase.length}</p>
</div>
)}
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">2. 求解与验证</h2>
<button
onClick={handleSolve}
disabled={!currentCase || loading}
className="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 disabled:bg-gray-400 mb-4"
>
运行选中算法
</button>
{solveResults && (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">算法</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">结果</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">耗时 ()</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{solveResults.map((res, idx) => (
<tr key={idx}>
<td className="px-4 py-2 text-sm font-medium text-gray-900">{res.algorithm}</td>
<td className="px-4 py-2 text-sm text-gray-500">{res.result}</td>
<td className="px-4 py-2 text-sm text-gray-500">{res.time_seconds?.toFixed(6)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* Section 2: Benchmark */}
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold">3. 性能对比测试</h2>
<button
onClick={handleBenchmark}
disabled={loading}
className="bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 disabled:bg-gray-400"
>
运行性能测试
</button>
</div>
{benchmarkResults && (
<div className="h-96 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={benchmarkResults}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="size" label={{ value: '数据规模 (N)', position: 'insideBottomRight', offset: -10 }} />
<YAxis label={{ value: '耗时 (秒)', angle: -90, position: 'insideLeft' }} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="brute_force" stroke="#ff0000" name="暴力求解" />
<Line type="monotone" dataKey="divide_conquer" stroke="#00ff00" name="分治算法" />
<Line type="monotone" dataKey="kadane" stroke="#0000ff" name="Kadane算法" />
</LineChart>
</ResponsiveContainer>
</div>
)}
</div>
</div>
);
};
export default MaxSubarray;

View File

@ -0,0 +1,174 @@
import React, { useState } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { generateNQueensCase, solveNQueensCase, runNQueensBenchmark } from '../api';
const NQueens = () => {
const [n, setN] = useState(8);
const [currentSolution, setCurrentSolution] = useState(null);
const [solutionCount, setSolutionCount] = useState(null);
const [solveTime, setSolveTime] = useState(null);
const [benchmarkResults, setBenchmarkResults] = useState(null);
const [loading, setLoading] = useState(false);
const handleSolve = async () => {
setLoading(true);
try {
// Generate is trivial (just N), so we skip explicit generate step for UI simplicity
// and just call solve directly
const results = await solveNQueensCase(n, ["backtracking"]);
if (results && results.length > 0) {
const res = results[0];
setCurrentSolution(res.first_solution);
setSolutionCount(res.solution_count);
setSolveTime(res.time_seconds);
}
} catch (error) {
console.error("Error solving N-Queens:", error);
alert("求解失败");
}
setLoading(false);
};
const handleBenchmark = async () => {
setLoading(true);
try {
const sizes = [4, 8, 10, 12, 13];
const results = await runNQueensBenchmark(sizes, ["backtracking"]);
const chartData = results.map(item => {
const point = { size: item.size };
item.algorithms.forEach(algo => {
if (algo.time_seconds !== undefined && algo.time_seconds !== null) {
point[algo.algorithm] = algo.time_seconds;
}
});
return point;
});
setBenchmarkResults(chartData);
} catch (error) {
console.error("Error running benchmark:", error);
alert("性能测试失败");
}
setLoading(false);
};
// Render the chess board
const renderBoard = () => {
if (!currentSolution) return null;
const boardSize = 400;
const cellSize = boardSize / n;
return (
<div
className="relative border-2 border-gray-800"
style={{ width: boardSize, height: boardSize }}
>
{Array.from({ length: n }).map((_, row) => (
Array.from({ length: n }).map((_, col) => {
const isBlack = (row + col) % 2 === 1;
const hasQueen = currentSolution[row] === col;
return (
<div
key={`${row}-${col}`}
className={`absolute flex items-center justify-center ${isBlack ? 'bg-gray-600' : 'bg-white'}`}
style={{
width: cellSize,
height: cellSize,
top: row * cellSize,
left: col * cellSize
}}
>
{hasQueen && <span className="text-red-500 font-bold" style={{ fontSize: cellSize * 0.7 }}></span>}
</div>
);
})
))}
</div>
);
};
return (
<div className="p-6 space-y-8">
<h1 className="text-3xl font-bold text-gray-800">N皇后问题 (N-Queens)</h1>
<p className="text-gray-600">目标 N×N 的棋盘上放置 N 个皇后使得它们互不攻击</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Controls */}
<div className="bg-white p-6 rounded-lg shadow space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700">棋盘大小 (N)</label>
<input
type="number"
min="4"
max="14"
value={n}
onChange={(e) => setN(parseInt(e.target.value))}
className="mt-1 block w-full border rounded-md p-2"
/>
<p className="text-xs text-gray-500 mt-1">建议 N &le; 14否则计算时间过长</p>
</div>
<button
onClick={handleSolve}
disabled={loading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
>
{loading ? '计算中...' : '求解 (Backtracking)'}
</button>
{solutionCount !== null && (
<div className="mt-4 p-4 bg-green-50 rounded-md">
<p className="font-medium text-green-800">找到解的数量: {solutionCount}</p>
<p className="text-sm text-green-600">耗时: {solveTime?.toFixed(6)} </p>
</div>
)}
</div>
{/* Visualization */}
<div className="bg-white p-6 rounded-lg shadow flex justify-center items-center min-h-[450px]">
{currentSolution ? renderBoard() : (
<div className="text-gray-400 text-center">
<p>点击求解以查看结果</p>
</div>
)}
</div>
</div>
{/* Benchmark */}
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold">性能测试 (N=4 to 13)</h2>
<button
onClick={handleBenchmark}
disabled={loading}
className="bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 disabled:bg-gray-400"
>
运行性能测试
</button>
</div>
{benchmarkResults && (
<div className="h-96 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={benchmarkResults}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="size" label={{ value: 'N', position: 'insideBottomRight', offset: -10 }} />
<YAxis label={{ value: '耗时 (秒)', angle: -90, position: 'insideLeft' }} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="backtracking" stroke="#ff0000" name="回溯算法" />
</LineChart>
</ResponsiveContainer>
</div>
)}
</div>
</div>
);
};
export default NQueens;

3
frontend/src/index.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

10
frontend/src/main.jsx Normal file
View File

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

7
frontend/vite.config.js Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})

44
nginx.conf Normal file
View File

@ -0,0 +1,44 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream backend {
server backend:8000;
}
server {
listen 80;
server_name localhost;
# Frontend static files
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# Proxy API requests to backend
location /api/ {
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Proxy docs to backend
location /docs {
proxy_pass http://backend/docs;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /openapi.json {
proxy_pass http://backend/openapi.json;
proxy_set_header Host $host;
}
}
}