动态规划基础题
除数博弈(归纳法+动态规划)
题目解析:
有一个N,选一个x是N的约数&&比0大比N小,N=N-x
归纳法:
基本思路:
- 数字N如果是奇数,它的约数必然都是奇数;若为偶数,则其约数可奇可偶。
- 无论N初始为多大的值,游戏最终只会进行到N=2时结束,那么谁轮到N=2时谁就会赢。
- 因为爱丽丝先手,N初始若为偶数,爱丽丝则只需一直选1,使鲍勃一直面临N为奇数的情况,这样爱丽丝稳赢;
- 若N初始值奇数,那么爱丽丝第一次选完之后N必为偶数,那么鲍勃只需一直选1就会稳赢。
- 因此,奇则输,偶则赢。直接:
class Solution {
public:
bool divisorGame(int N) {
return N%2==0;
}
};
动态规划:
class Solution {
public:
bool divisorGame(int N) {
if(N == 1) return false;
if(N == 2) return true;
//bool dp[N + 1];
//memset(dp, 0, sizeof(dp));
vector<bool> dp(N+1,false);
//dp[1] = false;
dp[2] = true;
for(int i = 3; i <= N; i++){
for(int j = 1; j < i; j++){
if(i % j == 0 && dp[i - j] == false){
dp[i] = true;
break;
}
}
}
return dp[N];
}
};
最大子序列和(动态规划+)
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
动态规划:
dp[i]表示nums中以nums[i]结尾的最大子序和
dp[0]=nums[0]
dp[i] = max(dp[i-1]+nums[i],nums[i]);
dp[i] 时当前数字,要么是与前面的最大子序列的和
时间复杂度O(n) 可优化O(1)
class Solution {
public:
int maxSubArray(vector<int>& nums) {
//类似寻找最大最小值的题目,初始值一定要定义成理论上的最小最大值
int result = INT_MIN;
int len=nums.size();
vector<int> dp(len);
dp[0]=nums[0];
result=dp[0];
for(int i=1;i<len;i++){
dp[i]=max(dp[i-1]+nums[i],nums[i]);
result=max(result,dp[i]);
}
return result;
}
};
贪心:
从左向右迭代,一个个数字加过去,如果sum<0,重新开始找子序串
class Solution {
public:
int maxSubArray(vector<int>& nums) {
//类似寻找最大最小值的题目,初始值一定要定义成理论上的最小最大值
int result = INT_MIN;
int len=nums.size();
int sum=0;
for(int i=0;i<len;i++){
sum += nums[i];
result = max(result,sum);
if(sum<0){
sum=0;
}
}
return result;
}
};
爬楼梯
就是那个入门的青蛙的问题!
我觉得看了这个示例反而不会了
DP入门案例一:简单的一维数组
假设你正在爬楼梯。需要 n 阶(正整数)你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
4. 1 阶 + 1 阶 + 1 阶
5. 1 阶 + 2 阶
6. 2 阶 + 1 阶
class Solution {
public:
int climbStairs(int n) {
//dp[i]=dp[i-1]+dp[i-2];
//从第 n-1 级或 n-2 级 跳上来;算所有可能的跳法的
if(n==1){
return 1;
}
vector<int> dp(n+1);
//dp[0]=0;
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
};
股票问题
股票问题的方法就是 动态规划,因为它包含了重叠子问题,即买卖股票的最佳时机是由之前买或不买的状态决定的,而之前买或不买又由更早的状态决定的…
由于本题只有一笔交易(买入卖出),因此除了动态规划,我们还可以使用更加简便的方法实现。
法一(一次遍历)
遍历一遍数组,计算每次 到当天为止的最小股票价格和最大利润
class Solution {
public:
int maxProfit(vector<int>& prices) {
int minprice = int(1e9);
int maxprofit = 0;
for (auto price : prices){
maxprofit = max(maxprofit, price - minprice);
minprice = min(minprice, price);
}
return maxprofit;
}
};
法二(动态规划)
- 动态规划做题步骤
明确 dp(i)dp(i) 应该表示什么(二维情况:dp(i)(j)dp(i)(j));
根据 dp(i)dp(i) 和 dp(i-1)dp(i−1) 的关系得出状态转移方程;
确定初始条件,如 dp(0)dp(0)。
- 本题思路
其实方法一的思路不是凭空想象的,而是由动态规划的思想演变而来。这里介绍一维动态规划思想。
dp[i]表示前 i天的最大利润
因为我们始终要使利润最大化,则:
dp[i] = max(dp[i-1], prices[i]-minprice)
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n == 0) return 0; // 边界条件
int minprice = prices[0];
vector<int> dp (n, 0);
for (int i = 1; i < n; i++){
minprice = min(minprice, prices[i]);
dp[i] = max(dp[i - 1], prices[i] - minprice);
}
return dp[n - 1];
}
};
打家劫舍(第一次自己做对(#°Д°))
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 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 。
class Solution {
public:
int rob(vector<int>& nums) {
int len=nums.size();
if(len==0)return 0;
if(len==1)return nums[0];
vector<int> dp(len,0); //dp[i]前i号偷窃到的最高金额
dp[0]=nums[0]; //初始化
dp[1]=max(dp[0],nums[1]);
for(int i=2;i<len;i++){
dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[len-1];
}
};
使用最小花费爬楼梯
数组的每个索引做为一个阶梯,第 i个阶梯对应着一个非负数的体力花费值 cost [ i ] (索引从0开始)。
每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。
您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。
示例 1:
输入: cost = [10, 15, 20]
输出: 15
解释: 最低花费是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。
示例 2:
输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低花费方式是从cost[0]开始,逐个经过那些1,跳过cost[3],一共花费6。
注意:
dp[0]=0; //!!!是0不是cost[0]
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int len=cost.size();
vector<int> dp(len);
dp[0]=0; //!!!是0不是cost[0]
dp[1]=min(cost[0],cost[1]);
for(int i=2 ;i<len;i++)
{
dp[i]=min(dp[i-1]+cost[i],dp[i-2]+cost[i-1]);
}
return dp[len-1];
}
};
区域和检索-数组不可变
给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。
示例:
给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
说明:
你可以假设数组不可变。
会多次调用 sumRange 方法。
分析
可以通过sum[j] = sum[i-1]来获取[i,j]的和
class NumArray {
public:
int *sum;
NumArray(vector<int>& nums) {
int len = nums.size();
if(len==0)return;
sum=new int[len];
sum[0]=nums[0];
for(int i=1;i<len;i++){
sum[i]=nums[i]+sum[i-1];
}
}
int sumRange(int i, int j) {
if(i==0)return sum[j];
return sum[j]-sum[i-1];
}
};
/**
* Your NumArray object will be instantiated and called as such:
* NumArray* obj = new NumArray(nums);
* int param_1 = obj->sumRange(i,j);
*/
判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1:
s = "abc", t = "ahbgdc"
返回 true.
示例 2:
s = "axc", t = "ahbgdc"
返回 false.
后续挑战 :
如果有大量输入的 S,称作S1, S2, ... , Sk 其中 k >= 10亿,
你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
题解一. 直接判断子序列这个没什么好说的了,直接O(n)就可以得出结果,不需要动态规划,反而麻烦了。
bool isSubsequence(string s, string t) {
int len = s.size();
if(len==0)return true;
int a=0;
for(int i=0;i<t.size();i++){
if(s[a]==t[i])
a++;
if(a==len)return true;
}
return false;
}
题解二. 后续挑战,输入量大,小写字母创建25的二维数组,存储t的坐标,这样就可以把s的判断直接转为坐标的判断,
dp[0]代表了存储了a出现在t的所有的位置,逐个字符判断s的字符顺序是否在t内,直接返回结果。
时间复杂度O(t.size()+2000):分别为创建数组需要O(t.size()),
索引是递增的使用二分查找s的单个字符20次之内就可找到需要O(100*20)。
适用大量的输入判断子序列。
bool isSubsequence(string s, string t) {
vector<vector<int>>dp(26);
int tag=-1;
for(int i=0;i<t.size();i++)
dp[t[i]-'a'].push_back(i);
for(int i=0;i<s.size();i++){
int now=s[i]-'a';
int left=0,right=dp[now].size()-1;
while(left<right){
int mid=(left+right)/2;
if(dp[now][mid]>tag)
right=mid;
else
left=mid+1;
}
if(right<left || dp[now][left]<tag)return false;
tag=dp[now][left];
}
return true;
}
面试题:三步问题(结果取模)
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007=。
示例1:
输入:n = 3
输出:4
说明: 有四种走法
示例2:
输入:n = 5
输出:13
提示:
n范围在[1, 1000000]之间
class Solution {
public:
int waysToStep(int n) {
vector<int> dp(n+2);
if(n==1)return 1;
if(n==2)return 2;
if(n==3)return 4;
dp[1]=1;dp[2]=2;dp[3]=4;
for(int i=4;i<=n;i++){
//dp[i]=((dp[i-1]%1e9+7+dp[i-2]%1e9+7)%1e9+7+dp[i-3]%1e9+7)%1e9+7; //这个+7容易误解,用下面的更好
dp[i]=((dp[i-1]%1000000007+dp[i-2]%1000000007)%1000000007+dp[i-3]%1000000007)%1000000007;
}
return dp[n];
}
};
连续数列(为什么有一个res????)
(和最大子序列和 题一样)
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。```
```cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int len=nums.size();
vector<int> dp(len+3);
dp[0]=nums[0];
int res= dp[0];
for(int i=1 ;i<len;i++){
dp[i] = max(dp[i-1]+nums[i],nums[i]);
res = max(res,dp[i]);
}
return res;
}
};
按摩师
一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
class Solution {
public:
int massage(vector<int>& nums) {
int len=nums.size();
if(len==0)return 0;
if(len==1)return nums[0]; //区分len==0和下标==0
vector<int> dp(len);
dp[0]=nums[0];
dp[1]=max(nums[0],nums[1]);
for(int i=2;i<len;i++){
dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[len-1];
}
};