BUG :生成数字的颜色不一致,无法正确区分答案的颜色和题目数字的颜色
SudoKu(数独) 🎯 核心任务
1.数独游戏的算法实现,回溯法实现数独,不继承MonoBehavior,使用静态方法创建。因为数独的逻辑判定是纯数据逻辑,与GameObject无关,不依赖于游戏对象。
核心思想:关于数独算法的实现,使用回溯法去实现,所有可能的候选解,并在发现当前选择无法导致最终解时进行回溯,撤销上一步的选择并尝试其他可能性。在数独求解中,该算法从第一个空白格子开始,依次尝试填入数字1到9,并通过递归处理后续的空格。如果某个数字的填入导致后续无法合法填充,算法会回溯到上一个空格,尝试其他数字,直到找到解或确定无解。
算法详解:
遍历数独数组,寻找值为0(表示空白)的单元格。对于找到的空白值,尝试找到从 1 - 9的每个值,在填入数字前,使用IsValid方法检查该数字在当
前行、当前列以及所在的3x3子网格中是否已经存在。如果存在重复,则尝试下一个数字。(遍历指定行,确保没有重复数字,遍历指定列,确保没有重复数字,计算当前单元格所在的3x3子网格的起始位置,然后遍历该子网格内的所有单元格,检查是否有重复数字 )。如果该数字有效,使用BackTrack方法处理下一个空白单元格。如果在递归过程中发现当前数字的填入导致后续无解,则将当前单元格重置为0(EMPTY_CELL),并尝试下一个数字。如果所有数字(1-9)都尝试过后仍无法找到解,则方法返回false,触发上一层的回溯,当算法处理完所有单元格(即row达到BOARD_SIZE)时,意味着找到了一个有效解,返回true。
唯一解检测:首先遍历数独板,找到第一个空白单元格,对于找到的空白格,尝试填入数字1到9。每当填入一个有效数字后,就递归调用HasUniqueSolution本身来检查剩余的空格能否形成有效解。每次递归调用成功(即找到一种解)时,解的数量solutionCount增加1,无论递归调用结果如何,在尝试下一个数字前,都必须将当前单元格重置为EMPTY_CELL,以确保不会影响后续的尝试。最后,如果解的数量solutionCount恰好为1,则返回true,表示有唯一解;否则返回false
Solve类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 public class Solve { public const int BOARD_SIZE = 9 ; public const int SUBGRID_SIZE = 3 ; public const int EMPTY_CELL = 0 ; public static int [,] SolveSudoku(int [,] unSolvedBoard){ int [,] solvedBoard = new int [BOARD_SIZE, BOARD_SIZE]; Array.Copy(solvedBoard, unSolvedBoard, unSolvedBoard.Length); BackTrack(solvedBoard, 0 , 0 ); return solvedBoard; } public static bool BackTrack (int [,] board, int row, int col ) { if (row == BOARD_SIZE) return true ; if (col == BOARD_SIZE) BackTrack(board, row + 1 , 0 ); if (board[row, col] != 0 ) return BackTrack(board, row, col + 1 ); for (int i = 1 ; i <= BOARD_SIZE; i++){ if (IsValid(board, row, col, i)){ board[row, col] = i; if (BackTrack(board, row, col + 1 )) return true ; board[row, col + 1 ] = 0 ; } } return false ; } public static bool HasUniqueSolution (int [,] board ) { int solutionCount = 0 ; int row = -1 ; int col = -1 ; for (int r = 0 ; r < BOARD_SIZE; r++){ for (int c = 0 ; c < BOARD_SIZE; c++){ if (board[r, c] == EMPTY_CELL){ row = r; col = c; break ; } } if (row != -1 ) break ; } if (row == -1 ) return true ; for (int i = 1 ; i <= BOARD_SIZE; i++){ if (IsValid(board, row, col, i)){ board[row, col] = i; if (HasUniqueSolution(board)) solutionCount++; board[row, col] = EMPTY_CELL; } } return solutionCount == 1 ; } public static bool IsValid (int [,] board, int row, int col, int val ) { for (int i = 0 ; i < BOARD_SIZE; i++){ if (board[row, i] == val) return false ; } for (int i = 0 ; i < BOARD_SIZE; i++){ if (board[i, col] == val) return false ; } int subgridRow = row / SUBGRID_SIZE * SUBGRID_SIZE; int subgridCol = col / SUBGRID_SIZE * SUBGRID_SIZE; for (int r = subgridRow; r < subgridRow + SUBGRID_SIZE; r++){ for (int c = subgridCol; c < subgridCol + SUBGRID_SIZE; c++){ if (board[r, c] == val) return false ; } } return true ; } }
GeneratorSudoku
生成完整数独(InitBoard和 FillGrid方法) :
初始化第一行 :使用 Shuffle方法随机排列数字1-9,并填入数独板的第一行。
回溯填充剩余部分 :从第二行第一列(索引 [1, 0])开始,调用 FillGrid方法。该方法为每个空白单元格(初始时除第一行外均为空)随机尝试数字1-9(使用 Shuffle打乱尝试顺序),并通过 IsValid方法检查其有效性(即行、列、3x3宫内无重复)。如果数字有效则填入,并递归地填充下一个单元格。如果后续填充失败(FillGrid返回 false),则回溯,将当前单元格重置为 EMPTY_CELL(0),并尝试下一个候选数字。当成功填充完所有单元格(row达到 BOARD_SIZE)时,返回 true,表示生成了一个完整的有效数独。
按难度挖空(RemoveSquares方法) :
确定挖空数量 :根据传入的 Difficulty枚举(Easy, Medium, Hard),使用 Random.Range在特定范围内随机确定要移除的数字数量 squareToRemove。
随机选择并安全移除 :在一个 while循环中,随机选择一个非空单元格,临时保存其值后将其设为 EMPTY_CELL。然后调用 Solve.HasUniqueSolution(此处 Solve类未提供,但应是判断唯一解的关键)来检查移除该数字后,整个数独板是否仍然有且只有一个解。
确认移除或回溯 :如果仍有唯一解,则确认此次移除,并减少 squareToRemove计数。如果导致多解,则将该单元格的值恢复(回溯),继续尝试移除其他单元格,直到移除足够数量的数字。
有效性验证(IsValid方法) :
检查目标单元格所在行 、列 以及**3x3子网格(宫)**中是否已存在待填入的数字 val。这通过遍历行、列,以及计算当前单元格所在宫的起始位置(subgridRow = row / SUBGRID_SIZE * SUBGRID_SIZE, subgridCol = col / SUBGRID_SIZE * SUBGRID_SIZE)并遍历该宫来实现。只要在任何一处发现 val已存在,则返回 false,否则返回 true。
辅助方法(Shuffle方法) :
使用 Fisher-Yates 洗牌算法随机打乱给定泛型列表 List<T>的元素顺序。这用于为第一行生成随机排列,以及为回溯填充过程提供随机的数字尝试顺序,增加生成数独的随机性。该算法遍历列表,每次循环中随机选择一个元素(索引 k)与当前未处理部分的最后一个元素交换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 using System.Collections;using System.Collections.Generic;using UnityEngine;public class GeneratorSudoku { public enum Difficulty { Easy, Medium, Hard } private const int BOARD_SIZE = 9 ; private const int SUBGRID_SIZE = 3 ; private const int MIN_SQUARE_COUNT = 30 ; private const int MAX_SQUARE_COUNT = 50 ; private const int EMPTY_CELL = 0 ; public static int [,] GenerateSudoku(Difficulty difficulty){ int [,] grid = new int [BOARD_SIZE,BOARD_SIZE]; int squareToRemove = 0 ; switch (difficulty){ case Difficulty.Easy: squareToRemove = Random.Range(MIN_SQUARE_COUNT, MAX_SQUARE_COUNT + 5 ); break ; case Difficulty.Medium: squareToRemove = Random.Range(MIN_SQUARE_COUNT + 5 , MIN_SQUARE_COUNT + 10 ); break ; case Difficulty.Hard: squareToRemove = Random.Range(MIN_SQUARE_COUNT + 10 , MAX_SQUARE_COUNT); break ; } InitBoard(grid); RemoveSquares(grid, squareToRemove); return grid; } public static void InitBoard (int [,] grid ) { List<int > number = new List<int >{1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 }; Shuffle(number); for (int i = 0 ;i < BOARD_SIZE;i++){ grid[0 ,i] = number[i]; } FillGrid(grid, 1 , 0 ); } public static bool FillGrid (int [,] grid, int row, int col ) { if (row == BOARD_SIZE){ return true ; } if (col == BOARD_SIZE){ return FillGrid(grid, row + 1 , 0 ); } List<int > number = new List<int >{1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 }; Shuffle(number); foreach (int num in number){ if (IsValid(grid, row, col, num)){ grid[row,col] = num; if (FillGrid(grid, row, col + 1 )){ return true ; } } } grid[row,col] = EMPTY_CELL; return false ; } public static void RemoveSquares (int [,] grid, int squareToRemove ) { while (squareToRemove > 0 ){ int row = Random.Range(0 , BOARD_SIZE); int col = Random.Range(0 , BOARD_SIZE); if (grid[row,col] != EMPTY_CELL){ int temp = grid[row,col]; grid[row,col] = EMPTY_CELL; if (Solve.HasUniqueSolution(grid)){ squareToRemove--; } else { grid[row,col] = temp; } } } } public static bool IsValid (int [,] board,int row,int col,int val ) { for (int i = 0 ;i < BOARD_SIZE;i++){ if (board[row,i] == val) return false ; } for (int i = 0 ;i < BOARD_SIZE;i++){ if (board[i,col] == val) return false ; } int subgridRow = row / SUBGRID_SIZE * SUBGRID_SIZE; int subgridCol = col / SUBGRID_SIZE * SUBGRID_SIZE; for (int r = subgridRow;r < subgridRow + SUBGRID_SIZE;r++){ for (int c = subgridCol;c < subgridCol + SUBGRID_SIZE;c++){ if (board[r,c] == val) return false ; } } return true ; } public static void Shuffle <T >(List<T> list ) { int n = list.Count; while (n > 1 ){ int k = Random.Range(0 , n); T temp = list[n - 1 ]; list[n - 1 ] = list[k]; list[k] = temp; n--; } } }
Cell类
核心属性与状态管理
位置信息 :row和 col记录单元格在数独网格中的位置。
数值与状态 :value存储单元格的数字,IsLocked标识单元格是否可编辑(通常题目给定的数字会被锁定),IsCorrect标识玩家输入的数字是否正确。
视觉组件 :通过 SerializeField 私有的 _BgSprite(SpriteRenderer) 和 _valueText(TMP_Text) 分别控制单元格的背景和数字显示。
这些状态在 Init(int value)方法中进行初始化。该方法根据传入的 value设置单元格的初始状态:若值为 0,则单元格为空白、未锁定且可编辑;若值非 0,则单元格被锁定,显示给定的数字,玩家无法修改
多层次的颜色反馈系统
基础颜色 (Basic Colors) :定义了单元格初始状态的颜色,包括解锁状态(空白格)和锁定状态(题目给定数字)的背景色与文字色。
高亮颜色 (Highlight Colors) :用于提示用户当前操作相关的单元格(如相同数字、同行列等),区分了锁定格、解锁格、正确输入和错误输入的不同高亮效果。
选中颜色 (Selected Colors) :当单元格被玩家直接选中时使用的特定颜色,同样区分了正确和错误的状态。
重置颜色 (Reset Colors) :用于将单元格视觉状态重置到其当前逻辑状态对应的颜色。
这种多层次的颜色系统极大地增强了游戏的交互性和可玩性,让玩家能够清晰地掌握游戏状态。
状态控制与视觉更新方法
该类提供了多个公共方法来改变单元格的状态并同步更新其视觉表现:
SetHighlight(): 根据单元格的锁定状态和答案正确性,应用不同的高亮颜色组合。这对于提示玩家非常有用,例如高亮所有相同数字或冲突数字。
SetSelected(): 当单元格被玩家选中时调用,使用特定的选中颜色,使其从界面中凸显出来。
Reset(): 将单元格的视觉外观重置为其当前逻辑状态(锁定/解锁,正确/错误)所对应的“基础”颜色,通常在高亮或选中状态结束后调用。
updateValue(int value): 这是玩家与单元格交互的核心方法。它首先检查单元格是否被锁定(题目给定数字不可修改)。然后更新单元格的值和显示文本。最后,根据新值更新视觉状态:如果值为 0(清空),则恢复为未锁定的基础状态;如果输入了数字,则使用特定的颜色(代码中注释表明意图是使用不同颜色区分,但当前实现仍使用了锁定状态的颜色,这可能需要根据实际需求调整)。
updateWin(): 一个特殊的方法,可能在游戏胜利时被调用,将单元格设置为统一的胜利状态颜色。
GameManager类
游戏初始化与棋盘生成 (Start 和 GenerateBoard 方法)
Start() : 游戏入口点,初始化游戏状态(hasGameFinished = false),创建 9x9 单元格数组,并调用 GenerateBoard()生成游戏界面。
GenerateBoard() : 核心初始化方法。从 PlayerPrefs 读取或创建新关卡数据,计算 9 个子网格的精确位置(基于 _StartPos和偏移量),实例化子网格预制体。为每个单元格设置正确的行列坐标和初始值,并建立完整的单元格引用矩阵 _cells[,]。
用户交互处理 (Update 和 UpdateValue 方法)
Update() : 每帧检测鼠标点击。通过射线检测获取点击的单元格,校验是否可操作(非锁定状态),然后设置选中状态并触发高亮效果。
UpdateValue(int value) : 处理数字输入。更新选中单元格的值,触发高亮效果,并立即检查游戏是否胜利。
游戏状态验证与胜利检测 (CheckWin 和 IsValid 方法)
CheckWin() : 胜利条件检测。遍历所有单元格,要求每个单元格都有非零值且通过 IsValid验证(IsCorrect = true)。胜利后触发胜利视觉效果并安排进入下一关。
IsValid(Cell cell, Cell[,] cells) : 数独规则验证器。检查指定单元格的值在行、列和 3x3 宫格内是否重复。采用临时清空策略避免自比较问题。
视觉反馈系统 (Highlight 和 ResetGrid 方法)
Highlight() : 多层次高亮系统。首先设置所有单元格的正确性状态,然后高亮当前选中单元格的关联区域(同行、同列、同宫格),最后对选中单元格应用特殊选中效果。
ResetGrid() : 重置所有单元格的视觉状态到基础外观,清除之前的高亮和选中效果。
关卡管理与持久化 (CreatAndStoreLevel 和 GetCurrentLevel 方法)
CreatAndStoreLevel(int[,] board, int level) : 关卡生成器。根据难度级别生成数独题目,将棋盘数据转换为字符串格式,并使用 PlayerPrefs 进行持久化存储。
GetCurrentLevel(int[,] board) : 关卡加载器。从 PlayerPrefs 读取存储的关卡数据,解析字符串并填充到棋盘数组中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 using UnityEngine;using TMPro;using System.Collections.Generic;using UnityEngine.SceneManagement;public class GameManager : MonoBehaviour { [Header("Board Settings" ) ] [SerializeField ] private Vector3 _StartPos; [SerializeField ] private float _offsetX,_offsetY; [SerializeField ] private SubGrid _subGridPrefab; [SerializeField ] private TMP_Text _LevelText; private bool hasGameFinished; private Cell[,] _cells; private Cell selectedCell; private const int BOARD_SIZE = 9 ; private const int SUBGRID_SIZE = 3 ; public void Start () { hasGameFinished = false ; _cells = new Cell[BOARD_SIZE, BOARD_SIZE]; selectedCell = null ; GenerateBoard(); ResetGrid(); } public void GenerateBoard () { int [,] board = new int [BOARD_SIZE, BOARD_SIZE]; int level = PlayerPrefs.GetInt("Level" , 0 ); if (level == 0 ){ CreatAndStoreLevel(board,1 ); level = 1 ; } else { GetCurrentLevel(board); } _LevelText.text = "Level: " + level.ToString(); for (int i = 0 ; i < BOARD_SIZE; i++){ Vector3 pos = _StartPos + i % 3 * _offsetX * Vector3.right + i / 3 * _offsetY * Vector3.up; SubGrid subGrid = Instantiate(_subGridPrefab, pos, Quaternion.identity); List<Cell> subGridCells = subGrid._cells; int startRow = i / 3 * SUBGRID_SIZE; int startCol = i % 3 * SUBGRID_SIZE; for (int j = 0 ; j < BOARD_SIZE; j++){ subGridCells[j].row = startRow + j / 3 ; subGridCells[j].col = startCol + j % 3 ; int cellValue = board[startRow + j / 3 , startCol + j % 3 ]; subGridCells[j].Init(cellValue); _cells[subGridCells[j].row, subGridCells[j].col] = subGridCells[j]; } } } public void Update () { if (hasGameFinished || !Input.GetMouseButtonDown(0 )) return ; Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition); Vector2 mousePos2D = new Vector2(mousePos.x, mousePos.y); RaycastHit2D hit = Physics2D.Raycast(mousePos2D, Vector2.zero); Cell tempCell; if (!(hit && hit.collider.TryGetComponent(out tempCell) && tempCell != selectedCell && !tempCell.IsLocked)) { return ; } else { ResetGrid(); selectedCell = tempCell; Highlight(); } } public void UpdateValue (int value ) { if (hasGameFinished || selectedCell == null ) return ; selectedCell.updateValue(value ); Highlight(); CheckWin(); } private void CheckWin () { for (int i = 0 ; i < BOARD_SIZE; i++){ for (int j = 0 ; j < BOARD_SIZE; j++){ if (_cells[i,j].IsCorrect||_cells[i,j].value == 0 ) return ; } } hasGameFinished = true ; for (int i = 0 ; i < BOARD_SIZE; i++){ for (int j = 0 ; j < BOARD_SIZE; j++){ _cells[i,j].updateWin(); } } Invoke("ToNextLevel" , 2f ); } public void ResetGrid () { for (int i = 0 ; i < BOARD_SIZE; i++){ for (int j = 0 ; j < BOARD_SIZE; j++){ _cells[i,j].Reset(); } } } private void Highlight () { for (int i = 0 ; i < BOARD_SIZE; i++){ for (int j = 0 ; j < BOARD_SIZE; j++){ _cells[i,j].IsCorrect = !IsValid(_cells[i,j],_cells); } } int currentRow = selectedCell.row; int currentCol = selectedCell.col; int subGridRow = currentRow / SUBGRID_SIZE * SUBGRID_SIZE; int subGridCol = currentCol / SUBGRID_SIZE * SUBGRID_SIZE; for (int i = 0 ; i < BOARD_SIZE; i++){ _cells[i,currentCol].SetHighlight(); _cells[currentRow,i].SetHighlight(); _cells[subGridRow + i % 3 ,subGridCol + i / 3 ].SetHighlight(); } _cells[currentRow,currentCol].SetSelected(); } private bool IsValid (Cell cell,Cell[,] cells ) { int row = cell.row; int col = cell.col; int value = cell.value ; cell.value = 0 ; if (value == 0 ) return true ; for (int i = 0 ; i < BOARD_SIZE; i++){ if (cells[row,i].value == value )return false ; if (cells[i,col].value == value )return false ; } int subgridRow = row / SUBGRID_SIZE * SUBGRID_SIZE; int subgridCol = col / SUBGRID_SIZE * SUBGRID_SIZE; for (int i = subgridRow; i < subgridRow + SUBGRID_SIZE; i++){ for (int j = subgridCol; j < subgridCol + SUBGRID_SIZE; j++){ if (cells[i,j].value == value )return false ; } } cell.value = value ; return true ; } public void Restart () { SceneManager.LoadScene(0 ); } public void CreatAndStoreLevel (int [,] board,int level ) { int [,] tempBoard = GeneratorSudoku.GenerateSudoku((GeneratorSudoku.Difficulty)(level / 100 )); string arrayString = "" ; for (int i = 0 ; i < BOARD_SIZE; i++){ for (int j = 0 ; j < BOARD_SIZE; j++){ arrayString += tempBoard[i,j].ToString() + "," ; board[i,j] = tempBoard[i,j]; } } arrayString = arrayString.TrimEnd(',' ); PlayerPrefs.SetInt("Level" , level); PlayerPrefs.SetString("grid" ,arrayString); } public void GetCurrentLevel (int [,] board ) { string arrayString = PlayerPrefs.GetString("grid" ); string [] arrayValue = arrayString.Split(',' ); int index = 0 ; for (int i = 0 ; i < BOARD_SIZE; i++){ for (int j = 0 ; j < BOARD_SIZE; j++){ board[i,j] = int .Parse(arrayValue[index]); index++; } } } public void ToNextLevel () { int level = PlayerPrefs.GetInt("Level" , 0 ); CreatAndStoreLevel(new int [BOARD_SIZE, BOARD_SIZE],level + 1 ); Restart(); } }
SubGrid类
1 2 3 4 5 6 7 8 9 using System.Collections;using System.Collections.Generic;using UnityEngine;public class SubGrid : MonoBehaviour { [SerializeField ] public List<Cell> _cells; }
Solitaire(纸牌游戏) 游戏的基础数据结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 namespace SolitaireGame { public enum Suit{ Hearts, Diamonds, Clubs, Spades } public enum Rank{ Ace = 1 , Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King } public class Card { public Suit Suit{get ;set ;} public Rank Rank{get ;set ;} public bool IsFaceUp { get ; set ; } } public Card (Suit suit, Rank rank, bool isFaceUp = false ) { Suit = suit; Rank = rank; IsFaceUp = isFaceUp; } public bool IsBlack () { if (Suit == Suit.Clubs||Suit == Suit.Spades) return true ; } }
随机化洗牌算法 旧Fisher-Yates算法
步骤
牌堆数组 (Deck)
抽取卡牌数组 (Drawn Pile)
操作描述
初始状态
[C1, C2, C3, ..., Cn]
[]
牌堆包含所有卡牌,抽取堆为空。
第1次抽取
[C1, C3, ..., Cn]
[C2]
从牌堆随机移除 一张牌(如C2),添加 到抽取堆。
第2次抽取
[C1, ..., Cn]
[C2, Cx]
从剩余牌堆随机移除一张新牌(如Cx),追加 到抽取堆末尾。
… 重复
逐渐变少
逐渐增多
每次迭代都从剩余牌堆中随机抽取一张,移入抽取堆。
最终状态
[]
[C2, Cx, ..., Cy]
牌堆为空,所有卡牌以随机顺序转移至抽取堆,洗牌完成。
随机性保证 :每次从剩余牌堆中等概率 地随机选择一张牌,这保证了每种可能的排列顺序出现的概率是相等的。
时间复杂度 :由于每次牌堆数组每次弹出卡牌的复杂度为O(n),整个算法的时间复杂度为O(n²)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Program { static List<string > DrawShuffle (List<string > deck ) { List<string > drawnPile = new List<string >(); Random random = new Random(); while (deck.Count > 0 ) { int index = random.Next(0 , deck.Count); drawnPile.Add(deck[index]); deck.RemoveAt(index); } return drawnPile; } static void Main () { List<string > originalDeck = new List<string > { "A♠" , "2♠" , "3♠" , "4♠" , "5♠" , "6♠" , "7♠" , "8♠" , "9♠" , "10♠" , "J♠" , "Q♠" , "K♠" }; List<string > shuffledDeck = DrawShuffle(new List<string >(originalDeck)); Console.Write("洗牌结果: " ); Console.WriteLine(string .Join(", " , shuffledDeck)); } }
新Fisher-Yates算法 后向遍历 :
从牌堆数组末尾开始往前遍历,设置一个变量i用于维护已洗牌和未洗牌区的边界,从n - 1开始,再未洗牌区设置一个随机索引j,交换数组第 i个元素和第 j个元素,此时i - 1,循环往复,直到i为0,代表洗牌完毕
步骤
变量 i (当前边界)
变量 j (随机索引)
牌堆数组状态(示例: [A, B, C, D, E])
操作描述
初始状态
n - 1(4)
-
[A, B, C, D, E]
i指向数组末尾,划分整个数组为“未洗牌区”。
第1次循环
4
随机生成 (如: 1)
[A, E, C, D, B]
交换 i=4(E) 和 j=1(B) 的元素。i减1。
第2次循环
3
随机生成 (如: 0)
[D, E, C, A, B]
交换 i=3(此时是D) 和 j=0(A) 的元素。i减1。
第3次循环
2
随机生成 (如: 2)
[D, E, C, A, B]
交换 i=2(C) 和 j=2(C) 的元素(自身交换)。i减1。
第4次循环
1
随机生成 (如: 0)
[E, D, C, A, B]
交换 i=1(D) 和 j=0(E) 的元素。i减1。
循环结束
0
-
[E, D, C, A, B]
i为0,循环终止,洗牌完成。
无偏性与高效性 :Fisher-Yates 算法能够等概率地生成所有可能的排列,确保洗牌的公平性。它的时间复杂度是 O(n),空间复杂度是 O(1)(原地操作),效率非常高
随机索引的范围 :这是实现中的关键。随机索引 j必须在 [0, i]的闭区间内选取。如果错误地选成了 [0, n-1],会导致洗牌结果出现偏差。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 using System;public class FisherYatesShuffle { private static Random rng = new Random(); public static void Shuffle <T >(T[] array ) { if (array == null || array.Length <= 1 ) return ; int n = array.Length; for (int i = n - 1 ; i > 0 ; i--) { int j = rng.Next(0 , i + 1 ); T temp = array[i]; array[i] = array[j]; array[j] = temp; } } } class Program { static void Main (string [] args ) { int [] deck = new int [52 ]; for (int i = 0 ; i < deck.Length; i++) { deck[i] = i + 1 ; } Console.WriteLine("洗牌前: " + string .Join(", " , deck)); FisherYatesShuffle.Shuffle(deck); Console.WriteLine("洗牌后: " + string .Join(", " , deck)); } }
发牌和牌面生成算法 纸牌接龙