编程总结
每每刷完一道题后,其思想和精妙之处没有地方记录,本篇博客用以记录刷题过程中的遇到的算法和技巧
首先想到的是递归法;
记忆化搜索,自顶向下的解决问题。
动态规划,自下而上的解决问题;
70. 爬楼梯
手法1. 首先容易想到的是使用递归来求解,但递归的效率很低
// 递归
int climbStairs(int n) {
if (n <= 2) {
return n;
}
return climbStairs(n - 1) + climbStairs(n - 2);
}
其实记忆化搜索就是在递归的条件上,为减少递归次数而产生的
比如下述代码中对于 mem[n] !=0 直接return memo[n].
我们总是习惯自顶向下思考问题,而不容易自底向上思考问题
手法2:记忆化搜索 – 自顶向下
// 记忆化搜索 -- 自顶往下
int memo[64] = { 0 };
int climbStairs(int n) {
if (n <= 2) {
return n;
}
// 如果满足条件则直接返回记忆数组里的值,减少递归次数
if (n < 64 && memo[n] != 0) {
return memo[n];
}
// 不满足条件才进行递归 O(n)
memo[n] = climbStairs(n-1) + climbStairs(n-2);
return memo[n];
}
手法3:动态规划 – 自底往上
// 动态规划 -- 自底往上
int climbStairs(int n) {
if (n <= 2) {
return n;
}
int a1 = 1;
int a2 = 2;
int memo = 0;
// 自下而上的进行计算,动态规划
for (int i = 3; i <= n; i++) {
memo = a1 + a2;
a1 = a2;
a2 = memo;
}
return memo;
}
343. 整数拆分
将 i 拆分成 j 和 i−j 的和,且 i−j 不再拆分成多个正整数,此时的乘积是 j * (i-j);
将 i 拆分成 j 和 i−j 的和,且 i−j 继续拆分成多个正整数,此时的乘积是 j * dp[i−j];
// 记忆化搜索 -- 自顶往下
int memo[64] = {0}; // memo[i]表示将i分割后可以获得的最大乘积
int integerBreak(int n) {
int res = -1;
if (n == 1 || n == 2) { // memo[1] || memo[2] == 1
return 1;
}
if (memo[n] != 0) {
return memo[n];
}
// res i*(n-i) i*integerBreak(n-i)
// 1 1*2 1*1
// 2 2*1 2*1
for (int i = 1; i <= n-1; i++) { // memo[3] == 2
res = max3(res, i * (n-i), i * integerBreak(n - i));
}
memo[n] = res;
return res;
}
// 动态规划-- 自底往上
int memo[64] = {0}; // memo[i]表示将i分割后可以获得的最大乘积
int integerBreak(int n) {
memo[1] = 1;
memo[2] = 1;
for(int i = 3; i <= n; i++){
// 求解memo[i]
for(int j = 1; j <= i - 1; j++){
// 将i分割成 j+(i-j), memo[i-j]为i-j可以获得的最大乘积
memo[i] = max3( memo[i], j*memo[i-j], j*(i-j) );
}
}
return memo[n];
}
198. 打家劫舍
如果房屋数量大于两间,应该如何计算能够偷窃到的最高总金额呢?对于第 k (k>2)间房屋,有两个选项:
- 偷窃第 k 间房屋,那么就不能偷窃第 k−1 间房屋,偷窃总金额为前 k−2 间房屋的最高总金额与第 k 间房屋的金额之和。
- 不偷窃第 k 间房屋,偷窃总金额为前 k−1 间房屋的最高总金额。
在两个选项中选择偷窃总金额较大的选项,该选项对应的偷窃总金额即为前 k 间房屋能偷窃到的最高总金额。
dp[i] 表示前 i 间房屋能偷窃到的最高总金额,那么就有如下的状态转移方程:
int rob(int *nums, int numsSize) {
if (numsSize == 1) {
return nums[0];
}
int dp[numsSize+1];
dp[0] = nums[0];
dp[1] = fmax(nums[0], nums[1]);
for(int i = 2; i < numsSize; i++) {
// 偷窃第k间房屋,那么就不能偷窃第k−1间房屋,总金额为前k−2间房屋的最高总金额与第k间房屋的金额之和
// 不偷窃第k间房屋,偷窃总金额为前k−1间房屋的最高总金额
dp[i] = fmax(dp[i-2] + nums[i], dp[i-1]);
}
return dp[numsSize - 1];
}
64. 最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
#define ROWS 200
#define COLUMNS 200
int minPathSum(int **grid, int gridSize, int *gridColSize)
{
int rows = gridSize, columns = gridColSize[0];
int dp[ROWS][COLUMNS];
if (rows == 0 || columns == 0) {
return 0;
}
dp[0][0] = grid[0][0];
// 1.处理第一行
for (int i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 2.处理第一列
for (int j = 1; j < columns; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 3.处理中间部分
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
dp[i][j] = fmin(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[rows - 1][columns - 1];
}
int main(void)
{
int m = 3;
int n = 7;
int **grid = NULL;
int gridColSize = 4;
int row0[4] = { 1,3,4,8 };
int row1[4] = { 3,2,2,4 };
int row2[4] = { 5,7,1,9 };
int row3[4] = { 2,3,2,3 };
grid = (int **)malloc(sizeof(int *)*4);
for (int i = 0; i < 4; i++) {
grid[i] = (int *)malloc(sizeof(int) * 4);
}
grid[0] = row0;
grid[1] = row1;
grid[2] = row2;
grid[3] = row3;
minPathSum(grid, 4, &gridColSize);
for (int i = 0; i < 4; i++) {
free(grid[i]);
}
free(grid);
}
303. 区域和检索 - 数组不可变
由于会进行多次检索,即多次调用 \text{sumRange}sumRange,因此为了降低检索的总时间,应该降低 sumRange 的时间复杂度,最理想的情况是时间复杂度 O(1)。为了将检索的时间复杂度降到 O(1),需要在初始化的时候进行预处理.
typedef struct {
int *sums;
} NumArray;
NumArray *numArrayCreate(int *nums, int numsSize)
{
NumArray *ret = malloc(sizeof(NumArray));
ret->sums = malloc(sizeof(int) * (numsSize + 1)); // 预处理,计算出前缀和
ret->sums[0] = 0; // 并没有memset,这样可以省掉
for (int i = 0; i < numsSize; i++) {
ret->sums[i + 1] = ret->sums[i] + nums[i];
}
return ret;
}
// [i, j]之间value == obj->sums[j + 1] - obj->sums[i];
int numArraySumRange(NumArray* obj, int i, int j)
{
return obj->sums[j + 1] - obj->sums[i];
}
void numArrayFree(NumArray* obj) {
free(obj->sums);
}
304. 二维区域和检索 - 矩阵不可变
typedef struct {
int **sums;
int sumsSize;
} NumMatrix;
NumMatrix *numMatrixCreate(int **matrix, int matrixSize, int *matrixColSize)
{
NumMatrix *ret = malloc(sizeof(NumMatrix));
ret->sums = malloc(sizeof(int *) * matrixSize);
ret->sumsSize = matrixSize;
for (int i = 0; i < matrixSize; i++) {
ret->sums[i] = malloc(sizeof(int) * (matrixColSize[i] + 1));
ret->sums[i][0] = 0;
for (int j = 0; j < matrixColSize[i]; j++) {
// 初始化时对矩阵的每一行计算前缀和
ret->sums[i][j + 1] = ret->sums[i][j] + matrix[i][j];
}
}
return ret;
}
int numMatrixSumRegion(NumMatrix *obj, int row1, int col1, int row2, int col2)
{
int sum = 0;
// sumRange[i,j] = sums[j+1]−sums[i]
for (int i = row1; i <= row2; i++) {
sum += obj->sums[i][col2 + 1] - obj->sums[i][col1];
}
return sum;
}
void numMatrixFree(NumMatrix *obj) {
for (int i = 0; i < obj->sumsSize; i++) {
free(obj->sums[i]);
}
free(obj->sums);
}
1143. 最长公共子序列
int longestCommonSubsequence(char* text1, char* text2) {
int m = strlen(text1), n = strlen(text2);
int dp[m + 1][n + 1];
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= m; i++) {
char c1 = text1[i - 1];
for (int j = 1; j <= n; j++) {
char c2 = text2[j - 1];
if (c1 == c2) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = fmax(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
264. 丑数 II
这题能马上反映出是用动态规划来做,关键是找到递归方程:
没有同时求min的函数,我们可以两两处理min来求.
int nthUglyNumber(int n) {
int gMemu[n + 1];
gMemu[1] = 1;
int p2 = 1, p3 = 1, p5 = 1;
for (int i = 2; i <= n; i++) {
int num2 = gMemu[p2] * 2, num3 = gMemu[p3] * 3, num5 = gMemu[p5] * 5;
gMemu[i] = fmin(fmin(num2, num3), num5);
if (gMemu[i] == num2) {
p2++;
}
if (gMemu[i] == num3) {
p3++;
}
if (gMemu[i] == num5) {
p5++;
}
}
return gMemu[n];
}
416. 分割等和子集