*62. 不同路径
每个单元格的数值都是有上方和左侧的单元格决定的,因此需要先将最上方和最左侧的单元格初始化。因为题目要求只能向下和向右移动,因此最上方和最左侧的单元格初始化为1。
时间复杂度:
O
(
m
∗
n
)
O(m*n)
O(m∗n)
空间复杂度:
O
(
m
∗
n
)
O(m*n)
O(m∗n)
// c++
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector(n,0));
for(int i=0; i<m; i++) dp[i][0] = 1;
for(int j=0; j<n; j++) dp[0][j] = 1;
for(int i=1; i<m; i++){
for(int j=1; j<n; j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
63. 不同路径 II
和上一题思路相似,在上一题的基础上加入了障碍。dp数组代表到达(i,j)单元格的路径数,障碍位置在dp数组中为0。在初始化第一行和第一列始,若路径中出现障碍,则障碍物后边的单元格都无法到达,对应dp数组值均为0。
时间复杂度:
O
(
m
∗
n
)
O(m*n)
O(m∗n)
空间复杂度:
O
(
m
∗
n
)
O(m*n)
O(m∗n)
// c++
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
vector<vector<int>> dp(obstacleGrid.size(), vector(obstacleGrid[0].size(), 0));
// 标志单元格是否可到达
bool flag = true;
// 初始化第一行
for(int i=0; i<obstacleGrid.size(); i++){
if(obstacleGrid[i][0] == 1) {
// 可以直接break
flag = false;
}
if(flag) dp[i][0] = 1;
else dp[i][0] = 0;
}
// 标志单元格是否可到达
flag = true;
// 初始化第一列
for(int j=0; j<obstacleGrid[0].size(); j++){
if(obstacleGrid[0][j] == 1) {
// 可以直接break
flag = false;
}
if(flag) dp[0][j] = 1;
else dp[0][j] = 0;
}
// 计算dp数组
for(int i=1; i<obstacleGrid.size(); i++){
for(int j=1; j<obstacleGrid[0].size(); j++){
if(obstacleGrid[i][j]==1) continue;
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[obstacleGrid.size()-1][obstacleGrid[0].size()-1];
}
};
343. 整数拆分
动态规划的思路。dp数组的第k个位置存储k拆分后的最大乘积。在分析题目的过程中,可以发现在找最大乘积的拆分的时候类似于把一个数字拆分成一颗二叉树,父节点是左右子节点的和。在列举2-10的最大乘积的过程中就可以发现一个规律:最大乘积总是出现在中点附近。
什么意思呢?下面是几个例子:
- n=2, 最大乘积的拆分是1*1=1,拆出来的1是2的中点,即n/2
- n=3, 最大乘积的拆分是1*2=2,拆出来的1是3的整数中点,即n/2
- n=4, 最大乘积的拆分是2*2=4,拆出来的2是4的中点,即n/2
- n=5, 最大乘积的拆分是2*3=6,拆出来的2是5的整数中点,即n/2
- n=6,最大乘积的拆分是3*3=9,拆出来的3是6的中点,即n/2
- n=7,最大乘积的拆分是3*4=12,拆出来的3是7的整数中点,即n/2(这里拆成2和5再把5拆成2和3也是最大乘积)
- n=8,最大乘积的拆分是332=18,拆出来的是3和5,5再拆3和2,第一个拆出来的(3,5)中的3是中点前一个位置,即n/2-1
- n=9,最大乘积的拆分是333=27,拆出来的是3和6,6在拆3和3,第一个拆出来的(3,6)中的3是中点前一个位置,即n/2-1
通过上面的例子就可以清晰的看出规律了。最大乘积出现在中点或中点前一个位置,而后面的计算会用到前面已经计算出来的最大乘积。因此,代码的逻辑如下:
- 首先初始化dp数组为0。
- 从2开始计算每个位置的最大乘积。在计算当前位置乘积时,需要借助前面已经计算出来的乘积。
- 计算当前位置时分别求出从中点拆分和从中点前一个位置拆分的最大乘积,取二者最大值存入当前位置。
tips:需要注意的是,在取前面以及计算出来的dp值时需要比较当前值和拆分以后的值哪一个更大,要取大的那个才能获得最大乘积,若拆分后的值小于当前值则不必拆分,例如,2拆分后最大乘积为1,因此当有元素拆分出2之后就没有必要继续往下拆分了。
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
n
)
O(n)
O(n)
// c++
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n+1, 0);
for(int i=2; i<=n; i++){
int mid = i/2;
int left = mid-1;
int result1 = max(dp[mid], mid)*max(dp[i-mid], i-mid);
int result2 = max(dp[left], left)*max(dp[i-left], i-left);
dp[i] = result1>result2?result1:result2;
}
return dp[n];
}
};
本题还有一种解法就是尽可能多的拆分出3既可求得最大乘积,但需要数学证明。
96. 不同的二叉搜索树
不同的输入序列就会生成不同的二叉搜索树。
n个字符的不同排列方式有
1
n
+
1
C
2
n
n
\frac{1}{n+1}C_{2n}^{n}
n+11C2nn种。直接计算既可。
tips:在计算乘积的过程中为防止溢出,要在能整除的时候直接整除,不要留到最后再一起除。
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
n
)
O(n)
O(n)
// c++
class Solution {
public:
int cal(int n){
int k = 2*n;
long long result = 1;
long long sub = 1;
for(int i=1; i<=n; i++){
result*=k;
if(result%i==0) result/=i;
else sub*=i;
k--;
}
result/=sub;
result/=n+1;
return (int)result;
}
int numTrees(int n) {
return cal(n);
}
};