LeetCode刷题总结:(7)动态规划相关问题

如果一个问题具有重叠最优子结构的性质,那么一般就是可以用动态规划来解决的。其实动态规划相关的问题不是写动态规划的实现代码,而是抽象出一个符合动态规划问题的结构,这里会涉及到具体的状态定义,以及状态之间的转移关系,这个抽象的过程是很难的,也是最灵活的!

 

70. 爬楼梯

假设你正在爬楼梯。需要 n 步你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 步 + 1 步
2.  2 步

示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1 步 + 1 步 + 1 步
2.  1 步 + 2 步
3.  2 步 + 1 步
// 思路: 原问题可以分解为两个相同结构的子问题,即n阶楼梯的走法=n-1阶楼梯的走法+n-2阶楼梯的走法
class Solution {

private:

	vector memo;

	int calWays(int n) {

		/*if (n == 1)
			return 1;
		if (n == 2)
			return 2;*/

		// 边界条件也可以从0开始,这样后续也是对的,而且memo能沾满
		if (n == 0)
			return 1;
		if (n == 1)
			return 1;

		// 当前的阶数问题还没有求解过
		if (memo[n] == 0) {
			memo[n] = calWays(n - 1) + calWays(n - 2);
		}

		return memo[n];
	}

public:
	int climbStairs(int n) {

		int res = 0;

		memo = vector(n + 1, 0);	// 索引和阶数对应

		res = calWays(n);
		return res;

		// 也可以直接递推
		memo = vector(n + 1, 0);
		memo[0] = 1;
		memo[1] = 1;
		for (int i = 2; i <= n; i++) {
			memo[i] = memo[i - 1] + memo[i - 2];
		}

		return memo[n];
	}
};

 

120. 三角形最小路径和

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

例如,给定三角形:

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]

自顶向下的最小路径和为 11(即,3 + 1 = 11)。

说明:

如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。

// 思路: 经过当前节点的最小路径为左右节点路径的最小值加上自身
class Solution1 {

private:

	vector> memo;
	int m;		// 行数

	// i,j代表当前节点的索引
	int minPathSum(vector>& triangle, int i, int j) {

		if (i == m - 1)
			return triangle[i][j];

		// 相同结构子问题的递归
		if (memo[i][j] == -1) {
			memo[i][j] = min(minPathSum(triangle, i + 1, j), minPathSum(triangle, i + 1, j + 1)) + triangle[i][j];
			cout <<"memo["<>& triangle) {

		if (triangle.size() == 0)
			return 0;

		// triangle中应该都是正数吧
		int res;
		m = triangle.size();
		for (int i = 0; i < m; i++) {
			vector tmp(triangle[i].size(), -1);
			memo.push_back(tmp);
		}

		res = minPathSum(triangle, 0, 0);
		return res;
	}
};

 

64. 最小路径和

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例:

输入:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
// 思路: 经过当前节点的最小路径为下,右节点路径的最小值加上自身
class Solution2 {

private:

	vector> memo;
	int m;
	int n;

	// 得到坐标(i,j)的最短路径
	int _minPathSum(vector>& grid, int i, int j) {

		if (i == m - 1 && j == n - 1) {
			return grid[i][j];
		}
		
		if (memo[i][j] == -1) {
			// 要超界处理一下
			if (i + 1 >= m) {
				memo[i][j] = _minPathSum(grid, i, j + 1) + grid[i][j];
			}
			else if(j+1>=n) {
				memo[i][j] = _minPathSum(grid, i + 1, j) + grid[i][j];
			}
			else {
				memo[i][j] = min(_minPathSum(grid, i, j + 1), _minPathSum(grid, i + 1, j)) + grid[i][j];
			}			
		}

		return memo[i][j];
	}


public:
	int minPathSum(vector>& grid) {

		m = grid.size();
		n = grid[0].size();
		memo = vector>(m, vector(n, -1));

		return _minPathSum(grid, 0, 0);
	}
};

 

343. 整数拆分

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

例如,给定 n = 2,返回1(2 = 1 + 1);给定 n = 10,返回36(10 = 3 + 3 + 4)。

注意:你可以假设 不小于2且不大于58。

// 思路: 正整数n的最大乘积为: 1*(n-1)的最大乘积,2*(n-2)的最大乘积......(n-1)*1的最大乘积中的最大值
class Solution3 {

private:

	vector memo;

	// 将n进行分割(至少分割成两部分),求得可以获得的最大乘积值
	int _integerBreak(int n) {

		if (n == 1)
			return 1;

		if (memo[n] == -1) {
			int res = -1;
			// 遍历所有可能的分割
			for (int i = 1; i <= n - 1; i++) {
				// 当前分割下,比较分割成两部分和向下分割成更多部分两者比较,然后再和当前最大值比较,然后储存
				res = max(res, max(i*(n - i), i*_integerBreak(n - i)));
			}
			memo[n] = res;
		}

		return memo[n];
	}


public:
	int integerBreak(int n) {

		assert(n >= 2);

		memo = vector(n + 1, -1);

		return _integerBreak(n);
	}
};

 

279. 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

示例 1:

输入: n = 12
输出: 3 
解释: 12 = 4 + 4 + 4.

示例 2:

输入: n = 13
输出: 2
解释: 13 = 4 + 9.
// 思路: n最小完全平方数个数=min((n-1)最小完全平方数个数+1,(n-4)最小完全平方数个数+1,......,(n-i*i)最小完全平方数个数+1)
class Solution4 {

private:

	vector memo;

	// 返回n的最小完全平方数个数
	int _numSquares(int n) {

		// 如果是完全平方数直接返回1
		for (int i = 1;n-i*i>=0; i++) {
			if (n == i*i)
				return 1;
		}

		// 递归
		if (memo[n] == 0) {
			int res = INT_MAX;
			for (int i = 1; n - i*i > 0; i++) {
				res = min(res, _numSquares(n - i*i) + 1);
			}
			memo[n] = res;
		}

		return memo[n];
	}

public:
	int numSquares(int n) {

		assert(n >= 1);
		memo = vector(n + 1, 0);

		return _numSquares(n);
	}
};

 

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?

例如,上图是一个7 x 3 的网格。有多少可能的路径?

说明:m 和 的值均不超过 100。

示例 1:

输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右

示例 2:

输入: m = 7, n = 3
输出: 28
// 思路: 当前节点到达终点的路径数=右侧节点到达终点的路径数+下面节点到达终点的路径数
class Solution5 {

	vector> memo;
	int m, n;

	int _uniquePaths(int i, int j) {

		// 达到终点
		if (i == m - 1 && j == n - 1)
			return 1;

		if (memo[i][j] == 0) {
			// 超界检测
			if (i >= m - 1)
				memo[i][j] = _uniquePaths(i, j + 1);
			else if (j >= n - 1)
				memo[i][j] = _uniquePaths(i + 1, j);
			else
				memo[i][j] = _uniquePaths(i + 1, j) + _uniquePaths(i, j + 1);

			cout << "memo[" << i << "][" << j << "] : " << memo[i][j] << endl;
		}

		return memo[i][j];
	}

public:
	int uniquePaths(int m, int n) {

		assert(m >= 1 && n >= 1);
		this->m = m;
		this->n = n;
		memo = vector>(m, vector(n, 0));

		return _uniquePaths(0, 0);
	}
};

 

63. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 1 和 0 来表示。

说明:m 和 的值均不超过 100。

示例 1:

输入:
[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
// 思路: 和上一题一致,只不过在具体递归的时候不加入对障碍点的递归,跳过此点即可
class Solution6 {

private:

	vector> memo;
	int m, n;

	int _uniquePaths(vector>& obstacleGrid, int i, int j) {

		// 达到终点
		if (i == m - 1 && j == n - 1) {
			if (obstacleGrid[i][j] == 1)
				return 0;
			return 1;
		}


		if (memo[i][j] == -1) {
			// 超界检测
			if (i >= m - 1){	// 在最后一行的情况
				if (obstacleGrid[i][j + 1] == 1)
					memo[i][j] = 0;
				else
					memo[i][j] = _uniquePaths(obstacleGrid, i, j + 1);
			}
			else if (j >= n - 1) {	// 在最后一列的情况
				if (obstacleGrid[i + 1][j] == 1)
					memo[i][j] = 0;
				else
					memo[i][j] = _uniquePaths(obstacleGrid, i + 1, j);
			}				
			else {	// 不在边界的情况
				if (obstacleGrid[i][j + 1] == 1 && obstacleGrid[i + 1][j] != 1)	// 右侧是障碍物
					memo[i][j] = _uniquePaths(obstacleGrid, i + 1, j);
				else if (obstacleGrid[i + 1][j] == 1 && obstacleGrid[i][j + 1] != 1)	// 下侧是障碍物
					memo[i][j] = _uniquePaths(obstacleGrid, i, j + 1);
				else if (obstacleGrid[i + 1][j] != 1 && obstacleGrid[i][j + 1] != 1)
					memo[i][j] = _uniquePaths(obstacleGrid, i + 1, j) + _uniquePaths(obstacleGrid, i, j + 1);
				else
					memo[i][j] = 0;
			}


			cout << "memo[" << i << "][" << j << "] : " << memo[i][j] << endl;
		}

		return memo[i][j];
	}

public:
	int uniquePathsWithObstacles(vector>& obstacleGrid) {

		if (obstacleGrid[0][0] == 1)
			return 0;

		m = obstacleGrid.size();
		n = obstacleGrid[0].size();
		memo = vector>(m, vector(n, -1));

		return _uniquePaths(obstacleGrid, 0, 0);
	}
};

 

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:

输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。
// 思路: 抢劫nums[0...n-1]的最大值=max( 抢劫了0号房子+抢劫nums[2...n-1]的最大值,
// 抢劫了1号房子+抢劫nums[3...n-1]的最大值,......,抢劫了n-1号房子+抢劫nums[n+1...n-1]的最大值 )
class Solution7 {

private:

	// memo[i]记录的是抢劫nums[i...n)所能获得的最大收益
	vector memo;
	int n;

	// 试图抢劫[index,n-1]之间的房子以获得最大收益
	int tryRob(const vector& nums, int index) {

		// 终止条件,抢劫从索引n开始的房子,其实后面已经没有房子了,所以返回0
		if (index >= n)
			return 0;

		if (memo[index] == -1) {
			int res = -1;
			// 选择第一个要抢劫的房子
			for (int i = index; i < n; i++) 
				res = max(res, nums[i] + tryRob(nums, i + 2));
			memo[index] = res;
		}

		return memo[index];
	}


public:
	int rob(vector& nums) {

		n = nums.size();
		memo = vector(n, -1);
		return tryRob(nums, 0);
	}
};

 

额外题:01背包问题

w[n],v[n]为n种物品的重量以及相应的价值,要求填充容积为c的背包使之价值最大,各种物品只能只用一次

class Knapsack01 {

private:

	vector> memo;

	// 用[0,index]的物品,填充容积为c的背包使之价值最大(有两个变量的约束,不再像是之前的只有一个变量了,意思两个变量指定一个状态)
	int bestValue(const vector& w, const vector& v, int index, int c) {
		
		//终止条件: 没有物品可放了 或者 背包满了 那么这次放置就没有价值了
		if (index < 0 || c <= 0)
			return 0;

		if (memo[index][c] == -1) {
			// 索引为index的物品不放得到一个最大值
			int res = bestValue(w, v, index - 1, c);
			// 放置当前索引指向的值,又会得到一个最大值
			if (w[index] <= c)
				res = max(res, bestValue(w, v, index - 1, c - w[index]));

			memo[index][c] = res;
		}

		return memo[index][c];
	}

public:

	int knapsack01(const vector& w, const vector& v, int C) {

		assert(w.size() == v.size());
		int n = w.size();
		memo = vector>(n, vector(C + 1, -1));

		return bestValue(w, v, n - 1, C);
	}

	// 递推版
	int knapsack01Recursive(const vector& w, const vector& v, int C) {
		assert(w.size() == v.size() && C >= 0);
		int n = w.size();
		if (n == 0 || C == 0)
			return 0;

		 memo = vector>(n, vector(C + 1, -1));

		// 基础情况
		for (int j = 0; j <= C; j++)
			memo[0][j] = (j >= w[0] ? v[0] : 0);

		// 后续递推
		for (int i = 1; i < n; i++) {
			for (int j = 0; j <= C; j++) {
				memo[i][j] = memo[i - 1][j];
				if (j >= w[i])
					memo[i][j] = max(memo[i][j], v[i] + memo[i - 1][j - w[i]]);
			}
		}

		return memo[n - 1][C];
	}

};

 

416. 分割等和子集

给定一个只包含正整数非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:

  1. 每个数组中的元素不会超过 100
  2. 数组的大小不会超过 200

示例 1:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

 

示例 2:

输入: [1, 2, 3, 5]

输出: false

解释: 数组不能分割成两个元素和相等的子集.
// 思路: nums[0,...,index]这些数据能不能填充sum = [0,...,index-1]这些值能不能填充sum值 || [0,...,index-1]这些值能不能填充sum-nums[index]值
class Solution8 {

private:

	// memo[i][c] 表示使用索引为[0,...,i]这些元素,是否可以完全填充一个容量为c的背包
	// -1 表示为未计算; 0 表示不可以填充; 1 表示可以填充
	vector> memo;

	// 状态: 使用nums[0,...,index],是否可以完全填充一个容量为sum的背包
	bool tryPartition(const vector& nums, int index, int sum) {

		// 当前的背包刚好满了
		if (sum == 0)
			return true;

		// 数字用完了或者背包被放得溢出了
		if (index < 0 || sum < 0)
			return false;

		if (memo[index][sum] == -1) {
			memo[index][sum] = tryPartition(nums,index-1,sum) || tryPartition(nums,index-1,sum-nums[index]);
		}

		return memo[index][sum];
	}

public:
	bool canPartition(vector& nums) {

		// nums能否内平分
		int sum = 0;
		for (int i = 0; i < nums.size(); i++)
			sum += nums[i];

		if (sum % 2 != 0)
			return false;

		memo = vector>(nums.size(), vector(sum / 2 + 1, -1));

		return tryPartition(nums, nums.size() - 1, sum / 2);
	}


	// 直接递推版
	bool canPartitionRecursive(vector& nums) {

		// nums能否内平分
		int sum = 0;
		for (int i = 0; i < nums.size(); i++)
			sum += nums[i];

		if (sum % 2 != 0)
			return false;

		int n = nums.size();
		int C = sum / 2;
		vector memo(C + 1, false);

		for (int i = 0; i <= C; i++)
			memo[i] = (nums[0] == i);

		for (int i = 1; i < n; i++) {
			for (int j = C; j >= nums[i]; j--) {
				memo[j] = memo[j] || memo[j - nums[i]];
			}
		}

		return memo[C];
	}
};

 

322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1

示例 1:

输入: coins = [1, 2, 5], amount = 11
输出: 3 
解释: 11 = 5 + 5 + 1

示例 2:

输入: coins = [2], amount = 3
输出: -1

说明:
你可以认为每种硬币的数量是无限的。

// 思路: amountd的最小硬币组合 = min( (amount-coins[0])的最小硬币组合+1, (amount-coins[1])的最小硬币组合+1,...,(amount-coins[n-1])的最小硬币组合+1 )
class Solution9 {

private:

	int n;
	// memo初始化为0,代表没有检索到过(用0是安全的,程序中不会出现0),-1代表没有组合,正数代表最小有几种组合
	vector memo;

	// 寻找amount的最小硬币数
	int _coinMinChange(vector& coins, int amount) {

		// 终止条件
		if (amount < 0)		// amount小于0,意思就是没有组合凑成
			return -1;
		for (int i = 0; i < n; i++) {		// amount刚好等于硬币中的某一枚
			if (amount == coins[i])
				return 1;
		}

		if (memo[amount] == 0) {
			int res=INT16_MAX;
			for (int i = 0; i < n; i++) {		// 遍历一下选择第一枚的选项
				int tmp = _coinMinChange(coins, amount - coins[i]);
				if(tmp!=-1)
					res = min(res, tmp+1);		// 只有amount - coins[i]能找到组合时,才在本来的最小个数上加1,如果找不见那么保持找不见的标志位-1
			}	
			if (res == INT16_MAX)	// 遍历完第一次的选项后,对应的子结构也都没有解,那么当前的状态也是没有解的
				res = -1;
			memo[amount] = res;
		}

		return memo[amount];
	}

public:
	int coinChange(vector& coins, int amount) {

		if (amount < 0 || coins.size() == 0)
			return -1;

		if (amount == 0)
			return 0;

		n = coins.size();
		memo = vector(amount+1, 0);

		return _coinMinChange(coins, amount);
	}
};

 

377. 组合总和 Ⅳ

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

nums = [1, 2, 3]
target = 4

所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7

进阶:
如果给定的数组中含有负数会怎么样?
问题会产生什么变化?
我们需要在题目中添加什么限制来允许负数的出现?

// 思路: 典型的拥有确定解空间的题,输出的是某一个状态下解空间的某些特性,那么很明显了,这题可以用记忆化搜索+递归的方法,也可以在此基础上升级为递推的动态规划
// 状态转移: target的组合数 = (target-nums[0])的组合数 + (target-nums[1])的组合数 + ... + (target-nums[n-1])的组合数
class Solution10 {

	// 记忆相应target的组合个数
	vector memo;
	int n;

	int conbinationCount(vector& nums, int target) {
		
		if (target < 0)		// 没有组合返回0
			return 0;
		//for (int i = 0; i < n; i++) {	// 找到组合的终点返回1,不应该这么写,这么写的话到特定值就终止了不继续了
		//	if (nums[i] == target)
		//		return 1;
		//}

		if (target == 0)
			return 1;

		if (memo[target] == -1) {
			int res = 0;
			for (int i = 0; i < n; i++) {
				res += conbinationCount(nums, target - nums[i]);	// 将所有的子状态的组合数都加起来
			}
			memo[target] = res;
		}

		return memo[target];
	}

public:
	int combinationSum4(vector& nums, int target) {

		assert(target > 0);
		if (nums.size() == 0)
			return 0;

		n = nums.size();
		memo = vector(target + 1, -1);

		return conbinationCount(nums, target);
	}
};

 

300. 最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4

说明:

  • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
  • 你算法的时间复杂度应该为 O(n2) 。

进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

// 思路: 状态:以nums[j]为最后一个值的最长上升子序列LIS[j]的长度 = max(以nums[i]为最后一个值的最长上身子序列LIS[j]的长度,其中nums[i]& nums) {

		// 直接给出递推的动态规划解法
		if (nums.size() == 0)
			return 0;

		// memo[i] 表示以 nums[i] 为结尾的最长上升子序列的长度
		int res = 1;
		vector memo(nums.size(), 1);
		for (int i = 0; i < nums.size(); i++) {
			for (int j = 0; j < i; j++) {
				if (nums[j] < nums[i])
					memo[i] = max(memo[i], memo[j] + 1);
			}
			res = max(res, memo[i]);
		}

		return res;
	}
};

 

 

你可能感兴趣的:(LeetCode刷题记录)