草莓♭布丁

导航

数织游戏求解工具设计及相关算法研究(C#实现)

一、数织游戏简介

1,数织游戏的每行每列都有提示信息,数字代表有多少个连续的黑格

2,两个数字之间的黑格不连续,即中间必须有叉叉隔开

3,数织游戏的解可能不唯一,满足所有的行列条件即可

 

二、求解程序

1,程序整体设计

 

 

程序分为交互界面和求解程序两部分,求解程序使用新的线程求解,避免交互界面卡死。

本文主要介绍求解程序的设计,基于C#的交互界面不做过多赘述

2,程序算法概述

这个问题很像N皇后问题,当行列限制条件全部为1时,将变成不考虑对角线的N皇后问题,所以基础算法就是深度优先搜索。在算法上,本文将讨论以下四个问题:

(1)解决单个行列求解问题,比较递归法和动态规划算法

(2)解决数织的整体求解问题,利用单行的解,在列方向上进行深度优先搜索

(3)在复杂问题上(搜索树超过10^25时)的推导算法

(4)探讨推导算法的优化可能

 

三,数据结构和行列条件读入问题

1,交互窗口简介

 Form_Main类是设计的窗口,具体造型如图所示,不做过多介绍

 主要的功能就是将行列限制条件输入,然后点击计算结果进行计算。有多个解时,可以点击按钮切换。

注:在本程序中,需要上色的格子用1表示,叉叉用0表示。

2,求解类数据结构

        private Form_Main main_Form;
        private int[][] definiteGird;           //100%确定结果的格子 
        private int[][] dfsGird;                //深度优先遍历所有单行的解
        private List<int[][]> puzzleResult;     //最终的解集
        private int resultIndex;

        //地图信息
        public int Row, Col;                    //地图行列
        public List<List<int>> RowLimit;        //行限制
        public List<List<int>> ColLimit;        //列限制

        //解谜信息
        private Thread thread = null;
        private List<LinkedList<int[]>> EveryRowAllSolve;        //每行:只考虑单行限制的所有解
        private List<LinkedList<int[]>> EveryColAllSolve;        //每列:只考虑单列限制的所有解

首先,需要两个二维数组,一个存储已经确定答案的格子,另一个用于深度优先搜索。

每搜索到一个解,都存入puzzleResult中。

在地图信息方面,行列限制将会原封不动地读入RowLimit和ColLimit。由于单个行列的所有解不需要以下标访问,只需要增删和遍历,所以使用链表EveryRowAllSolve和EveryColAllSolve保存,便于增删操作。

3,行列限制的读入

        private void button_TryDo_Click(object sender, EventArgs e)
        {
            //解析行列条件
            var rowLimit = ReadLimitData(textBox_Row.Text);
            var colLimit = ReadLimitData(textBox_Col.Text);
            nonoPuzzle?.StopAnalyze();                          //杀死原线程
            nonoPuzzle = new NonoPuzzle(rowLimit, colLimit);
            //求解
            nonoPuzzle.TryDoPuzzle(this);
        }

        private List<List<int>> ReadLimitData(string data)
        {
            List<List<int>> limit = new List<List<int>>();
            string[] LimitR = data.Split("\r\n");
            LimitR = LimitR.Where(s => !string.IsNullOrEmpty(s)).ToArray();
            for (int i = 0; i < LimitR.Length; i++)
            {
                string[] LimitS = LimitR[i].Split(" ");
                List<int> singleLimit = new List<int>();
                for (int j = 0; j < LimitS.Length; j++)
                {
                    singleLimit.Add(int.Parse(LimitS[j]));
                }
                limit.Add(singleLimit);
            }
            return limit;
        }

 C#窗口的字符串换行有一个\r,所以是以\r\n分割字符串。文本或控制台读入的字符串直接\n即可。

每一行(列)的条件最终都是List<int>,将字符串整体转化成列表List<List<int>>后,再由构造函数传入解谜类NonoPuzzle:

        public NonoPuzzle(List<List<int>> rowLimit, List<List<int>> colLimit)
        {
            RowLimit = rowLimit;
            ColLimit = colLimit;
            Row = RowLimit.Count;
            Col = ColLimit.Count;
        }

        /// <summary>
        /// 停止当前计算
        /// </summary>
        public void StopAnalyze()
        {
            if (thread != null && thread.IsAlive)
            {
                thread.IsBackground = true;
                thread.Interrupt();
            }
        }

多说一句杀死线程的问题,这里使用的是.net core3.1,并不支持Abort,所以只能把原线程扔到后台,让系统自行回收。

4,结果的输出

计算完成后,默认输出第一个解,剩下的需要手动切换。

 public string PrintResult(int index)
        {
            if (index < 0 || index >= puzzleResult.Count)
                return "";

            string res = "";
            for (int i = 0; i < Row; i++)
            {
                for (int j = 0; j < Col; j++)
                {
                    res += puzzleResult[index][i][j];
                    if (j != Col - 1)
                        res += " ";
                }
                if (i != Row - 1)
                    res += "\r\n";
            }
            return res;
        }

        public string PrintNextResult()
        {
            if (resultIndex < puzzleResult.Count - 1)
                resultIndex++;
            return PrintResult(resultIndex);
        }

        public string PrintPreResult()
        {
            if (resultIndex > 0)
                resultIndex--;
            return PrintResult(resultIndex);
        }

 

四,数据初筛

本步骤的作用有两点:

1:限制数据长度初步判定:每行每列要能放得下这么多1
2:先根据限制中较大的数字,填入确定格

        public void TryDoPuzzle(Form_Main main_Form)
        {
            this.main_Form = main_Form;
            this.main_Form.ClearConsole();

            bool preProceSuccess = PreProcessPuzzle();         //数据初筛
            if(preProceSuccess)
            {
                //TrySavePuzzle();
                thread = new Thread(TrySavePuzzle);          //开新线程解谜,防止阻塞窗口显示
                thread.Start();
            }
        }

这里由Form_Main主窗口进行调用(不记得的向上翻行列限制读入那块),所有带“Console”的都是主界面的输出,本文不再详细介绍。

当数据通过初筛后,才开启新线程进行计算(计算时间可能较长,单线程的话,窗口会卡死闪退)

那么如何填入初始的确定格呢?我们不妨来看五种情况:

(1)15*15的地图,行限制是0或15,那么就是全0/全1

(2)15*15的地图,行限制是13,那么它就是??111  11111  111??,中间的1是可以确定的格子,而两头的?是不能确定的格子。倘若中间没有这么长,就凑不够13个连续的格子了。

(3)还是15*15的地图,行限制是6 5,那么它就是???11  1????  11???。方法如下:

先把6填在最左边(用x表示),假设是xxxxx  x0???  ?????,有一个0是因为6和5中间必须至少空一格。那么就是剩下8个?中要填入一个5,即为???11???,中间两个必为1

所以得到了xxxxx  x0??? 11???,然后再假设最右边5个,求左边的6,方法相同

(4)还是15*15的地图,行限制是4 4 3,那么它是??11?  ??11?  ??1??

方法和上面类似,比如求中间这个4,先把其余数字的4 3紧密排好,就是xxxx0 ?????  ?0xxx。然后在剩下6个?中要填入一个4,即为??11??

左边的4和右边的3方法类似,也是先把剩下的紧密排好,在剩余的?中填入数字。

(5)15*15的地图,行限制是2 2 1,无法确定任何格子

总结:对于任意数字,可以将其他数字先在两边顶死,中间空出剩余方格,转化为(2)的情况。若剩余方格<2*当前数字,则能确定部分格子

其中,少的部分即为确定的格子数量,例如在6个?中填入4,确定格子数量=2*4-6=2

 

依据上述逻辑,我们假设有a b c d这样的限制条件,其中b最大,那么就先看b是否可以确定格子。

先算出一共至少需要M=a+1+b+1+c+1+d个格子

从最大的数字开始看,填充格子数量=2*b-剩余格子=2*b-(总长度-(M-b))=b-总长度+M

如果b满足条件,则继续看第二大的数字。

 

        /// <summary>
        /// 重置确定格子
        /// </summary>
        private void ResetDefiniteGird()
        {
            definiteGird = new int[Row][];
            for (int i = 0; i < Row; i++)
            {
                definiteGird[i] = new int[Col];
                for (int j = 0; j < Col; j++)
                {
                    definiteGird[i][j] = -1;            //-1代表不能确定的格子
                }
            }
        }

/// <summary>
        /// 数据初筛/// </summary>
        private bool PreProcessPuzzle()
        {
            ResetDefiniteGird();            //初始化固定格

            for (int i = 0; i < Row; i++)
            {
                int minGird = -1;       //最小长度第一组连续1左边不需要格子
                int maxNum = -1;        //最大数
                int maxindex = -1;

                for (int j = 0; j < RowLimit[i].Count; j++)
                {
                    minGird += RowLimit[i][j];
                    minGird++;
                    if(RowLimit[i][j]>maxNum)
                    {
                        maxNum = RowLimit[i][j];
                        maxindex = j;
                    }
                }
                //长度检验,避免下一步陷入死循环
                if (minGird > Col)
                {
                    this.main_Form.SetConsole("初筛错误:第" + i + "行数据溢出,请检查条件是否过大");
                    return false;
                }
                //填入确定格
                if(maxNum==0)
                {
                    for (int j = 0; j < Col; j++)
                    {
                        definiteGird[i][j] = 0;         //填入一行0
                    }
                }
                else
                {
                    int canDefiniteGirdNum = maxNum - Col + minGird;
                    //数字足够大,可以确定某些格子
                    //例如7,2填入12个格子,14>12-10+7=9,且14-9=5,则可以填入7之前五个格子3-7
                    List<int> doneIndex = new List<int>();          //从大到小依次处理
                    while (canDefiniteGirdNum > 0)
                    {
                        int tail = -2;      //第一组1左边没格子,还要去掉后面空0的格子,所以-2
                        //计算到可填充末尾格
                        for (int j = 0; j <= maxindex; j++)
                        {
                            tail += RowLimit[i][j];
                            tail++;
                        }
                        //向前填充确定格
                        for (int j = 0; j < canDefiniteGirdNum; j++)
                        {
                            int dfIndex = tail - j;
                            if (dfIndex >= 0 && dfIndex < Col)
                            {
                                definiteGird[i][dfIndex] = 1;
                            }
                            else
                            {
                                this.main_Form.SetConsole("初筛错误:第" + i + "行填入固定格错误");
                                return false;
                            }
                        }
                        //看看是否还存在其他确定格
                        doneIndex.Add(maxindex);
                        maxNum = -1;        //寻找下一个最大数
                        maxindex = -1;
                        for (int j = 0; j < RowLimit[i].Count; j++)
                        {
                            if (RowLimit[i][j] > maxNum && !doneIndex.Contains(j))
                            {
                                maxNum = RowLimit[i][j];
                                maxindex = j;
                            }
                            canDefiniteGirdNum = maxNum - Col + minGird;
                        }
                    }
                }
            }

            for (int j = 0; j < Col; j++)
            {
                int minGird = -1;       //最小长度第一组连续1上边不需要格子
                int maxNum = -1;        //最大数
                int maxindex = -1;
                for (int i = 0; i < ColLimit[j].Count; i++)
                {
                    minGird += ColLimit[j][i];
                    minGird++;
                    if (ColLimit[j][i] > maxNum)
                    {
                        maxNum = ColLimit[j][i];
                        maxindex = i;
                    }
                }
                if (minGird > Row)
                {
                    this.main_Form.SetConsole("初筛错误:第" + j + "列数据溢出,请检查条件是否过大");
                    return false;
                }
                //填入确定格
                if (maxNum == 0)
                {
                    for (int i = 0; i < Row; i++)
                    {
                        definiteGird[i][j] = 0;         //填入一列0
                    }
                }
                else
                {
                    int canDefiniteGirdNum = maxNum - Row + minGird;
                    //数字足够大,可以确定某些格子
                    //例如7,2填入12个格子,14>12-10+7=9,且14-9=5,则可以填入7之前五个格子3-7
                    List<int> doneIndex = new List<int>();          //从大到小依次处理
                    while (canDefiniteGirdNum > 0)
                    {
                        int tail = -2;      //第一组1上边没格子,还要去掉后面空0的格子,所以-2
                        //计算到可填充末尾格
                        for (int i = 0; i <= maxindex; i++)
                        {
                            tail += ColLimit[j][i];
                            tail++;
                        }
                        //向上填充确定格
                        for (int i = 0; i < canDefiniteGirdNum; i++)
                        {
                            int dfIndex = tail - i;
                            if (dfIndex >= 0 && dfIndex < Row)
                            {
                                definiteGird[dfIndex][j] = 1;
                            }
                            else
                            {
                                this.main_Form.SetConsole("初筛错误:第" + j + "列填入固定格错误");
                                return false;
                            }
                        }
                        //看看是否还存在其他确定格
                        doneIndex.Add(maxindex);
                        maxNum = -1;        //寻找下一个最大数
                        maxindex = -1;
                        for (int i = 0; i < ColLimit[j].Count; i++)
                        {
                            if (ColLimit[j][i] > maxNum && !doneIndex.Contains(i))
                            {
                                maxNum = ColLimit[j][i];
                                maxindex = i;
                            }
                            canDefiniteGirdNum = maxNum - Col + minGird;
                        }
                    }
                }
            }
            this.main_Form.SetConsole("数据通过初筛,正在计算中。目前确定格如下:");
            this.main_Form.AddConsole(PrintDefiniteGird());
            return true;
        }

 

然后再补充一下上面代码最后两行,输出的问题。正常在控制台输出只需要/n即可。

        /// <summary>
        /// 输出当前已经确定的格子
        /// </summary>
        private string PrintDefiniteGird()
        {
            string res = "";
            for (int i = 0; i < Row; i++)
            {
                for (int j = 0; j < Col; j++)
                {
                    res += definiteGird[i][j];
                    if (j != Col - 1)
                        res += " ";
                }
                if (i != Row - 1)
                    res += "\r\n";
            }
            return res;
        }

 

五,单个行列求解

经过漫长的准备工作,终于来到了求解环节,我们先来整体看一下求解的过程。

        private void TrySavePuzzle()
        {
            bool lineSolveSuccess = GetEveryLineSolve();                    //解析每行每列的所有可能
            if(!lineSolveSuccess)
            {
                main_Form.AddConsole("行列解析失败");
                return;
            }

            bool bitExcludeSuccess = BitExclude();
            if(!bitExcludeSuccess)
            {
                main_Form.AddConsole("位运算解析失败");
                return;
            }

            DFSSearchResult();          //深度优先解析
            resultIndex = 0;
            main_Form.ShowResult(PrintResult(0));
        }

我们首先来看行列单个行列求解问题,还是举个例子,假设15*15的地图中,某行(列)的条件是2 2 1,那么这一行最终一定长这样:*110*110*1*。在*位中,可以填入若干个0,也就是剩下的8个0需要填入4个*的位置。那么该问题就转化成:x个0放入y个坑中,单个坑可以为空,列举出所有的放法。

设该问题为P,则P(x,y)=P(x,y-1),1+P(x-1,y-1),2+P(x-2,y-1)……x+P(0,y-1),其中y=1时,P(x,y)=x。

简单解释一下,就是如果第一个坑放一个,那么剩下y-1个坑就需要放x-1个。至此,该问题被转化为一个递归求解的问题。我们也可以使用动态规划法来加快求解的速度,即先求P(0,i),P(1,i),P(2,i)……P(x,i),然后再利用这些数据去求P(0,i+1),P(1,i+1),P(2,i+1)……P(x,i+1)。

/// <summary>
        /// 获取所有行列的可能解
        /// </summary>
        public bool GetEveryLineSolve()
        {
            //解析每一行每一列的所有可能
            EveryRowAllSolve = new List<LinkedList<int[]>>();
            EveryColAllSolve = new List<LinkedList<int[]>>();
            for (int i = 0; i < Row; i++)
            {
                EveryRowAllSolve.Add(GetLineAllSolve(i, true));
                if (EveryRowAllSolve[i] == null)
                {
                    return false;
                }
                main_Form.AddConsole("完成第" + i + "行的解析,共获得" + EveryRowAllSolve[i].Count + "种情况");
                //main_Form.AddConsole(PrintOneLineSolve(EveryRowAllSolve[i]));
                if (EveryRowAllSolve[i].Count == 0)
                {
                    main_Form.AddConsole("无解!");
                    return false;
                }
            }

            for (int j = 0; j < Col; j++)
            {
                EveryColAllSolve.Add(GetLineAllSolve(j, false));
                if (EveryColAllSolve[j] == null)
                {
                    return false;
                }
                main_Form.AddConsole("完成第" + j + "列的解析,共获得" + EveryColAllSolve[j].Count + "种情况");
                //main_Form.AddConsole(PrintOneLineSolve(EveryColAllSolve[j]));
                if(EveryColAllSolve[j].Count == 0)
                {
                    main_Form.AddConsole("无解!");
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// 动态规划法获取每行/列的所有解,排除不符合已确定格子的解
        /// </summary>
        public LinkedList<int[]> GetLineAllSolve(int lineIndex,bool isRow)
        {
            LinkedList<int[]> allSolve = new LinkedList<int[]>();
            List<int> lineLimit = isRow ? RowLimit[lineIndex] : ColLimit[lineIndex];
            int lineSize = isRow ? Col : Row;           //行列的长度:解一行的数据,获取列长度
            if (lineLimit[0] == 0)
            {
                //返回全0的情况
                int[] zeroSolve = new int[lineSize];
                for (int i = 0; i < lineSize; i++)
                {
                    zeroSolve[i] = 0;
                }
                allSolve.AddLast(zeroSolve);
                return allSolve;
            }

            //先找出可以放0的地方,以及多余0的数量
            int minGird = -1;
            for (int i = 0; i < lineLimit.Count; i++)
            {
                minGird += lineLimit[i];
                minGird++;
            }

            int zeroPlace = lineLimit.Count + 1;   //可放0的位置:两端都能放,所以+1
            int zeroNum = lineSize - minGird;                    //总格子-最紧凑的情况,剩下的数量就是多的0

            //P空插N个0,就等于P位置插0-N个,(P-1)位置插N-0个,这N种情况之和
            //从左侧第一个空位开始插,如果遇到不符合的情况可以直接在链表中去掉
            LinkedList<int[]>[] lastSolve = new LinkedList<int[]>[zeroNum + 1];         //存上一轮P-1
            LinkedList<int[]>[] curSolve = new LinkedList<int[]>[zeroNum + 1];          //存当前轮P
            for (int i = 0; i <= zeroNum; i++)
            {
                lastSolve[i] = new LinkedList<int[]>();
                int[] baseSolve = new int[lineSize];
                for (int j = 0; j < lineSize; j++)
                {
                    baseSolve[j] = -1;
                }
                lastSolve[i].AddLast(baseSolve);
            }
            //完成初始化,开始动态规划
            for (int i = 0; i < zeroPlace; i++)
            {
                //i代表当前考虑前i+1个空档(每两组1之间,以及两侧都有空档)
                for (int j = 0; j <= zeroNum; j++)
                {
                    //j表示当前位置及之前一共插入多少个0
                    curSolve[j] = new LinkedList<int[]>();
                    if (i == (zeroPlace-1))
                    {
                        j = zeroNum;
                        curSolve[j] = new LinkedList<int[]>();
                    }
                    for (int n = 0; n <= j; n++)
                    {
                        if (i == 0)
                        {
                            n = j;
                        }
                        //n表示这一轮插入多少个0,那么上一轮之前插入了j-n个0
                        foreach (var item in lastSolve[j - n])
                        {
                            int[] curSingleSolve = new int[lineSize];
                            bool putcheck = true;           //是否符合插入要求
                            int curInputIndex = -1;         //当前轮插入位置
                            //先把上一轮的数据拷贝过来
                            for (int index = 0; index < lineSize; index++)
                            {
                                if (item[index] == -1 && curInputIndex == -1)
                                    curInputIndex = index;
                                curSingleSolve[index] = item[index];
                            }
                            //先插入当前轮的0
                            int zeroEndIndex = curInputIndex + n;           //0结束,插下一轮1的位置
                            if (i == zeroPlace - 1)
                            {
                                if (zeroEndIndex != lineSize && zeroEndIndex != -1)
                                {
                                    main_Form.AddConsole("解析第" + lineIndex + "行数据错误:动态规划结束时0的数量错误");
                                    return null;
                                }
                            }
                            for (int index = curInputIndex; index < zeroEndIndex; index++)
                            {
                                curSingleSolve[index] = 0;
                                putcheck = putcheck && CheckLinePutData(lineIndex, index, 0, isRow);
                            }
                            if (i < zeroPlace - 1)
                            {
                                //再插入后面的1,为下一轮做准备
                                int oneEndIndex = zeroEndIndex + lineLimit[i];
                                for (int index = zeroEndIndex; index < oneEndIndex; index++)
                                {
                                    curSingleSolve[index] = 1;
                                    putcheck = putcheck && CheckLinePutData(lineIndex, index, 1, isRow);
                                }
                                if (i < zeroPlace - 2)                  //倒数第二轮的1后面不需要0
                                {
                                    curSingleSolve[oneEndIndex] = 0;    //1后面至少跟一个0,这个0不计入动态规划
                                    putcheck = putcheck && CheckLinePutData(lineIndex, oneEndIndex, 0, isRow);
                                }
                            }
                            if(putcheck)
                                curSolve[j].AddLast(curSingleSolve);
                        }
                    }
                }
                lastSolve = curSolve;
                curSolve = new LinkedList<int[]>[zeroNum + 1];
            }
            allSolve = lastSolve[zeroNum];
            return allSolve;
        }

        public bool CheckLinePutData(int LineIndex,int index,int putData,bool isRow)
        {
            if (isRow)
                return CheckPutData(LineIndex, index, putData);
            else
                return CheckPutData(index, LineIndex, putData);
        }

        /// <summary>
        /// 检验某个位置是否能填对应数据
        /// </summary>
        public bool CheckPutData(int xRow,int yCol,int putdata)
        {
            if (definiteGird[xRow][yCol] == -1)
                return true;
            else if (definiteGird[xRow][yCol] == putdata)
            {
                return true;
            }
            else
                return false;
        }

在求解过程中,我们还需要考虑到该位置是否满足之前的初筛条件,不满足条件的就不要了,不然后面搜索树大小是很恐怖的。

 

六,深度优先搜索

接下来,我们先看深度优先搜索的过程(位运算是后面的推导算法,可以加快计算速度,不调用也能正常运行)

这一块没什么可以说的,就是把每一行的解拼到一起,看看是否满足列条件。

  /// <summary>
        /// 使用深度优先搜索谜题的所有解
        /// </summary>
        public void DFSSearchResult()
        {
            //统计搜索树大小
            float rowPossibleCount = 1;
            for (int i = 0; i < Col; i++)
            {
                rowPossibleCount *= EveryRowAllSolve[i].Count;
            }
            main_Form.AddConsole("开始DFS搜索,搜索树大小为:" + rowPossibleCount);

            int depth = 0;          //搜索深度
            int refreshCount = 0;   //进度汇报
            bool needBack = false;  //是否需要回退
            ResetDfsGird();
            puzzleResult = new List<int[][]>();
            LinkedListNode<int[]>[] dfsNodeVector = new LinkedListNode<int[]>[Row];         //存储当前遍历的节点
            dfsNodeVector[0] = EveryRowAllSolve[0].First;
            while (depth >= 0)
            {
                //统计进度
                refreshCount++;
                if (refreshCount > 5000000)
                {
                    //计算当前每行回溯到了第几个
                    int[] nodeCounts = new int[Row];
                    for (int i = 0; i < Row; i++)
                    {
                        if (dfsNodeVector[i] == null)
                            nodeCounts[i] = 1;
                        else
                        {
                            int count = 1;
                            foreach (var item in EveryRowAllSolve[i])
                            {
                                if (item == dfsNodeVector[i].Value)
                                {
                                    break;
                                }
                                count++;
                            }
                            nodeCounts[i] = count;
                        }
                    }
                    float totalTime = nodeCounts[Row - 1];
                    float factor = 1;
                    //统计已经计算的次数
                    for (int i = Row - 1; i > 0; i--)
                    {
                        factor *= EveryRowAllSolve[i].Count;
                        totalTime += (nodeCounts[i - 1] - 1) * factor;
                    }
                    main_Form.AddConsole("已经回溯了" + totalTime + "种情况,进度:" + totalTime / rowPossibleCount * 100 + "%");
                    refreshCount = 0;
                }

                dfsGird[depth] = dfsNodeVector[depth].Value;            //将当前遍历的节点数据加入分析数组
                if (DfsGirdIsColPossible())
                {
                    //当前行通过了列检验
                    if (depth == Row - 1)
                    {
                        //已经检索到最后一行,保存这个解后回退
                        int[][] oneRes = new int[Row][];
                        for (int i = 0; i < Row; i++)
                        {
                            oneRes[i] = new int[Col];
                            for (int j = 0; j < Col; j++)
                            {
                                oneRes[i][j] = dfsGird[i][j];
                            }
                        }
                        puzzleResult.Add(oneRes);
                        needBack = true;
                    }
                    else
                    {
                        //还没到最后一行,从下一行第一个解开始遍历
                        depth++;
                        dfsNodeVector[depth] = EveryRowAllSolve[depth].First;
                    }
                }
                else
                {
                    //当前行未通过列检验,检验当前行的下一个解
                    if (dfsNodeVector[depth] == EveryRowAllSolve[depth].Last)
                    {
                        //已经是最后一个解了,这一路分支需要回退
                        needBack = true;
                    }
                    else
                    {
                        //检验下一个解
                        dfsNodeVector[depth] = dfsNodeVector[depth].Next;
                    }
                }

                //深度回退
                if (needBack)
                {
                    while (depth >= 0 && dfsNodeVector[depth] == EveryRowAllSolve[depth].Last)
                    {
                        ResetDfsGirdRow(depth);
                        depth--;
                    }
                    if (depth >= 0)
                    {
                        //回退后继续检验回退行的下一个解
                        dfsNodeVector[depth] = dfsNodeVector[depth].Next;
                    }
                    needBack = false;
                }
            }
            main_Form.AddConsole("已解析完成,共获得" + puzzleResult.Count + "个解");
        }

        /// <summary>
        /// 重置深度优先搜索格子
        /// </summary>
        private void ResetDfsGird()
        {
            dfsGird = new int[Row][];
            for (int i = 0; i < Row; i++)
            {
                dfsGird[i] = new int[Col];
                for (int j = 0; j < Col; j++)
                {
                    dfsGird[i][j] = -1;            //-1代表不能确定的格子
                }
            }
        }

        /// <summary>
        /// 重置某一行的格子
        /// </summary>
        private void ResetDfsGirdRow(int row)
        {
            dfsGird[row] = new int[Col];
            for (int j = 0; j < Col; j++)
            {
                dfsGird[row][j] = -1;            //-1代表不能确定的格子
            }
        }

        /// <summary>
        /// 判断当前深度优先的数据格,在列方向上是否满足条件
        /// </summary>
        private bool DfsGirdIsColPossible()
        {
            for (int j = 0; j < Col; j++)
            {
                List<int> curColNum = new List<int>();
                int num = 0;
                int i;
                for (i = 0; i < Row; i++)
                {
                    if (dfsGird[i][j] == 0)
                    {
                        if (num > 0)
                        {
                            curColNum.Add(num);
                            num = 0;
                        }
                    }
                    else if (dfsGird[i][j] == 1)
                    {
                        num++;
                    }
                    else if(dfsGird[i][j] == -1)
                    {
                        break;
                    }
                    else
                    {
                        main_Form.AddConsole("列检验错误:dfsGird数值异常!");
                        return false;           //地图只能为-1、0和1
                    }
                }
                //最后一格结束,统计贴边的数据
                if (num > 0)
                {
                    curColNum.Add(num);
                }
                //统计没有数字的列
                if (curColNum.Count == 0)
                    curColNum.Add(0);

                //比较当前列数据和列限制数据
                if (curColNum.Count > ColLimit[j].Count)
                    return false;
                else
                {
                    for (int index = 0; index < curColNum.Count; index++)
                    {
                        if(curColNum[index]!= ColLimit[j][index])
                        {
                            if (index != curColNum.Count - 1 || curColNum[index] > ColLimit[j][index])
                                return false;       //只有当前列的最后一个数字,可以小于限制条件,因为后面还能放1
                            else
                            {
                                //查看剩余位置是否足够放下所有的1
                                int leftGird = Row - i;
                                int needGird = ColLimit[j][index] - curColNum[index];
                                for (int temp = index+1; temp < ColLimit[j].Count; temp++)
                                {
                                    needGird++;             //中间的空格
                                    needGird += ColLimit[j][temp];
                                }
                                if (leftGird < needGird)
                                    return false;
                            }
                        }
                    }
                }
            }
            return true;
        }        

考虑到数据量可能较大(当搜索树达到10^24时,需要计算好几个小时),每隔5000000次操作就统计一次进度,然后输出,告诉用户现在计算了多少了。

 

七,推导算法

经过上述的复杂计算,终于求出了数织游戏的所有解。但是这样的暴力解法似乎,有那么亿点慢。

那么我们回到之前的初筛,初筛过程中能够确定一部分格子,那么能否根据这些格子进行进一步推导呢?这就涉及到确定格子的本质了。

例如15*15的格子要填入一个13,它有三种可能:

11111  11111  11100

01111  11111  11110

00111  11111  11111

而最终的确定格是:??111  11111  111??

在这些确定为1的地方,所有的可能,在这个位置都是1。那么,0也是相同的道理,例如一行的当前情况是??1??  ?????  ?????,而需要填入一个5,由于已经确定了一个1,那么这行的可能解还剩:

11111  00000  00000

01111  10000  00000

00111  11000  00000

推导后的确定格是:??111  ??000  00000

那么,只需要比对单个行(列)的所有解(使用AND和OR运算实现),全为1的就是1,全为0的就是0。这样就可以进行循环推导:

(1),利用位运算确定部分格子,如果新确定的格子和之前的确定格冲突,则无解。
(2),基于当前确定格,清除不符合要求的行列解
(3),当2不能清除解的时候,推导计算结束,进入回溯法遍历

 

/// <summary>
        /// 利用位运算排除可能
        /// </summary>
        public bool BitExclude()
        {
            main_Form.AddConsole("正在尝试使用位运算排除可能");
            int bitTurn = 0;        //轮次统计
            bool hasRemove = true;
            while (hasRemove)
            {
                hasRemove = false;
                int confirmGirdNum = 0;
                //行条件进行位运算
                for (int i = 0; i < Row; i++)
                {
                    int[] curRowOR = new int[Col];        //或起来等于0的格子确定为0
                    int[] curRowAND = new int[Col];       //与起来等于1的格子确定为1
                    for (int index = 0; index < Col; index++)
                    {
                        curRowOR[index] = 0;
                        curRowAND[index] = 1;
                    }
                    foreach (var item in EveryRowAllSolve[i])
                    {
                        for (int index = 0; index < Col; index++)
                        {
                            curRowOR[index] |= item[index];
                            curRowAND[index] &= item[index];
                        }
                    }
                    for (int index = 0; index < Col; index++)
                    {
                        if (curRowOR[index] == 0)
                        {   //或起来为0的格子必定为0
                            if (definiteGird[i][index] == -1)
                            {
                                definiteGird[i][index] = 0;
                                confirmGirdNum++;
                            }
                            else if (definiteGird[i][index] != 0)
                            {
                                main_Form.AddConsole("" + i + index + "格无解");
                                return false;
                            }
                        }
                        if (curRowAND[index] == 1)
                        {   //与起来为1的格子必定为1
                            if (definiteGird[i][index] == -1)
                            {
                                definiteGird[i][index] = 1;
                                confirmGirdNum++;
                            }
                            else if (definiteGird[i][index] != 1)
                            {
                                main_Form.AddConsole("" + i + index + "格无解");
                                return false;
                            }
                        }
                    }
                }
                //列条件进行位运算
                for (int j = 0; j < Col; j++)
                {
                    int[] curColOR = new int[Row];
                    int[] curColAND = new int[Row];
                    for (int index = 0; index < Row; index++)
                    {
                        curColOR[index] = 0;
                        curColAND[index] = 1;
                    }
                    foreach (var item in EveryColAllSolve[j])
                    {
                        for (int index = 0; index < Row; index++)
                        {
                            curColOR[index] |= item[index];
                            curColAND[index] &= item[index];
                        }
                    }
                    for (int index = 0; index < Row; index++)
                    {
                        if (curColOR[index] == 0)
                        {   //或起来为0的格子必定为0
                            if (definiteGird[index][j] == -1)
                            {
                                definiteGird[index][j] = 0;
                                confirmGirdNum++;
                            }
                            else if (definiteGird[index][j] != 0)
                            {
                                main_Form.AddConsole("" + index + j + "格无解");
                                return false;
                            }
                        }
                        if (curColAND[index] == 1)
                        {   //与起来为1的格子必定为1
                            if (definiteGird[index][j] == -1)
                            {
                                definiteGird[index][j] = 1;
                                confirmGirdNum++;
                            }
                            else if (definiteGird[index][j] != 1)
                            {
                                main_Form.AddConsole("" + index + j + "格无解");
                                return false;
                            }
                        }
                    }
                }
                //移除确定格子后,不符合条件的行列可能
                for (int i = 0; i < Row; i++)
                {
                    List<LinkedListNode<int[]>> removeNodes = new List<LinkedListNode<int[]>>();
                    LinkedListNode<int[]> curNode = EveryRowAllSolve[i].First;
                    while(curNode != null)
                    {   //使用链表节点遍历,完成后统一删除。不能foreach时直接删除,否则会报null
                        for (int index = 0; index < Col; index++)
                        {
                            if (curNode.Value[index] != definiteGird[i][index] && definiteGird[i][index] != -1)
                            {
                                removeNodes.Add(curNode);
                                hasRemove = true;
                                break;
                            }
                        }
                        curNode = curNode.Next;
                    }
                    foreach (var item in removeNodes)
                    {
                        EveryRowAllSolve[i].Remove(item);
                    }
                }
                for (int j = 0; j < Col; j++)
                {
                    List<LinkedListNode<int[]>> removeNodes = new List<LinkedListNode<int[]>>();
                    LinkedListNode<int[]> curNode = EveryColAllSolve[j].First;
                    while (curNode != null)
                    {
                        for (int index = 0; index < Row; index++)
                        {
                            if (curNode.Value[index] != definiteGird[index][j] && definiteGird[index][j] != -1)
                            {
                                removeNodes.Add(curNode);
                                hasRemove = true;
                                break;
                            }
                        }
                        curNode = curNode.Next;
                    }
                    foreach (var item in removeNodes)
                    {
                        EveryColAllSolve[j].Remove(item);
                    }
                }
                bitTurn++;
                main_Form.AddConsole("" + bitTurn + "轮位运算完成,本轮确定了" + confirmGirdNum + "个格子");
            }
            return true;
        }

 

 

八,推导算法的局限性和改进思路

1,为什么每个格子不用位存储?

我们知道N皇后问题的一种改进思路就是:用一位来存储一格,一个int32可以代表一行。但是数织游戏大小不确定,有部分游戏特别大,比如50*50,此时int32不能胜任,考虑到程序的通用性,就没有使用位存储。而且位存储只是一种加速思路,并不能从本质上改变算法的时间复杂度。

2,推导算法的时间复杂度如何,是一种高效算法么?

之前的展示图中,最后搜索树大小只剩1了,是否可以说明推导算法是高效算法呢?

推导算法看上去像是O(n^3)的高效算法,但是其实受制于单个行列的解集大小,例如35*35的地图,某行是1 2 1 1,之前动态规划给出的解集将会非常庞大。

它的时间复杂度是O(O(P)*n^2),这个x个0放入y个坑的解集问题P(x,y),是一个排列组合问题,O(P)=C(y+x,x-1),x和y受到行列限制数字量和数字大小的影响。

不过再怎么说,它也比深度优先算法O(2^n^n)要好多了。

3,无法推导的时候怎么办,有什么改进思路?

本程序中,无法推导之后,就回到深度优先搜索。虽然之前的截图,推导完成后搜索树已经为1,解决了问题,但是很多时候推导会卡壳。

那种和N皇后一样,行列限制条件全为1(或者填充格子太少)的极端情况就不看了。

正常的数织游戏,如果设计巧妙,就和数独一样,有的时候硬靠条件是无法推导的。这个无法推导倘若已经确定了大部分格子那还好,搜索树依然不会很大,但是如果在前期就无法推导,那么本程序就变回了深度优先算法。

人类在解决数独的时候是怎么做的呢:假设部分格子,问题可能就迎刃而解了。

那么数织问题也是类似的,具体的改进思路和可能遇到的问题:

(1),无法推导时,假设部分格子,然后对每种情况分别求解。

(2),这个假设应当有一些前提条件,例如(这些数值没研究过,留给改进者自行研究):

总填充格子>总格子的30%(太少了就变成类似N后问题那种极端情况了),其中在计算条件时,还要排除全0行列的影响

这个条件还应当是:未确定格子>总格子的60%,倘若已经确定了大部分格子,就没必要用假设法了

(3)分别求解的过程可以用求解队列,或者多线程分别求解。

(4)当假设的求解又无法推导了,又有两种解决思路:

a,继续递归假设

b,先把所有假设都推导完,然后整合确定格

(5),假设格子的选取也很有讲究,究竟选择哪些格子?选择多少格子?倘若选取不好,这个假设就是失败的,根本无法继续推导。

这些问题就留给后人继续研究吧0.0,太复杂了。

posted on 2021-09-19 23:46  草莓♭布丁  阅读(1129)  评论(0编辑  收藏  举报

Live2D