leetcode(8) | BFS && DFS

广度优先搜索(BFS)

总结:

适用场景

代码模板

具体题目

Word Ladder(mid)

Word Ladder II(hard)

Surrounded Regions

 

深度优先算法

总结

适用场景

思考的步骤

代码模板

深搜与回溯法的区别

深搜与递归的区别

具体题目

Additive Number(hard)

Palindrome Partitioning(mid)

Unique Paths(easy)

Unique Paths II (easy)

N-Queens(hard)

N-Queens II (mid)

Restore IP Addresses(mid)

Combination Sum(mid)

Combination Sum II (mid)

Combination Sum III(easy)

Generate Parentheses(mid)

Word Search

Word Search II(hard,值得注意)

Sudoku Solver(hard)


广度优先搜索(BFS)

总结:

主要参考:https://soulmachine.gitbooks.io/algorithm-essentials/content/java/bfs/bfs-summary.html

适用场景

输入数据:没什么特征,不像深搜,需要有“递归”的性质。如果是树或者图,概率更大。

状态转换图:树或者DAG图。

求解目标:多阶段最优化问题

 

代码模板

(1)递归伪代码:

考虑清楚递归的条件

DFS(dep,、、、)        //dep代表目前DFS的深度
{
    if(找到解 || 走不下去){
        、、、     //在此处进行相应的操作
        return ;
    }
    枚举下一种情况,DFS(dep+1,、、、)
}

(2)非递归

广搜需要一个队列,用于一层一层扩展,一个hashset,用于判重一棵树(只求长度时不需要),用于存储整棵树

1.对于队列,可以用queue,也可以把vector当做队列使用。当求长度时,有两种做法:

  1. 只用一个队列,但在状态结构体state_t里增加一个整数字段level,表示当前所在的层次,当碰到目标状态,直接输出level即可。这个方案,可以很容易的变成A*算法,把queue替换为priority_queue即可。
  2. 用两个队列,current, next,分别表示当前层次和下一层,另设一个全局整数level,表示层数(也即路径长度),当碰到目标状态,输出level即可。这个方案,状态里可以不存路径长度,只需全局设置一个整数level,比较节省内存;

2.对于hashset,如果有完美哈希方案,用布尔数组(bool visited[STATE_MAX]vector visited(STATE_MAX, false))来表示;如果没有,可以用STL里的setunordered_set

3.对于树,如果用STL,可以用unordered_map father表示一颗树,代码非常简洁。如果能够预估状态总数的上限(设为STATE_MAX),可以用数组(state_t nodes[STATE_MAX]),即树的双亲表示法来表示树,效率更高,当然,需要写更多代码。

 

具体题目

Word Ladder(mid)

Given two words (beginWord and endWord), and a dictionary's word list, find the length of shortest transformation sequence from beginWordto endWord, such that:

  1. Only one letter can be changed at a time.
  2. Each transformed word must exist in the word list. Note that beginWord is not a transformed word.

Note:

  • Return 0 if there is no such transformation sequence.
  • All words have the same length.
  • All words contain only lowercase alphabetic characters.
  • You may assume no duplicates in the word list.
  • You may assume beginWord and endWord are non-empty and are not the same.

Example 1:

Input:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]

Output: 5

Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog",
return its length 5.

Example 2:

Input:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]

Output: 0

Explanation: The endWord "cog" is not in wordList, therefore no possible transformation.

思路:

(1)为什么用BFS而不是DFS?

DFS相当于一条路走到黑啊,你走的那条道不一定是最短的啊。而BFS相当于一个小圈慢慢的一层一层扩大,相当于往湖里扔个石头,一圈一圈扩大的水波纹那种感觉,当水波纹碰到湖上的树叶时,那么此时水圈的半径就是圆心到树叶的最短距离

(2)相似单词的查找

为了提高查找效率,我们使用HashSet保存所有的单词(更容易查找相似词在不在wordlist里,如果用vector需要每次都重新遍历)(unordered_map的实现即是一个哈希表),然后对其每个位置上的字符,用26个字母进行替换,如果此时和结尾单词相同了,就可以返回取出词在哈希表中的值加一。如果替换词在字典中存在但在哈希表中不存在,则将替换词排入队列中,并在哈希表中的值映射为之前取出词加一。如果循环完成则返回0

class Solution {
public:
    int ladderLength(string beginWord, string endWord, vector& wordList) {
        // 列表初始化
        unordered_set wordSet(wordList.begin(),wordList.end());
        // endWord不包含,直接返回0
        if(wordSet.count(endWord) == 0) return 0;
        
        queue> q;
        q.push(make_pair(beginWord,1));
        // 注意删除,否则会出现死循环
        wordSet.erase(beginWord);
        while(!q.empty()){
            pair cur = q.front();q.pop();
            if(cur.first == endWord) return cur.second;
            // 找出相似词
            for(int i = 0;i < cur.first.size();i++){
                string str = cur.first;
                for(int j = 0;j < 26;j++){
                    str[i] = 'a'+j;
                    if(wordSet.count(str) == 1){
                        q.push(make_pair(str,cur.second+1));
                        wordSet.erase(str);
                    }
                }
            }
        }
        return 0;
    }
};

这个就是图论算法中的单源最短路, 求单源最短路比较通用的算法是BFS和Dijkstra, 其区别是BFS不能用于带权重的图中, 而后者可以, 可以看出在本题中两个字符串之间是无权重的, 也就是如果连通就是1, 不联通就是无穷. BFS和Dijkstra的区别是前者的时间复杂度是O(n), 后者最多优化到O(m log n), 所以如果条件成立一般选择BFS要更好.

其思路就是先把起点加到队列中, 然后每次将字典中与队首距离为1的字符串加进队列, 直到最后出队列的是终点字符串

而在本题中在寻找与一个字符串相距为1的的字典中另一个字符串时:

(1) 如果一个个遍历字典消耗时间比较多, 每次时间复杂度是O(n).

(2) 在单个字符串不是很长的情况下, 一个个查看改变一个字符然后在字典中查看是否存在效率要更高些, 其时间复杂度是O(k log n), 其中k为单个字符串长度, n为字典长度.

 

Word Ladder II(hard)

Given two words (beginWord and endWord), and a dictionary's word list, find all shortest transformation sequence(s) from beginWord to endWord, such that:

  1. Only one letter can be changed at a time
  2. Each transformed word must exist in the word list. Note that beginWord is not a transformed word.

Note:

  • Return an empty list if there is no such transformation sequence.
  • All words have the same length.
  • All words contain only lowercase alphabetic characters.
  • You may assume no duplicates in the word list.
  • You may assume beginWord and endWord are non-empty and are not the same.

Example 1:

Input:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]

Output:
[
  ["hit","hot","dot","dog","cog"],
  ["hit","hot","lot","log","cog"]
]

Example 2:

Input:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]

Output: []

Explanation: The endWord "cog" is not in wordList, therefore no possible transformation.

思路:使用BFS找到最短路径,使用DFS回溯获得路径

和Word Ladder相比多了要记录路径, 这样我们为每一个结点记录一下父结点, 这样最后的时候我们就从终点开始一个个往前找其父结点, 直到找到起始点, 然后翻转一下加入结果集合中即可.
大概过程差不多, 但是有点不同的是当我们将字典中的一个字符串删除的时候在另一条路径上可能还会用到这个字符. 也就是像这样:

A -> C -> D,    B->C->D

他们都会经过C, 并且两个都是最短的路径, 在A的时候搜索到C, 并且将C从字典中删除, 当B在搜索与其距离为1的字符串时, C已经不在字典中了, 那么怎么办呢? 我们设置一个hash表用来存储一个字符串的父结点集合, 这样C不在字典中再去查hash表看C是否在hash表中, 如果在的话并且C的父结点层次和B一样, 那么就将B也加入到C的父结点结合中去. 可以知道, 一个字符串的父结点集合的距离起点的距离必然是相等的, 也就是说他们都是最短距离.

最后遍历完所有的点之后, 再用DFS从终点往前找出所有集合即可.

class Solution {
public:
    // 保存路径
    vector> res;
    // 字典:一个词:它上一步的词和它上一步词走过路径长
    unordered_map>> hash;
    
    vector> findLadders(string beginWord, string endWord, vector& wordList) {
        unordered_set wordSet(wordList.begin(),wordList.end());
        if(!wordSet.count(endWord)) return res;
        
        wordSet.erase(beginWord);
        queue> q;
        q.push(make_pair(beginWord,1));
        
        while(!q.empty()){
            pair cur = q.front(); q.pop();
            if(cur.first == endWord) break;
            for(int i = 0;i < cur.first.size();i++){
                string str = cur.first;
                for(int j = 0;j < 26;j++){
                    str[i] = 'a'+j;
                    if(wordSet.count(str) == 1){
                        q.push(make_pair(str,cur.second+1));
                        wordSet.erase(str);
                        // 增加保存上一个节点的步骤,使得后面能够回溯
                        hash[str].insert(cur);
                    } 
                    // 这里有另外一种情况,如ABC和DBC,第一次A已经把B消去了
                    // 故要判断是否B已经在hash中,且B的上一个A的路径长与当前路径长一样,一样则把当前节点也加入到B的上一个节点中
                    // 注意这里不需再入队,第一次B已经入队了
                    else if(hash.count(str) && hash[str].begin()->second == cur.second){
                        hash[str].insert(cur);
                    }
                }
            }
        }
        DFS(hash[endWord],vector{endWord});
        return res;
    }
    
    // 使用dfs递归的回溯出路径,这里的vec保存着一条路径,res则是保存所有路径的最终结果
    void DFS(set> cur,vector vec){
        for(auto temp:cur){
            vec.push_back(temp.first);
            // 终止条件
            if(temp.second == 1){
                reverse(vec.begin(),vec.end());
                res.push_back(vec);
            }
            DFS(hash[temp.first],vec);
            vec.pop_back();
        }       
    }
};

 

Surrounded Regions

Given a 2D board containing 'X' and 'O' (the letter O), capture all regions surrounded by 'X'.

A region is captured by flipping all 'O's into 'X's in that surrounded region.

Example:

X X X X
X O O X
X X O X
X O X X

After running your function, the board should be:

X X X X
X X X X
X X X X
X O X X

Explanation:

Surrounded regions shouldn’t be on the border, which means that any 'O' on the border of the board are not flipped to 'X'. Any 'O' that is not on the border and it is not connected to an 'O' on the border will be flipped to 'X'. Two cells are connected if they are adjacent cells connected horizontally or vertically.

思路:这题题目把我唬住了,其实写起来很简单,主要是怎样把这种问题抽象成BFS

(1)扫矩阵的四条边,如果有O,则用DFS遍历,将所有连着的O都变成另一个字符,比如$,这样剩下的O都是被包围的

(2)然后将这些O变成X,把$变回O就行了。

class Solution {
public:
    void solve(vector>& board) {
        if(!board.size()) return;
        
        // 遍历四个边界
        for(int i = 0;i < board[0].size();i++){
            if(board[0][i] == 'O') bfs(board,0,i);
            if(board[board.size()-1][i] == 'O') bfs(board,board.size()-1,i);
        }
        for(int i = 0;i < board.size();i++){
            if(board[i][0] == 'O') bfs(board,i,0);
            if(board[i][board[0].size()-1] == 'O') bfs(board,i,board[0].size()-1);
        }
        // 恢复S
        for(int i = 0;i < board.size();i++){
            for(int j = 0;j < board[0].size();j++){
                if(board[i][j] == 'O') board[i][j] = 'X';
                if(board[i][j] == 'S') board[i][j] = 'O';
            }
        }
    }
    
    void bfs(vector>& board,int i,int j){
        if(i >= board.size() || j >= board[0].size() || i < 0 || j < 0 || board[i][j] != 'O') return;
        board[i][j] = 'S';
        bfs(board,i+1,j);
        bfs(board,i-1,j);
        bfs(board,i,j+1);
        bfs(board,i,j-1);
    }
};

 



深度优先算法

参考(必看):https://soulmachine.gitbooks.io/algorithm-essentials/content/java/dfs/dfs-summary.html

总结

适用场景

使用范围:

(1)要求返回所有符合要求解的答案,十有八九都是要利用到递归

(2)求到底有多少种解

输入数据

如果是递归数据结构,如单链表,二叉树,集合,则百分之百可以用深搜;

如果是非递归数据结构,如一维数组,二维数组,字符串,图,则概率小一些。

状态转换图:树或者DAG。

求解目标:多阶段存在性问题。必须要走到最深(例如对于树,必须要走到叶子节点)才能得到一个解,这种情况适合用深搜。

 

思考的步骤

  1. 是求路径条数,还是路径本身(或动作序列)?深搜最常见的三个问题,求可行解的总数,求一个可行解,求所有可行解。

    1. 如果是路径条数,则不需要存储路径。
    2. 如果是求路径本身,则要用一个数组path[]存储路径。跟宽搜不同,宽搜虽然最终求的也是一条路径,但是需要存储扩展过程中的所有路径,在没找到答案之前所有路径都不能放弃;而深搜,在搜索过程中始终只有一条路径,因此用一个数组就足够了。
  2. 只要求一个解,还是要求所有解?如果只要求一个解,那找到一个就可以返回(如数独问题);如果要求所有解,找到了一个后,还要继续扩展,直到遍历完。广搜一般只要求一个解,因而不需要考虑这个问题(广搜当然也可以求所有解,这时需要扩展到所有叶子节点,相当于在内存中存储整个状态转换图,非常占内存,因此广搜不适合解这类问题)。

  3. 如何表示状态?即一个状态需要存储哪些些必要的数据,才能够完整提供如何扩展到下一步状态的所有信息。跟广搜不同,深搜的惯用写法,不是把数据记录在状态struct里,而是添加函数参数(有时为了节省递归堆栈,用全局变量)struct里的字段与函数参数一一对应。

  4. 如何扩展状态?这一步跟上一步相关。状态里记录的数据不同,扩展方法就不同。对于固定不变的数据结构(一般题目直接给出,作为输入数据),如二叉树,图等,扩展方法很简单,直接往下一层走,对于隐式图,要先在第1步里想清楚状态所带的数据,想清楚了这点,那如何扩展就很简单了。

  5. 终止条件是什么?终止条件是指到了不能扩展的末端节点。对于树,是叶子节点,对于图或隐式图,是出度为0的节点。

  6. 收敛条件是什么?收敛条件是指找到了一个合法解的时刻。如果是正向深搜(父状态处理完了才进行递归,即父状态不依赖子状态,递归语句一定是在最后,尾递归),则是指是否达到目标状态;如果是逆向深搜(处理父状态时需要先知道子状态的结果,此时递归语句不在最后),则是指是否到达初始状态。

由于很多时候终止条件和收敛条件是是合二为一的,因此很多人不区分这两种条件。仔细区分这两种条件,还是很有必要的。

 

为了判断是否到了收敛条件,要在函数接口里用一个参数记录当前的位置(或距离目标还有多远)。如果是求一个解,直接返回这个解;如果是求所有解,要在这里收集解,即把第一步中表示路径的数组path[]复制到解集合里。}

  1. 关于判重

    1. 是否需要判重?如果状态转换图是一棵树,则不需要判重,因为在遍历过程中不可能重复;如果状态转换图是一个DAG,则需要判重。这一点跟BFS不一样,BFS的状态转换图总是DAG,必须要判重。
    2. 怎样判重?跟广搜相同,见第 ??? 节。同时,DAG说明存在重叠子问题,此时可以用缓存加速,见第8步。 
  2. 如何加速?

    1. 剪枝。深搜一定要好好考虑怎么剪枝,成本小收益大,加几行代码,就能大大加速。这里没有通用方法,只能具体问题具体分析,要充分观察,充分利用各种信息来剪枝,在中间节点提前返回。
    2. 缓存。

      1. 前提条件:状态转换图是一个DAG。DAG=>存在重叠子问题=>子问题的解会被重复利用(如下面的字典树),用缓存自然会有加速效果。如果依赖关系是树状的(例如树,单链表等),没必要加缓存,因为子问题只会一层层往下,用一次就再也不会用到,加了缓存也没什么加速效果。
      2. 具体实现:可以用数组或HashMap。维度简单的,用数组;维度复杂的,用HashMap,C++有map,C++ 11以后有unordered_map,比map快。

拿到一个题目,当感觉它适合用深搜解决时,在心里面把上面8个问题默默回答一遍,代码基本上就能写出来了。对于树,不需要回答第5和第8个问题。如果读者对上面的经验总结看不懂或感觉“不实用”,很正常,因为这些经验总结是我做了很多题目后总结出来的,从思维的发展过程看,“经验总结”要晚于感性认识,所以这时候建议读者先做做前面的题目,积累一定的感性认识后,再回过头来看这一节的总结,一定会有共鸣。

 

代码模板

/**
 * dfs模板.
 * @param[in] input 输入数据指针
 * @param[out] path 当前路径,也是中间结果
 * @param[out] result 存放最终结果
 * @param[inout] cur or gap 标记当前位置或距离目标的距离
 * @return 路径长度,如果是求路径本身,则不需要返回长度
 */
void dfs(type &input, type &path, type &result, int cur or gap) {
    if (数据非法) return 0;   // 终止条件
    if (cur == input.size()) { // 收敛条件
    // if (gap == 0) {
        将path放入result
    }

    if (可以剪枝) return;

    for(...) { // 执行所有可能的扩展动作
        执行动作,修改path
        dfs(input, step + 1 or gap--, result);
        恢复path
    }
}

 

深搜与回溯法的区别

深搜(Depth-first search, DFS)的定义见 http://en.wikipedia.org/wiki/Depth_first_search,回溯法(backtracking)的定义见 http://en.wikipedia.org/wiki/Backtracking

回溯法 = 深搜 + 剪枝。一般大家用深搜时,或多或少会剪枝,因此深搜与回溯法没有什么不同,可以在它们之间画上一个等号。本书同时使用深搜和回溯法两个术语,但读者可以认为二者等价。

深搜一般用递归(recursion)来实现,这样比较简洁。

深搜能够在候选答案生成到一半时,就进行判断,抛弃不满足要求的答案,所以深搜比暴力搜索法要快。

 

深搜与递归的区别

深搜经常用递归(recursion)来实现,二者常常同时出现,导致很多人误以为他俩是一个东西。

深搜,是逻辑意义上的算法,递归,是一种物理意义上的实现,它和迭代(iteration)是对应的。深搜,可以用递归来实现,也可以用栈来实现;而递归,一般总是用来实现深搜。可以说,递归一定是深搜,深搜不一定用递归

递归有两种加速策略,一种是 剪枝(prunning),对中间结果进行判断,提前返回;一种是缓存,缓存中间结果,防止重复计算,用空间换时间。

其实,递归+缓存,就是 memorization。所谓memorization(翻译为备忘录法,见第 ??? 节,就是"top-down with cache"(自顶向下+缓存),它是Donald Michie 在1968年创造的术语,表示一种优化技术,在top-down 形式的程序中,使用缓存来避免重复计算,从而达到加速的目的。

memorization 不一定用递归,就像深搜不一定用递归一样,可以在迭代(iterative)中使用 memorization 。递归也不一定用 memorization,可以用memorization来加速,但不是必须的。只有当递归使用了缓存,它才是 memorization 。

既然递归一定是深搜,为什么很多书籍都同时使用这两个术语呢?在递归味道更浓的地方,一般用递归这个术语,在深搜更浓的场景下,用深搜这个术语,读者心里要弄清楚他俩大部分时候是一回事。在单链表、二叉树等递归数据结构上,递归的味道更浓,这时用递归这个术语;在图、隐式图等数据结构上,深搜的味道更浓,这时用深搜这个术语。

 

具体题目

Additive Number(hard)

Additive number is a string whose digits can form additive sequence.

A valid additive sequence should contain at least three numbers. Except for the first two numbers, each subsequent number in the sequence must be the sum of the preceding two.

Given a string containing only digits '0'-'9', write a function to determine if it's an additive number.

Note: Numbers in the additive sequence cannot have leading zeros, so sequence 1, 2, 03 or 1, 02, 3 is invalid.

Example 1:

Input: "112358"
Output: true 
Explanation: The digits can form an additive sequence: 1, 1, 2, 3, 5, 8. 
1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8

Example 2:

Input: "199100199"
Output: true 
Explanation: The additive sequence is: 1, 99, 100, 199. 1 + 99 = 100, 99 + 100 = 199

Follow up:
How would you handle overflow for very large input integers?

思路:

(1)一个基本的思想就是确定了第一个和第二个数之后, 以后的数就是验证了, 所以只要枚举第一和第二个数, 然后不断验证下面的字符串子串是否是前两个数的和即可.

(2)因为数字可能超出整数的范围, 因此在验证一个数是否是另外两个和的时候, 可以用字符串相加来模拟整数相加.

(3)另外需要注意的是'0'字符,可以单独存在,不能一刀切break,否则101这种情况会无法判断

class Solution {
public:
    bool isAdditiveNumber(string num) {
        if(num.size() < 3) return false;
        for(int i = 1;i <= num.size()-2;i++){
            // substr(start_idx,len)
            string num1 = num.substr(0,i);
            // 注意这里num1.size()>1需要判断,否则101这种情况会出错
            if(num1.size()>1 && num1[0] == '0') break;
            for(int j = i+1;j <= num.size()-1;j++){
                string num2 = num.substr(i,j-i);
                if(num2.size()>1 && num2[0] == '0') break;
                递归往下
                if(dfs(num,num1,num2,j)) return true;
            }
        }
        return false;
    }
    
    bool dfs(string num,string num1,string num2,int start){
        if(start >= num.size()) return true;
        string sum = getSum(num1,num2);
        // cout << sum << endl;
        if(num.substr(start,sum.size()) == sum && dfs(num,num2,sum,start+sum.size())) return true;
        return false;
    }
    
    // 可处理任意长度字符串相加
    string getSum(string &s1,string &s2){
        string res;
        int i = s1.size()-1,j = s2.size()-1;
        int flag = 0,sum;
        while(i >= 0 || j >= 0){
            sum = 0;
            if(i >= 0) sum += s1[i--] - '0';
            if(j >= 0) sum += s2[j--] - '0';
            sum += flag;
            res += to_string(sum%10);
            flag = sum/10;
        }
        // 不要遗漏最后这里的加,否则100会变成00
        if(flag) res += to_string(flag);
        reverse(res.begin(),res.end());
        return res;
    }
};

 

Palindrome Partitioning(mid)

Given a string s, partition s such that every substring of the partition is a palindrome.

Return all possible palindrome partitioning of s.

Example:

Input: "aab"
Output:
[
  ["aa","b"],
  ["a","a","b"]
]

思路:递归

这题比上一题简单很多,也是一个dfs即可,但是这样会比较慢

class Solution {
public:
    vector> res;
    vector> partition(string s) {
        vector vec;
        dfs(s.substr(0,s.size()),vec);
        return res;
    }
    
    void dfs(string s,vector &vec){
        if(s.empty()) res.push_back(vec);
        for(int i = 1;i <= s.size();i++){
            string sub_s = s.substr(0,i);
            if(isPalindrome(sub_s)) {
                vec.push_back(sub_s);
                dfs(s.substr(i,s.size()-i),vec);
                vec.pop_back();
            }
        }
    }
    
    bool isPalindrome(string s){
        int i = 0,j = s.size()-1;
        while(i < j){
            if(s[i++] != s[j--]) return false;
        }
        return true;
    }
};

思路二:动态规划

上面的方法中,每次都要对字串判断是否为回文串,在递归过程中,其实会有很多重复判断的时候,所有可以采用动态规划去优化,先把回文字串的结果保存下来。谁能想到,我这样写完速度反而更慢了,令人费解。。。

我们可以先建立好字符串s的子串回文的dp数组,光这一部分就可以另出一个道题了 Palindromic Substrings,当我们建立好这样一个二维数组dp,其中 dp[i][j] 表示 [i, j] 范围内的子串是否为回文串,这样就不需要另外的子函数去判断子串是否为回文串了,大大的提高了计算的效率。递归部分与思路一相同

class Solution {
public:
    vector> res;
    vector> partition(string s) {
        if(s.empty()) return res;
        
        int n = s.size();
        vector> dp(n,vector(n));
        for(int i = 0;i < s.size();i++){
            for(int j = 0;j <= i;j++){
                // 首尾相同,且两者相差小于3或者两者中间的字串已经为回文,此时j到i的字串为回文
                if(s[i] == s[j] && (i-j <=2 || dp[j+1][i-1])){
                    dp[j][i] = true;
                }
            }
        }
        vector vec;
        dfs(s,0,dp,vec);
        return res;
    }
    
    void dfs(string s,int start,vector> dp,vector &vec){
        if(start >= s.size()) {res.push_back(vec); return;}
        for(int i = start;i < s.size();i++){
            if(dp[start][i]) {
                string sub_s = s.substr(start,i-start+1);
                vec.push_back(sub_s);
                dfs(s,i+1,dp,vec);
                vec.pop_back();
            }
        }
    }
};

 

Unique Paths(easy)

A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).

How many possible unique paths are there?


Above is a 7 x 3 grid. How many possible unique paths are there?

Note: m and n will be at most 100.

Example 1:

Input: m = 3, n = 2
Output: 3
Explanation:
From the top-left corner, there are a total of 3 ways to reach the bottom-right corner:
1. Right -> Right -> Down
2. Right -> Down -> Right
3. Down -> Right -> Right

Example 2:

Input: m = 7, n = 3
Output: 28

思路一:递归(超时了)

比较容易想到用递归,到达某一格的路径数量等于它的上面和左边的路径数之和,结束条件是走到行或者列的边缘。因为每条路径都会重新探索,时间复杂度是结果数量的量级,不是多项式的复杂度。

class Solution {
public:
    int count = 0;
    int uniquePaths(int m, int n) {
        if(!m || !n) return 0;
        dfs(0,0,m-1,n-1);
        return count;
    }
    
    void dfs(int i,int j,int m,int n){
        if(i == m && j == n) {count++; return;}
        if(i < m) dfs(i+1,j,m,n);
        if(j < n) dfs(i,j+1,m,n);
    }
};

思路二:动态规划(速度很快)

class Solution {
public:
    int count = 0;
    int uniquePaths(int m, int n) {
        if(!m || !n) return 0;
        vector> dp(m,vector(n));
        dp[0][0] = 1;
        for(int i = 0;i < m;i++){
            for(int j = 0;j < n;j++){
                if(i > 0) dp[i][j] += dp[i-1][j];
                if(j > 0) dp[i][j] += dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
};

对于空间可以看出我们每次只需要用到上一行当前列,以及前一列当前行的信息,我们只需要用一个一维数组存上一行的信息即可,然后扫过来依次更替掉上一行对应列的信息即可(因为所需要用到的信息都还没被更替掉),所以空间复杂度是O(n)(其实如果要更加严谨,我们可以去行和列中小的那个,然后把小的放在内层循环,这样空间复杂度就是O(min(m,n))

class Solution {
public:
    int uniquePaths(int m, int n) {
        if(!m || !n) return 0;
        vector dp(n);
        dp[0] = 1;
        for(int i = 0;i < m;i++){
            for(int j = 1;j < n;j++){
                dp[j] += dp[j-1];
            }
        }
        return dp[n-1];
    }
};

 

Unique Paths II (easy)

A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).

Now consider if some obstacles are added to the grids. How many unique paths would there be?

An obstacle and empty space is marked as 1 and 0 respectively in the grid.

Note: m and n will be at most 100.

Example 1:

Input:
[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]
Output: 2
Explanation:
There is one obstacle in the middle of the 3x3 grid above.
There are two ways to reach the bottom-right corner:
1. Right -> Right -> Down -> Down
2. Down -> Down -> Right -> Right
class Solution {
public:
    long long uniquePathsWithObstacles(vector>& obstacleGrid) {
        if(obstacleGrid.empty() || obstacleGrid[0].empty() ||obstacleGrid[0][0]) return 0;
        int m = obstacleGrid.size(),n = obstacleGrid[0].size();
        vector dp(n);
        dp[0] = 1;
        for(int i = 0;i < m;i++){
            for(int j = 0;j < n;j++){
                if(obstacleGrid[i][j] == 1) dp[j] = 0;
                else if(j > 0) dp[j] += dp[j-1];
            }
        }
        return dp[n-1];
    }
};

 

N-Queens(hard)

The n-queens puzzle is the problem of placing n queens on an n×n chessboard such that no two queens attack each other.

Given an integer n, return all distinct solutions to the n-queens puzzle.

Each solution contains a distinct board configuration of the n-queens' placement, where 'Q' and '.' both indicate a queen and an empty space respectively.

Example:

Input: 4
Output: [
 [".Q..",  // Solution 1
  "...Q",
  "Q...",
  "..Q."],

 ["..Q.",  // Solution 2
  "Q...",
  "...Q",
  ".Q.."]
]
Explanation: There exist two distinct solutions to the 4-queens puzzle as shown above.

参考:https://www.cnblogs.com/TenosDoIt/p/3801621.html

解法一:递归dfs

这种棋盘类的题目一般是回溯法, 依次放置每行的皇后。在放置的时候,要保持当前的状态为合法,即当前放置位置的同一行、同一列、两条对角线上都不存在皇后。

这个解法速度太慢

class Solution {
public:
    vector> res;
    vector> solveNQueens(int n) {
        if(n == 0) return res;
        vector vec(n,string(n,'.'));
        dfs(vec,0);
        return res;
    }
    
    // 按行放置queen
    void dfs(vector vec,int row){
        if(row >= vec.size()) {res.push_back(vec); return;}
        for(int col = 0;col < vec[0].size();col++){
            if(isValid(vec,row,col)){
                vec[row][col] = 'Q';
                dfs(vec,row+1);
                vec[row][col] = '.';   
            }
        }
    }
    
    // 判断vec[row][col]放置皇后是否合法
    bool isValid(vector vec,int row,int col){
        // 检查列
        for(int i = 0;i < row;i++){
            if(vec[i][col] == 'Q') return false;
        }
        // 检查左对角线上半部分,因为后面的行还没有开始放置
        for(int i = row-1,j = col-1;i >= 0 && j >=0;i--,j--){
            if(vec[i][j] == 'Q') return false;
        }
        // 检查右对角线上半部分,因为后面的行还没有开始放置
        for(int i = row-1,j = col+1;i >= 0 && j <= vec[0].size();i--,j++){
            if(vec[i][j] == 'Q') return false;
        }
        return true;
    }
};

解法二:递归dfs,使用一维数组作为标记

其实只需要用一个一位数组来存放当前皇后的状态。假设数组为int state[n], state[i]表示第 i 行皇后所在的列。那么在新的一行 k 放置一个皇后后:

  • 判断列是否冲突,只需要看state数组中state[0…k-1] 是否有和state[k]相等;
  • 判断对角线是否冲突:如果两个皇后在同一对角线,那么|row1-row2| = |column1 - column2|,(row1,column1),(row2,column2)分别为冲突的两个皇后的位置

这样速度会比解法1快一半

class Solution {
public:
    vector> res;
    vector> solveNQueens(int n) {
        if(n == 0) return res;
        vector state(n,-1);
        dfs(state,0);
        return res;
    }
    
    // 按行放置queen
    void dfs(vector state,int row){
        int n = state.size();
        if(row >= n){
            vector vec(n,string(n,'.'));
            for(int i = 0;i < n;i++){
                vec[i][state[i]] = 'Q';
            }
            res.push_back(vec);
            return;
        }
        for(int col = 0;col < n;col++){
            if(isValid(state,row,col)){
                state[row] = col;
                dfs(state,row+1);
                state[row] = -1;   
            }
        }
    }
    
    // 判断vec[row][col]放置皇后是否合法
    bool isValid(vector state,int row,int col){
        // 检查列,检查左右对角线上半部分,因为后面的行还没有开始放置
        for(int i = 0;i < row;i++){
            if(state[i] == col || abs(i-row) == abs(state[i]-col)) return false;
        }
        return true;
    }
};

解法三:解法二的非递归版本

class Solution {
public:
    vector> res;
    vector> solveNQueens(int n) {
        if(n == 0) return res;
        vector state(n,-1);
        // 这里注意,col必须提前定义!!!因为for循环以后,要根据col的值去判断是否需要回溯
        for(int row = 0,col; ; ){
            // 从上一次放置的位置后面开始寻找合法位置
            for(col = state[row]+1;col < n;col++){
                // 找到合法位置
                if(isValid(state,row,col)){
                    state[row] = col;
                    // 已经找到一个解
                    if(row == n-1){
                        vector vec = vector(n,string(n,'.'));
                        for(int i = 0;i < n;i++){
                            vec[i][state[i]] = 'Q';
                        }
                        res.push_back(vec);
                    }
                    // 继续寻找下一行的合法位置
                    else {row++; break;}
                }
            }
            // 当前行没有合法位置(因为上一行位置其实不合法),回溯上一行
            if(col == n){
                // 所有状态遍历完成,退出循环
                if(row == 0) break;
                // 否则清除当前行状态,回到上一行
                state[row] = -1;
                row--;
            }
        }
        return res;
    }
    
    
    // 判断vec[row][col]放置皇后是否合法
    bool isValid(vector state,int row,int col){
        // 检查列,检查左右对角线上半部分,因为后面的行还没有开始放置
        for(int i = 0;i < row;i++){
            if(state[i] == col || abs(i-row) == abs(state[i]-col)) return false;
        }
        return true;
    }
};

 

N-Queens II (mid)

上一题的简化版本,只需返回数量,不需返回具体排列情况

class Solution {
public:
    int count = 0;
    int totalNQueens(int n) {
        if(n == 0) return 0;
        
        vector state(n,-1);
        for(int row = 0,col;;){
            for(col = state[row]+1;col < n;col++){
                if(isValid(state,row,col)){
                    state[row] = col;
                    if(row == n-1) {count++;}
                    else {row++;break;}
                }
            }
            if(col == n){
                if(row == 0) break;
                state[row] = -1;
                row--;
            }  
        }
        return count;
    }
    
    bool isValid(vector state,int row,int col){
        for(int i = 0;i < row;i++){
            if(state[i] == col || (abs(i-row) == abs(state[i] - col))) return false;
        }
        return true;
    }
};

 

Restore IP Addresses(mid)

Given a string containing only digits, restore it by returning all possible valid IP address combinations.

Example:

Input: "25525511135"
Output: ["255.255.11.135", "255.255.111.35"]

思路:使用DFS递归完成

class Solution {
public:
    vector res;
    vector restoreIpAddresses(string s) {
        if(s.empty()) return res;
        string out;
        dfs(s,0,out,0);
        return res;
    }
    
    void dfs(string s,int start,string out,int out_size){
        if(start >= s.size() && out_size == 4) res.push_back(out.substr(1,out.size()-1));
        if(out_size > 4) return;
        
        for(int i = 1;i <= 3 && start+i-1 < s.size();i++){
            string sub_str = s.substr(start,i);
            if(isValid(sub_str)){
                out = out + "." + sub_str;
                dfs(s,start+i,out,out_size+1);
            } 
            
            out = out.substr(0,out.size()-sub_str.size()-1);
        }
    }
    
    bool isValid(string str){
        //010这种为非法
        if(str.size() > 1 && str[0] == '0') return false;
        
        int num = stoi(str);
        if(num < 0 || num > 255) return false;
        return true;
    }
};

 

Combination Sum(mid)

Given a set of candidate numbers (candidates(without duplicates) and a target number (target), find all unique combinations in candidates where the candidate numbers sums to target.

The same repeated number may be chosen from candidates unlimited number of times.

Note:

  • All numbers (including target) will be positive integers.
  • The solution set must not contain duplicate combinations.

Example 1:

Input: candidates = [2,3,6,7], target = 7,A solution set is:
[
  [7],
  [2,2,3]
]

Example 2:

Input: candidates = [2,3,5], target = 8,A solution set is:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

思路:像这种结果要求返回所有符合要求解的题十有八九都是要利用到递归,而且解题的思路都大同小异,需要另写一个递归函数

class Solution {
public:
    vector> res;
    vector> combinationSum(vector& candidates, int target) {
        if(candidates.empty()) return res;
        sort(candidates.begin(),candidates.end());
        vector out;
        dfs(candidates,0,out,target);
        return res;
    }
    
    void dfs(vector& candidates,int start,vector out,int target){
        if(target == 0) res.push_back(out);
        if(target < candidates[start]) return;
        for(int i = start;i < candidates.size();i++){
            out.push_back(candidates[i]);
            dfs(candidates,i,out,target-candidates[i]);
            out.pop_back();
        }
    }
};

解法二:动态规划

建立一个三维数组dp,这里dp[i]表示目标数为i的所有解法集合

这里的i就从1遍历到target即可,对于每个i,我们都新建一个二维数组cur,然后遍历candidates数组,如果遍历到的数字大于i,说明当前及之后的数字都无法组成i,直接break掉。否则如果相等,那么把当前数字自己组成一个数组,并且加到cur中。否则就遍历dp[i - candidates[j] - 1] 中的所有数组,如果当前数字大于数组的首元素,则跳过,因为我们的结果要求是要有序的。否则就将当前数字加入数组的开头,并且将数组放入cur之中即可

class Solution {
public:
    vector> res;
    vector> combinationSum(vector& candidates, int target) {
        if(candidates.empty()) return res;
        sort(candidates.begin(),candidates.end());
        vector>> dp;
        // target从1到目标,dp就是把每个前面的结果存下来,后面直接调用
        for(int i = 1;i <= target;i++){
            vector> cur;
            for(int j = 0;j < candidates.size();j++){
                if(i < candidates[j]) break;
                // 相等直接push vector {i}即可
                if(i == candidates[j]) {cur.push_back({i});break;}
                // 否则调用前面的结果
                for(auto can : dp[i-candidates[j]-1]){
                    // 如果当前数字大于数组的首元素,则跳过,因为我们的结果要求是要有序的
                    // 否则会出现223,322这种重复情况
                    if(can[0] < candidates[j]) continue;
                    can.insert(can.begin(),candidates[j]);
                    cur.push_back(can);
                }
            }
            dp.push_back(cur);
        }
        return dp[target-1];
    }
};

 

Combination Sum II (mid)

Given a collection of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sums to target.

Each number in candidates may only be used once in the combination.

Note:

  • All numbers (including target) will be positive integers.
  • The solution set must not contain duplicate combinations.

Example 1:

Input: candidates = [10,1,2,7,6,1,5], target = 8,
A solution set is:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]

Example 2:

Input: candidates = [2,5,2,1,2], target = 5,
A solution set is:
[
  [1,2,2],
  [5]
]

思路:这题比上一题难一点,之前那道题给定数组中的数字可以重复使用,而这道题不能重复使用,只需要在之前的基础上修改两个地方即可

(1)首先在递归的for循环里加上if (i > start && candidates[i] ==candidates[i - 1]) continue; 这样可以防止res中出现重复项注意这里i > start才continue,等于start的无论重复不重复,在这层里都是还没重复的,不能丢掉!!!!

(2)然后就在递归调用combinationSum2DFS里面的参数换成 i+1,这样就不会重复使用数组中的数字

class Solution {
public:
    vector> res;
    vector> combinationSum2(vector& candidates, int target) {
        if(candidates.empty()) return res;
        sort(candidates.begin(),candidates.end());
        vector out;
        dfs(candidates,0,out,target);
        return res;
    }
    
    void dfs(vector& candidates,int start,vector out,int target){
        if(target == 0) res.push_back(out);
        if(start >= candidates.size() || target < candidates[start]) return;
        for(int i = start;i < candidates.size();i++){
            // 注意这个判断,i == start的时候,是需要执行的,接下来如果出现重复,在这一层里不进循环
            if(i > start && candidates[i] == candidates[i-1]) continue;
            out.push_back(candidates[i]);
            dfs(candidates,i+1,out,target-candidates[i]);
            out.pop_back();
            
        }
    }
};

 

Combination Sum III(easy)

Find all possible combinations of k numbers that add up to a number n, given that only numbers from 1 to 9 can be used and each combination should be a unique set of numbers.

Note:

  • All numbers will be positive integers.
  • The solution set must not contain duplicate combinations.

Example 1:

Input: k = 3, n = 7
Output: [[1,2,4]]

Example 2:

Input: k = 3, n = 9
Output: [[1,2,6], [1,3,5], [2,3,4]]

思路:简单递归

class Solution {
public:
    vector> res;
    vector> combinationSum3(int k, int n) {
        if(k == 0 || n == 0 || k > n) return res;
        vector cur;
        dfs(k,n,1,cur);
        return res;
    }
    
    void dfs(int k,int target,int start,vector cur){
        if(target == 0 && k == 0) {res.push_back(cur); return;}
        for(int i = start;i <= 9 && i <= target;i++){
            cur.push_back(i);
            dfs(k-1,target - i,i+1,cur);
            cur.pop_back();
        }
    }
};

 

Generate Parentheses(mid)

Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.

For example, given n = 3, a solution set is:

[
  "((()))",
  "(()())",
  "(())()",
  "()(())",
  "()()()"
]

思路:这题难度不大,考虑清楚边界条件进行递归即可。

class Solution {
public:
    vector res;
    vector generateParenthesis(int n) {
        if(n == 0) return res;
        
        string out;
        dfs(out,n,0,0);
        return res;
    }
    
    void dfs(string out,int n,int first_num,int second_num){
        if(first_num == n){
            while(second_num != n) {out += ')'; second_num++;}
            res.push_back(out);
            return;
        }
        if(first_num >= second_num){
            out += '(';
            dfs(out,n,first_num+1,second_num);
        }
        if(first_num > second_num){
            out = out.substr(0,out.size()-1);
            out += ')';
            dfs(out,n,first_num,second_num+1);
        }
    }
};

 

Word Search

Given a 2D board and a word, find if the word exists in the grid.

The word can be constructed from letters of sequentially adjacent cell, where "adjacent" cells are those horizontally or vertically neighboring. The same letter cell may not be used more than once.

Example:

board =
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

Given word = "ABCCED", return true.
Given word = "SEE", return true.
Given word = "ABCB", return false.

这里有两个坑:

(1)注意['a', 'a'],"aaa"的情况,会重复使用,故这里用 $ 去标记已使用的字符,防止产生重复

(2)&board,直接在board上修改,否则会新建副本,引起内存超标

(3)word前不加&,否则第一轮结束后,由于取子串,word无法恢复成原始状态,这样会出现问题

class Solution {
public:
    bool exist(vector>& board, string word) {
        if(board.empty()) return word.empty();
        for(int i = 0;i < board.size();i++){
            for(int j = 0;j < board[0].size();j++){
                if(dfs(board,word,i,j)) return true;
            }
        }
        return false;
    }
    
    bool dfs(vector> &board, string word, int i,int j){
        if(word.empty()) return true;
        int row = board.size()-1,col = board[row].size()-1;
        if(i < 0 || j < 0 || i > row || j > col || word[0] != board[i][j]) return false;
        
        char target = word[0];
        word = word.substr(1,word.size()-1);
        // 防止重复使用
        board[i][j] = '$';
        if(dfs(board,word,i-1,j)) return true;
        if(dfs(board,word,i+1,j)) return true;
        if(dfs(board,word,i,j-1)) return true;
        if(dfs(board,word,i,j+1)) return true;
        // 还原
        board[i][j] = target;
        return false;
    }
};

 

Word Search II(hard,值得注意)

Given a 2D board and a list of words from the dictionary, find all words in the board.

Each word must be constructed from letters of sequentially adjacent cell, where "adjacent" cells are those horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.

Example:

Input: 
words = ["oath","pea","eat","rain"] and board =
[
  ['o','a','a','n'],
  ['e','t','a','e'],
  ['i','h','k','r'],
  ['i','f','l','v']
]

Output: ["eat","oath"]

 

思路:我最开始是直接通过暴力调用上一题,对每个word进行判断,但是这样会引起时间非常的长。这里其实有个trick,当ab找不到时,abc也找不到,应该进行剪枝。这里通过构建字典树(前缀树)来进行剪枝,字典树如下:

一个保存了8个键的trie结构,"A", "to", "tea", "ted", "ten", "i", "in", and "inn".如下图所示:

leetcode(8) | BFS && DFS_第1张图片

字典树主要有如下三点性质:

1. 根节点不包含字符,除根节点意外每个节点只包含一个字符。

2. 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串

3. 每个节点的所有子节点包含的字符串不相同。

 

字母树的插入(Insert)、删除( Delete)和查找(Find)都非常简单,用一个一重循环即可,即第i 次循环找到前i 个字母所对应的子树,然后进行相应的操作。实现这棵字母树,我们用最常见的数组保存(静态开辟内存)即可,当然也可以开动态的指针类型(动态开辟内存)。至于结点对儿子的指向,一般有三种方法:

1、对每个结点开一个字母集大小的数组,对应的下标是儿子所表示的字母,内容则是这个儿子对应在大数组上的位置,即标号;

2、对每个结点挂一个链表,按一定顺序记录每个儿子是谁;

3、使用左儿子右兄弟表示法记录这棵树。

三种方法,各有特点。第一种易实现,但实际的空间要求较大;第二种,较易实现,空间要求相对较小,但比较费时;第三种,空间要求最小,但相对费时且不易写。下面使用的是第一种写法,我们在这题中只要实现字典树中的insert功能就行了,查找单词和前缀就没有必要了:

struct treeNode{
    string word;
    // 这里是指针数组
    treeNode *child[26];
    treeNode(){
        word = "";
        for(auto &a:child) a = nullptr;
    }
};

// 建立字典树
class Tree{
public:
    treeNode *root;

    Tree(){
        root = new treeNode();
    }

    void insert(string s){
        treeNode *p = root;
        for(auto &a:s){
            int i = a-'a';
            if(p->child[i] == nullptr) p->child[i] = new treeNode();
            p = p->child[i];
        }
        p->word = s;
    }
};


class Solution {
public:
    vector res;
    vector findWords(vector>& board, vector& words) {
        if(board.empty() || words.empty() || board[0].empty()) return res;
        Tree T;
        for(auto &word:words) T.insert(word);
        for(int i = 0;i < board.size();i++){
            for(int j = 0;j < board[i].size();j++){
                if(T.root->child[board[i][j]-'a']) dfs(board,T.root,i,j);
            }
        }
        return res;
    }

    void dfs(vector> &board, treeNode *p,int i,int j){
        auto row = board.size()-1,col = board[0].size()-1;

        // 这里增加board[i][j]=='$'的判断,否则会出现p->child[board[i][j]-'a']非法
        if(i < 0 || j < 0 || i > row || j > col || board[i][j]=='$' ||!p->child[board[i][j]-'a']) return;

        p = p->child[board[i][j]-'a'];
        if(!p->word.empty()) {
            res.push_back(p->word);
            // 这里注意清空,否则board = ['a','a'], words = ["a"]的情况会出现重复的两次加入"a"
            p->word = "";
        }

        // 防止重复使用
        char target = board[i][j];
        board[i][j] = '$';
        dfs(board,p,i-1,j);
        dfs(board,p,i+1,j);
        dfs(board,p,i,j-1);
        dfs(board,p,i,j+1);
        // 还原
        board[i][j] = target;
    }
};

 

Sudoku Solver(hard)

Write a program to solve a Sudoku puzzle by filling the empty cells.

A sudoku solution must satisfy all of the following rules:

  1. Each of the digits 1-9 must occur exactly once in each row.
  2. Each of the digits 1-9 must occur exactly once in each column.
  3. Each of the the digits 1-9 must occur exactly once in each of the 9 3x3 sub-boxes of the grid.

Empty cells are indicated by the character '.'.


A sudoku puzzle...

leetcode(8) | BFS && DFS_第2张图片
...and its solution numbers marked in red.

Note:

  • The given board contain only digits 1-9 and the character '.'.
  • You may assume that the given Sudoku puzzle will have a single unique solution.
  • The given board size is always 9x9.

思路:

这题跟N-Queens是一个套路,回溯法尝试所有解。

需要注意的区别是:

本题找到解的处理是return true,因此返回值为bool

N-Queen找到解的处理是保存解,因此返回值为void

 

按行进行遍历,一行结束换下一行

对于每个空位'.',遍历1~9,check合理之后往下一个位置递归。

由于这里路径尝试本质上是有序的,即1~9逐个尝试,因此无需额外设置状态位记录已经尝试过的方向。

注意:只有正确达到最终81位置(即成功填充)的填充结果才可以返回,若不然,将会得到错误的填充。

因此辅助函数solve需要设为bool而不是void

class Solution {
public:
    void solveSudoku(vector>& board) {
        dfs(board,0,0);
    }
    
    // 有返回值,找到正解立即返回,不需要保存解然后继续搜索新的解
    bool dfs(vector>& board,int row,int col){
        if(row == 9) return true;
        if(col >= 9) return dfs(board,row+1,0);
        if(board[row][col] != '.') return dfs(board,row,col+1);
        else{
            for(int i = 1;i <= 9;i++){
                board[row][col] = (char)(i+'0');
                if(isValid(board,row,col)){
                    if(dfs(board,row,col+1)) return true;
                }
                board[row][col] = '.';
            }
        }
        return false;
    }
    
    bool isValid(vector>& board,int row,int col){
        for(int i = 0;i < 9;i++){
            if(i!= row && board[i][col] == board[row][col]) return false;
        }
        for(int i = 0;i < 9;i++){
            if(i!= col && board[row][i] == board[row][col]) return false;
        }
        for(int i = (row/3)*3;i < (row/3)*3+3;i++){
            for(int j = (col/3)*3;j < (col/3)*3+3;j++){
                if((i!= row && j!= col) && board[i][j] == board[row][col]) return false;
            }
        }
        return true;
    }
};

 

dfs用于染色问题:886. 可能的二分法

给定一组 N 人(编号为 1, 2, ..., N), 我们想把每个人分进任意大小的两组。

每个人都可能不喜欢其他人,那么他们不应该属于同一组。

形式上,如果 dislikes[i] = [a, b],表示不允许将编号为 a 和 b 的人归入同一组。

当可以用这种方法将每个人分进两组时,返回 true;否则返回 false。

示例 1:

输入:N = 4, dislikes = [[1,2],[1,3],[2,4]]
输出:true
解释:group1 [1,4], group2 [2,3]
示例 2:

输入:N = 3, dislikes = [[1,2],[1,3],[2,3]]
输出:false
示例 3:

输入:N = 5, dislikes = [[1,2],[2,3],[3,4],[4,5],[1,5]]
输出:false

思路:

题目可以抽象成用两种颜色,给图染色,这里的图是有好几个散着的图组成的

用dfs去给图染色

class Solution {
public:
    bool possibleBipartition(int N, vector>& dislikes) {
        if(dislikes.empty()) return true;
        graph = vector>(N);
        for(auto d:dislikes){
            graph[d[0]-1].push_back(d[1]-1);
            graph[d[1]-1].push_back(d[0]-1);
        }
        colors = vector(N,0);
        for(int i = 0;i < N;i++){
            // 0:没有被访问过,-1和1是两种着色
            // 如果没有被访问过,且dfs判断结果为false,则不能染色
            // 这里初始设置1或者-1,其实都行,因为dfs会把一整个连通图给走完,所以这里入口其实只初始化了起点的颜色
            if(colors[i] == 0 && !dfs(i,1)) return false;
        }
        return true;
    }

private:
    vector> graph;
    vector colors;
    bool dfs(int cur,int cur_color){
        colors[cur] = cur_color;
        for(auto next:graph[cur]){
            if(colors[next] == cur_color) return false;
            // 没有被访问过,则置-cur_color,通过dfs其实可以把相关的一条圈都走完
            if(colors[next] == 0 && !dfs(next,-cur_color)) return false;
        }
        return true;
    }
};

 

你可能感兴趣的:(LeetCode刷题记录)