代码随想录第三十天|332.重新安排行程、51.N皇后、37.解数独、回溯总结

332.重新安排行程(二刷回看)

思路:

用回溯记录可能的行程,用vector<bool>used记录是否使用过,用如下代码判断是否放进path:

if(tickets[i][0]!=path.back())

                continue;

            if(used[i]==true)

                continue;

注意中止条件是

if(path.size()==(tickets.size()+1))

        {

            result.push_back(path);

            return;

        }

搜索所有路径会超时,应该找如何搜索时找最优解。


看代码后:

这道题目有几个难点:

1. 一个行程中,如果航班处理不好容易变成一个圈,成为死循环

2. 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?

3. 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?

4. 搜索的过程中,如何遍历一个机场所对应的所有机场。

这道题难是难在容器的选择和使用上。

第二问:

一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset。

在遍历unordered_map<出发机场, map<到达机场, 航班次数>> targets的过程中,可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。

如果“航班次数”大于零,说明目的地还可以飞,如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。

相当于说不删就做一个标记!

函数返回值用的是bool!因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线.

代码如下:

unordered_map<string,map<string,int>> targets;

    bool backtracking(int ticketNum, vector<string> &result)

    {

        if(result.size()==ticketNum+1)

            return true;

        for(pair<const string, int> &target : targets[result[result.size()-1]])

        {

            if(target.second > 0)

            {

                result.push_back(target.first);

                target.second--;

                if(backtracking(ticketNum,result)) return true;

                result.pop_back();

                target.second++;

            }

        }

        return false;

    }

    vector<string> findItinerary(vector<vector<string>>& tickets) {

        targets.clear();

        vector<string> result;

        for(const vector<string> & vec:tickets)

            targets[vec[0]][vec[1]]++;

        result.push_back("JFK");

        backtracking(tickets.size(),result);

        return result;


    }



51.N皇后(二刷回看)

思路:

无思路。

看视频:

画成树之后能发现问题变成了在每一行遍历回溯每个节点是否能放皇后。

定义参数:

vector<vector<string>> result;//存放结果

vector<string> chessboard(n, string(n,'.'));//初始化棋盘

void backtracking(int n, int row, vector<string> &chessboard)//n为棋盘边界,row为行,传入棋盘

终止条件:

if(row == n)

        {

            result.push_back(chessboard);

            return;

        }//到达叶子节点

单层逻辑:

for(int col=0;col<n;col++)

        {

            if(isValid(row,col,chessboard,n))

            {

                chessboard[row][col] = 'Q';

                backtracking(n,row+1, chessboard);

                chessboard[row][col] = '.';

            }

        }

是否符合条件的判断:

bool isValid(int row, int col, vector<string> &chessboard, int n)

    {

        for (int i = 0; i < row; i++) 

        {

            if(chessboard[i][col]== 'Q')

                return false;

        }// 同列的情况

        for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--)

        {

            if(chessboard[i][j]== 'Q')

                return false;

        }//45°的情况

        for(int i=row-1,j=col+1;i>=0 && j<n; i--,j++)

        {

            if(chessboard[i][j]=='Q')

                return false;

        }

        return true;

    }//135°的情况



37.解数独(二刷回看)

思路:

isValid函数中:判断横竖是否有相同元素,以及3*3中有没有相同元素。

回溯算法中:

中止条件为横竖都等于9,限制条件是如果元素不是'.'就跳过.但具体回溯不知道怎么写

看视频后:

递归函数的返回值需要是bool类型, 因为只需要找到一个符合的情况就返回

本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。

本题是二维递归:一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能。

bool backtracking(vector<vector<char>>& board)

    {

        for(int i=0;i<board.size();i++)

        {

            for(int j=0;j<board[0].size();j++)

            {

                if(board[i][j]!='.')

                    continue;

                for(char k='1'; k<='9';k++)

                {

                    if(isValid(i,j,k,board))

                    {

                        board[i][j]=k;

                        if(backtracking(board)) return true;

                        board[i][j]='.';

                    }

                }

                return false;

            }

        }

        return true;

    }

讨论是否合法:同行是否重复;同列是否重复;9宫格里是否重复

bool isValid(int row, int col, char val, vector<vector<char>> &board)

    {

        for(int i=0;i<9;i++)

        {

            if(board[row][i]==val)

                return false;

        }

        for(int j=0;j<9;j++)

        {

            if(board[j][col]==val)

                return false;

        }

        int startRow=(row/3)*3;

        int startCol=(col/3)*3;

        for(int i=startRow;i<startRow+3;i++)

        {

            for(int j=startCol;j<startCol+3;j++)

            {

                if(board[i][j]==val)

                    return false;

            }

        }

        return true;

    }



回溯总结:

回溯是递归的副产品,只要有递归就会有回溯,因此会和二叉树遍历和深度优先搜索结合。

回溯能解决如下问题:

组合问题:N个数里面按一定规则找出k个数的集合

排列问题:N个数按一定规则全排列,有几种排列方式

切割问题:一个字符串按一定规则有几种切割方式

子集问题:一个N个数的集合里有多少符合条件的子集

棋盘问题:N皇后,解数独等等

理解回溯应该使用树形结构

回溯是用递归控制for循环嵌套的数量:for循环横向遍历,递归纵向遍历,回溯不断调整结果集

回溯的优化方法只有剪枝剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了

回溯的模板:

void backtracking(参数) {

    if (终止条件) {

        存放结果;

        return;

    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {

        处理节点;

        backtracking(路径,选择列表); // 递归

        回溯,撤销处理结果

    }

}

组合问题:

注意startIndex的应用:如果是一个集合来求组合的话,就需要startIndex,如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex。

切割问题:

解决以下问题

1.切割问题其实类似组合问题

2.如何模拟那些切割线

3.切割问题中递归如何终止

4.在递归循环中如何截取子串

5.如何判断回文

巧用startIndex和i,注意切割过的地方不能重复切割所以递归函数需要传入i + 1

子集问题:

在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果

子集问题不需要加终止条件,因为本来就需要遍历整棵树。注意递增子序列这道题。

排列问题:

排列问题不需要startIndex,因为每层都是从0开始搜索而不是startIndex。需要used数组记录path里都放了哪些元素了。

去重问题:

注意树层去重和树枝去重,可以用used数组或者set去重,但set效率比较低。组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上“使用过”,一个维度是同一树层上“使用过”。

例如:

used[i - 1] == true,说明同一树枝candidates[i - 1]使用过

used[i - 1] == false,说明同一树层candidates[i - 1]使用过

棋盘问题(二刷重点):

N皇后:棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度

解数独:使用二维递归,本题中棋盘的每一个位置都要放一个数字,并检查数字是否合法,解数独的树形结构要比N皇后更宽更深。

性能分析:

子集问题分析:

时间复杂度:O(2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)

空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)

排列问题分析:

时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。

空间复杂度:O(n),和子集问题同理。

组合问题分析:

时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。

空间复杂度:O(n),和子集问题同理。

N皇后问题分析:

时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n * (n-1) * .... * 1。

空间复杂度:O(n),和子集问题同理。

解数独问题分析:

时间复杂度:O(9^m) , m是'.'的数目。

空间复杂度:O(n^2),递归的深度是n^2

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

推荐阅读更多精彩内容