您的位置:首页 > 其它

LeetCode中级算法之动态规划

2019-06-22 18:21 134 查看

LeetCode中级算法之动态规划

跳跃游戏

Question:
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。

示例 1:
输入: [2,3,1,1,4]
输出: true
解释: 从位置 0 到 1 跳 1 步, 然后跳 3 步到达最后一个位置。

示例 2:
输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。

Solution:

bool ucanJump(vector<int>& nums, int pos) {
if(pos <= 0) return true;
for(int i = pos - 1; i >= 0; i--)
{
if(pos - i <= nums[i]) return ucanJump(nums, i);
}
return false;
}
bool canJump(vector<int>& nums) {
return ucanJump(nums,nums.size() - 1);
}

从数组末尾往前找,找到第一个能跳到末尾的数,再以这个数为末尾往前找,如果最后能到初始位置,则可以完成跳跃:

不同路径

Question:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
例如,上图是一个7 x 3 的网格。有多少可能的路径?
说明:m 和 n 的值均不超过 100。

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

示例 2:
输入: m = 7, n = 3
输出: 28

Solution:
解法一:递归,显然这种做法会造成很多不必要的重复计算,例如计算了两次到达初始节点右下角的节点的不同路径数:

int uniquePaths(int m, int n) {
if(m == 0 && n == 0) return 1;
if(m < 0 || n < 0) return 0;
return uniquePaths(m - 1, n) + uniquePaths(m, n - 1);
}

这种方法显然会超时,像这种题不能用递归来在限时内解决问题的话,就要考虑用空间换时间,建立一个mxn的数组,每个节点的值就是它上方和左方的节点的值的和:

int uniquePaths(int m, int n) {
int **path;
path=new int*
;
for(int i=0;i<n;i++)
path[i]=new int[m];
for(int i = 0; i < n; i++)
path[i][0] = 1;
for(int j = 0; j < m; j++)
path[0][j] = 1;
for(int i = 1; i < n; i++)
{
for(int j = 1; j < m; j++)
{
path[i][j] = path[i - 1][j] + path[i][j - 1];
}
}
return path[n - 1][m - 1];
}

这里我又练了一下动态声明二维数组,还可以直接动态生成vector,这样写:

vector<vector<int>>path(m, vector<int>(n, 1));

零钱兑换

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

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

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

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

Solution:
也是典型的动态规划思想,记录一个res[]向量来保存每个价格所需最少的硬币数,当遍历到一个新的价格时,对每一个硬币进行判断,状态转移方程为:

res[i]=min(res[i-coin1]+1, res[i-coin2]+1, …)

int coinChange(vector<int>& coins, int amount) {
sort(coins.begin(),coins.end());
vector<int> res(amount + 1,-1);
if(coins.empty()) return -1;
if(coins.size() == 1)
{
if(amount % coins[0] == 0) return amount/coins[0];
return -1;
}
res[0] = 0;
for(int i = 1; i <= amount; i++)
{
int min = INT_MAX;
for(auto c : coins)
{
if(i - c < 0) break;
if(res[i - c] >= 0) min = min > res[i - c] + 1 ? res[i - c] + 1 : min;
}
if(min!=INT_MAX) res[i]=min;
}
return res[amount];
}

Longest Increasing Subsequence☆

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

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

说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

Solution:
时间复杂度为O(n * n)的算法:
状态转移方程:

dp[i] =max(dp[j] + 1,1),其中num[j] <= num[i] && j < i

int lengthOfLIS(vector<int>& nums) {
if(nums.empty()) return 0;
vector<int> dp(nums.size(),1);
int res = 1;
for(int i = 1; i < nums.size(); i++)
{
for(int j = 0; j < i; j++)
{
if(nums[j] < nums[i])
dp[i] = dp[i] > dp[j] +1  ? dp[i] : dp[j] + 1;
}
res = max(res, dp[i]);
}
return res;
}

时间复杂度为O(n * logn)的算法:
这个思路非常有意思,维护一个动态的“递增序列”,遍历原始数组,指针为i,在递增序列中查找第一个大于nums[i]的元素,这里使用二分查找,如果不存在,则直接在末尾加上nums[i],如果存在,则直接将这个元素替换成nums[i]。
表面上看起来毫无章法可言,我们拿一个例子来看看:

[10,9,2,5,3,7,101,18]

初始的递增序列是[10],依次更新为[9]、[2] 、[2,5] 、[2 3] 、[2 3 7] 、[2 3 7 101] 、[2 3 7 18],最终我们得到的就是一个最大的递增子序列,替换的意义在于后面遍历到的数字可以享受到之前最小的那个替换的数的较小值,从而继续在递增序列中往下走,如果成功走到递增序列的末尾,则可以在递增序列中增加这个数字,否则它将替换比他大的第一个数,因为之后的数只需要比它大,就可以继续在递增序列中往下走。
代码:

int lengthOfLIS(vector<int>& nums) {
vector<int> dp;
for (int i = 0; i < nums.size(); ++i){
int l = 0, r = dp.size();
while (l < r){
int mid = (l + r)/2;
if (dp[mid] < nums[i]) l = mid + 1;
else r = mid;
}
if (r == dp.size())
dp.push_back(nums[i]);
else dp[r] = nums[i];
}
return dp.size();
}

这个思路真的很难想,不可谓不是顶级的动态规划哈哈哈哈哈

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: