diff --git a/src/App.tsx b/src/App.tsx index 0f42d15..7d2f5fb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -57,6 +57,7 @@ import StackingBlocksPage from './pages/StackingBlocksPage'; import ParityBitsGamePage from './pages/ParityBitsGamePage'; import CrapsGamePage from './pages/CrapsGamePage'; import NeighborSumAvoidancePage from './pages/NeighborSumAvoidancePage'; +import BurnsidesLemmaPage from './pages/BurnsidesLemmaPage'; const queryClient = new QueryClient(); @@ -83,6 +84,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/BurnsidesLemma.tsx b/src/components/BurnsidesLemma.tsx new file mode 100644 index 0000000..97e8c17 --- /dev/null +++ b/src/components/BurnsidesLemma.tsx @@ -0,0 +1,359 @@ +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Slider } from '@/components/ui/slider'; +import { Badge } from '@/components/ui/badge'; +import SocialShare from './SocialShare'; +import { RotateCw, Palette } from 'lucide-react'; + +interface BurnsidesLemmaProps { + showSocialShare?: boolean; + shareUrl?: string; +} + +interface Necklace { + colors: number[]; + id: string; +} + +const BurnsidesLemma = ({ showSocialShare = false, shareUrl }: BurnsidesLemmaProps) => { + const [numBeads, setNumBeads] = useState(6); + const [numColors, setNumColors] = useState(3); + const [allNecklaces, setAllNecklaces] = useState([]); + const [equivalenceClasses, setEquivalenceClasses] = useState([]); + const [showFormula, setShowFormula] = useState(false); + + const colorPalette = [ + 'hsl(var(--primary))', + 'hsl(var(--accent))', + 'hsl(220, 90%, 56%)', + 'hsl(142, 76%, 36%)', + 'hsl(38, 92%, 50%)', + 'hsl(280, 70%, 60%)', + ]; + + // Generate all possible colorings + const generateAllColorings = (beads: number, colors: number): Necklace[] => { + const total = Math.pow(colors, beads); + const necklaces: Necklace[] = []; + + for (let i = 0; i < total; i++) { + const coloring: number[] = []; + let num = i; + for (let j = 0; j < beads; j++) { + coloring.push(num % colors); + num = Math.floor(num / colors); + } + necklaces.push({ + colors: coloring, + id: coloring.join(',') + }); + } + + return necklaces; + }; + + // Rotate a necklace by k positions + const rotate = (colors: number[], k: number): number[] => { + const n = colors.length; + const rotated = [...colors]; + return rotated.slice(k % n).concat(rotated.slice(0, k % n)); + }; + + // Get canonical form (lexicographically smallest rotation) + const getCanonical = (colors: number[]): string => { + const n = colors.length; + let minRotation = colors; + + for (let i = 1; i < n; i++) { + const rotated = rotate(colors, i); + if (rotated.join(',') < minRotation.join(',')) { + minRotation = rotated; + } + } + + return minRotation.join(','); + }; + + // Group necklaces into equivalence classes + const groupByEquivalence = (necklaces: Necklace[]): Necklace[][] => { + const canonicalMap = new Map(); + + for (const necklace of necklaces) { + const canonical = getCanonical(necklace.colors); + if (!canonicalMap.has(canonical)) { + canonicalMap.set(canonical, []); + } + canonicalMap.get(canonical)!.push(necklace); + } + + return Array.from(canonicalMap.values()); + }; + + // Calculate using Burnside's Lemma + const calculateBurnsideLemma = (): number => { + let sum = 0; + const n = numBeads; + const k = numColors; + + // For each rotation (including identity) + for (let r = 0; r < n; r++) { + // Count colorings fixed by this rotation + const gcd = getGCD(n, r); + sum += Math.pow(k, gcd); + } + + return sum / n; + }; + + const getGCD = (a: number, b: number): number => { + return b === 0 ? a : getGCD(b, a % b); + }; + + useEffect(() => { + const necklaces = generateAllColorings(numBeads, numColors); + setAllNecklaces(necklaces); + const classes = groupByEquivalence(necklaces); + setEquivalenceClasses(classes); + }, [numBeads, numColors]); + + const renderNecklace = (necklace: Necklace, size: 'small' | 'medium' = 'medium') => { + const radius = size === 'small' ? 30 : 50; + const beadRadius = size === 'small' ? 8 : 12; + const svgSize = size === 'small' ? 80 : 130; + const center = svgSize / 2; + + return ( + + + {necklace.colors.map((color, i) => { + const angle = (2 * Math.PI * i) / necklace.colors.length - Math.PI / 2; + const x = center + radius * Math.cos(angle); + const y = center + radius * Math.sin(angle); + + return ( + + + + ); + })} + + ); + }; + + const burnsideResult = calculateBurnsideLemma(); + + return ( +
+ + + + + Burnside's Lemma: Necklace Colorings + + + How many distinct necklaces can you make with {numBeads} beads using {numColors} colors? + Two necklaces are the same if one can be rotated to look like the other. + + + + {/* Controls */} +
+
+
+ + {numBeads} +
+ setNumBeads(value[0])} + min={3} + max={8} + step={1} + className="w-full" + /> +
+ +
+
+ + {numColors} +
+ setNumColors(value[0])} + min={2} + max={4} + step={1} + className="w-full" + /> +
+
+ + {/* Results Summary */} +
+ + +
{allNecklaces.length}
+
Total Colorings
+
({numColors}^{numBeads})
+
+
+ + + +
{equivalenceClasses.length}
+
Distinct Necklaces
+
(under rotation)
+
+
+ + + +
{burnsideResult}
+
Burnside's Formula
+
matches exactly!
+
+
+
+ + {/* Formula Explanation */} +
+ + + {showFormula && ( + + +
+

Burnside's Lemma

+

+ The number of distinct objects under group action equals the average number of objects fixed by each symmetry. +

+
+ +
+
+ |X/G| = (1/|G|) × Σ |Fix(g)| +
+
+
• |X/G| = number of distinct necklaces
+
• |G| = number of rotations = {numBeads}
+
• |Fix(g)| = colorings unchanged by rotation g
+
+
+ +
+

For rotation by k positions:

+

+ A coloring is fixed if beads repeat every gcd(n,k) positions. + Number of such colorings = {numColors}^gcd(n,k) +

+
+ +
+
+ {Array.from({ length: numBeads }, (_, i) => { + const gcd = getGCD(numBeads, i); + return ( +
+ Rotation by {i}: {numColors}^{gcd} = {Math.pow(numColors, gcd)} fixed colorings +
+ ); + })} +
+ Sum = {Array.from({ length: numBeads }, (_, i) => Math.pow(numColors, getGCD(numBeads, i))).reduce((a, b) => a + b, 0)} +
+
+ Result = {Array.from({ length: numBeads }, (_, i) => Math.pow(numColors, getGCD(numBeads, i))).reduce((a, b) => a + b, 0)} / {numBeads} = {burnsideResult} +
+
+
+
+
+ )} +
+ + {/* Equivalence Classes Display */} +
+
+

+ All {equivalenceClasses.length} Distinct Necklaces +

+

+ Each card shows one representative from each equivalence class. + Necklaces in the same class can be rotated to match each other. +

+
+ + {equivalenceClasses.length <= 20 && ( +
+ {equivalenceClasses.map((eqClass, idx) => ( + + + {renderNecklace(eqClass[0], 'medium')} +
+ + Class {idx + 1} + + {eqClass.length > 1 && ( +
+ {eqClass.length} rotations +
+ )} +
+
+
+ ))} +
+ )} + + {equivalenceClasses.length > 20 && ( +
+

+ Too many distinct necklaces to display ({equivalenceClasses.length} total). +

+

+ Try reducing the number of beads or colors to see all distinct necklaces. +

+
+ )} +
+
+
+ + {showSocialShare && ( + + )} +
+ ); +}; + +export default BurnsidesLemma; diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx index c9f3b9e..b25fd91 100644 --- a/src/components/InteractiveIndex.tsx +++ b/src/components/InteractiveIndex.tsx @@ -252,6 +252,15 @@ const allInteractives: Interactive[] = [ path: '/themes/discrete-math/graphs/neighbor-sum-avoidance', theme: 'Discrete Math', dateAdded: '2025-01-22' + }, + { + id: 'burnsides-lemma', + title: "Burnside's Lemma", + description: 'Explore how to count distinct objects under symmetry by creating necklaces with different bead and color combinations.', + tags: ['group-theory', 'symmetry', 'combinatorics', 'counting', 'algebra', 'structures'], + path: '/themes/discrete-math/structures/burnsides-lemma', + theme: 'Discrete Math', + dateAdded: '2025-01-22' } ]; diff --git a/src/pages/BurnsidesLemmaPage.tsx b/src/pages/BurnsidesLemmaPage.tsx new file mode 100644 index 0000000..c3b7303 --- /dev/null +++ b/src/pages/BurnsidesLemmaPage.tsx @@ -0,0 +1,14 @@ +import Layout from '@/components/Layout'; +import BurnsidesLemma from '@/components/BurnsidesLemma'; + +const BurnsidesLemmaPage = () => { + return ( + +
+ +
+
+ ); +}; + +export default BurnsidesLemmaPage; diff --git a/src/pages/theme-pages/discrete-math/Structures.tsx b/src/pages/theme-pages/discrete-math/Structures.tsx index 4023ddb..be94b99 100644 --- a/src/pages/theme-pages/discrete-math/Structures.tsx +++ b/src/pages/theme-pages/discrete-math/Structures.tsx @@ -1,11 +1,118 @@ -import ComingSoon from "@/components/ComingSoon"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Search } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import Layout from '@/components/Layout'; +import { useState } from 'react'; +import BurnsidesLemma from '@/components/BurnsidesLemma'; + +interface Interactive { + id: string; + title: string; + description: string; + tags: string[]; + path: string; + component: React.ComponentType<{ showSocialShare?: boolean; shareUrl?: string }>; +} + +const interactives: Interactive[] = [ + { + id: 'burnsides-lemma', + title: "Burnside's Lemma", + description: 'Explore how to count distinct objects under symmetry by creating necklaces with different bead and color combinations. Visualize equivalence classes and verify results using the formula.', + tags: ['group-theory', 'symmetry', 'combinatorics', 'counting', 'algebra'], + path: '/themes/discrete-math/structures/burnsides-lemma', + component: BurnsidesLemma + }, +]; const Structures = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedInteractive, setSelectedInteractive] = useState(null); + + const filteredInteractives = interactives.filter(interactive => + interactive.title.toLowerCase().includes(searchTerm.toLowerCase()) || + interactive.description.toLowerCase().includes(searchTerm.toLowerCase()) || + interactive.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())) + ); + + if (selectedInteractive) { + const InteractiveComponent = selectedInteractive.component; + const shareUrl = `${window.location.origin}${selectedInteractive.path}`; + return ( +
+
+
+ +
+ +
+
+ ); + } + return ( - + +
+
+
+

Structures

+

+ Understand algebraic structures, relations, functions, and abstract mathematical systems through interactive visualizations and examples. +

+
+ + {/* Search bar */} +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+ + {/* Gallery */} +
+ {filteredInteractives.map(interactive => ( + setSelectedInteractive(interactive)} + > + + {interactive.title} + + {interactive.description} + + + +
+ {interactive.tags.map(tag => ( + + {tag} + + ))} +
+
+
+ ))} +
+ + {filteredInteractives.length === 0 && ( +
+

No interactives found matching your search.

+
+ )} +
+
+
); };