解题关键:
理解结构特征,抽象出状态,写成状态转移方程。
题目索引
1.三角形找一条从顶到底的最小路径
分析
设状态为 f (i; j ),表示从从位置 (i; j ) 出发,路径的最小和,则状态转移方程为
f(i,j)=min{f(i+1,j),f(i+1,j+1)}+(i,j)
2.最大子数组和
设状态为 f[j],表示以 S[j] 结尾的最大连续子序列和,状态转移方程如下:
f=max(f+A[i],A[i]);//对于数组里的一个整数,它只有两种 选择:1、加入之前的 SubArray;2. 自己另起一个 SubArray。
maxsum=max(maxsum,f);// 求字串中最大的
3.回文最小划分次数
对输入的字符串划分为一组回文字符串,最小的分割次数
所以要转换成一维 DP。如果每次,从 i 往右扫描,每找到一个回文就算一次 DP 的话,就可以
转换为 f(i)= 区间 [i, n-1] 之间最小的 cut 数,n 为字符串长度,则状态转移方程为
4.最佳时间买卖股票
设状态f(i)表示区间[0,i-1]上的最大利润,设置状态g(i),表示区间[i,n-1]上最大利润。
则最大利润为max{f(i)+g(i)};允许在一天内买进又卖出,相当于不交易,因为题目的规定是最多两次,而不是一定要两次
5. 判断字符串s3是否由s1,s2交叉存取组成
设状态 f[i][j],表示 s1[0,i] 和 s2[0,j],匹配 s3[0, i+j]。如果 s1 的最后一个字符等 于 s3 的最后一个字符,则
f[i][j]=f[i-1][j];
如果 s2 的最后一个字符等于 s3 的最后一个字符, 则
f[i][j]=f[i][j-1]。
因此状态转移方程如下:
f[i][j] = (s1[i - 1] == s3 [i + j - 1] && f[i - 1][j]) || (s2[j - 1] == s3 [i + j - 1] && f[i][j - 1]);
6.给定一个矩形表格,求从顶到底的最小和
Minimum Path Sum
设状态为 f[i][j],表示从起点 (0; 0) 到达 (i; j ) 的最小路径和,则状态转移方程为:
f[i][j]=min(f[i-1][j], f[i][j-1])+grid[i][j]
7.使两个字符串相等,最小的编辑次数
Edit Distance
设状态为 f[i][j],表示 A[0,i] 和 B[0,j] 之间的最小编辑距离。设 A[0,i] 的形式是
str1c,B[0,j] 的形式是 str2d,
1. 如果 c==d,则 f[i][j]=f[i-1][j-1];
2. 如果 c!=d,
(a) 如果将 c 替换成 d,则 f[i][j]=f[i-1][j-1]+1;
(b) 如果在 c 后面添加一个 d,则 f[i][j]=f[i][j-1]+1;
(c) 如果将 c 删除,则 f[i][j]=f[i-1][j]+1;
8.给定一串数字,1对应A,2对应B,26对应Z,求有多少种解码方式
Decode Ways
和爬楼梯问题一样,
设 f (n) 表示爬 n 阶楼梯的不同方法数,为了爬到第 n 阶楼梯,有两个选择:
• 从第 n-1 阶前进 1 步;
• 从第 n-2 阶前进 2 步;
因此,有 f (n) = f (n-1) + f (n-2)。 这是一个斐波那契数列。
9. 不同的子序列Distinct Subsequences
给定2个字符串a, b,求b在a中出现的次数。要求可以是不连续的,但是b在a中的顺序必须和b以前的一致。
Here is an example: S = "rabbbit", T = "rabbit"
Return 3.
类似于数字分解的题目。dp[i][j]表示:b的前j个字符在a的前i个字符中出现的次数。
如果S[i]==T[j],那么dp[i][j] = dp[i-1][j-1] + dp[i-1][j]。
意思是:如果当前S[i]==T[j],那么当前这个字母即可以保留也可以抛弃,所以变换方法等于保留这个字母的变换方法加上不用这个字母的变换方法。
如果S[i]!=T[i],那么dp[i][j] = dp[i-1][j],
意思是如果当前字符不等,那么就只能抛弃当前这个字符
递归公式中用到的dp[0][0] = 1,dp[i][0] = 0(把任意一个字符串变换为一个空串只有一个方法
10.单词分解Word Break
字符串是否可以分解为给定的单词
For example, given
s = "leetcode",
dict = ["leet", "code"].
dp[i] 表示源串的前i个字符可以满足分割,那么 dp[ j ] 满足分割的条件是存在k 使得 dp [k] && substr[k,j]在字典里。
真题:
1.Triangle
Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.
For example, given the following triangle
[ [2], [3,4], [6,5,7], [4,1,8,3] ]
The minimum path sum from top to bottom is 11
(i.e., 2 + 3 + 5 + 1 = 11).
Note:
Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle.
Find the contiguous subarray within an array (containing at least one number) which has the largest sum.
For example, given the array [−2,1,−3,4,−1,2,1,−5,4]
,
the contiguous subarray [4,−1,2,1]
has the largest sum = 6
.
设状态为 f[j],表示以 S[j] 结尾的最大连续子序列和,状态转移方程如下:
f=max(f+A[i],A[i]);//对于数组里的一个整数,它只有两种 选择:1、加入之前的 SubArray;2. 自己另起一个 SubArray。
maxsum=max(maxsum,f);// 求字串中最大的
代码:
class Solution {
public:
int maxSubArray(int A[], int n) {
if(0==n) return 0;
int f=0;//f[j],表示以 A[j] 结尾的最大连续子序列和
int maxsum=A[0];
for(int i=0;i<n;++i)
{
f=max(f+A[i],A[i]);//是否需要另起一个字串,如果之前的对我没贡献,还不如另起一个字串。
maxsum=max(maxsum,f); //字串中最大的
}
return maxsum;
}
};
3.Palindrome Partitioning II
Given a string s, partition s such that every substring of the partition is a palindrome.
Return the minimum cuts needed for a palindrome partitioning of s.
For example, given s = "aab"
,
Return 1
since the palindrome partitioning ["aa","b"]
could be produced using 1 cut.
题意分析: 对输入的字符串划分为一组回文字符串,最小的分割次数
分析
定义状态 f(i,j) 表示区间 [i,j] 之间最小的 cut 数,则状态转移方程为
这是一个二维函数,实际写代码比较麻烦。
所以要转换成一维 DP。如果每次,从 i 往右扫描,每找到一个回文就算一次 DP 的话,就可以
转换为 f(i)= 区间 [i, n-1] 之间最小的 cut 数,n 为字符串长度,则状态转移方程为
一个问题出现了,就是如何判断 [i,j] 是否是回文?每次都从 i 到 j 比较一遍?太浪费了,这 里也是一个 DP 问题。
定义状态 P[i][j] = true if [i,j] 为回文,那么
P[i][j] = str[i] == str[j] && P[i+1][j-1]
代码
// LeetCode, Palindrome Partitioning II
// 时间复杂度 O(n^2),空间复杂度 O(n^2)
class Solution {
public:
int minCut(string s)
{
const int n = s.size();
int f[n+1];
bool p[n][n];
fill_n(&p[0][0], n * n, false);
//the worst case is cutting by each char
for (int i = 0; i <= n; i++)
f[i] = n - 1 - i; // 最后一个 f[n]=-1
for (int i = n - 1; i >= 0; i--)
{
for (int j = i; j < n; j++)
{
if (s[i] == s[j] && (j - i < 2 || p[i + 1][j - 1]))
{
p[i][j] = true;
f[i] = min(f[i], f[j + 1] + 1);
}
}
}
return f[0];
}
}
4.Maximal Rectangle
描述
Given a 2D binary matrix filled with 0’s and 1’s, find the largest rectangle containing all ones and return
its area.
题目就是给一个矩阵,找一个全是一的最大子矩阵。
Say you have an array for which the ith element is the price of a given stock on day i.
Design an algorithm to find the maximum profit. You may complete at most two transactions.
Note:
You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).
分析:
设状态f(i)表示区间[0,i-1]上的最大利润,设置状态g(i),表示区间[i,n-1]上最大利润。
则最大利润为max{f(i)+g(i)};允许在一天内买进又卖出,相当于不交易,因为题目的规定是最多两次,而不是一定要两次。
代码
// LeetCode, Best Time to Buy and Sell Stock III
// 时间复杂度 O(n),空间复杂度 O(n)
class Solution {
public:
int maxProfit(vector<int>& prices)
{
if (prices.size() < 2) return 0;
const int n = prices.size();
vector<int> f(n, 0);
vector<int> g(n, 0);
for (int i = 1, valley = prices[0]; i < n; ++i) {
valley = min(valley, prices[i]);
f[i] = max(f[i - 1], prices[i] - valley);
}
for (int i = n - 2, peak = prices[n - 1]; i >= 0; --i) {
peak = max(peak, prices[i]);
g[i] = max(g[i], peak - prices[i]);
}
int max_profit = 0;
for (int i = 0; i < n; ++i)
max_profit = max(max_profit, f[i] + g[i]);
return max_profit;
}
};
6.Interleaving String
Given s1, s2, s3, find whether s3 is formed by the interleaving of s1 and s2.
For example,
Given:
s1 = "aabcc"
,
s2 = "dbbca"
,
When s3 = "aadbbcbcac"
, return true.
When s3 = "aadbbbaccc"
, return false.
分析:判断字符串s3是否由s1,s2交叉存取组成
设状态 f[i][j],表示 s1[0,i] 和 s2[0,j],匹配 s3[0, i+j]。如果 s1 的最后一个字符等 于 s3 的最后一个字符,则
f[i][j]=f[i-1][j];
如果 s2 的最后一个字符等于 s3 的最后一个字符, 则
f[i][j]=f[i][j-1]。
因此状态转移方程如下:
f[i][j] = (s1[i - 1] == s3 [i + j - 1] && f[i - 1][j]) || (s2[j - 1] == s3 [i + j - 1] && f[i][j - 1]);
1 class Solution { 2 private: 3 bool f[1000][1000]; 4 public: 5 bool isInterleave(string s1, string s2, string s3) { 6 // Start typing your C/C++ solution below 7 // DO NOT write int main() function 8 if (s1.size() + s2.size() != s3.size()) 9 return false; 10 11 f[0][0] = true; 12 for(int i = 1; i <= s1.size(); i++) 13 f[i][0] = f[i-1][0] && (s3[i-1] == s1[i-1]); 14 15 for(int j = 1; j <= s2.size(); j++) 16 f[0][j] = f[0][j-1] && (s3[j-1] == s2[j-1]); 17 18 for(int i = 1; i <= s1.size(); i++) 19 for(int j = 1; j <= s2.size(); j++) 20 f[i][j] = (f[i][j-1] && s2[j-1] == s3[i+j-1]) || (f[i-1][j] && s1[i-1] == s3[i+j-1]); 21 22 return f[s1.size()][s2.size()]; 23 } 24 };
动规 + 滚动数组
// LeetCode, Interleaving String
// 二维动规 + 滚动数组,时间复杂度 O(n^2),空间复杂度 O(n)
class Solution {
public:
bool isInterleave(string s1, string s2, string s3)
{
if (s1.length() + s2.length() != s3.length())
return false;
if (s1.length() < s2.length())
return isInterleave(s2, s1, s3);
vector<bool> f(s2.length() + 1, true);
for (size_t i = 1; i <= s2.length(); ++i)
f[i] = s2[i - 1] == s3[i - 1] && f[i - 1];
for (size_t i = 1; i <= s1.length(); ++i)
{
f[0] = s1[i - 1] == s3[i - 1] && f[0];
for (size_t j = 1; j <= s2.length(); ++j)
f[j] = (s1[i - 1] == s3[i + j - 1] && f[j]) || (s2[j - 1] == s3[i + j - 1] && f[j - 1]);
}
return f[s2.length()];
}
};
7.Scramble String(混杂字符串)
Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrings recursively.
Below is one possible representation of s1 = "great"
:
great / \ gr eat / \ / \ g r e at / \ a t
To scramble the string, we may choose any non-leaf node and swap its two children.
For example, if we choose the node "gr"
and swap its two children, it produces a scrambled string "rgeat"
.
rgeat / \ rg eat / \ / \ r g e at / \ a t
We say that "rgeat"
is a scrambled string of "great"
.
Similarly, if we continue to swap the children of nodes "eat"
and "at"
, it produces a scrambled string "rgtae"
.
rgtae / \ rg tae / \ / \ r g ta e / \ t a
We say that "rgtae"
is a scrambled string of "great"
.
Given two strings s1 and s2 of the same length, determine if s2 is a scrambled string of s1.
8.Minimum Path Sum
描述
Given a m n grid filled with non-negative numbers, find a path from top left to bottom right which
minimizes the sum of all numbers along its path.
Note: You can only move either down or right at any point in time
分析:
设状态为 f[i][j],表示从起点 (0; 0) 到达 (i; j ) 的最小路径和,则状态转移方程为:
f[i][j]=min(f[i-1][j], f[i][j-1])+grid[i][j]
代码:
备忘录法
// LeetCode, Minimum Path Sum
// 备忘录法
class Solution
{
public:
int minPathSum(vector<vector<int> > &grid)
{
const int m = grid.size();
const int n = grid[0].size();
this->f = vector<vector<int> >(m, vector<int>(n, -1));
return dfs(grid, m-1, n-1);
}
private:
vector<vector<int> > f; // 缓存
int dfs(const vector<vector<int> > &grid, int x, int y)
{
if (x < 0 || y < 0) return INT_MAX; // 越界,终止条件,注意,不是 0
if (x == 0 && y == 0) return grid[0][0]; // 回到起点,收敛条件
return min(getOrUpdate(grid, x - 1, y),
getOrUpdate(grid, x, y - 1)) + grid[x][y];
}
int getOrUpdate(const vector<vector<int> > &grid, int x, int y)
{
if (x < 0 || y < 0)
return INT_MAX; // 越界,注意,不是 0
if (f[x][y] >= 0)
return f[x][y];
else
return f[x][y] = dfs(grid, x, y);
}
};
动规
// LeetCode, Minimum Path Sum
// 二维动规
class Solution
{
public:
int minPathSum(vector<vector<int> > &grid)
{
if (grid.size() == 0) return 0;
const int m = grid.size();
const int n = grid[0].size();
int f[m][n];
f[0][0] = grid[0][0];
for (int i = 1; i < m; i++)
{
f[i][0] = f[i - 1][0] + grid[i][0];
}
for (int i = 1; i < n; i++)
{
f[0][i] = f[0][i - 1] + grid[0][i];
}
for (int i = 1; i < m; i++)
{
for (int j = 1; j < n; j++)
{
f[i][j] = min(f[i - 1][j], f[i][j - 1]) + grid[i][j];
}
}
return f[m - 1][n - 1];
}
};
动规 + 滚动数组
// LeetCode, Minimum Path Sum
// 二维动规 + 滚动数组
class Solution {
public:
int minPathSum(vector<vector<int> > &grid)
{
const int m = grid.size();
const int n = grid[0].size();
int f[n];
fill(f, f+n, INT_MAX); // 初始值是 INT_MAX,因为后面用了 min 函数。
f[0] = 0;
for (int i = 0; i < m; i++)
{
f[0] += grid[i][0];
for (int j = 1; j < n; j++)
{
// 左边的 f[j],表示更新后的 f[j],与公式中的 f[i[[j] 对应
// 右边的 f[j],表示老的 f[j],与公式中的 f[i-1][j] 对应
f[j] = min(f[j - 1], f[j]) + grid[i][j];
}
}
return f[n - 1];
}
};
描述
Given two words word1 and word2, find the minimum number of steps required to convert word1 to
word2. (each operation is counted as 1 step.)
You have the following 3 operations permitted on a word:
• Insert a character
• Delete a character
• Replace a character
分析
设状态为 f[i][j],表示 A[0,i] 和 B[0,j] 之间的最小编辑距离。设 A[0,i] 的形式是
str1c,B[0,j] 的形式是 str2d,
1. 如果 c==d,则 f[i][j]=f[i-1][j-1];
2. 如果 c!=d,
(a) 如果将 c 替换成 d,则 f[i][j]=f[i-1][j-1]+1;
(b) 如果在 c 后面添加一个 d,则 f[i][j]=f[i][j-1]+1;
(c) 如果将 c 删除,则 f[i][j]=f[i-1][j]+1;
动规
// LeetCode, Edit Distance
// 二维动规,时间复杂度 O(n*m),空间复杂度 O(n*m)
class Solution {
public:
int minDistance(const string &word1, const string &word2) {
const size_t n = word1.size();
const size_t m = word2.size();
// 长度为 n 的字符串,有 n+1 个隔板
int f[n + 1][m + 1];
for (size_t i = 0; i <= n; i++)
f[i][0] = i;
for (size_t j = 0; j <= m; j++)
f[0][j] = j;
for (size_t i = 1; i <= n; i++) {
for (size_t j = 1; j <= m; j++) {
if (word1[i - 1] == word2[j - 1])
f[i][j] = f[i - 1][j - 1];
else {
int mn = min(f[i - 1][j], f[i][j - 1]);
f[i][j] = 1 + min(f[i - 1][j - 1], mn);
}
}
}
return f[n][m];
}
};
动规 + 滚动数组
// LeetCode, Edit Distance
// 二维动规 + 滚动数组
// 时间复杂度 O(n*m),空间复杂度 O(n)
class Solution {
public:
int minDistance(const string &word1, const string &word2) {
if (word1.length() < word2.length())
return minDistance(word2, word1);
int f[word2.length() + 1];
int upper_left = 0; // 额外用一个变量记录 f[i-1][j-1]
for (size_t i = 0; i <= word2.size(); ++i)
f[i] = i;
for (size_t i = 1; i <= word1.size(); ++i) {
upper_left = f[0];
f[0] = i;
for (size_t j = 1; j <= word2.size(); ++j) {
int upper = f[j];
if (word1[i - 1] == word2[j - 1])
f[j] = upper_left;
else
f[j] = 1 + min(upper_left, min(f[j], f[j - 1]));
upper_left = upper;
}
}
return f[word2.length()];
}
};
10 Decode Ways
描述
A message containing letters from A-Z is being encoded to numbers using the following mapping:
'A' -> 1
'B' -> 2
...
'Z' -> 26
Given an encoded message containing digits, determine the total number of ways to decode it.
For example, Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12).
The number of ways decoding "12" is 2
分析 :
和爬楼梯问题一样,
设 f (n) 表示爬 n 阶楼梯的不同方法数,为了爬到第 n 阶楼梯,有两个选择:
• 从第 n-1 阶前进 1 步;
• 从第 n-2 阶前进 2 步;
因此,有 f (n) = f (n-1) + f (n-2)。 这是一个斐波那契数列。
这里也一样,多一些约束条件而已。判断两个数字时,是否小于26
代码
// LeetCode, Decode Ways
// 动规,时间复杂度 O(n),空间复杂度 O(1)+滚动数组
class Solution {
public:
int numDecodings(const string &s) {
if (s.empty() || s[0] == '0') return 0;
int prev = 0;//f(0)=0
int cur = 1;//f(1)=1
// 长度为 n 的字符串,有 n+1 个阶梯
for (size_t i = 1; i <= s.size(); ++i) {
if (s[i-1] == '0')
cur = 0;
if (i < 2 || !(s[i - 2] == '1' || (s[i - 2] == '2' && s[i - 1] <= '6')))
prev = 0;
int tmp = cur;
cur = prev + cur;//f(i)=f(i-2)+f(i-1)
prev = tmp;
}
return cur;
}
};
11 Distinct Subsequences(不同的子序列)
分析
类似于数字分解的题目。dp[i][j]表示:b的前j个字符在a的前i个字符中出现的次数。
递归公式中用到的dp[0][0] = 1,dp[i][0] = 0(把任意一个字符串变换为一个空串只有一个方法
代码
// LeetCode, Distinct Subsequences
// 二维动规 + 滚动数组
// 时间复杂度 O(m*n),空间复杂度 O(n)
class Solution {
public:
int numDistinct(const string &S, const string &T) {
vector<int> f(T.size() + 1);
f[0] = 1;
for (int i = 0; i < S.size(); ++i) {
for (int j = T.size() - 1; j >= 0; --j) {
f[j + 1] += S[i] == T[j] ? f[j] : 0;
}
}
return f[T.size()];
}
};
描述
Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated
sequence of one or more dictionary words.
For example, given
s = "leetcode",
dict = ["leet", "code"].
Return true because "leetcode" can be segmented as "leet code".
分析:
Given a string s and a dictionary of words dict, add spaces in s to construct a sentence where each word is a valid dictionary word.
Return all such possible sentences.
For example, given
s = "catsanddog"
,
dict = ["cat", "cats", "and", "sand", "dog"]
.
A solution is ["cats and dog", "cat sand dog"]
.
跟第一题一样,就是要返回所有可能的切分, 做切分反应是用回溯,但是不剪枝肯定要超时。
这里用了一个math[i][j] 来表示 i--j 这一段是否可以切分,然后在dfs的时候利用看最后剩余的子串能否切分来剪枝