贪心与动态规划相关题目

贪心算法一般分为如下四步:
(1)将问题分解为若干个子问题
(2)找出适合的贪心策略
(3)求解每一个子问题的最优解
(4)将局部最优解堆叠成全局最优解
其实这个分的有点细了,真正做题的时候很难分出这么详细的解题步骤,可能就是因为贪心的题目往往还和其他方面的知识混在一起。
对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
确定dp数组(dp table)以及下标的含义
(1)确定递推公式
(2)dp数组如何初始化
(3)确定遍历顺序
(4)举例推导dp数组
一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢?因为一些情况是递推公式决定了dp数组要如何初始化!后面的讲解中我都是围绕着这五点来进行讲解。可能刷过动态规划题目的同学可能都知道递推公式的重要性,感觉确定了递推公式这道题目就解出来了。其实 确定递推公式 仅仅是解题里的一步而已!一些同学知道递推公式,但搞不清楚dp数组应该如何初始化,或者正确的遍历顺序,以至于记下来公式,但写的程序怎么改都通过不了。
后序的讲解的大家就会慢慢感受到这五步的重要性了。

  1. 贪心-分发饼干
    最简单的贪心题目,对于贪心题目,还有一个常用的技巧就是能排序就先排序。
class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int start = 0;
        int count = 0;
        for (int i = 0; i < s.length && start < g.length; i++) {
            if (s[i] >= g[start]) {
                start++;
                count++;
            }
        }
        return count;
    }
}
  1. 动态规划-斐波那契数列
    这里需要严格遵循动态规划的5个步骤
class Solution {
    public int fib(int n) {
        if(n <= 1)
            return n;
        Integer[] dp = new Integer[n+1];
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2; i <= n; i ++){
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}
  1. 贪心-最大子序和
    这道题也可以使用动态规划来做,动态规划的状态转移方程就是要么去累计和,若为负数就去当前的dp[i]。
class Solution {
    public int maxSubArray(int[] nums) {
        if(nums.length == 1)
            return nums[0];
        int result = Integer.MIN_VALUE;
        int count = 0;
        for(int i = 0; i < nums.length; i ++){
            count += nums[i];
            // 取区间累计的最大值(相当于不断确定最大子序终止位置)
            if(count > result){
                result = count;
            }
            // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
            if(count <= 0){
                count = 0;
            }
        }
        return result;
    }
}
  1. 动态规划-使用最小花费爬楼梯(每上一层楼梯有一个cost,可以爬一层或者两层)
//原创算法
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        if(cost.length <= 1)
            return cost[0];
        if(cost.length == 2)
            return Math.min(cost[0], cost[1]);
        Integer[] dp = new Integer[cost.length];
        dp[0] = cost[0];
        dp[1] = cost[1];
        for(int i = 2; i < cost.length; i ++){
            if(i == cost.length - 1){
                return Math.min(dp[i - 1], dp[i - 2] + cost[i]);
            }
            dp[i] = Math.min(dp[i - 1] + cost[i], dp[i - 2] + cost[i]);
            System.out.println(i + " " + dp[i]);
        }
        return dp[cost.length - 1];
    }
}
  1. 贪心-跳跃游戏
    发现贪心是真的没有固定的套路,只能针对每一个问题具体分析
class Solution {
    public boolean canJump(int[] nums) {
        int cover = 0;
        if(nums.length == 1)
            return true;
        for(int i = 0; i <= cover; i ++){
            cover = Math.max(i + nums[i], cover);
            if(cover >= nums.length - 1)
                return true;
        }
        return false;
    }
}
  1. 动态规划-不同路径2(路径中存在障碍)
    主要思路就是存在障碍的地方dp为0,状态转移方程不变
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int n = obstacleGrid.length, m = obstacleGrid[0].length;
        int[][] dp = new int[n][m];
        dp[0][0] = 1 - obstacleGrid[0][0];
        for (int i = 1; i < m; i++) {
            if (obstacleGrid[0][i] == 0 && dp[0][i - 1] == 1) {
                dp[0][i] = 1;
            }
        }
        for (int i = 1; i < n; i++) {
            if (obstacleGrid[i][0] == 0 && dp[i - 1][0] == 1) {
                dp[i][0] = 1;
            }
        }
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < m; j++) {
                if (obstacleGrid[i][j] == 1) continue;
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[n - 1][m - 1];
    }
}
  1. 动态规划-01背包
    状态转移方程为什么是这个:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    总体来说就是看第i个物品放不放进背包里,放进背包里产生的价值大还是不放进背包里产生的价值更大。
    首先dp[i][j] = dp[i-1][j]是容易理解的,就是第i个物品不放进背包里,所以dp[i][j]与dp[i-1][j]的价值是一样的;dp[i][j] = dp[i-1][j-weight[i]] + value[i]的意思就是i放进背包里,dp[i-1][j-weight[i]]中的j-weight[i]就是为即将放入的第i个物品腾出容量,看看前i-1个物品,目前容量是j-weight[i],还能放入一个weight[i]重量的物品,而第i个物品恰好重量就是weight[i],所以将第i个物品放入背包中,同时再加上value[i],就是第i个物品放入背包中产生的最大价值。
void test_2_wei_bag_problem1() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    // 二维数组
    vector<vector<int>> dp(weight.size() + 1, vector<int>(bagWeight + 1, 0));

    // 初始化
    for (int j = weight[0]; j <= bagWeight; j++) {
        dp[0][j] = value[0];
    }

    // weight数组的大小 就是物品个数
    // 遍历物品
    for(int i = 1; i < weight.size(); i++) { 
    // 遍历背包容量,这里j为什么不从weight[0]开始而是从0开始,
    //如果weight是按照从小到大的顺序排列的,那么从0或者从weight[0]开始
    //结果都是一样的,但是如果weight没有按照从小到大开始排序的的话,那么从weight[0]开始
    //的话,weight中较小的数据就不会正常的执行,也就是j较小的那几列数据就是错误的
        for(int j = 0; j <= bagWeight; j++) {
        	//这里如果不加if的话,会j-weight[i]会负数越界
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

        }
    }

    cout << dp[weight.size() - 1][bagWeight] << endl;
}
  1. 目标和(给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。返回可以使最终数组和为目标数 S 的所有添加符号的方法数。)
    第一思路是回溯,也确实可以用回溯,然后就是想办法使用DP降低时间复杂度,使用下述思路进行转化:
    最终将数组中的数据分为正数堆left与需要转化成负数的堆right,left-right=S,left+right=sum,sum是固定的,left=(S+sum)/2,最终将问题转化为在数组中查找和为left的方法数。
    dp[j]表示填满j(包括j)这么大容积的包,有dp[j]种方法
    递推公式:不考虑nums[i]的情况下,填满容量为j - nums[i]的背包,有dp[j - nums[i]]种方法。那么只要搞到nums[i]的话,凑成dp[j]就有dp[j - nums[i]] 种方法。举一个例子,nums[i] = 2: dp[3],填满背包容量为3的话,有dp[3]种方法。那么只需要搞到一个2(nums[i]),有dp[3]方法可以凑齐容量为3的背包,相应的就有多少种方法可以凑齐容量为5的背包。那么需要把 这些方法累加起来就可以了,dp[j] += dp[j - nums[i]]
    所以求组合类问题的公式,都是类似这种:
dp[j] += dp[j - nums[i]]

初始化:dp[0]=1,装满容量为0的背包,方法有1种,就是装0件物品。
dp[j]其他下标对应的数值应该初始化为0,从递归公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。
对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。
代码如下:

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) sum += nums[i];
        if ((target + sum) % 2 != 0) return 0;
        int size = (target + sum) / 2;
        int[] dp = new int[size + 1];
        dp[0] = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = size; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[size];
    }
}

  1. 组合总和
    给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
    示例:
    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。

***注意:
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
本题为完全背包问题,完全背包问题在遍历内循环时需要从前往后遍历,这样可以对元素进行重复使用。
所以本题遍历顺序最终遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历。

class Solution {
    public int combinationSum4(int[] nums, int target) {
         int[] dp = new int[target + 1];
         dp[0] = 1;
         for(int j = 0; j <= target; j ++){
             for(int i = 0; i < nums.length; i ++){
                 if(j - nums[i] >= 0)
                    dp[j] += dp[j - nums[i]];
             }
         }
         return dp[target];
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值