52. N-Queens II ,全排列类型DFS的理解

Jun 23 更新
昨天看sudoku solver又陷入强烈的疑惑中,为什么所有人的dfs函数都是boolean的返回值??

今天回顾了一下之前知乎的回答,怎么让N QUEENS在找到一个解后就跳出循环?其实之前理解得不够深。今天又仔细跟了一下,发现:

情形一:void返回值

  1. ** 一次return只能跳出一层递归。**
    下面的图,为什么4 QUEENS的情形第一次是在第二层就停止,不会进入第三层?因为,在进入第三层之前调用了四次checkValid()发现第三层都不满足,然后就会继续往下走了,也就是走出for循环,程序结束,也就是隐含的return void,回到上一层继续for循环。只跳出了一层递归。
  2. 找到一个解{1, 3, 0, 2}之后,加到解集里面去,然后return,会发生什么?
            if (checkValid(row, col)) {
                dfs(result, row + 1, n, col);
            }

row 原本等于4了,但回来之后回到进去下一层之前的状态,row = 3,再次for循环,不满足, col: {1, 3, 0, 3}。

  • 这时候,函数执行完了,相当于return到上一层,row = 2。

  • 继续for循环发现,row = 2的后三个位置也不行了,又执行完了,return的上一层,row = 1。

  • row = 1的i本来就在第四个位置了,回到上一层,row = 0.

  • 那因为row = 0 的时候我们checkValid一定是true,所以就进入下一层DFS,row = 1。这时候会触发我们加的if (result.size() > 0) return;回到上一层。刚才i是等于2的,这时候就继续吧,i = 3。然后往复进入下一层再回来,i = 4,也就是说在它进入row = 1的for循环之前让它回到row = 1。那i = 4函数就执行完了。这已经是最顶层的梦了,也就真的醒来了。

也就是说,只有在回溯到了第一层,有机会进入下一层的时候才能让它一步步return,不然它又往第二层第三层去找了。

这种方法对应的是把if (result.size() > 0) return;加到if (row == n) {
...
result.add(new ArrayList<String>(cell));
return;
}
后面的情况,所以还要再找一遍234层,最后回到第一层的时候,col变成了{1,3,3,3},最后在从1层下到2层的时候才能一步步return,那优化一下,把这个return放到for循环里面:

        for (int i = 0; i < n; i++) {
            if (result.size() > 0)
                return;
            col[row] = i;
            if (checkValid(row, col)) {
                dfs(result, row + 1, n, col);
            }
        }

这样的话在backtracking的时候就不会再找1303这样的数了,在{1,3,0,2}的状态下就return了(也就是说都不用全局变量保存了,直接读取这个数组的内容就行了)。

情形二(重点):boolean 递归中的if(dfs()){ return true;}的意义

刚才我又看了一遍,boolean类型的dfs函数真正的意义,其实是..

  public boolean dfs(List<List<String>> result, int row, int n, int[] col) {
        if (row == n) {
            //保存解到解集
            //...
            return true;
        }
        for (int i = 0; i < n; i++) {
            col[row] = i;
            if (checkValid(row, col)) {
                if (dfs(result, row + 1, n, col)) return true;
            }
        }
        return false;
    }

这时候如果找到一个解,return true的话,回到了上一层,上一层的入口的返回值也就成了true,这时候连for循环都不用走,直接就一层层退出了!!!

这里的也不一定是boolean,用某个int值也行。

对于sudoku solver:

sudoku solver那题题目说,You may assume that there will be only one unique solution. 那我如果不及时让他停下,他必然会找到解后又继续找,最后回到原始状态(为什么是最原始的状态呢)。
那我能想到让它变成void的方法就是找到一个解后保存下来,然后随他去吧,恢复了也无所谓;当然也可以在for循环第一句里写上:if (res != null) return; 但是这题不是要return一个结果,而是直接对传进来的board进行操作,但我最后把board赋值回res,发现不行,就先不搞了。

1:11 AM 24/06/2017


原po:


盗梦.jpg

上面这张图是我思考的全排列类型DFS的运行过程,这种DFS的特点就是for循环里面有递归,举个N Queens的例子:

        for (int i = 0; i < n; i++) {
            //i表示Q所在的列 从头到尾遍历一遍
            col[row] = i;
            if (checkValid(row, col)) {
                dfs(result, row + 1, n, col);
            }
        }

一开始理解这种题的时候我完全不懂,现在懂一些了。原因就是这道N QueensII,因为这道题求解的是Queens的个数,我就在想,这个递归是找到一个答案就结束还是找到所有答案再结束呢?因为终止条件是row == n的时候return,但是return了之后recursion并没有结束,因为经过了for循环。如图,其实对于每一个row,for循环都要从1到n-1走一遍,如果valid就进行到下一层的dfs里去,那么:

什么时候会backtracking(回溯)?

  1. 找到第一个解之后return的时候会backtracking。
    第一张图里的row==1的时候,Q在第三格发现后面checkVaild都失效了,就移动到第四格;然后走走走,到第二张图的情形row == n找到一个解return的时候,这时候row回到row - 1的状态,也就是上一层的梦境(第二张图),彼时i = 2,那么i这时候继续走,i= 3 了,checkValid失效,这时候遇到第二种:
  2. row = 3的for循环走完了的时候。
    这时候发现栈中还有上一层的递归没有执行完,上一层的梦醒了,继续做。也就是把row =2的Q移动到第二个格子。

感谢盗梦空间。

怎么判断是什么时候回溯?用IDE调试着看。
我还有个问题,怎么让NQUEENS找到一个解就停止?

关于怎么让NQUEENS找到一个解就停止,我在斗鱼上提问了,覃超立刻回应了,就是加个终止条件。我加了这么一句就可以了:

if (result.size()>0) return;

对应于上面的图二,就是每次回到上一层的梦,就发现这一层已经不满足了,所以一直回到第一层的梦,最后醒来。

参数「归去来兮」的问题

也就是还原,恢复现场。这题的debug:

1303.png

就在图中这一步之前,col一直是[1,3,0,2]的状态,没有因为return就回到之前的[1,3,0,0]的状态。但这题很特殊,因为第四个格子的数字2反正会被覆盖所以不用还原。

我终于理解了!!intellij idea的左下角显示的是函数栈

左下角是函数栈

一直以来我不太理解什么时候归去来兮,现在终于好像开窍了 。上面这题是Generate Paratheses的递归,我观察到IDEA的左下角,竟然有9个dfs函数,按照9~1排列,点开之后,我发现右边的variables grid里显示的是每一个dfs的状态!太神奇了。。哈哈,这里面的StringBuilder就不断地在变化,第1个是"",第9个就是图中的"(((())))"。
然后,找到一个解之后return的时候,回到dfs stack 8,结果8执行完了没有执行stack7而是直接执行了stack4,sb变成了"(((",然后执行5,变成"((()"。这时候递归已经不太好理解了,大概就是因为left对应着多个right,才导致left4的4个right执行完了就开始执行left3的right们。我就不去深究了。重要的事,这里为什么不需要还原sb的问题,如果写成:

        if (right < left) {
            sb.append((")"));
            dfs(left, right + 1, result, sb, n);
            sb.deleteCharAt(sb.length() - 1);
        }

这样是要还原的,因为这里sb没有被当做参数传到下一层递归里面去,只是sb的指针(也就是内存地址)传到dfs下一层去了,所以要手动还原,同样的道理,52题的N-QUEENSII也是这样,左边的栈中我们会看到每个栈的col里面都是一样的值,这是因为col没有在参数中被改变,而是作为一个「全局」变量在使用,内存地址没有改变。
而Generate Parentheses这题就不一样,回到上一层的梦境的时候,sb已经回到之前的状态了。

覃超对于word search那道题不需要还原的解释:

参数的时候不要归去来兮 值拷贝的
drunkpiano 22:48:46
定义在参数里面,并且参数做了值拷贝,就帮你还原了
drunkpiano 22:49:01

传递到board里面 只是把指针传进去了 所以要手动还原
drunkpiano 22:49:14
currentWord值拷贝了,不用手动还原

再来理解一下,为什么对于递归参数中的i,j这些index的值不需要还原,因为它们是基本数据类型啊,本身就没有引用,也就是说,只有在递归的参数是指向原来的某个引用的指针的时候,才需要还原。
这也是覃超为什么说Generate Parentheses 那题用StringBuilder虽然需要恢复现场,但好处是不会每次都创建新的对象。

今天弄懂了这些还是挺开心的。感觉对dfs的理解加深了不少。想起MK书里写的,你做得不好不代表你不能做啊。。做得慢不代表你不会做啊。。慢慢来吧。

对了,差点忘了贴一下我盲写的这题的AC代码,注意checkValid那边。

public class Solution {
    int num = 0;

    public int totalNQueens(int n) {
        int[] col = new int[n];
        dfsHelper(n, col, 0);
        return num;
    }

    private void dfsHelper(int n, int[] col, int row) {
        if (row == n) {
            num++;
            return;
        }

        for (int i = 0; i < n; i++) {
            col[row] = i;
            if (checkValid(row, col)) {
                dfsHelper(n, col, row + 1);
            }
        }
    }

    private boolean checkValid(int row, int[] col) {
        for (int i = 0; i < row; i++)
//            for (int j = i + 1; j < row; j++) {
            //不要用double for cycle,因为row之前的已经valid了不需要比对了,只需要把每个i跟row比
            if (Math.abs(i - row) == Math.abs(col[i] - col[row]) || col[i] == col[row]) {
                return false;
//                }
            }
        return true;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容