10. Regular Expression Matching

题目:

Implement regular expression matching with support for '.' and '*'.

'.' Matches any single character.
'*' Matches zero or more of the preceding element.

The matching should cover the entire input string (not partial).

The function prototype should be:
bool isMatch(const char *s, const char *p)

Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "a*") → true
isMatch("aa", ".*") → true
isMatch("ab", ".*") → true
isMatch("aab", "c*a*b") → true

链接:http://leetcode.com/problems/regular-expression-matching/

题解:

这道题也卡了很久,一开始就在考虑很多边界条件。后来才理解到s是普通字符串,p是含有'.'或者'*'的字符串。有几种方法可以解题。

一开始是recursive的普通解法,分别考虑p的长度为0, 为1,以及大于1的情况。

public class Solution {
    public boolean isMatch(String s, String p) {
        if(p == null || s == null)
            return false;
        
        if (p.length() == 0) 
            return s.length() == 0;
        
        if (p.length() == 1) 
            return s.length() == 1 && (p.charAt(0) == '.' || p.charAt(0) == s.charAt(0)) ;
        
        if (p.charAt(1) == '*') {
            if (isMatch(s, p.substring(2))) 
                return true;
            return s.length() > 0 && (p.charAt(0) == '.' || s.charAt(0) == p.charAt(0)) && isMatch(s.substring(1), p);
        } else {
            return s.length() > 0 && (p.charAt(0) == '.' || s.charAt(0) == p.charAt(0)) && isMatch(s.substring(1), p.substring(1));
        }
    }    
}

 

下面是dp解法,先对s构建一个数组, 从p的尾部向前遍历,当p的字符为'*'时,从s尾部向前遍历。否则从s头部向后遍历。

Time Complexity - O(m * n), Space Complexity - O(n)。 

public class Solution {
    public boolean isMatch(String s, String p) {            //dp
        boolean[] match = new boolean[s.length() + 1];
        match[s.length()] = true;
        
        for(int i = p.length() - 1; i >= 0; i--) {
            if(p.charAt(i) == '*') {
                for(int j = s.length() - 1; j >= 0; j--) 
                    match[j] = match[j] || (match[j + 1] && (p.charAt(i - 1) == '.' || s.charAt(j) == p.charAt(i - 1)));
                i--;
            } else {
                for(int j = 0; j < s.length(); j++) 
                    match[j] = match[j + 1] && (p.charAt(i) == '.' || p.charAt(i) == s.charAt(j));
                match[s.length()] = false;
            }
        }
        
        return match[0];
    }
}

 

更好的方法是使用regular expression的本质,利用NFA和有向图来计算,代码会写很长,先放在reference里。

 

二刷:

又做到了这一题,又被卡,实在不想背答案。也不想每道类似的题去找一个特别的recursive或者dp解。决定还是好好学习学习自动机Automata。下面是学了Sedgewick关于Regular Expression这一章后的一些想法。编写边学习,主要目的是理顺思路,有错误的话看官也别生气。

Regular expression is a notation to specify a set of strings, 这个set有可能是infinite。基本操作一般有以下,我们先不管操作的优先级, concatenation - 就是ABC match ABC; or - '|',  AA | BAAB match  AA或者 BAAB;closure - '*',满足0个或者多个之前的字符, 这道题目里面就有'*', 比如 AB*A可以match AA,也可以match ABBBBA;最后就是括号 parentheses '('')'了,比如 (AB)*A可以match A或者 ABABABABA。 还有其他的操作符,比如 wildcard - '.', 可以match任意字符,这个这道题目里面有,我们在wildcard matching里也会遇到;character class - [A-Z] match word Capitailized; at least 1 - A(BC)+DE - matches ABCDE或者 ABCBCDE一类的; exact K [0-9]{5}  -  match 12345。

自动机里常用的有DFA (Deterministic Finite Automata)和NFA(Non-deterministic Finite Automata),DFA和NFA可以互相转换。主要可以使用在String searching,Pattern Matching之类的。像KMP String searching就可以做一个DFA来完成matching的过程。 DFA: Machine to recognize whether a given string is in a given set。 Kleene's theorem说明对于任何DFA, 我们都可以招待一个RE来描述同样的same set of strings,对于任何RE,我们也可以找到一个DFA来识别same set of strings.

一般构造RE识别有至少三种方法,第一种是把所有的NFA转换为一个DFA,然后用DFA来进行识别,这种建立DFA的时间会较长O(2m),但识别时间为O(n)。第二种是直接模拟NFA过程,这样建立NFA大约是O(m),但识别时间为O(mn)。第三种是用回溯backtracking来识别,但运行时常最坏情况下可能是O(2n)

Ken Thompson用来识别的方法就是模拟NFA,也是我打算主要学习的方法,在塞神的video里,RE matching NFA主要有几个特征:

  1. RE enclosed in parentheses (我们可以不用做)
  2. One state per RE character(start = 0, accept = M)
  3. epsilon-transition (change state, but don't scan text): 这个epsilon-transition是代表在这一步我们有多少种可能转移的状态,比如 A*B,我们可以走回A,可以停留在*,也可以进一步走到B,这里我们有三种可能的下一步,该怎么选择呢? 在此我们就要转化这个小问题成为一个在有向图中单源的reachability的问题,要构建一个有向图,用DFS来计算到底这一步我们能够到达多少个状态,然后把这些状态放入一个临时的结果集里面,再进行下一步计算。 这个转移矩阵就代表NFA和DFA的本质区别, non-determinism,不可确定性。  在DFA里我们每一步都有一个明确的下一步,但在NFA的这一步里,我们不能确定下一步究竟怎么走。
  4. Match transition (change state and scan to next text char):  这里就是说我们找到了一个match,要进行下一步状态转换,同时扫描输入text的下一个字符, 比如 s = "ABC", p = "ABC",当i = 0时,因为s(0)='A',p(0)也等于'A',所以他们match,我们可以继续转移状态进行下一步来对比'B'和'B''
  5. After scanning all text characters, accept if any sequence of transitions ends in accept state。在扫描完毕所有text之后,假如结果集合中,有任何一个state = accept state,那么match成功。

NFA representation:

State names: Integers from 0 to M

Match-transitions:  Keep regular expression in array re[]

Epsilon-transitions:  Store in digraph G

How to efficiently simulate an NFA? :  Maintain set of all possible states that NFA could be in after reading in the first i text characters. 在读取了i个字符后,保存下来所有NFA可能到达的state。

如何从step i 扩展到step i+1呢?

  1. 这里首先保存all states reachable after reading i symbols
  2. 然后尝试读取 (i + 1)st symbol c, 根据matching transitions来获取当前NFA可能到达的state,这里一般我们可以重新建立一个Bag或者Set来保存新的state。这一步结束后我们已经读取了第i+1个字符
  3. 这时我们就可以看一看possible null transitions,就是在上一步的基础上,不读取下一个输入text字符的情况下,根据Epsilon-transitions我们能够到达的state,把这些state加入我们的Bag/Set里
  4. 到这里我们就完成了从step i 扩展到step i+1, 这时Bag/Set里保存的就是我们的NFA可以到达的所有state

Digraph reachability: Find all vertices reachable from a given source or set of vertices:   Run DFS from each source without unmarking vertices

上面是谈了如何simulate NFA matching process,那么如何根据RE构建NFA?

Concatenation:   Add match-transition edge from state corresponding to characters in the alphabet to next state

Parentheses: Add epsilon-transition edge to next state

Closure: '*', add three eplison transition edges for each * operator,  比如  state 2 = 'A', state 3 = '*', state 4 = 'B',那么我们可以在eplison-transitions edges里面假如 2 -> 3, 3 -> 2以及 3 -> 4。这个操作也对closure expression成立

Or: '|' addd two epsilon transition edges.  -  G.addEdge(lp, or + 1);  G.addEdge(or, i); 这里假设所有的RE都在Parentheses中。对于这种包含Parentheses的RE,我们需要维护一个栈来保存上一个left Parenthese lp的位置。比较复杂,先不考虑。

 

Java:

为什么Time Complexity是O(mn)呢?  假定输入是长为n的String s,  Pattern是长为m的pattern,那么我们构建NFA的时候最多会有3 * M条边 (全部都是3条边的Epsilon transitions),而遍历用dfs计算reachability的时候复杂度是O(V + E)。那么对于s中的每一个字符,我们们都要计算一遍dfs,最后结果就是  n * O(V + E) = n * O(m) = O(mn)。  空间复杂度没仔细算,也就是对n的每一个元素都进行dfs计算,n次dfs,乘以我们每次dfs的开销O(bm),b是branching factor,这里忽略,总的来说也是O(mn)。

Time Complexity - O(mn), Space Complexity - O(mn)

public class Solution {
    private Digraph graph;   // Digraph to record epsilon transitions
    
    public boolean isMatch(String s, String p) {
        buildNFA(p);
        DirectedDFS dfs = new DirectedDFS(graph, 0);
        Set<Integer> reachableState = new HashSet<>();
        for (int v = 0; v < graph.v(); v++) {
            if (dfs.marked(v)) {
                reachableState.add(v);
            }
        }
        
        for (int i = 0; i < s.length(); i++) {
            Set<Integer> match = new HashSet<>();
            for (int v : reachableState) {      // calculate each possible match transition
                if (v == p.length()) {      // accept state
                    continue;
                }
                if (p.charAt(v) == s.charAt(i) || p.charAt(v) == '.') { // match transition
                    match.add(v + 1);
                }
            }
            dfs = new DirectedDFS(graph, match);
            reachableState = new HashSet<>();
            for (int v = 0; v < graph.v(); v++) {   // from each match transition, expand to epsilon transitions
                if (dfs.marked(v)) {
                    reachableState.add(v);
                }
            }
            if (reachableState.size() == 0) {
                return false;
            }
        }
        
        for (int v : reachableState) {      // after scaned all s characters, if any NFA sequence leads to accept state
            if (v == p.length()) {
                return true;
            }
        }
        return false;
    }
    
    private void buildNFA(String p) {
        this.graph = new Digraph(p.length() + 1);   // each char a state, plus one accept state
        for (int i = 0; i < p.length(); i++) {
            char c = p.charAt(i);
            if (c == '*'){
                if (i > 0) {
                    graph.addEdge(i, i - 1);
                    graph.addEdge(i - 1, i);
                }
                graph.addEdge(i, i + 1);
            }
        }
    }
    
    private class DirectedDFS {
        private boolean[] marked;
        
        public DirectedDFS(Digraph graph, int start) {  // G and start state s
            marked = new boolean[graph.v()];
            dfs(graph, start);
        }
        
        public DirectedDFS(Digraph graph, Iterable<Integer> sources) {
            marked = new boolean[graph.v()];
            for (int v : sources) {
                if (!marked[v]) {
                    dfs(graph, v);
                }
            }
        }
        
        private void dfs(Digraph graph, int v) {
            marked[v] = true;
            for (int w : graph.adj[v]) {
                if (!marked[w]) {
                    dfs(graph, w);
                }
            }
        }
        
        public boolean marked(int v) {
            return marked[v];
        }
    }
    
    private class Digraph {
        private int V;    // number of vertices
        private Set<Integer>[] adj;     // adjacency list representation of Digraph
        
        public Digraph(int v) {
            this.V = v;
            adj = (Set<Integer>[])new Set[v];        //type unsafe
            for (int i = 0; i < v; i++) {
                adj[i] = new HashSet<Integer>();
            }
        } 
        
        public void addEdge(int v, int w) {
            adj[v].add(w);
        }
        
        public Iterable<Integer> adj(int v) {   // verticies pointing from v
            return adj[v];
        }
        
        public int v() {
            return V;
        }
        
    }
}

或者

public class Solution {
    private Digraph G;
    
    public boolean isMatch(String s, String p) {
        buildNFA(p);
        DirectedDFS dfs = new DirectedDFS(G, 0);
        Set<Integer> reachableState = new HashSet<>();
        for (int v = 0; v < G.V; v++) {
            if (dfs.marked[v]) {
                reachableState.add(v);
            }
        }
        
        for (int i = 0; i < s.length(); i++) {
            Set<Integer> match = new HashSet<>();
            for (int v : reachableState) {
                if (v == p.length()) {
                    continue;
                }
                if (p.charAt(v) == s.charAt(i) || p.charAt(v) == '.') {
                    match.add(v + 1);
                }
            }
            dfs = new DirectedDFS(G, match);
            reachableState = new HashSet<>();
            for (int v = 0; v < G.V; v++) {
                if (dfs.marked[v]) {
                    reachableState.add(v);
                }
            }
            if (reachableState.size() == 0) {
                return false;
            }
        }
        for (int v : reachableState) {
            if (v == p.length()) {
                return true;
            }
        }
        return false;
    }
    
    private void buildNFA(String p) {       // use p build NFA
        this.G = new Digraph(p.length() + 1); // each char a state, plus one accept state
        for (int i = 0; i < p.length(); i++) {
            char c = p.charAt(i);
            if (c == '*') {
                if (i > 0) {
                    G.addEdge(i - 1, i);
                    G.addEdge(i, i - 1);
                }
                G.addEdge(i, i + 1);
            }
        }
    }
    
    private class DirectedDFS {
        public boolean[] marked;
        
        public DirectedDFS(Digraph G, int num) {
            marked = new boolean[G.V];
            dfs(G, 0);
        }
        
        public DirectedDFS(Digraph G, Iterable<Integer> nums) {
            marked = new boolean[G.V];
            for (int v : nums) {
                if(!marked[v]) {
                    dfs(G, v);    
                }
            }
        }
        
        private void dfs(Digraph G, int v) {
            marked[v] = true;
            for (int w : G.adj.get(v)) {
                if (!marked[w]) {
                    dfs(G, w);
                }
            }
        }
    }
    
    private class Digraph {
        public int V;    // num of vertices
        public Map<Integer, Set<Integer>> adj;   // adjacency list representation of Digraph
        
        public Digraph(int num) {
            this.V = num;
            adj = new HashMap<>();
            for (int i = 0; i < num; i++) {
                adj.put(i, new HashSet<>());
            }
        }
        
        private void addEdge(int v, int w) {
            adj.get(v).add(w);
        }
    }
}

 

假如不考虑比较复杂的 | 和 ()的话。假设我们有下面两个变型题:

1.  *号代表之前字符出现一次或者0次(其实相当于"+"), 那么在建立Epsilon transitions的时候我们就没有G.addEdge(i - 1, i)这条代表0次matching的边, 只有 G.addEdge(i, i - 1)表示一次多次,以及通向下一state的G.addEdge(i, i + 1)

2.  LeetCode no.44  Wildcard Matching:  用构建NFA的方法来做的话,在最后一个例子会超时,强行跳过才可以。我其实每想透该如何构建 Epsilon transition以及Recoganize,因为这里既有Epsilon transition也有matching transition。所以我打算先加入一个预处理步骤 -  在那道题目里 ?可以带表任意字符, * 代表 0个或者多个任意字符, 那么其实联系到这道 Regular Expression matching的话,可以把Wildcard Matching里的 '*' 看成是这里的 “.*”两个字符的组合, 意思是用 '.'来代表任意字符,然后用'*'来代表前面的字符出现一次或者多次, 这样就可以同时包括matching transition以及 epsilon transition。所以实现的话,在预处理里面把'*'替换为"?*", 之后就套用这道题目的code, 再跳过最后一个case就可以了。

总结是:

Simulate NFA的方法,对于OJ这两道题目虽然不是最快的,甚至不是较快的解法, 但却是一种通用的解,深度优化的话时间复杂度是O(mn),符合理论,虽然不能达到 DFA的 O(n)速度,但构建比较容易,理解起来也比较简单。我打算再加深对其他RE operator的理解,比如 |,  (), +,[-],等等。

 

使用DP的方法: jianchao.li.fighter和xiaohui7解释得非常清楚。

  1. 首先建立dp矩阵res[][] = new boolean[m + 1][n + 1]
  2. res[0][0] = true表示s和p都为""的时候match成功
  3. 接下来对pattern p的首行'*'号的0 match情况进行初始化,res[0][j] = res[0][j - 2] && p.charAt(j - 1) == '*'
  4. 之后从1, 1开始对res矩阵进行dp,主要分为两种情况
    1. p.charAt(i - 1)不等于'*': 这里表示matching transition,假如s和p的上一个字符match, 即res[i - 1][j - 1] == true,同时新的字符s.charAt(i - 1) == p.charAt(j - 1),或者p.charAt(j - 1) == '.', 那么我们可以设定res[i][j] = true,这表示到 s 和 p 到 i - 1和j - 1的位置是match的
    2. 假如p.charAt(i - 1) == '*': 这里表示epsilon transition,系统可能处于不同的状态,我们要分多种情况进行考虑,只要有一个状态为true,在这个位置的结果就为true,是一个“或”的关系:
      1. res[i][j - 2] == true,即s.charAt(i - 1) match p.charAt(j - 3),这里'*'号和其之前的字符可以当作"",表示0 matching,这种情况下,我们可以认为状态合理,res[i][j] = true。 例 "C" match "CA*"
      2. 系统也可能处于另外一个状态,一个或者多个字符的matching。这里我们判断res[i - 1][j],代表s.charAt(i - 2)和 p.charAt(j - 1),假如这个状态成立,我们再继续判断。例“AA” match "A*",我们先反回去看"A"是否match "A*"。假如上面状态成立,我们再看p.charAt(j - 2)是否和s.charAt(i - 1)能match,这里根matching transition一样,需要判断p.charAt(j - 2) == s.charAt(i - 1) || p.charAt(i - 1) == '.'。例子还是"AA" match "A*",当第一个A match了之后,我们判断s中第二个A和p中第一个"A"是否match。
  5. 最后我们返回res[m][n] 

Time Complexity - O(mn), Space Complexity - O(mn)。

public class Solution {
    public boolean isMatch(String s, String p) {
        if (s == null || p == null) {
            return false;
        }
        if (p.length() == 0) {
            return s.length() == 0;
        }
        int m = s.length(), n = p.length();
        boolean res[][] = new boolean[m + 1][n + 1];
        res[0][0] = true;
        for (int j = 2; j < res[0].length; j += 2) {        // 0 matching
            res[0][j] = res[0][j - 2] && p.charAt(j - 1) == '*';
        }
        
        for (int i = 1; i < res.length; i++) {
            for (int j = 1; j < res[0].length; j++) {
                if (p.charAt(j - 1) == '*') {   // epsilon transition
                    if (j > 1 && res[i][j - 2]) {   // 0 matching
                        res[i][j] = true;
                    }
                    if (j > 1 && res[i - 1][j]          // one or more matching
                        && (p.charAt(j - 2) == '.' || p.charAt(j - 2) == s.charAt(i - 1))) {
                        res[i][j] = true;
                    }
                } else {                    // matching transition
                    if (res[i - 1][j - 1] && (p.charAt(j - 1) == s.charAt(i - 1) || p.charAt(j - 1) == '.')) {
                        res[i][j] = true;
                    }
                }
            }
        }
        
        return res[m][n];
    }
}

 

Python: 

class Solution(object):
    def isMatch(self, s, p):
        """
        :type s: str
        :type p: str
        :rtype: bool
        """
        if len(p) == 0:
            return len(s) == 0
        m = len(s)
        n = len(p)
        res = []
        for i in range(0, m + 1):
            tmp = []
            for j in range(0, n + 1):
                tmp.append(False)
            res.append(tmp)
        res[0][0] = True
        for j in range(2, n + 1):
            res[0][j] = res[0][j - 2] and p[j - 1] == '*'
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if p[j - 1] == '*':
                    if j > 1 and res[i][j - 2]:
                        res[i][j] = True
                    if j > 1 and res[i - 1][j] and (s[i - 1] == p[j - 2] or p[j - 2] == '.'):
                        res[i][j] = True
                else:
                    if res[i - 1][j - 1] and (s[i - 1] == p[j - 1] or p[j - 1] == '.'):
                        res[i][j] = True
        return res[m][n]            

 

 

题外话:

以前思考过怎样学习最快。总是觉得要看书,看经典书,然后动手做,动手练习。今天觉得看书有的时候真不如看视频。可能对于某些特别聪明的人,看书一眼就可以理解意义,然后就可以应用到实践中。但比如这Regular Expression,算法第四版这本书里只有11页纸,只看书对我来说的话可能怎么也参不透。而视频内容超过1小时,有各个关键点的Demo以及为什么要这么做,配合lecture notes看起来就好吸收不少。各种资源整合起来可能更适合我这种普通算法爱好者来进行学习。 是不是我发现得太晚了。之后试了一下用NFA去解Wildcard Matching, 发现并没有什么用...还是不能ac  -_____-!! 等深入学习了DP以后再去尝试吧。

 

三刷:

nfa的方法,代码比较长,面试时也许写不及,代码一长就容易写错。已经理解了dp,这道题我们要注意阅读清楚题意, '.'是可以match任何单字符,'*'必须连同其前面的字符才能表示0个或者1个该字符。所以使用二维dp的话我们可以按照以下步骤来做:

  1. 找出base状态。我们先去掉一些边界条件,比如s和p为null,p长度为0,以及p开头为'*',这些情况都是不合理的。接下来我们分析得出,当s和p都为空字符串情况下, 两串是match的,应该返回true。
  2. 初始化。 这时候我们要创建2维DP矩阵。 矩阵里面的 dp[i][j]的意思是, 到串s的第i - 1个字符和串p的第j - 1个字符是否match。我们也要对i = 0时,即s为空串时的dp数组进行初始化。
  3. 分析转移方程。
    1. 当 p.charAt(j - 1) = '*'时,这时候我们要考虑系统可能处于的两种状态
      1. 当dp[i][j - 2] 为true时,这时候代表epsilon transition : '*'之前的字母出现0次这种情况,我们可以设置dp[i][j] = true
      2. 或者,当dp[i - 1][j]为true时,这时候说明 s.charAt(i - 2)可以match到p.charAt( j - 1),我们要从s串继续向下扩展一个字符,看s.charAt(i - 1)是否能匹配p.charAt(j - 1)。这里我们就可以比较s.charAt(i - 1)是否等于p.charAt(j - 2),即s.charAt(i - 1)是否和'*'之前的字母相等,或者p.charAt(j - 2)等于通配符'.'。这时候代表epislon transition : '*'之前的字母出现1次。我们可以设置dp[i][j] = true。
    2. 否则p.charAt(j - 1) != '*'。 这时候我们只需要考虑在之前两个字母匹配的情况下,即 dp[i - 1][j - 1] = true时,  现在s中的字符s.charAt(i - 1)是否等于现在p中的字符p.charAt(j - 1),或者现在p的字符为通配符'.'
  4. 返回结果

Java:

Time Complexity - O(mn), Space Complexity - O(mn)。

public class Solution {
    public boolean isMatch(String s, String p) {
        if (s == null || p == null) return s == p;
        if (p.length() == 0) return s.length() == 0;
        if (p.charAt(0) == '*') return false;
        int sLen = s.length(), pLen = p.length();
        boolean[][] dp = new boolean[sLen + 1][pLen + 1];
        dp[0][0] = true;
        for (int j = 2; j <= pLen; j++) {
            if (p.charAt(j - 1) == '*' && dp[0][j - 2]) dp[0][j] = true;
        }
        
        for (int i = 1; i <= sLen; i++) {
            for (int j = 1; j <= pLen; j++) {
                if (p.charAt(j - 1) == '*') {
                    if (dp[i][j - 2] || (dp[i - 1][j] && ((s.charAt(i - 1) == p.charAt(j - 2)) || (p.charAt(j - 2) == '.')))) {
                        dp[i][j] = true;
                    }
                } else {
                    if (dp[i - 1][j - 1] && (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.')) {
                        dp[i][j] = true;
                    }    
                }
            }
        }
        
        return dp[sLen][pLen];
    }
}

 

 

 

 

 

Reference:

http://algs4.cs.princeton.edu/54regexp/NFA.java.html              

https://en.wikipedia.org/wiki/Regular_expression 

https://en.wikipedia.org/wiki/Thompson%27s_construction

https://www.cs.ubc.ca/~kevinlb/teaching/cs322%20-%202008-9/Lectures/Search3.pdf

http://blog.csdn.net/linhuanmars/article/details/21145563

http://blog.csdn.net/linhuanmars/article/details/21198049

https://leetcode.com/discuss/18970/concise-recursive-and-dp-solutions-with-full-explanation-in

https://leetcode.com/discuss/9405/the-shortest-ac-code

https://leetcode.com/discuss/32424/clean-java-solution

https://leetcode.com/discuss/43860/9-lines-16ms-c-dp-solutions-with-explanations

https://leetcode.com/discuss/55253/my-dp-approach-in-python-with-comments-and-unittest

https://leetcode.com/discuss/8648/my-ac-dp-solution-for-this-problem-asking-for-improvements

https://leetcode.com/discuss/20470/fast-python-solution-with-backtracking-and-caching-solution

https://leetcode.com/discuss/26809/dp-java-solution-detail-explanation-from-2d-space-to-1d-space

https://leetcode.com/discuss/48436/accepted-solution-based-nondeterministic-finite-automata

https://leetcode.com/discuss/31950/my-dfa-deterministic-finite-automata-java-codes

https://leetcode.com/discuss/57880/solution-based-nfa-without-backtracking-only-4ms-from-russ

https://leetcode.com/discuss/75098/java-4ms-dp-solution-with-o-n-2-time-and-o-n-space-beats-95%25

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值