import React, { useState, useCallback } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { CheckCircle, XCircle, Info, ChevronDown, ChevronUp } from 'lucide-react'; import SocialShare from '@/components/SocialShare'; interface Rectangle { id: number; startRow: number; startCol: number; endRow: number; endCol: number; } interface GridTilingPuzzleProps { showSocialShare?: boolean; } const GridTilingPuzzle: React.FC = ({ showSocialShare = true }) => { const [gridSize, setGridSize] = useState(9); const [rectangles, setRectangles] = useState([]); const [isDrawing, setIsDrawing] = useState(false); const [startCell, setStartCell] = useState<{ row: number; col: number } | null>(null); const [hoverCell, setHoverCell] = useState<{ row: number; col: number } | null>(null); const [isValidating, setIsValidating] = useState(false); const [showInstructions, setShowInstructions] = useState(false); const [validationResult, setValidationResult] = useState<{ isValid: boolean; message: string; uncoveredCount: number; } | null>(null); const specialSizes = [4, 9, 25]; // Different shades of blue for rectangles - starting very dark and getting lighter const blueShades = [ 'bg-blue-900', 'bg-blue-800', 'bg-blue-700', 'bg-blue-600', 'bg-blue-500', 'bg-blue-400', 'bg-blue-300', 'bg-blue-200' ]; const resetPuzzle = () => { setRectangles([]); setIsDrawing(false); setStartCell(null); setHoverCell(null); setIsValidating(false); setValidationResult(null); }; const isCellCovered = (row: number, col: number): boolean => { return rectangles.some(rect => row >= rect.startRow && row <= rect.endRow && col >= rect.startCol && col <= rect.endCol ); }; const isCellInRectangle = (row: number, col: number, rect: Rectangle): boolean => { return row >= rect.startRow && row <= rect.endRow && col >= rect.startCol && col <= rect.endCol; }; const getCellClass = (row: number, col: number): string => { let classes = 'border border-gray-300 cursor-pointer transition-colors'; // Check if cell is covered by any rectangle const coveredRect = rectangles.find(rect => isCellInRectangle(row, col, rect)); if (coveredRect) { const rectIndex = rectangles.findIndex(rect => rect.id === coveredRect.id); const blueShade = blueShades[rectIndex % blueShades.length]; classes += ` ${blueShade} hover:${blueShade.replace('bg-', 'bg-').replace('-', '-')}`; return classes; } // Check if this is the start cell if (startCell && startCell.row === row && startCell.col === col) { classes += ' bg-green-500 hover:bg-green-600'; return classes; } // Check if this would be a valid completion (no overlap) if (startCell && hoverCell && !isCellCovered(row, col)) { const minRow = Math.min(startCell.row, hoverCell.row); const maxRow = Math.max(startCell.row, hoverCell.row); const minCol = Math.min(startCell.col, hoverCell.col); const maxCol = Math.max(startCell.col, hoverCell.col); if (row >= minRow && row <= maxRow && col >= minCol && col <= maxCol) { // Check if this preview rectangle would overlap with any existing rectangles const wouldOverlap = rectangles.some(existingRect => { return !(maxRow < existingRect.startRow || minRow > existingRect.endRow || maxCol < existingRect.startCol || minCol > existingRect.endCol); }); if (!wouldOverlap) { classes += ' bg-yellow-300 hover:bg-yellow-400'; return classes; } } } classes += ' bg-white hover:bg-gray-100'; return classes; }; const handleCellClick = (row: number, col: number) => { if (isCellCovered(row, col)) return; if (!isDrawing) { // Start drawing a rectangle setIsDrawing(true); setStartCell({ row, col }); } else { // Complete the rectangle if (startCell) { const newRect: Rectangle = { id: Date.now(), startRow: Math.min(startCell.row, row), startCol: Math.min(startCell.col, col), endRow: Math.max(startCell.row, row), endCol: Math.max(startCell.col, col) }; // Check if the new rectangle would overlap with any existing rectangles const wouldOverlap = rectangles.some(existingRect => { return !(newRect.endRow < existingRect.startRow || newRect.startRow > existingRect.endRow || newRect.endCol < existingRect.startCol || newRect.startCol > existingRect.endCol); }); if (wouldOverlap) { // Don't place the rectangle if it would overlap setIsDrawing(false); setStartCell(null); setHoverCell(null); return; } setRectangles(prev => [...prev, newRect]); setIsDrawing(false); setStartCell(null); setHoverCell(null); } } }; const handleCellHover = (row: number, col: number) => { if (isDrawing && startCell) { setHoverCell({ row, col }); } }; const handleCellLeave = () => { setHoverCell(null); }; const validateSolution = () => { setIsValidating(true); // Check if each row and column has exactly one uncovered square let uncoveredCount = 0; const rowUncovered = new Array(gridSize).fill(0); const colUncovered = new Array(gridSize).fill(0); for (let row = 0; row < gridSize; row++) { for (let col = 0; col < gridSize; col++) { if (!isCellCovered(row, col)) { uncoveredCount++; rowUncovered[row]++; colUncovered[col]++; } } } const isValid = rowUncovered.every(count => count === 1) && colUncovered.every(count => count === 1); let message = ''; if (isValid) { message = `Perfect! Your solution is valid. You used ${rectangles.length} rectangles.`; } else { const invalidRows = rowUncovered.map((count, i) => count !== 1 ? i + 1 : null).filter(Boolean); const invalidCols = colUncovered.map((count, i) => count !== 1 ? i + 1 : null).filter(Boolean); message = `Invalid solution. Rows ${invalidRows.join(', ')} and columns ${invalidCols.join(', ')} don't have exactly one uncovered square.`; } setValidationResult({ isValid, message, uncoveredCount }); setIsValidating(false); }; const removeRectangle = (rectId: number) => { setRectangles(prev => prev.filter(rect => rect.id !== rectId)); setValidationResult(null); }; return (
Grid Tiling Puzzle

This is P6 on IMO 2025. The given grid size was 2025 in the original problem.

Place rectangular tiles on the grid so that each row and each column has exactly one uncovered square. What's the smallest number of rectangles you can use?

3 { setGridSize(parseInt(e.target.value)); resetPuzzle(); }} className={`w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer ${ specialSizes.includes(gridSize) ? 'slider-special' : 'slider-normal' }`} style={{ '--thumb-color': specialSizes.includes(gridSize) ? '#10b981' : '#6b7280' } as React.CSSProperties} /> 25
Rectangles: {rectangles.length} Grid: {gridSize}×{gridSize} {specialSizes.includes(gridSize) && }
{/* Instructions */} setShowInstructions(!showInstructions)} >
Instructions {showInstructions ? ( ) : ( )}
{showInstructions && (
  • Click on any empty square to start drawing a rectangle
  • Click on another square to complete the rectangle
  • Each row and column must have exactly one uncovered square
  • Rectangles cannot overlap
  • Try to use the minimum number of rectangles possible
Legend: Green = Start, Yellow = Preview, Blue shades = Placed Rectangles
)}
{/* Grid */}
{Array.from({ length: gridSize * gridSize }, (_, index) => { const row = Math.floor(index / gridSize); const col = index % gridSize; return (
handleCellClick(row, col)} onMouseEnter={() => handleCellHover(row, col)} onMouseLeave={handleCellLeave} /> ); })}
{/* Validation */}
{/* Validation Result */} {validationResult && (
{validationResult.isValid ? ( ) : ( )}

{validationResult.message}

Total uncovered squares: {validationResult.uncoveredCount}

)} {/* Rectangle List */} {rectangles.length > 0 && ( Placed Rectangles
{rectangles.map((rect, index) => (
Rectangle {index + 1}:
({rect.startRow + 1},{rect.startCol + 1}) to ({rect.endRow + 1},{rect.endCol + 1})
))}
)} {/* Social Share */} {showSocialShare && ( )}
); }; export default GridTilingPuzzle;