1. 程式人生 > >徐鬆亮演算法教學-基於C語言的數獨(九宮格)求解(含多解和解數統計)

徐鬆亮演算法教學-基於C語言的數獨(九宮格)求解(含多解和解數統計)

目錄

一,前言

電腦系統

編譯器

程式語言

流程

演示

一,前言

  • 數獨,說實話我玩過,且並不是很喜歡玩,覺得無聊也太浪費時間,當然玩的水平也不咋樣。
  • 但是我為什麼又寫這篇文章又編寫程式碼的去做呢?
    • 因為它是一個很好的適合計算機處理的數學演算法的問題。
    • 數獨規則很簡單
    • 解題需要計算機程式設計的遞迴
    • 出題需要矩陣變換
  • 數獨的最大終盤數量
    • 目前普遍認為是6670903752021072936960,也就是所如果完全隨機,一個人一輩子玩數獨很難遇到萬全一樣的題,這個數量級大大出乎了我的意料。
    • 刨去對稱,換行等等等規則後,也有5472730538個終盤。
    • 本人一開始想用窮舉給算出來,看到這個數後就服了,即使用最快的計算機就算把計算機算壞了也算不完,而且統計這麼大的數,還要做大數計算程式,目前的64位計算機肯定是不行。
  • 本文著重說明通過C語言來解析數獨問題,至於數獨的隨機出題問題由下一篇文章說明
  • 本文只針對標準的9宮格數獨,不討論其他變種數獨,也不討論數獨的難度等級等其他問題。
  • 本文為了簡化程式碼,題目直接在原始碼中,結果直接終端列印,沒有做檔案資料的輸入輸出等操作。
  • 本文有免費程式碼下載(複製即可),可以計算並打印出第一個解。
  • 本文另有收費程式碼,可以計算並打印出指定數量的解,並且統計題目有效解的總數量。
    • 程式碼只建議用來學習,並沒有做太多的容錯處理。
    • 這是我個人第一個收費的程式碼,且只是象徵性收取5元哦,對我來說,只是跑跑流程,玩一玩,看看這玩意能不能賺到錢,哈哈,畢竟打字程式設計改良驗證每個環節也消耗了我的一些精力。所以太認真較真講究值與不值的朋友就別買了。

二,開發環境

  • 電腦系統

    • windows10
  • 編譯器

    • cygwin下的gcc
  • 程式語言

    • C語言

三,程式設計思路與流程

  • 流程

    • 本著自頂向下的程式設計理念,從使用者的角度來設計程式碼。
  • 設計數獨資料結構

    • 滿足條件:
      • 靈活載入題目
      • 使用者可以自由設定求解的數量(打印出來解的數量)
      • 可以統計指定題目的所有解的數量(不列印具體解的內容,只統計解的個數)
    • 程式實現如下,按輸入,輸入輸出,輸出三類,給出如下程式結構並標註。
  • 編寫測試程式碼框架

    • 滿足條件:
      • 1,自由載入數獨題目
      • 2,配置解析引數
        • 解一個解就行?
          • 耗時最小
        • 解指定數量的解,並列印?
          • 耗時依照題目和設定數量而定
        • 解指定數量的解,並且要統計出所有解的數量?
          • 耗時最長,因為得出指定數量的解之後程式依然不能停止,需要持續計算,直到窮舉所有資料為止,只有這樣才會統計出所有的解的數量。
    • 程式實現如下:
  • 編寫核心程式碼---介面函式

    • 滿足條件:
      • 只有一個結構體指標引數,計算的結果也列印到這個結構體裡,這樣使用者只要弄明白這個結構體即可。
      • 裡面包含兩層函式
        • 1,初始化
        • 2,執行計算
    • 程式實現如下:
  • 編寫核心程式碼---尋找下一個空單元

    • 滿足條件
      • 每個單元有效的數字為1-9,而初始的0就可以用來表達是空單元。
      • 為了加快計算速度,我們每次不是從頭找,而是從引數指定行開始搜尋。
    • 程式實現如下:
  • 編寫核心程式碼---利用遞歸回溯尋找空單元並填入合規資料

    • 程式架構
      • 函式(結構體指標,行,列)
        {
            引數
            迴圈
            {
                1,編寫數獨規則
                    1-9都嘗試完了,仍不合規,則回溯
                    填充違規,則繼續嘗試下個數
                2,尋找下一個空單元
                    找不到下一個空單元,說明每個單元都已經填入合規數,解成立。
                3,利用遞迴嘗試向下一單元填入合規數
            }
        }
    • 程式實現:

      • 數獨規則核心程式碼

四,完整原始碼(免費精簡版)

/**
  ******************************************************************************
  * @file    xsl_game_sudo.c
  * @author  徐鬆亮 許紅寧([email protected])
  * @version V1.0.0
  * @date    2018/11/01
  ******************************************************************************
  * @attention
  * 待解決
  * 1,數獨出題
  * GNU General Public License (GPL)
  *
  * <h2><center>&copy; COPYRIGHT 2017 XSLXHN</center></h2>
  ******************************************************************************
  */

/* Includes ------------------------------------------------------------------*/
#include <stdio.h>
#include <stdint.h>
// 用於計算耗時統計
#include <time.h>
/* Private define ------------------------------------------------------------*/
#define XSLGAMESUDO_SUDOKU_LEVEL 9
#define OK 0
#define ERR 1
/* Private typedef -----------------------------------------------------------*/
typedef struct
{
	// 輸入---計算前使用者設定
	uint8_t cells[XSLGAMESUDO_SUDOKU_LEVEL][XSLGAMESUDO_SUDOKU_LEVEL];
	//-----最大輸出答案數量(1,為了防止pOutBuf資料溢位;2,為了計算適可而止;)
	uint32_t MaxOutAnswersCount;
	// 輸入輸出
	//-----輸出快取(NULL---無解輸出)
	uint8_t *pOutBuf;
	//-----解最大數量(NULL---不求此值則非全域性搜尋)
	uint32_t *pAnswersCount;
	// 輸出---計算後的統計結果
	uint32_t OutAnswersCount;
} XSLGAMESUDO_S_SUDO;
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
static const uint8_t XslGameSudo_SudoBuf[XSLGAMESUDO_SUDOKU_LEVEL][XSLGAMESUDO_SUDOKU_LEVEL] = {
	8, 0, 0, 0, 0, 0, 0, 0, 0,
	0, 0, 3, 6, 0, 0, 0, 0, 0,
	0, 7, 0, 0, 9, 0, 2, 0, 0,
	0, 5, 0, 0, 0, 7, 0, 0, 0,
	0, 0, 0, 0, 4, 5, 7, 0, 0,
	0, 0, 0, 1, 0, 0, 0, 3, 0,
	0, 0, 1, 0, 0, 0, 0, 6, 8,
	0, 0, 8, 5, 0, 0, 0, 1, 0,
	0, 9, 0, 0, 0, 0, 4, 0, 0
	};
//
XSLGAMESUDO_S_SUDO XslGameSudo_s_Sudo;
/* Extern variables ----------------------------------------------------------*/
/* Private function prototypes -----------------------------------------------*/
/* Private functions ---------------------------------------------------------*/
/**
 * @brief   格式化列印
 * @note    列印指定行列的陣列
 * @param   *pXslGameSudoS	---	資料指標
 * 			mode			---	0-列印問題	1-列印答案
 * @return  null
 */
static void XslGameSudo_FormatPrint(XSLGAMESUDO_S_SUDO *pXslGameSudoS, uint8_t mode)
{
	uint8_t i, j, k;
	uint8_t *pbuf;
	// 列印問題
	if (mode == 0)
	{
		printf("Sudo Questions:\n");
		pbuf = (uint8_t *)pXslGameSudoS->cells;
		for (i = 0; i < XSLGAMESUDO_SUDOKU_LEVEL; i++)
		{
			for (j = 0; j < XSLGAMESUDO_SUDOKU_LEVEL; j++)
			{
				printf("%2d", pbuf[i * XSLGAMESUDO_SUDOKU_LEVEL + j]);
				if (j == (XSLGAMESUDO_SUDOKU_LEVEL - 1))
					printf("\n");
			}
		}
	}
	// 列印答案
	else if (mode == 1)
	{
		if (pXslGameSudoS->OutAnswersCount == 0)
		{
			printf("Sudo Processor : No Solution!\n");
			return;
		}
		//
		pbuf = pXslGameSudoS->cells;
		for (k = 0; k < pXslGameSudoS->OutAnswersCount; k++)
		{
			printf("Sudo Answers(%d):\n", k + 1);
			for (i = 0; i < XSLGAMESUDO_SUDOKU_LEVEL; i++)
			{
				for (j = 0; j < XSLGAMESUDO_SUDOKU_LEVEL; j++)
				{
					printf("%2d", pbuf[(k * XSLGAMESUDO_SUDOKU_LEVEL * XSLGAMESUDO_SUDOKU_LEVEL) + (i * XSLGAMESUDO_SUDOKU_LEVEL + j)]);
					if (j == (XSLGAMESUDO_SUDOKU_LEVEL - 1))
						printf("\n");
				}
			}
		}
	}
}
/**
 * @brief   尋找下一個未填充的單元
 * @note    尋找下一個未填充的單元
 * @param   buf			---	輸入輸出	---	資料
 * 			startrow	---	輸入		---	查詢起始行,此函式用於優化計算速度
 * 			*row		---
 * 			*col		---
 * @return  0-沒有空單元	1-有空單元
 */
static uint8_t XslGameSudo_findNextEmpty(XSLGAMESUDO_S_SUDO *pXslGameSudoS, int startrow, uint8_t *row, uint8_t *col)
{
	uint8_t i, j;

	for (i = startrow; i < XSLGAMESUDO_SUDOKU_LEVEL; i++)
	{
		for (j = 0; j < XSLGAMESUDO_SUDOKU_LEVEL; j++)
		{
			if (pXslGameSudoS->cells[i][j] == 0)
			{
				*row = i;
				*col = j;
				return OK;
			}
		}
	}
	return ERR;
}
/**
 * @brief   尋找下一個未填充的單元
 * @note    尋找下一個未填充的單元
 * @param   arr
 * 			row
 * 			col
 * @return  0-沒有符合規則的演算法	1-已經填入符合規則的資料
 */
static int XslGameSudo_Cal(XSLGAMESUDO_S_SUDO *pXslGameSudoS, uint8_t row, uint8_t col)
{
	uint8_t i = 0, j = 0, n = 0;
	uint8_t next_row = 0, next_col = 0;
	while (1)
	{
		//-----------------------1,向空單元填數
	next_num:
		// 填充失敗判斷-->嘗試填充1-9都失敗
		++n;
		if (n >= (XSLGAMESUDO_SUDOKU_LEVEL + 1))
		{
			break;
		}
		// 填充違規判斷--1-->判斷行重複
		for (j = 0; j < XSLGAMESUDO_SUDOKU_LEVEL; j++)
		{
			if (pXslGameSudoS->cells[row][j] == n)
			{
				goto next_num;
			}
		}

		// 填充違規判斷--2-->判斷列重複
		for (i = 0; i < XSLGAMESUDO_SUDOKU_LEVEL; i++)
		{
			if (pXslGameSudoS->cells[i][col] == n)
			{
				goto next_num;
			}
		}

		// 填充違規判斷--3-->判斷所在小九宮格重複
		uint8_t x = (row / 3) * 3;
		uint8_t y = (col / 3) * 3;
		for (i = x; i < x + 3; i++)
		{
			for (j = y; j < y + 3; j++)
			{
				if (pXslGameSudoS->cells[i][j] == n)
				{
					goto next_num;
				}
			}
		}

		//填充確認-->可以填充
		pXslGameSudoS->cells[row][col] = n;
		//-----------------------2,尋找下一個空單元
		//如果9宮格已填滿
		if (ERR == XslGameSudo_findNextEmpty(pXslGameSudoS, row, &next_row, &next_col))
		{
			pXslGameSudoS->OutAnswersCount=1;
			return OK;
		}
		//-----------------------3,向下一個空單元填數
		if (ERR == XslGameSudo_Cal(pXslGameSudoS, next_row, next_col))
		{
			pXslGameSudoS->cells[row][col] = 0;
			continue;
		}
		else
		{
			return OK;
		}
	}
	// 失敗
	return ERR;
}
/**
 * @brief   數獨解析
 * @note    數獨解析,計算的結果都包含在引數結構體裡,所以並沒有返回值。
 * @param   *pXslGameSudoS	---	數獨結構體指標
 * @return  null
 */
void XslGameSudo_Processor(XSLGAMESUDO_S_SUDO *pXslGameSudoS)
{
	uint8_t row, col;
	//初始化
	XslGameSudo_findNextEmpty(pXslGameSudoS, 0, &row, &col);
	pXslGameSudoS->OutAnswersCount = 0;
	if (pXslGameSudoS->pAnswersCount != NULL)
	{
		*(pXslGameSudoS->pAnswersCount) = 0;
	}
	//計算
	XslGameSudo_Cal(pXslGameSudoS, row, col);
}
/**
 * @brief   main函式
 * @note    主函式入口
 * @param   null
 * @return  null
 */
uint8_t MemBuf[1024]; //81個位元組儲存一組解,1024可以儲存大於10個解
uint32_t SudoCount;
void main(int argc, char **argv)
{
	uint8_t res;
	uint32_t time1, time2;
	printf("--------------------------------------------------\n");
	printf("               XSL Sudo Processor(simply)         \n");
	printf("--------------------------------------------------\n");
	// 資料載入
	memcpy((uint8_t *)(XslGameSudo_s_Sudo.cells), (uint8_t *)&XslGameSudo_SudoBuf[0][0], XSLGAMESUDO_SUDOKU_LEVEL * XSLGAMESUDO_SUDOKU_LEVEL);
	// 設定配置
	//---最多解析10個解
	XslGameSudo_s_Sudo.MaxOutAnswersCount = 0;
	XslGameSudo_s_Sudo.pOutBuf = MemBuf;
	XslGameSudo_s_Sudo.pAnswersCount = &SudoCount;
	// 列印原始數獨
	XslGameSudo_FormatPrint(&XslGameSudo_s_Sudo, 0);
	// 啟動數獨測試
	time1 = GetTickCount();
	XslGameSudo_Processor(&XslGameSudo_s_Sudo);
	time2 = GetTickCount();
	// 列印結果
	XslGameSudo_FormatPrint(&XslGameSudo_s_Sudo, 1);
	// 列印耗時
	printf("Time(ms):%ld\n", time2 - time1);
	// 定住頁面,否則程式結束直接閃退,延時10秒自動退出
	time1 = time2 = 0;
	time1 = GetTickCount();
	while (((time2 - time1) < 100000) || (time2 == 0))
	{
		time2 = GetTickCount();
	}
}

五,免費精簡版原始碼結果演示

六,正常版原始碼下載連結與演示

  • 收費原始碼下載

    • 本來想用Demo大師,不過沒弄明白,還是用最笨的辦法吧。
    • QQ新增聯絡本博主:5387603
    • 支付寶/微信付費5元
    • 獲取獲取下載連結和下載碼
    • 操作半天,實屬扯犢子,太麻煩,說不上什麼時候就全開源了,但是讓我現在就開源,白折騰一通還心有不甘,這篇就這麼地吧,下回還是全開源的好!
       
  • 演示