leetcode 动态规划专题

最近一直在学习动态规划,据参加阿里腾讯面试的大佬透露说笔试题还是比较容易过的,但是百分之五十以上会有动态规划题,由此可见动态规划的重要性,同时动态规划也可以说在算法中是比较难的。我整理了一下这些天刷leetcode的DP题目,并总结了一些心得。
做DP题最重要的是,找到状态转移方程,最好是你会暴力递归解,然后再来优化它,也就是用空间换时间!DP题的优势在于去除冗余。有的题目用朴素的暴力解,那是永远算不完的。计算机做不到。递归虽然方便,但是计算机最不喜欢做的工作就是递归。
我们直接来看题目。
先从简单的来入手
leetcode 70
**假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。

  1. 1 阶 + 1 阶
  2. 2 阶
    示例 2:
    输入: 3
    输出: 3
    解释: 有三种方法可以爬到楼顶。
  3. 1 阶 + 1 阶 + 1 阶
  4. 1 阶 + 2 阶
  5. 2 阶 + 1 阶**

这题可以说相当简单,我们来先用递归来写。
递归需要考虑边界条件,有人说是 n == 1的时候

int f(int n)
{
	if(n == 1)
	return 1;
	
}

那么我们先写下来。再来看题目给定,每次可跳一次或两次,这里不求最少的跳法不求最多的方法,递归的话,我们根本不需要考虑那么多,写好了条件交给计算机工作就行。
跳两次的话 我们可以n - 1, 可以n - 2
那么代码如下

int f(int n)
{
	if(n == 1)
		return 1;
	return f(n - 1) + f(n - 2);
}

这样好像并不对,因为当n = 2 时,我们是不是需要考虑 n - 2的情况,那么就出现了f(0)这种情况,我们的边界情况没有考虑到,那就会进入死循环。
改进一下

int f(int n)
{
	if(n <= 1)
		return n;
	return f(n - 1) + f(n - 2);
}

这样就可以了,但是放到leetcode上是肯定不通过的,会超时。
那么怎么办,动态规划。
其实很好改,这是我的代码,在时间上击败了89%。那要注意的是每一个dp数组你都要考虑它的初始化!相信这题不难理解。dp[i]代表的是到了i层的总方法数。

int climbStairs(int n) {
    if(n == 0 || n == 1 || n == 2)
        return n;
    int dp[n];
    int i;
    dp[0] = 1;
    dp[1] = 2;
    for(i=2; i<n; i++)
    {
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n-1];
}

我们现在提高一点难度。
leetcode 62
一个机器人位于一个 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**

一样可以用递归,设置好边界条件 return f(n + 1, m) + f(n, m + 1)就行了。
动态规划我们需要考虑的是对于dp数组的初始化。dp数组代表的是达到这个坐标点的总方法数。而只能向右和向下。因为我们是自底向上推。我们先初始化边界。
我们考虑边界就是第一行和第一列!对于第一行和第一列任意的一个格子,我们都只有一种走法。
所以我们讲dp对应的第一行第一列所有的位置置为1。包括dp[0][0]。
接下来我们就可以放心大胆的往下做了。
非常好理解,我直接上代码。在C的提交中击败了93%的人。

int uniquePaths(int m, int n) {
    if(m == 0 || n == 0)
        return 0;
    if(m == 1 || n == 1)
        return 1;
    int dp[m][n];
    dp[0][0] = 1;
    int i,j;
    for(i=0; i < m; i ++)
        dp[i][0] = 1;
    for(j=0; j < n; j ++)
        dp[0][j] = 1;
    for(i=1; i < m; i ++)
    {
        for(j=1; j < n; j ++)
        {
            dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
    }
    return dp[m-1][n-1];
}

说到这里是不是觉得DP并不是那么难,那么我们继续往下写。

leetcode 52.
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

这题具有一定的经典性,但是同样简单,它求的是最大和。而且是需要连续的。
那么我们可以考虑dp数组了,很容易想到dp[i]即可设置为i项之前最大连续数组的和。
然后我们遍历一遍dp数组,找到最大的就可以了。
废话不多说,直接上代码,击败了80%.

int maxSubArray(int* nums, int numsSize) 
{
    int dp[numsSize];
    int i,max = nums[0];
    dp[0] = nums[0];
    for(i=1; i<numsSize; i++)
    {
        dp[i] = MAX(nums[i] + dp[i - 1] , nums[i]);
    }
    for(i=0; i<numsSize; i++)
    {
        if(max < dp[i])
            max = dp[i];
    }
    return max;
    
}
int MAX(int a,int b)
{
    int max = (a>b)?(a):(b);
    return max;
}

我们现在该做些有难度的题了。

leetcode 152.
给定一个整数数组 nums ,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

这题难度为中。
这题不像上面那么简单了,乍一看,是不是很像上面那题?但是我们不可以像那样写了,因为乘积的话对负数可就不像加法了。
比如本来是[2,3,-4,-4],如果你认为是6不就错的可笑了吗,两负数相乘就是正数了。
而最大的有可能一下子变成最小的,最小的也可能一下子变成最大的。
哈哈,想起一个笑话,两人互怼,一人对另一人说,我的智商是你的10倍。而另一个人同归于尽地回应,我的智商是负数。
哈哈,我们继续看题。
本来这题我也没思路,后来看了大神的解法才明白,真是脑洞大开,我们可以设置两个数组,一个数组放最小的,另一个数组放最大的。真是妙。
这种时候需要判断三种情况,nums[i], nums[i] *maxdp[i - 1], nums[i] * mindp[i - 1]
真的是很妙,一下子解决了负数的情况。
同时,我们需要维护两个数组,即存放最大的maxdp和存放最小的mindp。
需要在每一次更新维护。
这次我写的代码不太优雅…击败的也不多,也许还有更好的解法,但还好过了。

int maxProduct(int* nums, int numsSize) {
    int maxdp[numsSize];
    int mindp[numsSize];
    int i,MAX;
    maxdp[0] = mindp[0] = nums[0];
    MAX = maxdp[0];
    for(i=1; i < numsSize; i ++)
    {
        maxdp[i] = max(nums[i], nums[i] * maxdp[i-1], nums[i] * mindp[i-1]);
        mindp[i] = min(nums[i], nums[i] * maxdp[i-1], nums[i] * mindp[i-1]);
    }
    for(i=0; i < numsSize; i ++)
    {
        if(MAX < maxdp[i])
            MAX = maxdp[i];
    }
    return MAX;
}

int max(int a, int b, int c)
{
    int max;
    if(a >= b && a >= c)
        max = a;
    if(b >= a && b >= c)
        max = b;
    if(c >= a && c >= b)
        max = c;
    return max;
}

int min(int a, int b, int c)
{
    int min;
    if(a <= b && a <= c)
        min = a;
    if(b <= c && b <= a)
        min = b;
    if(c <= a && c <= b)
        min = c;
    return min;
}

我们趁热打铁,继续A一道中等难度的题目
leetcode279. 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.

这题是不是稍微有了些难度呢。
这题类似于找零钱,我选择了这一道题。
我们考虑该怎么做,先考虑dp数组吧,毫无疑问,dp[i]数组的意义就是组成i的最少的个数。
dp[0]即为0,千万不可置为1。我们继续上代码。我写的AC代码。效率不知道为什么不太高。

#define min(a, b) ((a) < (b) ? (a) : (b))
int numSquares(int n){
    int i, j;
    int dp[n + 1];
    dp[0] = 0;
    dp[1] = 1;
    for(i = 2; i <= n; i ++){
        dp[i] = n;
        for(j = 1; j * j <= i; j ++){
            dp[i] = min(dp[i], dp[i - j * j] + 1);
        }
    }
    return dp[n];
}


要注意的是dp[i]每一从初始化一个够大的数字,这个数字可以是n.

最后我们来看最经典的背包问题.
Charm bracelet (POJ 4131)

总时间限制: 1000ms 内存限制: 65536kB
描述
Bessie has gone to the mall’s jewelry store and spies a charm bracelet. Of course, she’d like to fill it with the best charms possible from the N(1 ≤ N≤ 3,402) available charms. Each charm iin the supplied list has a weight Wi(1 ≤ Wi≤ 400), a ‘desirability’ factor Di(1 ≤ Di≤ 100), and can be used at most once. Bessie can only support a charm bracelet whose weight is no more than M(1 ≤ M≤ 12,880).
Given that weight limit as a constraint and a list of the charms with their weights and desirability rating, deduce the maximum possible sum of ratings.

输入
Line 1: Two space-separated integers: N and M
Lines 2…N+1: Line i+1 describes charm i with two space-separated integers: Wi and Di
输出
Line 1: A single integer that is the greatest sum of charm desirabilities that can be achieved given the weight constraints
样例输入
4 6
1 4
2 6
3 12
2 7
样例输出
23

最经典的DP问题。
暴力的话,要达到指数级,2的n次方,时间复杂度过高了。
我们用DP的思想来考虑。
同样的可以先用递归的想法来考虑。我大概写了一个思路。

int curw; //背包当前的重量
int index; //物品的编号
int vlaue[i], weight[i] //分别代表价值和重量的数组 
int w; //容量 
int n; //物品总数 
 
int f(int index, int curw){
	int a[n][w]; 
	if(curw > w) //边界情况 
		return 0;
	if(index >= n)
		return 0;
	if(f(index, curw) >= 0) //我对递归的优化,这是一个记忆化,一旦大于0,就不用重复计算。
		return f(index, curw);
	a[index][curw] = max(f(index + 1, curw + weight[index]) + value[index], f(index + 1, curw)); //两种xuanze
	return a[index][curw];
}

具体的思路见注释。
有了这个我们就好改写了。
我们直接上代码。注意这里优化了空间,只用了一维数组。优化的方法则是在第二个循环逆序。

#include<stdio.h>
#define max(a,b) ((a)>(b)?(a):(b)) 

int n,m;
int sum;

typedef struct Item
{
	int w;//重量 
	int v;//价值 
}it;

it item[1010]; 

int dp[1010]; //在总体积不超过的条件下所能获得的最大价值 

int main()
{
	scanf("%d%d",&n,&m);
	int i,j,k;
	for(i = 1; i <= n; i ++)
	{
		scanf("%d %d",&item[i].w, &item[i].v);
	}
	for(j = 0; j <= m; j ++)
	{
		if(item[1].w <= j)
			dp[j] = item[1].v;
		else
			dp[j] = 0;
	}
	for(i = 2; i<=n; i ++)
	{
		for(j = m; j >= 0; j --)
		{
			if(item[i].w <= j)
			{
				dp[j] = max(dp[j], dp[j-item[i].w] + item[i].v);
			}
		}
	}
	printf("%d", dp[m]);
	return 0;
}


 

可能才接触的时候会好奇为什么for循环是需要逆序的。
我也困惑了一段时间,后来直到自己列出了二维数组,会发现如果正序是会影响到结果的,因为后面需要用到前面的初始化数据,但是此时已经被改变,所以得不到正确答案,逆序则可避免这样的情况发生。总之还是需要自己画图,可以更好的理解!

还有一种滚动数组,是一种小技巧,非常使用,即 mod 2.
下面是思路。

dp[0][0] = 0;
for(int index = 1; index <= n; index ++){
	dp[index % 2][0] = 0;
	for(int j = 1; j <= w; j ++){
		if(j >= weight[index])
			dp[index % 2][j] = max(dp[(index - 1) % 2][j - weight[index]] + value[index], dp[index % 2][j]);
	}
}

这样就有效的节省了空间,是很妙的一招。
以后还会更新各种各样的算法,这篇文章还是挺长的了,哈哈。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值