http://buttercola.blogspot.com/2014/09/leetcode-interleaving-string.html
https://robinliu.gitbooks.io/algorithms/content/shuang_xu_lie_xing.html
双序列型
state:
fucntion:
https://discuss.leetcode.com/topic/7728/dp-solution-in-java
We can use DP to solve this problem. So the steps are:
既然DFS可以,那么BFS也就坐不住了,也要出来浪一波。这里我们需要用队列queue来辅助运算,如果将解法一讲解中的那个二维dp数组列出来的TF图当作一个迷宫的话,那么BFS的目的就是要从(0, 0)位置找一条都是T的路径通到(n1, n2)位置,这里我们还要使用哈希集合,不过此时保存到是已经遍历过的位置,队列中还是存key值,key值的encode方法跟上面DFS解法的相同,初识时放个0进去。然后我们进行while循环,循环条件除了q不为空,还有一个是k小于n3,因为匹配完s3中所有的字符就结束了。然后由于是一层层的遍历,所以要直接循环queue中元素个数的次数,在for循环中,对队首元素进行解码,得到i和j值,如果i小于n1,说明s1还有剩余字符,如果s1当前字符等于s3当前字符,那么把s1的下一个位置i+1跟j一起加码算出key值,如果该key值不在于集合中,则加入集合,同时加入队列queue中;同理,如果j小于n2,说明s2还有剩余字符,如果s2当前字符等于s3当前字符,那么把s2的下一个位置j+1跟i一起加码算出key值,如果该key值不在于集合中,则加入集合,同时加入队列queue中。for循环结束后,k自增1。最后如果匹配成功的话,那么queue中应该只有一个(n1, n2)的key值,且k此时等于n3,所以当queue为空或者k不等于n3的时候都要返回false
https://discuss.leetcode.com/topic/6562/8ms-c-solution-using-bfs-with-explanation
https://discuss.leetcode.com/topic/19900/python-dp-solutions-o-m-n-o-n-space-bfs-dfs/2
X. DFS + cache
Time complexity : . is the length of and is the length of .
https://www.cnblogs.com/higerzhang/p/4122234.html
Given s1, s2, s3, find whether s3 is formed by the interleaving of s1 and s2.
For example,
Given:
s1 =
s2 =
Given:
s1 =
"aabcc"
,s2 =
"dbbca"
,
When s3 =
When s3 =
"aadbbcbcac"
, return true.When s3 =
"aadbbbaccc"
, return false.
X. DP
这道求交织相错的字符串和之前那道 Word Break 拆分词句 的题很类似,就想我之前说的只要是遇到字符串的子序列或是匹配问题直接就上动态规划Dynamic Programming,其他的都不要考虑,什么递归呀的都是浮云,千辛万苦的写了递归结果拿到OJ上妥妥Time Limit Exceeded,能把人气昏了,所以还是直接就考虑DP解法省事些。一般来说字符串匹配问题都是更新一个二维dp数组,核心就在于找出递推公式。那么我们还是从题目中给的例子出发吧,手动写出二维数组dp如下:
Ø d b b c a
Ø T F F F F F
a T F F F F F
a T T T T T F
b F T T F T F
c F F T T T T
c F F F T F T
首先,这道题的大前提是字符串s1和s2的长度和必须等于s3的长度,如果不等于,肯定返回false。那么当s1和s2是空串的时候,s3必然是空串,则返回true。所以直接给dp[0][0]赋值true,然后若s1和s2其中的一个为空串的话,那么另一个肯定和s3的长度相等,则按位比较,若相同且上一个位置为True,赋True,其余情况都赋False,这样的二维数组dp的边缘就初始化好了。下面只需要找出递推公式来更新整个数组即可,我们发现,在任意非边缘位置dp[i][j]时,它的左边或上边有可能为True或是False,两边都可以更新过来,只要有一条路通着,那么这个点就可以为True。那么我们得分别来看,如果左边的为True,那么我们去除当前对应的s2中的字符串s2[j - 1] 和 s3中对应的位置的字符相比(计算对应位置时还要考虑已匹配的s1中的字符),为s3[j - 1 + i], 如果相等,则赋True,反之赋False。 而上边为True的情况也类似,所以可以求出递推公式为:
dp[i][j] = (dp[i - 1][j] && s1[i - 1] == s3[i - 1 + j]) || (dp[i][j - 1] && s2[j - 1] == s3[j - 1 + i]);
其中dp[i][j] 表示的是 s2 的前 i 个字符和 s1 的前 j 个字符是否匹配 s3 的前 i+j 个字符,根据以上分析.
双序列型
f[i][j]
: 表示 s1 的前 i 个字符和 s2 的前 j 个字符能否组成 s3 的前 i+j 个字符fucntion:
f[i][j] = (f[i-1][j] && s1[i-1] == s3[i+j-1]) || (f[i][j-1] && s2[j-1] == s3[i+j-1])
https://discuss.leetcode.com/topic/7728/dp-solution-in-java
We can use DP to solve this problem. So the steps are:
- Define boolean dp[str1.length() + 1][str2.length() + 1], where dp[ i ][ j ] : means whether str1.charAt(0) to str1.charAt(i) and str2.charAt(0) to str2.charAt(j) could construct the string str3, from 0 to i + j.
- Transit function:
- dp[i][j] = str3.charAt(i + j) == str1.charAt(i) && dp[ i - 1][j] ||
- str3.charAt(i + j) == str2.charAt(j) && dp[i][j - 1]
- Initial State
- dp[0][0] = true;
- dp[0][j] = str3.charAt(j) == str2.charAt(j)
- dp[i][0] = str3.charAt(i) == str1.charAt(i)
- Final state dp[str1.length()][str2.length()].
public
boolean
isInterleave(String s1, String s2, String s3) {
if
(s1 ==
null
|| s2 ==
null
|| s3 ==
null
) {
return
false
;
}
if
(s1.length() ==
0
&& s2.length() ==
0
&& s3.length() ==
0
) {
return
true
;
}
if
(s1.length() + s2.length() != s3.length()) {
return
false
;
}
boolean
[][] dp =
new
boolean
[s1.length() +
1
][s2.length() +
1
];
dp[
0
][
0
] =
true
;
for
(
int
i =
1
; i <= s2.length(); i++) {
if
(s2.charAt(i -
1
) == s3.charAt(i -
1
)) {
dp[
0
][i] =
true
;
}
else
break
;
}
for
(
int
i =
1
; i <= s1.length(); i++) {
if
(s1.charAt(i -
1
) == s3.charAt(i -
1
)) {
dp[i][
0
] =
true
;
}
else
break
;
}
for
(
int
i =
1
; i <= s1.length(); i++) {
for
(
int
j =
1
; j <= s2.length(); j++) {
if
(s3.charAt(i + j -
1
) == s1.charAt(i -
1
)) {
dp[i][j] = dp[i -
1
][j];
}
if
(s3.charAt(i + j -
1
) == s2.charAt(j -
1
)) {
dp[i][j] = dp[i][j] || dp[i][j -
1
];
}
}
}
return
dp[s1.length()][s2.length()];
}
我们也可以把for循环合并到一起,用if条件来处理边界情况,整体思路和上面的解法没有太大的区别
public boolean isInterleave(String s1, String s2, String s3) {
if (s3.length() != s1.length() + s2.length()) {
return false;
}
boolean dp[][] = new boolean[s1.length() + 1][s2.length() + 1];
for (int i = 0; i <= s1.length(); i++) {
for (int j = 0; j <= s2.length(); j++) {
if (i == 0 && j == 0) {
dp[i][j] = true;
} else if (i == 0) {
dp[i][j] = dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1);
} else if (j == 0) {
dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1);
} else {
dp[i][j] = (dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1))
|| (dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1));
}
}
}
return dp[s1.length()][s2.length()];
}
http://www.jiuzhang.com/solutions/interleaving-string/public boolean isInterleave(String s1, String s2, String s3) { if (s1.length() + s2.length() != s3.length()) { return false; } boolean [][] interleaved = new boolean[s1.length() + 1][s2.length() + 1]; interleaved[0][0] = true; for (int i = 1; i <= s1.length(); i++) { if(s3.charAt(i - 1) == s1.charAt(i - 1) && interleaved[i - 1][0]) interleaved[i][0] = true; } for (int j = 1; j <= s2.length(); j++) { if(s3.charAt(j - 1) == s2.charAt(j - 1) && interleaved[0][j - 1]) interleaved[0][j] = true; } for (int i = 1; i <= s1.length(); i++) { for (int j = 1; j <= s2.length(); j++) { if(((s3.charAt(i + j - 1) == s1.charAt(i - 1) && interleaved[i - 1][j])) || ((s3.charAt(i + j - 1)) == s2.charAt(j - 1) && interleaved[i][j - 1])) interleaved[i][j] = true; } } return interleaved[s1.length()][s2.length()]; }
X.DP - Space Optimization
https://discuss.leetcode.com/topic/5087/my-solution-in-java-using-dp-time-o-n-m-and-space-o-m
https://discuss.leetcode.com/topic/5087/my-solution-in-java-using-dp-time-o-n-m-and-space-o-m
public boolean isInterleave(String s1, String s2, String s3) {
if (s3.length() != s1.length() + s2.length()) {
return false;
}
boolean dp[] = new boolean[s2.length() + 1];
for (int i = 0; i <= s1.length(); i++) {
for (int j = 0; j <= s2.length(); j++) {
if (i == 0 && j == 0) {
dp[j] = true;
} else if (i == 0) {
dp[j] = dp[j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1);
} else if (j == 0) {
dp[j] = dp[j] && s1.charAt(i - 1) == s3.charAt(i + j - 1);
} else {
dp[j] = (dp[j] && s1.charAt(i - 1) == s3.charAt(i + j - 1))
|| (dp[j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1));
}
}
}
return dp[s2.length()];
}
public boolean isInterleave(String s1, String s2, String s3) {
if (s1==null || s2==null || s3==null) return false;
if (s1.length()==0 && s2.length()==0 && s3.length()==0) return true;
if ((s1.length()+s2.length())!=s3.length()) return false;
boolean valid[] = new boolean[s2.length()+1];
valid[0] = false;
for (int j=1; j<=s2.length(); j++){
if (s2.charAt(j-1)==s3.charAt(j-1)) valid[j]=true;
}
for (int i=1; i<=s1.length(); i++){
if (s1.charAt(i-1)==s3.charAt(i-1)) valid[0]=true;
for (int j=1; j<=s2.length(); j++){
valid[j] = (valid[j] && s1.charAt(i-1)==s3.charAt(i+j-1)) ||
(valid[j-1] && s2.charAt(j-1)==s3.charAt(i+j-1));
}
}
return valid[s2.length()];
}
public static boolean isInterleaveOptz(String s1, String s2, String s3) {
if (s3.length() != s1.length() + s2.length()) return false;
boolean[] optimal = new boolean[s2.length() + 1]; //dp optimal
optimal[0] = true;
for (int j = 0; j < s2.length(); j++) { //check no s1 char is selected, if s2 could equals to s3
if (optimal[j] && s2.charAt(j) == s3.charAt(j)) optimal[j + 1] = true;
}
for (int i = 0; i < s1.length(); i++) { //check select i-th char in s1
if (optimal[0] && s1.charAt(i) == s3.charAt(i)) optimal[0] = true; //no char in s2 is selected
else optimal[0] = false;
for (int j = 0; j < s2.length(); j++) { //select j-th char
if ((s1.charAt(i) == s3.charAt(i + j + 1) && optimal[j + 1]) ||
s2.charAt(j) == s3.charAt(i + j + 1) && optimal[j]) {
optimal[j + 1] = true;
} else optimal[j + 1] = false;
}
}
return optimal[s2.length()];
}
时间O(m*n) 空间O(min(m,n))
public boolean isInterleave(String s1, String s2, String s3) {
if (s1.length() + s2.length() != s3.length()) {
return false;
}
String maxWord = s1.length() > s2.length() ? s1 : s2;
String minWord = s1.length() > s2.length() ? s2 : s1;
boolean[]dp = new boolean[minWord.length() + 1];
dp[0] = true;
for (int i = 0; i < minWord.length(); i++) {
if (minWord.charAt(i) == s3.charAt(i) && dp[i] == true) {
dp[i + 1] = true;
}
}
for (int i = 1; i <= maxWord.length(); i++) {
dp[0] = dp[0] && maxWord.charAt(i - 1) == s3.charAt(i - 1);
for (int j = 1; j <= minWord.length(); j++) {
dp[j] = s3.charAt(i + j - 1) == maxWord.charAt(i - 1) && dp[j] == true || s3.charAt(i + j - 1) == minWord.charAt(j - 1) && dp[j - 1] == true;
//此处不能再用if判断, 因为dp[j]已经有值 必须要赋值true/false
}
}
return dp[minWord.length()];
}
public boolean isInterleave(String s1, String s2, String s3) {
// write your code here
if (s1 == null || s2 == null || s3 == null) return false;
if (s1.length() == 0 && s2.length() == 0 && s3.length() == 0) return true;
if ((s1.length() + s2.length()) != s3.length()) return false;
String shorterString, longerString;
if (s1.length() < s2.length()) {
shorterString = s1;
longerString = s2;
} else {
shorterString = s2;
longerString = s1;
}
boolean[] valid = new boolean[shorterString.length() + 1];
valid[0] = true;
for (int i = 1; i <= shorterString.length(); i++) {
if (shorterString.charAt(i-1)==s3.charAt(i-1)) valid[i]=true;
}
for (int i = 1; i <= longerString.length(); i++) {
for (int j = 1; j <= shorterString.length(); j++) {
valid[j] = (valid[j] && longerString.charAt(i-1)==s3.charAt(i+j-1)) ||
(valid[j-1] && shorterString.charAt(j-1)==s3.charAt(i+j-1));
}
}
return valid[shorterString.length()];
}
X. Recursive version
- public boolean isInterleave(String s1, String s2, String s3) {
- if(s1.length() == 0){
- if( s2.equals(s3))return true;
- else return false;
- }else if(s2.length() == 0){
- if(s1.equals(s3)) return true;
- else return false;
- }
- if(s3.charAt(0) == s1.charAt(0) && s3.charAt(0) != s2.charAt(0)){
- return isInterleave(s1.substring(1), s2, s3.substring(1));
- }else if(s3.charAt(0) == s2.charAt(0) && s3.charAt(0) != s1.charAt(0)){
- return isInterleave(s1, s2.substring(1), s3.substring(1));
- }else if(s3.charAt(0) == s1.charAt(0) && s3.charAt(0) == s2.charAt(0)){
- return isInterleave(s1.substring(1), s2, s3.substring(1)) || isInterleave(s1, s2.substring(1), s3.substring(1));
- }else{
- return false;
- }
- }
既然DFS可以,那么BFS也就坐不住了,也要出来浪一波。这里我们需要用队列queue来辅助运算,如果将解法一讲解中的那个二维dp数组列出来的TF图当作一个迷宫的话,那么BFS的目的就是要从(0, 0)位置找一条都是T的路径通到(n1, n2)位置,这里我们还要使用哈希集合,不过此时保存到是已经遍历过的位置,队列中还是存key值,key值的encode方法跟上面DFS解法的相同,初识时放个0进去。然后我们进行while循环,循环条件除了q不为空,还有一个是k小于n3,因为匹配完s3中所有的字符就结束了。然后由于是一层层的遍历,所以要直接循环queue中元素个数的次数,在for循环中,对队首元素进行解码,得到i和j值,如果i小于n1,说明s1还有剩余字符,如果s1当前字符等于s3当前字符,那么把s1的下一个位置i+1跟j一起加码算出key值,如果该key值不在于集合中,则加入集合,同时加入队列queue中;同理,如果j小于n2,说明s2还有剩余字符,如果s2当前字符等于s3当前字符,那么把s2的下一个位置j+1跟i一起加码算出key值,如果该key值不在于集合中,则加入集合,同时加入队列queue中。for循环结束后,k自增1。最后如果匹配成功的话,那么queue中应该只有一个(n1, n2)的key值,且k此时等于n3,所以当queue为空或者k不等于n3的时候都要返回false
https://discuss.leetcode.com/topic/6562/8ms-c-solution-using-bfs-with-explanation
If we expand the two strings s1 and s2 into a chessboard, then this problem can be transferred into a path seeking problem from the top-left corner to the bottom-right corner. The key is, each cell (y, x) in the board corresponds to an interval between y-th character in s1 and x-th character in s2. And adjacent cells are connected with like a grid. A BFS can then be efficiently performed to find the path.
Better to illustrate with an example here:
Say s1 = "aab" and s2 = "abc". s3 = "aaabcb". Then the board looks like
o--a--o--b--o--c--o
| | | |
a a a a
| | | |
o--a--o--b--o--c--o
| | | |
a a a a
| | | |
o--a--o--b--o--c--o
| | | |
b b b b
| | | |
o--a--o--b--o--c--o
Each "o" is a cell in the board. We start from the top-left corner, and try to move right or down. If the next char in s3 matches the edge connecting the next cell, then we're able to move. When we hit the bottom-right corner, this means s3 can be represented by interleaving s1 and s2. One possible path for this example is indicated with "x"es below:
x--a--x--b--o--c--o
| | | |
a a a a
| | | |
o--a--x--b--o--c--o
| | | |
a a a a
| | | |
o--a--x--b--x--c--x
| | | |
b b b b
| | | |
o--a--o--b--o--c--x
Note if we concatenate the chars on the edges we went along, it's exactly s3. And we went through all the chars in s1 and s2, in order, exactly once.
Therefore if we view this board as a graph, such path finding problem is trivial with BFS. I use an
unordered_map
to store the visited nodes, which makes the code look a bit complicated. But a vector
should be enough to do the job.
Although the worse case timeis also O(mn), typically it doesn't require us to go through every node to find a path. Therefore it's faster than regular DP than average.
struct MyPoint {
int y, x;
bool operator==(const MyPoint &p) const {
return p.y == y && p.x == x;
}
};
namespace std {
template <>
struct hash<MyPoint> {
size_t operator () (const MyPoint &f) const {
return (std::hash<int>()(f.x) << 1) ^ std::hash<int>()(f.y);
}
};
}
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
if (s1.size() + s2.size() != s3.size()) return false;
queue<MyPoint> q;
unordered_set<MyPoint> visited;
bool isSuccessful = false;
int i = 0;
q.push(MyPoint { 0, 0 });
q.push(MyPoint { -1, -1 });
while (!(1 == q.size() && -1 == q.front().x)) {
auto p = q.front();
q.pop();
if (p.y == s1.size() && p.x == s2.size()) {
return true;
}
if (-1 == p.y) {
q.push(p);
i++;
continue;
}
if (visited.find(p) != visited.end()) { continue; }
visited.insert(p);
if (p.y < s1.size()) { // down
if (s1[p.y] == s3[i]) { q.push(MyPoint { p.y + 1, p.x }); }
}
if (p.x < s2.size()) { // right
if (s2[p.x] == s3[i]) { q.push(MyPoint { p.y, p.x + 1 }); }
}
}
return false;
}
};
Imagine a grid, which x-axis and y-axis are s1 and s2, matching s3 is the same as
finding a path from (0,0) to (len1, len2). It actually becomes a
BFS on grid. Since we don't need exact paths, a HashSet of
coordinates is used to eliminate duplicated paths.
finding a path from (0,0) to (len1, len2). It actually becomes a
BFS on grid. Since we don't need exact paths, a HashSet of
coordinates is used to eliminate duplicated paths.
public boolean isInterleave(String s1, String s2, String s3) {
int len1 = s1.length(),
len2 = s2.length(),
len3 = s3.length();
if (len1 + len2 != len3) return false;
Deque<Integer> queue = new LinkedList<>();
int matched = 0;
queue.offer(0);
Set<Integer> set = new HashSet<>();
while (queue.size() > 0 && matched < len3) {
int size = queue.size();
for (int i = 0; i < size; i++) {
int p1 = queue.peek() / len3,
p2 = queue.peek() % len3;
queue.poll();
if (p1 < len1 && s1.charAt(p1) == s3.charAt(matched)) {
int key = (p1 + 1) * len3 + p2;
if (!set.contains(key)) {
set.add(key);
queue.offer(key);
}
}
if (p2 < len2 && s2.charAt(p2) == s3.charAt(matched)) {
int key = p1 * len3 + (p2 + 1);
if (!set.contains(key)) {
set.add(key);
queue.offer(key);
}
}
}
matched++;
}
return queue.size() > 0 && matched == len3;
}
def isInterleave(self, s1, s2, s3):
r, c, l= len(s1), len(s2), len(s3)
if r+c != l:
return False
queue, visited = [(0, 0)], set((0, 0))
while queue:
x, y = queue.pop(0)
if x+y == l:
return True
if x+1 <= r and s1[x] == s3[x+y] and (x+1, y) not in visited:
queue.append((x+1, y)); visited.add((x+1, y))
if y+1 <= c and s2[y] == s3[x+y] and (x, y+1) not in visited:
queue.append((x, y+1)); visited.add((x, y+1))
return False
X. DFS + cache
public boolean is_Interleave(String s1, int i, String s2, int j, String s3, int k, int[][] memo) {
if (i == s1.length()) {
return s2.substring(j).equals(s3.substring(k));
}
if (j == s2.length()) {
return s1.substring(i).equals(s3.substring(k));
}
if (memo[i][j] >= 0) {
return memo[i][j] == 1 ? true : false;
}
boolean ans = false;
if (s3.charAt(k) == s1.charAt(i) && is_Interleave(s1, i + 1, s2, j, s3, k + 1, memo)
|| s3.charAt(k) == s2.charAt(j) && is_Interleave(s1, i, s2, j + 1, s3, k + 1, memo)) {
ans = true;
}
memo[i][j] = ans ? 1 : 0;
return ans;
}
public boolean isInterleave(String s1, String s2, String s3) {
int memo[][] = new int[s1.length()][s2.length()];
for (int i = 0; i < s1.length(); i++) {
for (int j = 0; j < s2.length(); j++) {
memo[i][j] = -1;
}
}
return is_Interleave(s1, 0, s2, 0, s3, 0, memo);
}
https://leetcode.com/articles/interleaving-strings/Time complexity : . is the length of and is the length of .
public boolean is_Interleave(String s1, int i, String s2, int j, String res, String s3) {
if (res.equals(s3) && i == s1.length() && j == s2.length())
return true;
boolean ans = false;
if (i < s1.length())
ans |= is_Interleave(s1, i + 1, s2, j, res + s1.charAt(i), s3);
if (j < s2.length())
ans |= is_Interleave(s1, i, s2, j + 1, res + s2.charAt(j), s3);
return ans;
}
public boolean isInterleave(String s1, String s2, String s3) {
return is_Interleave(s1, 0, s2, 0, "", s3);
}
https://www.cnblogs.com/higerzhang/p/4122234.html
然后就想用动态规划了,一开始我定义dp[i][j][k],表示s1的前i个和s2的前j个是否和s3的前k个匹配。bool型的。如下:
后面觉得不要用三维可以,可以把dp的第三维度干掉。dp[i][j]就表示s1的前i个和s2的前j个是否和s3的前i+j个匹配
bool isInterleave(string s1, string s2, string s3) { int len1 = s1.size(), len2 = s2.size(), len3 = s3.size(); if (len1 == 0) return s2 == s3; if (len2 == 0) return s1 == s3; if (len1 + len2 != len3) return false; bool dp[len1+1][len2+1][len3+1]; for (int i = 1; i <= len3; ++i) { if (i <= len1) dp[i][0][i] = s1[i-1] == s3[i-1]; if (i <= len2) dp[0][i][i] = s2[i-1] == s3[i-1]; } for (int i = 1; i <= len1; ++i) for (int j = 1; j <= len2; ++j) { int k = i + j; if (s1[i-1] == s3[k-1] && s1[i-1] != s2[j-1]) dp[i][j][k] = dp[i-1][j][k-1]; else if (s2[j-1] == s3[k-1] && s1[i-1] != s2[j-1]) dp[i][j][k] = dp[i][j-1][k-1]; else if (s1[i-1] == s2[j-1] && s1[i-1] == s3[k-1]) dp[i][j][k] = (dp[i][j-1][k-1] || dp[i-1][j][k-1]); else dp[i][j][k] = 0; } return dp[len1][len2][len3]; }
f[i][j]
表示第一个 sequence 的前 i 个, 与第二个 sequence 的前 j 个的关系方程:
f[i][j]
第 i 个和第 j 个的匹配关系初始化:
f[i][0]
和f[0][j]
答案:
f[m][n]