目录
一、概念
二、Fibonacci
三、字符串分割
四、三角矩阵
五、路径总数
六、最小路径和
七、背包问题
八、回文串分割
九、编辑距离
十、不同子序列
DP定义
动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无
在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果
动态规划具备了以下三个特点
动态规划问题一般从以下四个角度考虑
动态规划的本质:对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)
状态定义的要求:定义的状态一定要形成递推关系
适用场景:最大值/最小值, 可不可行, 是不是,方案个数
难度:Easy
状态:F(n)
状态转移方程:F(n) = F(n - 1) + F(n - 2)
初始值:F(1) = F(2) = 1
返回值:F(n)
#include
class Solution {
public:
int Fibonacci(int n)
{
std::vector v(40);
v[1] = v[2] = 1;
for(int i = 3;i <= n; ++i) {
v[i] = v[i - 1] + v[i - 2];
}
return v[n];
}
};
上述解法的空间复杂度为O(n)
其实F(n)只与相邻的前两项有关,没有必要保存所有子问题的解,只保存两个子问题的解即可
下面方法的空间复杂度将为O(1)
class Solution {
public:
int Fibonacci(int n) {
if(n == 1 || n == 2) return 1;
int Fib1 = 1,Fib2 = 1;
int ret = 0;
for(int i = 3;i <= n; ++i) {
ret = Fib1 + Fib2;
Fib1 = Fib2;
Fib2 = ret;
}
return ret;
}
};
难度:Medium
状态:
子状态:前1,2,3,...,n个字符能否根据词典中的词被成功分词
F(i):前i个字符能否根据词典中的词被成功分词
状态递推:
F(i):true{j < i && F(j) && substr[j+1,i]能在词典中找到} OR false
在j小于i中,只要能找到一个F(j)为true,并且从j+1到i之间的字符能在词典中找到,则F(i)为true
初始值:F(0) = true
返回值:F(n)
#include
#include
#include
using namespace std;
class Solution {
public:
bool wordBreak(string s, unordered_set& dict) {
if (s.empty()) return false;
if (dict.empty()) return false;
vector can_break(s.size() + 1, false);
can_break[0] = true;//初始状态(辅助状态)
for (int i = 1; i <= s.size(); ++i)
{
//j < i && F(j) && [j + 1, i]
for (int j = 0; j < i; ++j)
{
if (can_break[j] && dict.find(s.substr(j, i - j)) != dict.end())
{
can_break[i] = true;
break;
}
}
}
return can_break[s.size()];
}
};
难度:Medium
方法一:正向推导
状态:
子状态:从(0,0)到(1,0),(1,1),(2,0),...(n,n)的最短路径和
F(i,j):从(0,0)到(i,j)的最短路径和
状态转移方程:F(i,j) = min( F(i-1, j-1), F(i-1, j)) + triangle[i][j]
初始值:F(0,0) = triangle[0][0]
返回结果:min(F(n-1, i))
#include
using namespace std;
class Solution {
public:
int minimumTotal(vector >& triangle) {
if (triangle.empty()) return 0;
int row = triangle.size();
for (int i = 1; i < row; ++i) {
for (int j = 0; j <= i; ++j) {
if (j == 0) triangle[i][j] = triangle[i - 1][j] + triangle[i][j];//左边界
else if (j == i) triangle[i][j] = triangle[i - 1][j - 1] + triangle[i][j];//右边界
else triangle[i][j] = min(triangle[i - 1][j], triangle[i - 1][j - 1]) + triangle[i][j];
}
}
int min_sum = triangle[row - 1][0];
for (int j = 1; j < row; ++j) {
min_sum = min(min_sum, triangle[row - 1][j]);
}
return min_sum;
}
};
方法二:反向推导
状态:
子状态:从(n,n),(n,n-1),...(1,0),(1,1),(0,0)到最后一行的最短路径和
F(i,j):从(i,j)到最后一行的最短路径和
状态转移方程:
F(i,j) = min( F(i+1, j), F(i+1, j+1)) + triangle[i][j]
初始值:
F(n-1,0) = triangle[n-1][0] F(n-1,1) = triangle[n-1][1],..., F(n-1,n-1) = triangle[n-1][n-1]
返回结果:
F(0, 0)
这种逆向思维不需要考虑边界,也不需要最后寻找最小值,直接返回F(0,0)即可
#include
using namespace std;
class Solution {
public:
int minimumTotal(vector >& triangle) {
int row = triangle.size();
for (int i = row - 2; i >= 0; --i) {
for (int j = 0; j <= i; ++j) {
triangle[i][j] = min(triangle[i + 1][j], triangle[i + 1][j + 1]) + triangle[i][j];
}
}
return triangle[0][0];
}
};
难度:Easy
状态:
子状态:从(0,0)到达(1,0),(1,1),(2,1),...(m-1,n-1)的路径数
F(i,j):从(0,0)到达F(i,j)的路径数
状态递推:
F(i,j) = F(i-1,j) + F(i,j-1)
初始化:
特殊情况:第0行和第0列 F(0,i) = 1 F(i,0) = 1
返回结果:
F(m-1,n-1)
#include
using namespace std;
class Solution {
public:
int uniquePaths(int m, int n) {
if (m < 1 || n < 1) return 0;
vector> v(m, vector(n, 1));
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
v[i][j] = v[i - 1][j] + v[i][j - 1];
}
}
return v[m - 1][n - 1];
}
};
难度:Medium
状态:
子状态:从(0,0)到达(1,0),(1,1),(2,1),...(m-1,n-1)的最短路径
F(i,j):从(0,0)到达F(i,j)的最短路径
状态转移方程:
F(i,j) = min{ F(i-1,j) , F(i,j-1) } + (i,j)
初始值:
F(0,0) = (0,0) F(0,i) = F(0,i-1) + (0,i) F(i,0) = F(i-1,0) + (i,0) (i > 0)
返回结果:F(m - 1,n - 1)
class Solution {
public:
int minPathSum(vector >& grid) {
if(grid.empty()) return 0;
const int row = grid.size();
const int col = grid[0].size();
for(int i = 1;i < col; ++i) grid[0][i] += grid[0][i - 1];
for(int i = 1;i < row; ++i) grid[i][0] += grid[i - 1][0];
for(int i = 1; i < row; ++i) {
for(int j = 1; j < col; ++j) {
grid[i][j] += min(grid[i - 1][j], grid[i][j - 1]);
}
}
return grid[row - 1][col - 1];
}
};
难度:Medium
状态:F(i, j):前i个物品放入大小为j的背包中所获得的最大价值
状态递推:
对于第i个商品,若装不下:此时的价值与前 i-1 个的价值是一样的 F(i,j) = F(i-1,j)
若可以装入:需要在装或不装中选择最大的
F(i, j) = max{ F(i-1,j), F(i-1, j - A[i]) + V[i] }
F(i-1,j): 表示不把第i个物品放入背包中, 所以价值就是前i-1个物品放入大小为j的背包的最大价值
F(i-1, j - A[i]) + V[i]:表示第i个物品放入背包中,价值增加V[i],但需腾出A[i]的大小放第i个商品
初始化:第0行和第0列都为0,表示没有装物品时的价值都为0 F(0,j) = F(i,0) = 0
返回值:F(n,m)
class Solution {
public:
int backPackII(int m, vector& a, vector& v) {
if (a.empty() || v.empty() || m <= 0) return 0;
const int row = a.size() + 1;
const int col = m + 1;
vector> ret(row);
for (int i = 0; i < row; ++i) {
ret[i].resize(col, 0);
}
for (int i = 1; i < row; ++i) {
for (int j = 1; j < col; ++j) {
if (a[i - 1] > j) ret[i][j] = ret[i - 1][j];
else ret[i][j] = max(ret[i - 1][j], ret[i - 1][j - a[i - 1]] + v[i - 1]);
}
}
return ret[row - 1][col - 1];
}
};
优化版
每一行的结果都从上一行的结果推出,那么就可以优化空间复杂度为O(m),从后向前推导即可
class Solution {
public:
int backPackII(int m, vector& a, vector& v) {
if (a.empty() || v.empty() || m <= 0) return 0;
vector ret(m + 1);
for (int i = 1; i < a.size() + 1; ++i) {
for (int j = m; j >= 1; --j) {
if (a[i - 1] <= j) ret[j] = max(ret[j], ret[j - a[i - 1]] + v[i - 1]);
}
}
return ret[m];
}
};
难度:Hard
状态:
子状态:从第0个到第1,2,3,...,n个字符组成的字符串分割成回文 需要的最小分割数
F(i): 从第0个到第i个字符组成的字符串分割成回文 需要的最小分割数
状态递推:
若从j+1到i为回文字符串,且已经知道从第1个字符到第j个字符的最小切割数,那么只需要再切一次,就可以保证 1->j , j+1 -> i都为回文串
(j < i) && (j+1 -> i)为true的情况下,F(i) = min( F[i], F[j] + 1)
初始化:F(i) = i - 1
单个字符只需要切0次,因为单子符都为回文串,2个字符最大需要1次,3个2次......
返回值:F(n)
class Solution {
public:
bool isPal(string& s, int begin, int end)
{
while (begin < end) {
if (s[begin] != s[end]) return false;
++begin;
--end;
}
return true;
}
int minCut(string s) {
int size = s.size();
if (size == 0 || isPal(s, 0, size - 1)) return 0;
vector ret(size + 1);
for (int i = 1; i <= size; ++i) {
ret[i] = i - 1;
}
for (int i = 2; i <= size; ++i)
{
if (isPal(s, 0, i - 1)) {
ret[i] = 0;
continue;
}
for (int j = 1; j < i; ++j) {
if (isPal(s, j, i - 1)) ret[i] = min(ret[i], ret[j] + 1);
}
}
return ret[size];
}
};
上述方法两次循环时间复杂度是O(n^2),判断回文串时间复杂度是O(n),所以总时间复杂度为O(n^3)。对于过长的字符串,在OJ的时候会出现TLE(Time Limit Exceeded)
判断回文串的方法可以使用动态规划进行优化,使总体时间复杂度为O(n^2)
状态:
子状态:从第一个字符到第二个字符是不是回文串,第1-3,第2-5,...
F(i,j):字符区间 [i,j] 是否为回文串
状态递推:
F(i,j):true->{ s[i]==s[j] && F(i+1,j-1) } OR false
若字符区间首尾字符相同且在去掉区间首尾字符后字符区间仍为回文串,则原字符区间为回文串
从递推公式中可以看到第i处需要用到第i+1处的信息,所以i应该从字符串末尾遍历
初始化:F(i,j) = false
返回结果:矩阵F(n,n), 只更新一半值(i <= j),n^2 / 2
vector> GetMat(string& s) {
int size = s.size();
vector> ret(size, vector(size, false));
for (int i = size - 1; i >= 0; --i) {
for (int j = i; j < size; ++j) {
if (i == j) ret[i][j] = true;
else if (j == i + 1) ret[i][j] = (s[i] == s[j])
else ret[i][j] = ((s[i] == s[j]) && ret[i + 1][j - 1]);
}
}
return ret;
}
难度:Hard
状态:
子状态:word1的前1,2,3,...m个字符转换成word2的前1,2,3,...n个字符需要的编辑距离
F(i,j):word1的前i个字符于word2的前j个字符的编辑距离
状态递推:
F(i,j) = min { F(i-1,j)+ 1,F(i,j-1) + 1,F(i-1,j-1) + (w1[i] == w2[j] ? 0 : 1) }
上式表示从删除,增加和替换操作中选择一个最小操作数
F(i-1,j):w1[1,...,i-1]于w2[1,...,j]的编辑距离,删除w1[i]的字符--->F(i,j)
F(i,j-1):w1[1,...,i]于w2[1,...,j-1]的编辑距离,增加一个字符--->F(i,j)
F(i-1,j-1):w1[1,...,i-1]于w2[1,...,j-1]的编辑距离,若w1[i]与w2[j]相同,不做任何操作,编辑距离不变,若w1[i]与w2[j]不同,替换w1[i]的字符为w2[j]--->F(i,j)
初始值:初始化一定要是确定的值,如果这里不加入空串,初始值无法确定
F(i,0) = i:word与空串的编辑距离,删除操作
F(0,i) = i:空串与word的编辑距离,增加操作
返回值:返回结果:F(m,n)
class Solution {
public:
int minDistance(string word1, string word2) {
int row = word1.size();
int col = word2.size();
vector> ret(row + 1, vector(col + 1));
for (int i = 0; i <= col; ++i) ret[0][i] = i;
for (int i = 0; i <= row; ++i) ret[i][0] = i;
for (int i = 1; i <= row; ++i)
{
for (int j = 1; j <= col; ++j)
{
if (word1[i - 1] == word2[j - 1]) {
ret[i][j] = min(ret[i - 1][j], ret[i][j - 1]) + 1;
ret[i][j] = min(ret[i][j], ret[i - 1][j - 1]);
} else {
ret[i][j] = min(ret[i - 1][j], ret[i][j - 1]) + 1;
ret[i][j] = min(ret[i][j], ret[i - 1][j - 1] + 1);
}
}
}
return ret[row][col];
}
};
难度:Hard
状态:
子状态:S的前1,2,...,m个字符组成的字符串的子串 与T前1,2,...,n个字符组成的字符串相同的个数
F(i,j):S[1:i]中的子串与T[1:j]相同的个数
状态递推:
当S[i] = T[j]时:
若使用S[i]匹配T[j],则F(i,j) = F(i-1,j-1)
若不使用S[i]匹配T[j],则问题就变为S[1:i-1]中的子串与T[1:j]相同的个数,则F(i,j) = F(i-1,j)
故,S[i] == T[j]时,F(i,j) = F(i-1,j-1) + F(i-1,j)
当S[i] != T[j]:问题退化为S[1:i-1]中的子串与T[1:j]相同的个数。S[i] != T[j]时,F(i,j) = F(i-1,j)
初始化:引入空串进行初始化
F(i,0) = 1 S的子串与空串相同的个数,只有空串与空串相同
返回值:F(m,n)
class Solution {
public:
int numDistinct(string S, string T)
{
int row = S.size();
int col = T.size();
if(row < col) return 0;
if(T.empty()) return 1;
vector> ret(row + 1, vector(col + 1, 0));
ret[0][0] = 1;
for(int i = 1; i <= row; ++i)
{
ret[i][0] = 1;
for(int j = 1;j <= col; ++j)
{
if(S[i - 1] == T[j - 1])
ret[i][j] = ret[i - 1][j] + ret[i - 1][j - 1];
else
ret[i][j] = ret[i - 1][j];
}
}
return ret[row][col];
}
};