总结:中级算法练习中的动态规划问题,相较于初级算法练习来讲,寻找转态转移方程的时候会相对来讲抽象一点。但是寻找转态转移方程的时候要把握两点:一,是由题目总问题去寻找子问题,例如题目1,题目中问能否到达最后一个位置,我们先将这个总问题进行转化为在这个数组中可以跳跃的最长距离是否大于数组的长度,这样我们的子问题就是去寻找截止到数组某个元素时可以跳跃的最长距离;例如题目3,题目问金额为amount时所需的最小硬币数,我们的子问题就是金额为i(i<=amount)时所需的最小硬币数。二,要找到状态转移的过程,就是由一个子问题到达下一个规模更大的子问题之间的这个变化过程是什么,说的直白一些就是动态规划数组中的下标的变化究竟代表着什么,例如题目1,题目2,题目4中状态的变化都是原有数组中位置的变化;例如题目3中状态的变化则是目标金额的变化。
题目1:跳跃游戏
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
示例 1:
输入: [2,3,1,1,4]
输出: true
解释: 从位置 0 到 1 跳 1 步, 然后跳 3 步到达最后一个位置。
示例 2:
输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。
思路:用dp[i]表示到nums[i]为止可以到达的最远距离。跳到nums[i]时,如果从i点向后跳的距离大于到nums[i-1]为止可以到达的最远距离,那么此时的状态转移方程为dp[i]=i+nums[i];如果从i点向后跳的距离不大于到nums[i-1]为止可以到达的最远距离,那么此时的状态转移方程为dp[i]=dp[i-1]。如果可以跳到倒数第二个点,即nums[len-2],如果dp[len-2]大于等于len-1说明可以从nums[len-1]之前的这些节点跳到nums[len-1],反之则不可以到达。
bool canJump(vector<int>& nums)
{
int len = nums.size();
if(len<=1)
return true;
//到nums[i]为止可以到达的最远距离
int dp[len];
memset(dp,0,len*sizeof(int));
dp[0] = nums[0];
for(int i=1;i<len-1;i++)
{
//如果根本都跳不到i,那么也无法跳到i后面的节点
if(dp[i-1]<i)
return false;
dp[i] = i+nums[i]>dp[i-1]?i+nums[i]:dp[i-1];
if(dp[i]>=len-1)
return true;
}
return dp[len-2]>=len-1;
}
题目2:不同路径
一个机器人位于一个 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
思路:使用动态规划,dp[i][j]表示到达map[i][j]的路径数。因为机器人只能向下或向右走,所以到达map[i][j]上面方块的路径数与左面方块的路径数之和即为到达map[i][j]的路径数。所以状态转移方程为dp[i][j] = dp[i-1][j]+dp[i][j-1]。
int uniquePaths(int m, int n)
{
//记录到达map[n][m]时的路径数
int dp[n][m];
dp[0][0] = 1;
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
if(i==0)
{
if(j!=0)
dp[i][j] = dp[i][j-1];
continue;
}
if(j==0)
{
dp[i][j] = dp[i-1][j];
continue;
}
dp[i][j] = dp[i-1][j]+dp[i][j-1];
}
}
return dp[n-1][m-1];
}
题目3:零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
思路:使用动态规划,用dp[i]表示价格为i时所需的最小硬币数。而到达金额i的路径就是当金额为i-coins[j]时,加入一个硬币coins[j]所以此时的状态转移方程为dp[i]=dp[i-coins[j]]+1;而将各条路径进行计算之后得到的最小值就是dp[i]的值,最后一步一步求得的dp[amount]即为目标金额所需的最小硬币数。
PS:这里有一个设置无穷大的细节,如果不是太清楚可以点进去看一下。
int coinChange(vector<int> &coins,int amount)
{
int len = coins.size();
int count = 0;
if(len==0||amount==0)
return count;
sort(coins.begin(),coins.end());
//dp[i]表示价格为i时所需的最小硬币数
int dp[amount+1];
dp[0] = 0;
//给dp[i]都设置成无穷大
for(int i=1;i<amount+1;i++)
dp[i] = 0x3f3f3f3f;
for(int i=1;i<=amount;i++)
{
for(int j=0;j<len;j++)
{
//以为硬币数组已经排过序了,所以当前硬币大于当前总额那么后续硬币也不需要遍历
if(coins[j]>i)
break;
dp[i] = min(dp[i],dp[i-coins[j]]+1);
}
}
count = dp[amount]==0x3f3f3f3f?-1:dp[amount];
return count;
}
题目4:Longest Increasing Subsequence
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
思路:用dp[i]记录以nums[i]结尾的递增子序列的长度。将nums[i]与nums[j](j<i)进行比较,若nums[i]大,则此时以nums[i]结尾的递增子序列的长度dp[i]为dp[j]+1,即dp[i]=dp[j]+1;比较完所有的nums[j]之后所求得到的最大值即为dp[i]。而所有dp[i]中的最大值即为最长递增子序列的长度。
int lengthOfLIS(vector<int>& nums)
{
int len = nums.size();
if(len<=1)
return len;
//记录以nums[i]结尾的递增子序列的长度
int dp[len];
for(int i=0;i<len;i++)
dp[i] = 1;
int max = 1;
for(int i=1;i<len;i++)
{
for(int j=i-1;j>=0;j--)
{
if(nums[i]>nums[j])
{
dp[i] = dp[j]+1>dp[i]?dp[j]+1:dp[i];
max = dp[i]>max?dp[i]:max;
}
}
}
return max;
}