Add WARP.md and fix 100 prisoners MDX for build
This commit is contained in:
parent
3465643410
commit
56f49d988d
7 changed files with 1416 additions and 0 deletions
136
WARP.md
Normal file
136
WARP.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# WARP.md
|
||||
|
||||
This file provides guidance to WARP (warp.dev) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a multi-blog Astro application that deploys 13 separate blog collections to individual subdomains on Netlify at `neeldhara.blog`. Each blog collection (art, bfs, dfs, problems, puzzles, reviews, reflections, vibes, workflows, magic, contests, poetry, and blog) has its own subdomain (e.g., `art.neeldhara.blog`) but is built from a single codebase.
|
||||
|
||||
The architecture is based on the Charter Astro Template and uses:
|
||||
- Astro 5.x for static site generation
|
||||
- Tailwind CSS 4 for styling
|
||||
- React 19 for interactive components
|
||||
- shadcn/ui components
|
||||
- MDX for content with frontmatter
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Development
|
||||
```bash
|
||||
npm install # Install dependencies
|
||||
npm run dev # Start dev server at http://localhost:4321
|
||||
npm run preview # Preview production build locally
|
||||
```
|
||||
|
||||
### Building
|
||||
```bash
|
||||
npm run build # Build static site to dist/
|
||||
npm run astro check # Type check Astro files
|
||||
```
|
||||
|
||||
### Formatting
|
||||
```bash
|
||||
npm run format # Format code with Prettier
|
||||
```
|
||||
|
||||
### Deployment
|
||||
The site is deployed to Netlify with automatic builds on push. The `netlify.toml` configuration handles all subdomain routing. See `DEPLOYMENT.md` for details.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Content Collections
|
||||
All blog content is managed through Astro Content Collections in `src/content/config.ts`. Each collection (art, bfs, dfs, etc.) has:
|
||||
- Its own directory in `src/content/[collection-name]/`
|
||||
- Shared schema with required fields: `title`, `description`, `pubDate`, and optional `updatedDate`, `image`, `authorImage`, `authorName`
|
||||
- Supports both `.md` and `.mdx` files
|
||||
|
||||
### Routing Structure
|
||||
The site uses file-based routing with a consistent pattern across all collections:
|
||||
- `src/pages/[collection]/index.astro` - Collection homepage
|
||||
- `src/pages/[collection]/[...slug].astro` - Individual blog posts
|
||||
- `src/pages/[collection]/rss.xml.js` - RSS feed per collection
|
||||
|
||||
Global pages include:
|
||||
- `src/pages/index.astro` - Main landing page
|
||||
- `src/pages/latest.astro` - Aggregated latest posts from all collections
|
||||
- `src/pages/about.astro` - About page
|
||||
- Other utility pages: 404, contact, faq, login, signup, privacy, terms
|
||||
|
||||
### Subdomain Deployment
|
||||
All 13 collections are built into a single static site, but Netlify redirects handle subdomain routing:
|
||||
- Each subdomain (e.g., `art.neeldhara.blog`) rewrites to the main domain path (`/art/*`)
|
||||
- Static assets (`_astro/*`, `assets/*`) are shared across all subdomains
|
||||
- Special redirects prevent double-path issues (e.g., `/art/art/slug` → `/art/slug`)
|
||||
- Global pages (`/latest`, `/about`) redirect from subdomains to the main domain
|
||||
|
||||
### Component Organization
|
||||
```
|
||||
src/components/
|
||||
├── BaseHead.astro # Common <head> tags
|
||||
├── elements/ # Small reusable elements (header links, theme toggle)
|
||||
├── icons/ # Icon components
|
||||
├── problems/ # Interactive components for problem posts
|
||||
├── sections/ # Page sections (navbar, footer, blog-post, etc.)
|
||||
└── ui/ # shadcn/ui components (button, card, tabs, etc.)
|
||||
```
|
||||
|
||||
### TypeScript Configuration
|
||||
- Path aliases configured in `tsconfig.json`:
|
||||
- `@/*` → `./src/*`
|
||||
- `@components/*` → `./src/components/*`
|
||||
- `@layouts/*` → `./src/layouts/*`
|
||||
- `@lib/*` → `./src/lib/*`
|
||||
- Strict mode enabled with React JSX support
|
||||
|
||||
### Styling
|
||||
- Tailwind CSS 4 configured via Vite plugin in `astro.config.mjs`
|
||||
- Global styles in `src/styles/global.css`
|
||||
- Prettier with tailwindcss plugin for class sorting
|
||||
|
||||
## Adding a New Blog Collection
|
||||
|
||||
To add a new blog collection:
|
||||
|
||||
1. Create content directory: `src/content/[collection-name]/`
|
||||
2. Add collection definition in `src/content/config.ts`:
|
||||
```typescript
|
||||
const newCollection = defineCollection({
|
||||
loader: glob({ base: "./src/content/newCollection", pattern: "**/*.{md,mdx}" }),
|
||||
schema: blogSchema,
|
||||
});
|
||||
```
|
||||
3. Export in collections object
|
||||
4. Create page structure in `src/pages/[collection-name]/`:
|
||||
- `index.astro` (list view)
|
||||
- `[...slug].astro` (single post view)
|
||||
- `rss.xml.js` (RSS feed)
|
||||
5. Add subdomain redirects in `netlify.toml` following existing patterns
|
||||
6. Configure subdomain DNS in Netlify dashboard
|
||||
|
||||
## Key Files
|
||||
|
||||
- `astro.config.mjs` - Astro configuration with MDX, sitemap, React integrations
|
||||
- `netlify.toml` - Subdomain routing and redirect configuration
|
||||
- `src/content/config.ts` - Content collection definitions
|
||||
- `src/consts.ts` - Site metadata and global constants
|
||||
- `DEPLOYMENT.md` - Subdomain deployment documentation
|
||||
|
||||
## Interactive Components
|
||||
|
||||
Some blog posts (especially in the `problems` collection) include interactive React components for visualizations. These are stored in `src/components/problems/` and imported directly into MDX files. Use `client:only="react"` directive for React components that require browser APIs.
|
||||
|
||||
## Content Frontmatter Example
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: "Post Title"
|
||||
description: "Post description for SEO and preview"
|
||||
pubDate: 2025-01-15
|
||||
updatedDate: 2025-01-20
|
||||
image: "/images/post-image.jpg"
|
||||
authorName: "Author Name"
|
||||
authorImage: "/images/author.jpg"
|
||||
---
|
||||
|
||||
Post content here...
|
||||
```
|
||||
229
src/components/problems/PermutationExplorer.tsx
Normal file
229
src/components/problems/PermutationExplorer.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
|
||||
interface Cycle {
|
||||
elements: number[];
|
||||
length: number;
|
||||
}
|
||||
|
||||
interface PermutationData {
|
||||
permutation: number[];
|
||||
cycles: Cycle[];
|
||||
hasLongCycle: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
// Generate all permutations of [1, 2, ..., n]
|
||||
function generatePermutations(n: number): number[][] {
|
||||
if (n === 1) return [[1]];
|
||||
|
||||
const result: number[][] = [];
|
||||
const smaller = generatePermutations(n - 1);
|
||||
|
||||
for (const perm of smaller) {
|
||||
for (let i = 0; i <= perm.length; i++) {
|
||||
const newPerm = [...perm.slice(0, i), n, ...perm.slice(i)];
|
||||
result.push(newPerm);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Find cycles in a permutation
|
||||
function findCycles(permutation: number[]): Cycle[] {
|
||||
const n = permutation.length;
|
||||
const visited = new Array(n).fill(false);
|
||||
const cycles: Cycle[] = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (!visited[i]) {
|
||||
const cycle: number[] = [];
|
||||
let current = i;
|
||||
|
||||
while (!visited[current]) {
|
||||
visited[current] = true;
|
||||
cycle.push(current + 1); // Convert to 1-indexed
|
||||
current = permutation[current] - 1; // Convert back to 0-indexed
|
||||
}
|
||||
|
||||
cycles.push({
|
||||
elements: cycle,
|
||||
length: cycle.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return cycles.sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
const PermutationExplorer: React.FC = () => {
|
||||
const [n, setN] = useState(3);
|
||||
const [filter, setFilter] = useState<'all' | 'long' | 'short'>('all');
|
||||
|
||||
const allPermutations = useMemo(() => {
|
||||
const perms = generatePermutations(n);
|
||||
return perms.map((perm, idx) => {
|
||||
const cycles = findCycles(perm);
|
||||
const hasLongCycle = cycles.some(c => c.length > n / 2);
|
||||
|
||||
return {
|
||||
permutation: perm,
|
||||
cycles,
|
||||
hasLongCycle,
|
||||
id: `perm-${n}-${idx}`,
|
||||
};
|
||||
});
|
||||
}, [n]);
|
||||
|
||||
const filteredPermutations = useMemo(() => {
|
||||
if (filter === 'all') return allPermutations;
|
||||
if (filter === 'long') return allPermutations.filter(p => p.hasLongCycle);
|
||||
return allPermutations.filter(p => !p.hasLongCycle);
|
||||
}, [allPermutations, filter]);
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const withLong = allPermutations.filter(p => p.hasLongCycle).length;
|
||||
const withoutLong = allPermutations.length - withLong;
|
||||
return { total: allPermutations.length, withLong, withoutLong };
|
||||
}, [allPermutations]);
|
||||
|
||||
const formatCycleNotation = (cycles: Cycle[]) => {
|
||||
return cycles.map(cycle => `(${cycle.elements.join(' ')})`).join(' ');
|
||||
};
|
||||
|
||||
const formatStandardNotation = (perm: number[]) => {
|
||||
return `[${perm.join(', ')}]`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-white">
|
||||
<h3 className="text-xl font-semibold mb-4">Permutation Cycle Explorer</h3>
|
||||
|
||||
<div className="mb-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-md font-semibold mb-2">
|
||||
Size (n): {n}
|
||||
</label>
|
||||
<div className="flex items-center space-x-4">
|
||||
{[3, 4, 5].map(size => (
|
||||
<button
|
||||
key={size}
|
||||
onClick={() => setN(size)}
|
||||
className={`px-4 py-2 rounded font-semibold transition-colors ${
|
||||
n === size
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{size}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-md font-semibold mb-2">
|
||||
Filter Permutations:
|
||||
</label>
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
className={`px-4 py-2 rounded font-semibold transition-colors ${
|
||||
filter === 'all'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
All ({stats.total})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('long')}
|
||||
className={`px-4 py-2 rounded font-semibold transition-colors ${
|
||||
filter === 'long'
|
||||
? 'bg-red-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
With Long Cycle ({stats.withLong})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('short')}
|
||||
className={`px-4 py-2 rounded font-semibold transition-colors ${
|
||||
filter === 'short'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
Without Long Cycle ({stats.withoutLong})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-4 bg-blue-50 rounded">
|
||||
<p className="text-sm">
|
||||
<strong>Note:</strong> A "long cycle" is one with length > n/2. For n={n},
|
||||
that means cycles longer than {Math.floor(n / 2)}.
|
||||
There are <strong>{stats.withLong}</strong> permutations with at least one long cycle
|
||||
(about {((stats.withLong / stats.total) * 100).toFixed(1)}%).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{filteredPermutations.map((perm) => (
|
||||
<div
|
||||
key={perm.id}
|
||||
className={`p-3 rounded border-2 ${
|
||||
perm.hasLongCycle
|
||||
? 'bg-red-50 border-red-300'
|
||||
: 'bg-green-50 border-green-300'
|
||||
}`}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="font-semibold">Standard:</span>{' '}
|
||||
<code className="bg-white px-2 py-1 rounded">
|
||||
{formatStandardNotation(perm.permutation)}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Cycles:</span>{' '}
|
||||
<code className="bg-white px-2 py-1 rounded">
|
||||
{formatCycleNotation(perm.cycles)}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-600">
|
||||
Cycle lengths: {perm.cycles.map(c => c.length).join(', ')}
|
||||
{perm.hasLongCycle && (
|
||||
<span className="ml-2 text-red-700 font-semibold">
|
||||
⚠ Has long cycle!
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-gray-100 rounded">
|
||||
<h4 className="font-semibold mb-2">Understanding the Notation:</h4>
|
||||
<ul className="text-sm space-y-1 list-disc list-inside">
|
||||
<li>
|
||||
<strong>Standard notation [a, b, c, ...]:</strong> Position i contains value a[i].
|
||||
For example, [2,3,1] means box 1→2, box 2→3, box 3→1.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Cycle notation (a b c ...):</strong> Shows the path a→b→c→...→a.
|
||||
For example, (1 2 3) means 1→2→3→1.
|
||||
</li>
|
||||
<li>
|
||||
Permutations with long cycles (shown in red) would cause the loop strategy to fail
|
||||
for at least one prisoner.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermutationExplorer;
|
||||
222
src/components/problems/PrisonerGameSimulator.tsx
Normal file
222
src/components/problems/PrisonerGameSimulator.tsx
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface GameState {
|
||||
n: number;
|
||||
boxes: number[];
|
||||
currentPrisoner: number;
|
||||
openedBoxes: number[];
|
||||
foundNumber: boolean;
|
||||
gameOver: boolean;
|
||||
won: boolean;
|
||||
prisonersCompleted: number;
|
||||
attemptsLeft: number;
|
||||
}
|
||||
|
||||
const PrisonerGameSimulator: React.FC = () => {
|
||||
const [n, setN] = useState(10);
|
||||
const [gameState, setGameState] = useState<GameState | null>(null);
|
||||
|
||||
const initializeGame = (numPrisoners: number) => {
|
||||
// Create a random permutation
|
||||
const boxes = Array.from({ length: numPrisoners }, (_, i) => i + 1);
|
||||
for (let i = boxes.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[boxes[i], boxes[j]] = [boxes[j], boxes[i]];
|
||||
}
|
||||
|
||||
setGameState({
|
||||
n: numPrisoners,
|
||||
boxes,
|
||||
currentPrisoner: 1,
|
||||
openedBoxes: [],
|
||||
foundNumber: false,
|
||||
gameOver: false,
|
||||
won: false,
|
||||
prisonersCompleted: 0,
|
||||
attemptsLeft: Math.floor(numPrisoners / 2),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initializeGame(n);
|
||||
}, []);
|
||||
|
||||
const handleBoxClick = (boxIndex: number) => {
|
||||
if (!gameState || gameState.gameOver || gameState.openedBoxes.includes(boxIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOpenedBoxes = [...gameState.openedBoxes, boxIndex];
|
||||
const numberInBox = gameState.boxes[boxIndex];
|
||||
const foundTarget = numberInBox === gameState.currentPrisoner;
|
||||
const newAttemptsLeft = gameState.attemptsLeft - 1;
|
||||
|
||||
if (foundTarget) {
|
||||
// Prisoner found their number!
|
||||
const newPrisonersCompleted = gameState.prisonersCompleted + 1;
|
||||
|
||||
if (newPrisonersCompleted === gameState.n) {
|
||||
// All prisoners succeeded!
|
||||
setGameState({
|
||||
...gameState,
|
||||
openedBoxes: newOpenedBoxes,
|
||||
foundNumber: true,
|
||||
gameOver: true,
|
||||
won: true,
|
||||
prisonersCompleted: newPrisonersCompleted,
|
||||
attemptsLeft: newAttemptsLeft,
|
||||
});
|
||||
} else {
|
||||
// Move to next prisoner
|
||||
setGameState({
|
||||
...gameState,
|
||||
currentPrisoner: gameState.currentPrisoner + 1,
|
||||
openedBoxes: [],
|
||||
foundNumber: false,
|
||||
prisonersCompleted: newPrisonersCompleted,
|
||||
attemptsLeft: Math.floor(gameState.n / 2),
|
||||
});
|
||||
}
|
||||
} else if (newAttemptsLeft === 0) {
|
||||
// Ran out of attempts - game over
|
||||
setGameState({
|
||||
...gameState,
|
||||
openedBoxes: newOpenedBoxes,
|
||||
foundNumber: false,
|
||||
gameOver: true,
|
||||
won: false,
|
||||
attemptsLeft: 0,
|
||||
});
|
||||
} else {
|
||||
// Continue opening boxes
|
||||
setGameState({
|
||||
...gameState,
|
||||
openedBoxes: newOpenedBoxes,
|
||||
attemptsLeft: newAttemptsLeft,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
initializeGame(n);
|
||||
};
|
||||
|
||||
const handleNChange = (newN: number) => {
|
||||
setN(newN);
|
||||
initializeGame(newN);
|
||||
};
|
||||
|
||||
if (!gameState) return null;
|
||||
|
||||
return (
|
||||
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-gray-50">
|
||||
<div className="mb-6">
|
||||
<label className="block text-lg font-semibold mb-2">
|
||||
Number of Prisoners: {n}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="100"
|
||||
value={n}
|
||||
onChange={(e) => handleNChange(parseInt(e.target.value))}
|
||||
className="w-full h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer"
|
||||
disabled={gameState.prisonersCompleted > 0 && !gameState.gameOver}
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-gray-600 mt-1">
|
||||
<span>5</span>
|
||||
<span>100</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-4 bg-blue-100 rounded">
|
||||
{!gameState.gameOver && (
|
||||
<>
|
||||
<p className="text-lg font-semibold">
|
||||
Prisoner #{gameState.currentPrisoner} is searching for their number
|
||||
</p>
|
||||
<p className="text-md">
|
||||
Attempts remaining: {gameState.attemptsLeft} / {Math.floor(gameState.n / 2)}
|
||||
</p>
|
||||
<p className="text-md">
|
||||
Prisoners completed: {gameState.prisonersCompleted} / {gameState.n}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{gameState.gameOver && gameState.won && (
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
🎉 SUCCESS! All {gameState.n} prisoners found their numbers!
|
||||
</p>
|
||||
)}
|
||||
{gameState.gameOver && !gameState.won && (
|
||||
<p className="text-2xl font-bold text-red-600">
|
||||
❌ GAME OVER! Prisoner #{gameState.currentPrisoner} failed to find their number.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<h3 className="text-md font-semibold mb-2">Click on boxes to open them:</h3>
|
||||
<div
|
||||
className="grid gap-2"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${Math.min(10, Math.ceil(Math.sqrt(gameState.n)))}, minmax(0, 1fr))`,
|
||||
}}
|
||||
>
|
||||
{gameState.boxes.map((number, index) => {
|
||||
const isOpened = gameState.openedBoxes.includes(index);
|
||||
const isTarget = number === gameState.currentPrisoner && isOpened;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleBoxClick(index)}
|
||||
disabled={gameState.gameOver || isOpened}
|
||||
className={`
|
||||
aspect-square p-2 rounded border-2 font-semibold text-sm
|
||||
transition-all duration-200
|
||||
${isOpened
|
||||
? isTarget
|
||||
? 'bg-green-400 border-green-600 text-white'
|
||||
: 'bg-red-200 border-red-400 text-gray-700'
|
||||
: 'bg-white border-gray-400 hover:bg-blue-100 hover:border-blue-500 cursor-pointer'
|
||||
}
|
||||
${gameState.gameOver || isOpened ? 'cursor-not-allowed' : ''}
|
||||
`}
|
||||
>
|
||||
{isOpened ? (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="text-xs">Box {index + 1}</div>
|
||||
<div className="text-lg">{number}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
{index + 1}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Reset Game
|
||||
</button>
|
||||
|
||||
{gameState.openedBoxes.length > 0 && !gameState.gameOver && (
|
||||
<div className="mt-4 p-3 bg-yellow-100 rounded">
|
||||
<p className="text-sm">
|
||||
<strong>Last opened:</strong> Box {gameState.openedBoxes[gameState.openedBoxes.length - 1] + 1}
|
||||
{' → '}contains number {gameState.boxes[gameState.openedBoxes[gameState.openedBoxes.length - 1]]}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrisonerGameSimulator;
|
||||
225
src/components/problems/ProbabilityChart.tsx
Normal file
225
src/components/problems/ProbabilityChart.tsx
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
interface ProbabilityChartProps {
|
||||
strategy: 'naive' | 'loop';
|
||||
}
|
||||
|
||||
const ProbabilityChart: React.FC<ProbabilityChartProps> = ({ strategy }) => {
|
||||
const data = useMemo(() => {
|
||||
const points = [];
|
||||
const maxN = 50;
|
||||
|
||||
for (let n = 2; n <= maxN; n += 2) {
|
||||
let probability;
|
||||
|
||||
if (strategy === 'naive') {
|
||||
// Naive strategy: (1/2)^n
|
||||
probability = Math.pow(0.5, n);
|
||||
} else {
|
||||
// Loop strategy: 1 - sum(1/k for k from n/2+1 to n)
|
||||
let sum = 0;
|
||||
for (let k = Math.floor(n / 2) + 1; k <= n; k++) {
|
||||
sum += 1 / k;
|
||||
}
|
||||
probability = 1 - sum;
|
||||
}
|
||||
|
||||
points.push({ n, probability });
|
||||
}
|
||||
|
||||
return points;
|
||||
}, [strategy]);
|
||||
|
||||
// Find min and max for scaling
|
||||
const maxProb = Math.max(...data.map(d => d.probability));
|
||||
const minProb = Math.min(...data.map(d => d.probability));
|
||||
|
||||
// Use log scale for naive strategy since values get extremely small
|
||||
const useLogScale = strategy === 'naive';
|
||||
|
||||
const getY = (prob: number, height: number) => {
|
||||
if (useLogScale) {
|
||||
const logProb = prob > 0 ? Math.log10(prob) : -100;
|
||||
const logMax = Math.log10(maxProb);
|
||||
const logMin = Math.log10(minProb);
|
||||
return height - ((logProb - logMin) / (logMax - logMin)) * height;
|
||||
} else {
|
||||
return height - (prob / maxProb) * height;
|
||||
}
|
||||
};
|
||||
|
||||
const chartWidth = 600;
|
||||
const chartHeight = 300;
|
||||
const padding = { top: 20, right: 20, bottom: 50, left: 70 };
|
||||
const width = chartWidth - padding.left - padding.right;
|
||||
const height = chartHeight - padding.top - padding.bottom;
|
||||
|
||||
// Create path for the line
|
||||
const pathData = data.map((point, i) => {
|
||||
const x = (point.n / 50) * width;
|
||||
const y = getY(point.probability, height);
|
||||
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
|
||||
}).join(' ');
|
||||
|
||||
// Y-axis labels
|
||||
const yLabels = useLogScale
|
||||
? [-5, -10, -15, -20, -25, -30].map(exp => ({
|
||||
label: `10^${exp}`,
|
||||
value: Math.pow(10, exp),
|
||||
}))
|
||||
: [0, 0.1, 0.2, 0.3, 0.4, 0.5].map(val => ({
|
||||
label: val.toFixed(1),
|
||||
value: val,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-white">
|
||||
<h3 className="text-xl font-semibold mb-4">
|
||||
{strategy === 'naive' ? 'Naive Strategy: Random Box Selection' : 'Loop Strategy'}
|
||||
</h3>
|
||||
|
||||
<svg width={chartWidth} height={chartHeight} className="mx-auto">
|
||||
<g transform={`translate(${padding.left}, ${padding.top})`}>
|
||||
{/* Grid lines */}
|
||||
{yLabels.map(({ value }) => {
|
||||
const y = getY(value, height);
|
||||
return (
|
||||
<line
|
||||
key={value}
|
||||
x1={0}
|
||||
y1={y}
|
||||
x2={width}
|
||||
y2={y}
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis */}
|
||||
<line
|
||||
x1={0}
|
||||
y1={height}
|
||||
x2={width}
|
||||
y2={height}
|
||||
stroke="#374151"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Y-axis */}
|
||||
<line
|
||||
x1={0}
|
||||
y1={0}
|
||||
x2={0}
|
||||
y2={height}
|
||||
stroke="#374151"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Plot line */}
|
||||
<path
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
{/* Data points */}
|
||||
{data.filter((_, i) => i % 2 === 0).map((point) => {
|
||||
const x = (point.n / 50) * width;
|
||||
const y = getY(point.probability, height);
|
||||
return (
|
||||
<circle
|
||||
key={point.n}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={4}
|
||||
fill="#3b82f6"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis labels */}
|
||||
{[10, 20, 30, 40, 50].map((n) => {
|
||||
const x = (n / 50) * width;
|
||||
return (
|
||||
<text
|
||||
key={n}
|
||||
x={x}
|
||||
y={height + 25}
|
||||
textAnchor="middle"
|
||||
fontSize="14"
|
||||
fill="#374151"
|
||||
>
|
||||
{n}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Y-axis labels */}
|
||||
{yLabels.map(({ label, value }) => {
|
||||
const y = getY(value, height);
|
||||
return (
|
||||
<text
|
||||
key={value}
|
||||
x={-10}
|
||||
y={y + 5}
|
||||
textAnchor="end"
|
||||
fontSize="12"
|
||||
fill="#374151"
|
||||
>
|
||||
{label}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis title */}
|
||||
<text
|
||||
x={width / 2}
|
||||
y={height + 45}
|
||||
textAnchor="middle"
|
||||
fontSize="16"
|
||||
fontWeight="600"
|
||||
fill="#374151"
|
||||
>
|
||||
Number of Prisoners (n)
|
||||
</text>
|
||||
|
||||
{/* Y-axis title */}
|
||||
<text
|
||||
x={-height / 2}
|
||||
y={-50}
|
||||
textAnchor="middle"
|
||||
fontSize="16"
|
||||
fontWeight="600"
|
||||
fill="#374151"
|
||||
transform={`rotate(-90, ${-height / 2}, -50)`}
|
||||
>
|
||||
Probability of Success
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div className="mt-4 p-4 bg-gray-100 rounded">
|
||||
<p className="text-sm">
|
||||
{strategy === 'naive' ? (
|
||||
<>
|
||||
The naive strategy has probability <strong>(1/2)^n</strong> which decreases
|
||||
exponentially. Note the logarithmic scale on the y-axis!
|
||||
With 20 prisoners, the probability is already less than one in a million.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
The loop strategy maintains a probability around <strong>31%</strong> regardless
|
||||
of the number of prisoners, approaching 1 - ln(2) ≈ 0.3069 as n → ∞.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProbabilityChart;
|
||||
108
src/components/problems/README.md
Normal file
108
src/components/problems/README.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# 100 Prisoners Problem - Interactive Blog Post
|
||||
|
||||
This directory contains the interactive React components for the "100 Prisoners Problem" blog post, inspired by the interactive style of Nicky Case's explorable explanations.
|
||||
|
||||
## Files Created
|
||||
|
||||
### Blog Post
|
||||
- **`src/content/problems/100-prisoners.mdx`** - The main blog post with embedded interactive components
|
||||
|
||||
### Interactive Components
|
||||
|
||||
1. **`PrisonerGameSimulator.tsx`** - Interactive game simulation
|
||||
- Allows users to play the prisoner game themselves
|
||||
- Adjustable number of prisoners (5-100)
|
||||
- Manual box clicking to experience the problem
|
||||
- Visual feedback for success/failure
|
||||
- Tracks current prisoner and attempts remaining
|
||||
|
||||
2. **`ProbabilityChart.tsx`** - Probability visualization
|
||||
- Shows probability curves for naive and loop strategies
|
||||
- Uses logarithmic scale for naive strategy (values get extremely small)
|
||||
- Interactive SVG chart with proper axes and labels
|
||||
- Includes explanatory text
|
||||
|
||||
3. **`PermutationExplorer.tsx`** - Cycle structure explorer
|
||||
- Generates all permutations for n ∈ {3, 4, 5}
|
||||
- Displays both standard and cycle notation
|
||||
- Filter by presence of long cycles (> n/2)
|
||||
- Color-coded display (red for long cycles, green for short)
|
||||
- Shows statistics about cycle length distribution
|
||||
|
||||
4. **`StrategyComparison.tsx`** - Side-by-side strategy comparison
|
||||
- Compares naive vs. loop strategy on same chart
|
||||
- Shows probabilities up to n=100
|
||||
- Includes detailed numerical breakdown
|
||||
- Highlights the key 31.18% result at n=100
|
||||
|
||||
## Features
|
||||
|
||||
All components are:
|
||||
- ✅ Fully interactive with React hooks
|
||||
- ✅ Styled with Tailwind CSS classes
|
||||
- ✅ Responsive for mobile and desktop
|
||||
- ✅ Self-contained with no external dependencies beyond React
|
||||
- ✅ Client-side only (using `client:only="react"` in Astro)
|
||||
|
||||
## How It Works
|
||||
|
||||
### The Problem
|
||||
100 prisoners must each find their own number among 100 boxes by opening at most 50 boxes. If all prisoners succeed, they go free. They can agree on a strategy beforehand but cannot communicate during the challenge.
|
||||
|
||||
### The Naive Strategy
|
||||
Each prisoner opens 50 random boxes. Probability of success: (1/2)^100 ≈ 0%
|
||||
|
||||
### The Clever Strategy (Loop Following)
|
||||
1. Open the box with your own number
|
||||
2. Look at the number inside
|
||||
3. Open the box with that number
|
||||
4. Repeat until you find your number
|
||||
|
||||
This exploits the cycle structure of permutations. Success probability: ~31.18%!
|
||||
|
||||
### Key Mathematical Insight
|
||||
The prisoners succeed if and only if the permutation of numbers in boxes has no cycle longer than n/2. For n=100, the probability of this is:
|
||||
|
||||
P(success) = 1 - Σ(1/k) for k=51 to 100 ≈ 0.3118
|
||||
|
||||
As n→∞, this converges to 1 - ln(2) ≈ 0.3069.
|
||||
|
||||
## Usage in MDX
|
||||
|
||||
The components are imported and used like this:
|
||||
|
||||
\`\`\`mdx
|
||||
import PrisonerGameSimulator from '@/components/problems/PrisonerGameSimulator.tsx';
|
||||
import ProbabilityChart from '@/components/problems/ProbabilityChart.tsx';
|
||||
import PermutationExplorer from '@/components/problems/PermutationExplorer.tsx';
|
||||
import StrategyComparison from '@/components/problems/StrategyComparison.tsx';
|
||||
|
||||
<PrisonerGameSimulator client:only="react" />
|
||||
<ProbabilityChart strategy="naive" client:only="react" />
|
||||
<PermutationExplorer client:only="react" />
|
||||
<StrategyComparison client:only="react" />
|
||||
\`\`\`
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Inspired by Nicky Case's "Parable of the Polygons", these components aim to:
|
||||
- Make abstract mathematical concepts tangible through interaction
|
||||
- Allow readers to explore and discover patterns themselves
|
||||
- Provide immediate visual feedback
|
||||
- Balance playfulness with mathematical rigor
|
||||
- Guide understanding through progressive disclosure
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential additions:
|
||||
- Auto-play mode for the game simulator using the loop strategy
|
||||
- Animation showing cycle formation in real-time
|
||||
- Monte Carlo simulation running multiple trials
|
||||
- Variant problems (different box-opening rules, malicious director, etc.)
|
||||
- 3D visualization of cycle structures for larger n
|
||||
|
||||
## References
|
||||
|
||||
- [Wikipedia: 100 Prisoners Problem](https://en.wikipedia.org/wiki/100_prisoners_problem)
|
||||
- [Nicky Case's Explorable Explanations](https://ncase.me/)
|
||||
- Original problem: Gál & Miltersen (2003)
|
||||
313
src/components/problems/StrategyComparison.tsx
Normal file
313
src/components/problems/StrategyComparison.tsx
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
const StrategyComparison: React.FC = () => {
|
||||
const data = useMemo(() => {
|
||||
const points = [];
|
||||
const maxN = 100;
|
||||
|
||||
for (let n = 5; n <= maxN; n += 5) {
|
||||
// Naive strategy
|
||||
const naiveProb = Math.pow(0.5, n);
|
||||
|
||||
// Loop strategy: 1 - sum(1/k for k from n/2+1 to n)
|
||||
let sum = 0;
|
||||
for (let k = Math.floor(n / 2) + 1; k <= n; k++) {
|
||||
sum += 1 / k;
|
||||
}
|
||||
const loopProb = 1 - sum;
|
||||
|
||||
points.push({ n, naiveProb, loopProb });
|
||||
}
|
||||
|
||||
return points;
|
||||
}, []);
|
||||
|
||||
const chartWidth = 700;
|
||||
const chartHeight = 350;
|
||||
const padding = { top: 20, right: 100, bottom: 50, left: 70 };
|
||||
const width = chartWidth - padding.left - padding.right;
|
||||
const height = chartHeight - padding.top - padding.bottom;
|
||||
|
||||
// Only show loop strategy on linear scale (naive is too small to see)
|
||||
const maxProb = 0.5;
|
||||
|
||||
const getY = (prob: number) => {
|
||||
return height - (prob / maxProb) * height;
|
||||
};
|
||||
|
||||
const getX = (n: number) => {
|
||||
return (n / 100) * width;
|
||||
};
|
||||
|
||||
// Create paths for both strategies
|
||||
const loopPath = data.map((point, i) => {
|
||||
const x = getX(point.n);
|
||||
const y = getY(point.loopProb);
|
||||
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
|
||||
}).join(' ');
|
||||
|
||||
// For naive, we'll just show it near zero
|
||||
const naivePath = data.map((point, i) => {
|
||||
const x = getX(point.n);
|
||||
const y = height - 2; // Near the bottom (representing ~0)
|
||||
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-white">
|
||||
<h3 className="text-xl font-semibold mb-4">Strategy Comparison</h3>
|
||||
|
||||
<svg width={chartWidth} height={chartHeight} className="mx-auto">
|
||||
<g transform={`translate(${padding.left}, ${padding.top})`}>
|
||||
{/* Grid lines */}
|
||||
{[0, 0.1, 0.2, 0.3, 0.4, 0.5].map((val) => {
|
||||
const y = getY(val);
|
||||
return (
|
||||
<g key={val}>
|
||||
<line
|
||||
x1={0}
|
||||
y1={y}
|
||||
x2={width}
|
||||
y2={y}
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<text
|
||||
x={-10}
|
||||
y={y + 5}
|
||||
textAnchor="end"
|
||||
fontSize="12"
|
||||
fill="#374151"
|
||||
>
|
||||
{val.toFixed(1)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis */}
|
||||
<line
|
||||
x1={0}
|
||||
y1={height}
|
||||
x2={width}
|
||||
y2={height}
|
||||
stroke="#374151"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Y-axis */}
|
||||
<line
|
||||
x1={0}
|
||||
y1={0}
|
||||
x2={0}
|
||||
y2={height}
|
||||
stroke="#374151"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Loop strategy line */}
|
||||
<path
|
||||
d={loopPath}
|
||||
fill="none"
|
||||
stroke="#10b981"
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
{/* Naive strategy line (at the bottom) */}
|
||||
<path
|
||||
d={naivePath}
|
||||
fill="none"
|
||||
stroke="#ef4444"
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeDasharray="5,5"
|
||||
/>
|
||||
|
||||
{/* Data points for loop strategy */}
|
||||
{data.filter((_, i) => i % 2 === 0).map((point) => {
|
||||
const x = getX(point.n);
|
||||
const y = getY(point.loopProb);
|
||||
return (
|
||||
<circle
|
||||
key={point.n}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={4}
|
||||
fill="#10b981"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis labels */}
|
||||
{[20, 40, 60, 80, 100].map((n) => {
|
||||
const x = getX(n);
|
||||
return (
|
||||
<text
|
||||
key={n}
|
||||
x={x}
|
||||
y={height + 25}
|
||||
textAnchor="middle"
|
||||
fontSize="14"
|
||||
fill="#374151"
|
||||
>
|
||||
{n}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis title */}
|
||||
<text
|
||||
x={width / 2}
|
||||
y={height + 45}
|
||||
textAnchor="middle"
|
||||
fontSize="16"
|
||||
fontWeight="600"
|
||||
fill="#374151"
|
||||
>
|
||||
Number of Prisoners (n)
|
||||
</text>
|
||||
|
||||
{/* Y-axis title */}
|
||||
<text
|
||||
x={-height / 2}
|
||||
y={-50}
|
||||
textAnchor="middle"
|
||||
fontSize="16"
|
||||
fontWeight="600"
|
||||
fill="#374151"
|
||||
transform={`rotate(-90, ${-height / 2}, -50)`}
|
||||
>
|
||||
Probability of Success
|
||||
</text>
|
||||
|
||||
{/* Legend */}
|
||||
<g transform={`translate(${width + 20}, 20)`}>
|
||||
<rect x={0} y={0} width={15} height={15} fill="#10b981" />
|
||||
<text x={20} y={12} fontSize="14" fill="#374151">
|
||||
Loop Strategy
|
||||
</text>
|
||||
|
||||
<line
|
||||
x1={0}
|
||||
y1={35}
|
||||
x2={15}
|
||||
y2={35}
|
||||
stroke="#ef4444"
|
||||
strokeWidth={3}
|
||||
strokeDasharray="5,5"
|
||||
/>
|
||||
<text x={20} y={40} fontSize="14" fill="#374151">
|
||||
Naive Strategy
|
||||
</text>
|
||||
<text x={20} y={55} fontSize="11" fill="#6b7280">
|
||||
(≈ 0 for all n)
|
||||
</text>
|
||||
</g>
|
||||
|
||||
{/* Highlight key value */}
|
||||
<g>
|
||||
{(() => {
|
||||
const n100Point = data.find(p => p.n === 100);
|
||||
if (n100Point) {
|
||||
const x = getX(100);
|
||||
const y = getY(n100Point.loopProb);
|
||||
return (
|
||||
<>
|
||||
<circle
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={6}
|
||||
fill="none"
|
||||
stroke="#10b981"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<line
|
||||
x1={x}
|
||||
y1={y - 10}
|
||||
x2={x}
|
||||
y2={y - 40}
|
||||
stroke="#374151"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<text
|
||||
x={x}
|
||||
y={y - 45}
|
||||
textAnchor="middle"
|
||||
fontSize="12"
|
||||
fontWeight="600"
|
||||
fill="#10b981"
|
||||
>
|
||||
31.18%
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-red-50 border-2 border-red-300 rounded">
|
||||
<h4 className="font-semibold text-red-900 mb-2">❌ Naive Strategy</h4>
|
||||
<p className="text-sm mb-2">
|
||||
Each prisoner randomly selects 50 boxes.
|
||||
</p>
|
||||
<div className="text-xs space-y-1">
|
||||
<p><strong>n=10:</strong> P ≈ {(Math.pow(0.5, 10) * 100).toExponential(2)}%</p>
|
||||
<p><strong>n=20:</strong> P ≈ {(Math.pow(0.5, 20) * 100).toExponential(2)}%</p>
|
||||
<p><strong>n=50:</strong> P ≈ {(Math.pow(0.5, 50) * 100).toExponential(2)}%</p>
|
||||
<p><strong>n=100:</strong> P ≈ {(Math.pow(0.5, 100) * 100).toExponential(2)}%</p>
|
||||
</div>
|
||||
<p className="text-xs mt-2 italic">
|
||||
Essentially impossible for any reasonable n!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-50 border-2 border-green-300 rounded">
|
||||
<h4 className="font-semibold text-green-900 mb-2">✅ Loop Strategy</h4>
|
||||
<p className="text-sm mb-2">
|
||||
Each prisoner follows the cycle starting at their number.
|
||||
</p>
|
||||
<div className="text-xs space-y-1">
|
||||
{(() => {
|
||||
const calcProb = (n: number) => {
|
||||
let sum = 0;
|
||||
for (let k = Math.floor(n / 2) + 1; k <= n; k++) {
|
||||
sum += 1 / k;
|
||||
}
|
||||
return ((1 - sum) * 100).toFixed(2);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<p><strong>n=10:</strong> P ≈ {calcProb(10)}%</p>
|
||||
<p><strong>n=20:</strong> P ≈ {calcProb(20)}%</p>
|
||||
<p><strong>n=50:</strong> P ≈ {calcProb(50)}%</p>
|
||||
<p><strong>n=100:</strong> P ≈ {calcProb(100)}%</p>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<p className="text-xs mt-2 italic">
|
||||
Converges to 1 - ln(2) ≈ 30.69% as n → ∞
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 bg-blue-50 rounded">
|
||||
<p className="text-sm">
|
||||
<strong>Key Insight:</strong> The loop strategy transforms an impossible problem
|
||||
(probability ≈ 10<sup>-30</sup>) into a reasonable one (probability ≈ 31%).
|
||||
This astronomical improvement comes from exploiting the mathematical structure
|
||||
of permutations rather than relying on independent random trials.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyComparison;
|
||||
183
src/content/problems/100-prisoners.mdx
Normal file
183
src/content/problems/100-prisoners.mdx
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
---
|
||||
title: "The 100 Prisoners Problem: A Beautiful Puzzle in Probability"
|
||||
description: "An interactive exploration of one of the most counterintuitive puzzles in probability theory, where 100 prisoners can escape with over 30% chance using a clever strategy."
|
||||
pubDate: "Oct 13 2025"
|
||||
image: "https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=400&auto=format&fit=crop&q=60"
|
||||
authorImage: "/avatar/avatar1.png"
|
||||
authorName: "Neeldhara"
|
||||
---
|
||||
|
||||
import PrisonerGameSimulator from '@/components/problems/PrisonerGameSimulator.tsx';
|
||||
import ProbabilityChart from '@/components/problems/ProbabilityChart.tsx';
|
||||
import PermutationExplorer from '@/components/problems/PermutationExplorer.tsx';
|
||||
import StrategyComparison from '@/components/problems/StrategyComparison.tsx';
|
||||
|
||||
# The 100 Prisoners Problem
|
||||
|
||||
Imagine 100 prisoners, numbered 1 to 100, face an unusual challenge. In a room are 100 boxes, also numbered 1 to 100. Inside each box is a randomly assigned number from 1 to 100 (each number appearing exactly once). The prisoners must find their own number by opening boxes, but here's the catch: **each prisoner can open only 50 boxes**.
|
||||
|
||||
The prisoners can devise a strategy beforehand, but once the challenge begins, they cannot communicate. If **all 100 prisoners** find their number, everyone goes free. If even one prisoner fails, all remain imprisoned.
|
||||
|
||||
What's the best strategy? And what are the odds of success?
|
||||
|
||||
## Try It Yourself!
|
||||
|
||||
Before we dive into the mathematics, let's experience the problem firsthand. Below is a simulator where you can play the game yourself. Choose the number of prisoners (between 5 and 100), and then try to help each prisoner find their number by clicking on boxes.
|
||||
|
||||
<PrisonerGameSimulator client:only="react" />
|
||||
|
||||
---
|
||||
|
||||
## The Naive Strategy
|
||||
|
||||
The most obvious approach is for each prisoner to simply open 50 random boxes. This seems reasonable, but the probability of success is dismal.
|
||||
|
||||
For any single prisoner, the probability of finding their number in 50 random boxes out of 100 is exactly 1/2. Since all 100 prisoners must succeed, and their outcomes are independent under random selection, the probability that everyone succeeds is:
|
||||
|
||||
$$P(\text\{success\}) = \left(\frac\{1\}\{2\}\right)^\{100\} \approx 7.9 \times 10^\{-31\}$$
|
||||
|
||||
That's essentially zero! You're more likely to win the lottery while being struck by lightning... multiple times.
|
||||
|
||||
### Naive Strategy: Probability vs Number of Prisoners
|
||||
|
||||
Watch how the probability of success plummets as we increase the number of prisoners:
|
||||
|
||||
<ProbabilityChart strategy="naive" client:only="react" />
|
||||
|
||||
As you can see, even with just 10 prisoners, the probability drops to about 0.1%. With 100 prisoners, it's practically impossible.
|
||||
|
||||
---
|
||||
|
||||
## The Clever Strategy: Following the Loops
|
||||
|
||||
Here's where mathematics reveals something beautiful. Instead of opening random boxes, each prisoner should follow a simple rule:
|
||||
|
||||
1. **Start by opening the box with your own number**
|
||||
2. **Look at the number inside that box**
|
||||
3. **Open the box with that number next**
|
||||
4. **Continue following this chain**
|
||||
|
||||
Why does this work? The key insight involves understanding permutations and cycles.
|
||||
|
||||
### Understanding Cycles in Permutations
|
||||
|
||||
When we randomly assign 100 numbers to 100 boxes, we're creating a **permutation** of the numbers 1 through 100. Every permutation can be decomposed into **cycles**.
|
||||
|
||||
For example, if:
|
||||
- Box 1 contains number 4
|
||||
- Box 4 contains number 7
|
||||
- Box 7 contains number 1
|
||||
|
||||
Then we have a cycle: 1 -> 4 -> 7 -> 1
|
||||
|
||||
Let's explore this with smaller numbers:
|
||||
|
||||
<PermutationExplorer client:only="react" />
|
||||
|
||||
The interactive explorer above lets you:
|
||||
- Generate all permutations for small values of *n* (3, 4, or 5)
|
||||
- See both standard notation and cycle notation
|
||||
- Filter permutations that have at least one "long cycle" (longer than *n*/2)
|
||||
- Understand why long cycles matter
|
||||
|
||||
### Why This Strategy Works
|
||||
|
||||
Here's the crucial insight: **A prisoner succeeds if and only if they are in a cycle of length ≤ 50**.
|
||||
|
||||
When prisoner k follows the loop strategy:
|
||||
1. They start at box k
|
||||
2. They follow a path through boxes determined by the permutation
|
||||
3. This path forms a cycle that eventually leads back to box k
|
||||
4. If this cycle has length ≤ 50, they'll find their number within 50 opens
|
||||
|
||||
The prisoners **all succeed** if and only if there is **no cycle longer than 50** in the permutation.
|
||||
|
||||
### Calculating the Probability
|
||||
|
||||
The probability that a random permutation of 100 elements has no cycle longer than 50 is:
|
||||
|
||||
$$P(\text\{success\}) = 1 - \sum_\{k=51\}^\{100\} \frac\{1\}\{k\}$$
|
||||
|
||||
Computing this sum:
|
||||
|
||||
$$P(\text\{success\}) = 1 - \left(\frac\{1\}\{51\} + \frac\{1\}\{52\} + \cdots + \frac\{1\}\{100\}\right) \approx 0.3118$$
|
||||
|
||||
That's over **31%**! This is astronomically better than the naive strategy's probability of essentially zero.
|
||||
|
||||
### Loop Strategy: Probability vs Number of Prisoners
|
||||
|
||||
<StrategyComparison client:only="react" />
|
||||
|
||||
## The Mathematics Behind It
|
||||
|
||||
### Why Does the Cycle Length Matter?
|
||||
|
||||
In a permutation of n elements, prisoner i will find their number within k tries using the loop strategy if and only if i belongs to a cycle of length at most k.
|
||||
|
||||
For n = 100 and k = 50:
|
||||
- The probability that a specific prisoner is in a cycle of length > 50 is: Σ(j=51 to 100) of 1/100
|
||||
- But we care about whether ANY prisoner is in such a cycle
|
||||
- The probability that NO cycle has length > 50 is: 1 - Σ(j=51 to 100) of 1/j
|
||||
|
||||
### The Harmonic Series Connection
|
||||
|
||||
As *n* approaches infinity, if each prisoner can open *n*/2 boxes, the probability of success approaches:
|
||||
|
||||
The limit as n approaches infinity is: P_n = 1 - ln(2) ≈ 0.3069
|
||||
|
||||
This beautiful result connects combinatorics, probability, and analysis through the harmonic series!
|
||||
|
||||
### Optimality
|
||||
|
||||
Remarkably, this loop-following strategy is **optimal**. No other strategy can achieve a higher probability of success. The proof involves showing that the problem reduces to avoiding long cycles in a random permutation, and the loop strategy is the unique way to leverage this structure.
|
||||
|
||||
---
|
||||
|
||||
## Why This Problem is Beautiful
|
||||
|
||||
The 100 prisoners problem is beloved by mathematicians because:
|
||||
|
||||
1. **Counterintuitive Result**: The fact that 31% >> 0% is shocking. A seemingly hopeless problem becomes tractable with the right insight.
|
||||
|
||||
2. **Deep Connection**: It connects permutation theory, probability, and the harmonic series in an elegant way.
|
||||
|
||||
3. **Collaborative Success**: Unlike independent trials, the prisoners' fates are intertwined through the structure of the permutation.
|
||||
|
||||
4. **Real-World Metaphor**: It illustrates how understanding underlying structure (cycles in permutations) can dramatically improve outcomes.
|
||||
|
||||
---
|
||||
|
||||
## Variants and Extensions
|
||||
|
||||
### What if prisoners can open k < n/2 boxes?
|
||||
|
||||
As k decreases below n/2, the probability drops rapidly. The threshold at n/2 is special—it's where the strategy becomes remarkably effective.
|
||||
|
||||
### What about communication?
|
||||
|
||||
If prisoners could communicate between trials (but not during), they still can't improve the strategy. The beauty is that the strategy requires only coordination before any prisoner enters, not during.
|
||||
|
||||
### The Malicious Director
|
||||
|
||||
In an interesting variant, suppose the director can see the prisoners' strategy and arrange the boxes adversarially (not randomly). Can the director always force them to fail? Yes! The director can always create a single cycle of length n, guaranteeing failure.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The 100 prisoners problem shows us that:
|
||||
- Mathematical structure can be hidden in apparent randomness
|
||||
- Clever strategies can overcome seemingly impossible odds
|
||||
- Group coordination can be more powerful than independent action
|
||||
- Beautiful mathematics often lies beneath simple-seeming puzzles
|
||||
|
||||
The next time you face a problem that seems impossible, remember: there might be a hidden structure waiting to be discovered, turning hopeless odds into reasonable ones.
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Gál, A., & Miltersen, P. B. (2003). "The Cell Probe Complexity of Succinct Data Structures"](https://arxiv.org/abs/cs/0310027)
|
||||
- [Curtin, E., & Warshauer, M. (2006). "The Locker Puzzle"](https://www.jstor.org/stable/27642173)
|
||||
- [Wikipedia: 100 Prisoners Problem](https://en.wikipedia.org/wiki/100_prisoners_problem)
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue