15届蓝桥备赛(4)

蓝桥备赛(4)

动态规划

动态规划五部曲:①确定dp数组和下标i的含义②递推公式③递推数组如何初始化④遍历顺序⑤打印数组

01背包

主要题干:给你几个带有重量和价值的物品,和一个有一定容量的背包。让你用程序写出容量为m时,这个背包能装下最大价值的物品,前提时每个物品只能放一次!解决这种方法有两种,二维dp数组和一维dp数组,由于每一行都由上一行所确定,所以简化为一维dp数组最为合适,这样能够节省空间性能。现在的题目不可能出01背包的原题,通常是在基础上更改一些条件,亦或者是需要发掘题干中的意思,然后化简成01背包的问题,建立题目与01背包之间的联系十分重要。常见的问题有:在某一容量下能否达到其最大价值、求出最大容量时的最大价值、求出能够达到最大价值的方案数等等。

常用通式有:dp[j] = max(dp[j], dp[j-nums[i]]+val[i])—>求最大价值

​ dp[j] = dp[j] + dp[j-nums[i]]—>求方案数

遍历原则:先物品后背包,背包(内层循环)从后往前遍历

使用最小花费爬楼梯

[传送门]( 746. 使用最小花费爬楼梯 - 力扣(LeetCode) )

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int> dp(cost.size()+1);
        dp[0] = dp[1] = 0;
        for(int i = 2; i <= cost.size(); i++)
        {
            dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]);
        }
        return dp.back();
    }
};

这一题继承了爬楼梯的条件,还是只能爬一层或者两层。首先定义dp数组,dp[i]表示到达该层所需要的最小花费,题中给出可以从下标0或1开始,那么初始化dp[0]和dp[1]都为0,我们到达层是cost的后一个位置,所以dp初始化大小的时候是cost.size()+1,然后就是递推公式:第i层的最小花费=min(i-1层的最小花费+i-1层的cost,i-2层的最小花费+i-2层的cost)。最后返回dp.back()即可。总结:这一题没想清楚的点在于它还是只能爬1层或者两层导致不能推出递推式,再就是dp数组的长度一定要是cost.size()+1。

该题是普通递归,不是01背包

整数拆分

[传送门]( 343. 整数拆分 - 力扣(LeetCode) )

class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n+1);
        dp[0] = 0;
        dp[2] = dp[1] = 1;
        for(int i = 3; i <= n; i++)
        {
            for(int j = 1; j <= i/2; j++)
            {
                dp[i] = max(max(j*(i-j), j*dp[i-j]), dp[i]);
            }
        }

        return dp[n];
    }
};

我在第一步就卡住了,唉就是告诉我这个题用动态规划的方法来做,我都没有思路。。。我在想如何建立起dp数组和题目之间的桥梁,我发现我根本做不到,不知道如何发掘题目中的子问题。首先我们不知道一个数它要如何拆分才能使乘积最大,所以我们也不可能将所有拆分的情况记录下来。题解的dp[i]数组表示i拆分后各个元素相乘的最大值,i就表示输入的n。整数拆分可以分成两种:①拆分成两部分也就是j * (i-j),例如3拆分成1 * 2=2;②拆分成多个部分,也就是大于两个部分,10可以拆分成3 * 3 * 4=36;这里就衍生出了子问题,也就是第二种情况,可以以j为基准再乘i-j的最大拆分情况,那么就能表示成j * dp[i-j],那么递推公式就能写成dp[i] = max(j * (i-j), j * dp[i-j])。当写入到循环体中我们又要确保dp[i]的值只能是最大的乘积记录,所以又要对自己取个max,这样才能确保最后取到的一定是最大的乘积记录。

不同的二叉搜索树

[传送门]( 96. 不同的二叉搜索树 - 力扣(LeetCode) )

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n+1);
        dp[0] = dp[1] = 1;
        for(int i = 2; i <= n; i++)
        {
            for(int j = i-1; j >= 0; j--)
            {
                dp[i] += dp[j]*dp[i-1-j];
            }
        }
        return dp[n];
    }
};

由于这是一个树型结构,不难想出有子问题的,我尝试过构建父子之间的关系,但还是不能推出通用型的递推式,这一题卡在了递推式上。能够明白dp[i]数组表示i个结点所能构成二叉搜索树的个数。

题解关键的突破口在:假设以结点x为根节点,若左子树有m个结点,右子树有n个结点,那么这个以x为根节点一共有m * n个不同的二叉搜索树。那么在这先考虑特殊情况,假设左子树结点为0,右子树结点为3,我们怎么计算dp[0] * dp[3]呢?这里又涉及到二叉树的一个性质,空结点也是一颗二叉树,那么由此它也是一颗二叉搜索树,所以dp[0]为1,这也就是我们需要初始化dp[0]为1的原因。以n=3为例,dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2];那么我们这里需要两层循环来完善dp数组。

分割等和子集

[传送门]( 416. 分割等和子集 - 力扣(LeetCode) )

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        if(accumulate(nums.begin(), nums.end(), 0)%2 != 0) return false;

        int temp = accumulate(nums.begin(), nums.end(), 0)/2;
        vector<int> dp(20000, 0);

        //i代表物品序号,j代表物品容量
        for(int i = 0; i < nums.size(); i++)
        {
            for(int j = temp; j >= nums[i]; j--)
            {
                dp[j] = max(dp[j], dp[j-nums[i]]+nums[i]);
                if(dp[j] == temp) return true;
            }
        }
        //if(dp[temp] == temp) return true;
        return false;
    }
};

分割成等和的子集,假设总和为sum,那么sum/2就可以看成背包的价值,数组元素就可以看成物品,那么就可以把问题转化成从这个数组怎么选可以达到最大价值sum/2。还有一个问题,那么什么能看成重量呢?这里的关键是将重量和价值设为相等,那么在遍历的时候我们只要当dp[sum/2] == sum/2的时候也就是当最大容量的时候取到最大价值,这样就将抽象的问题转换成了01背包的问题。

背包问题总结:它分为两种解题方法,分别是二维数组和一维数组(滑动数组)。二维数组的解法中:下标i表示物品序列,下标j表示背包容量,dp[i] [j]表示在当前容量可容纳的最大价值。然后就是初始化方式第一列(也就是j=0那一列)都初始化为0,因为当容量为0的时候什么物品都不能装,第一行从j>0开始根据物品[i]的重量初始化。再就是递推关系式为:dp[i] [j] = max(dp[i-1] [j], dp[i-1] [j-weight[i]] + val[i]),注意这一题i和j的下标都是从0开始的哦。遍历的方式无论是先物品还是先背包都可以,代表for循环两行可以相互交换;在一维数组解法中:可以观察到当前行都要以上一行为基础,所以我们只需要定义一个dp[j]代表当前行,但是还是需要两层for循环,并且for循环的顺序是有讲究的,必须是先物品后背包因为我们将行给压缩了,所以一定要是先行(物品)后列(背包),除此之外内层循环一定要是从后往前遍历,如果从前往后遍历的话会导致原本的数据被覆盖,导致后面的数据会重复计算。

最后一块石头的重量II

[传送门]( 1049. 最后一块石头的重量 II - 力扣(LeetCode) )

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        vector<int> dp(1501, 0);
        int sum = accumulate(stones.begin(), stones.end(), 0);
        int target = sum/2;
        for(int i = 0; i < stones.size(); i++)
        {
            for(int j = target; j >= stones[i]; j--)
            {
                dp[j] = max(dp[j], dp[j-stones[i]]+stones[i]);
            }
        }
        return sum-dp[target]-dp[target];
    }
};

关键就在如何转换问题,怎么把这道题往01背包那里去靠呢?两个石头相撞,怎么样才能保证最后的结果是最小的呢?可以把石头分成两块,使两组石头重量接近相等,这样你无论怎么撞都能保证剩下最后是最小可能的重量。所以这道题的代码与上一题分割等和子集的代码几乎相同,也是将重量和价值视为相等。不同的是:最后求得的dp[target]代表的是两堆石头中较小的那一堆,那么另外一堆则用sum-dp[target]表示,所以最后的result=sum-dp[target]-dp[target]。这里的sum-dp[target]一定是大于或等于dp[target]的,由于target=sum/2是向下取整的,就例如是奇数和,分成两堆重量都一定是整数,所以一定会有一大一小。要是实在是捋不清谁打谁小就用abs求绝对值。

目标和

[传送门]( 494. 目标和 - 力扣(LeetCode) )

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if((sum+target)%2 != 0) return 0;
        if(abs(target) > sum) return 0;
        int capacity = (sum+target)/2;
        vector<int> dp(capacity+1, 0);
        dp[0] = 1;
        for(int i = 0; i < nums.size(); i++)
        {
            for(int j = capacity; j >= nums[i]; j--)
            {
                dp[j] += dp[j-nums[i]];
            }
        }
        return dp[capacity];
    }
};

最近这个刷题的效率还是欠佳啊!真就是一杯茶一包烟,一道动规写一天。这东西理解起来真是费劲。这里我们需要发现变量之间的恒等关系,这一点很难想到。首先这个数的加和减是可以分开讨论的,所以我们将nums分开放到两个集合,分别是加集合(之和为P)和减集合(之和为M),那么就有P-M=target和P+M=sum,通过等式的性质我们可以求出P=(sum+target)/2,这个就是我们加集合的容量,当然也可以求减集合,实现的代码有所不一样但是思想是一样的。然后就能把这个问题转化成01背包的问题了,物品是nums数组,容量就是P,唯一有点不同的就在题目中让我们求的是方案数,这里给出通式为dp[j] =dp[j]+dp[j-nums[i]],详细怎么来的还是移步到代码随想录的目标和这个视频吧。还有个需要注意的点就在这里的dp数组初始化,我们不能将其全部初始化为0,dp[0]需要初始化为1,代表凑出0这个数有1中方法,也就是不放(仅针对{1,1,1,1,1})这个案例。

一和零

[传送门]( 474. 一和零 - 力扣(LeetCode) )

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));

        for(string str : strs)
        {
            int x = count(str.begin(), str.end(), '0');
            int y = count(str.begin(), str.end(), '1');

            for(int i = m; i >= x; i--)
            {
                for(int j = n; j >= y; j--)
                {
                    dp[i][j] = max(dp[i][j], dp[i-x][j-y]+1);
                }
            }
        }
        return dp[m][n];
    }
};

感觉做不出来的问题在于我对于这种题目的畏惧,首先能分析出strs是物品,可是就是不知道什么表示重量和价值,有想到过二维的dp数组,可是一看到二维就心里暗示着放弃。。。这里的dp[i] [j]表示在容量i和j的时候所能存储的最大子集长度,像动态规划的题目,题目问的你什么,什么就是dp数组,因为答案就要从dp中来,所以可以从这里考虑。i和j分别表示此时能容纳’0’和’1’的数量,那么从而可以想出每个str里的’0’和’1’就是限定的条件,也就是01背包中提到的重量。这里的长度就是1,每次添加了一个字符串只需要加上1就行了。

完全背包

相较于01背包,完全背包的物品可以放置无限次,没有物品个数的限制。

递推公式相同,但遍历顺序不同。

求组合数(排列顺序不同的序列视为相同的组合,例如{1,1,2}和{2,1,1}是相同的):先物品后背包

求排列数(排列顺序不同的序列视为不同的组合,例如{1,1,2}和{2,1,1}是不同的):先背包后物品

零钱兑换II

[传送门]( 518. 零钱兑换 II - 力扣(LeetCode) )

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1, 0);
        dp[0] = 1;
        for(int i = 0; i < coins.size(); i++)
        {
            for(int j = coins[i]; j <= amount; j++)
            {
                dp[j] = dp[j] + dp[j-coins[i]];
            }
        }

        return dp.back();
    }
};

看到这个题,可以发现与前面目标和十分相似,不同就在每个元素可以使用无数次。所以这个题只需要将内层循环改成从前往后遍历即可。

这里自己写的时候出现了执行错误:原因在我dp初始化大小为coins.size(),这道题应该初始化为amount+1,因为这里的dp[j]表示零钱为j时的兑换方案数,那么最后我们要求得的是dp[amount],所以这里应该初始化成amount。

组合总和IV

[传送门]( 377. 组合总和 Ⅳ - 力扣(LeetCode) )

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target+1, 0);
        dp[0] = 1;

        for(int j = 0; j <= target; j++)
        {
            for(int i = 0; i < nums.size(); i++)
            {
                //下标存在并且dp[j]叠加之后不会超过INT_MAX
                if(j-nums[i] >= 0 && dp[j] < INT_MAX - dp[j-nums[i]]) dp[j] += dp[j-nums[i]];
            }
        }
        return dp[target];
    }
};

这一题呢又与零钱兑换II十分相似,不同的地方在这一题的测试案例中可以看出,顺序不同的序列被视为不同的组合,相较于上一题的兑换零钱就是顺序不同的序列被视为相同的组合。这就类似于排列数和组合数,所以这一题是让我们求出排列数,我们就需要先遍历背包后遍历物品。

零钱兑换

[传送门]( 322. 零钱兑换 - 力扣(LeetCode) )

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        //sort(coins.begin(), coins.end());
        vector<int> dp(amount+1, INT_MAX);
        dp[0] = 0;

        for(int i = 0; i < coins.size(); i++)
        {
            for(int j = coins[i]; j <= amount; j++)
            {
                dp[j] = min(dp[j], dp[j-coins[i]]+1);
            }
        }

        if(dp.back() == INT_MAX) return -1;
        else return dp.back();
    }
};

经历了10几道的动态规划题目,终于自己独立ac了一道,真是谢天谢地啊!题目中所求是最小硬币个数,所以递推公式中应当使用最小值min,那么我们在初始化的时候就应当初始化成INT_MAX,再依据题中所给案例,dp[0]就理所应当的初始化成0。还有,这里的遍历顺序没有要求,它求的不是方案数,所以就不需要考虑是排列数还是组合数,所以两层for循环可以颠倒。

完全平方数

[传送门]( 279. 完全平方数 - 力扣(LeetCode) )

class Solution {
public:
    int numSquares(int n) {
        vector<int> dp(n+1, INT_MAX);
        dp[0] = 0;

        for(int i = 1; i <= 100 && i*i <= n; i++)
        {
            for(int j = i*i; j <= n; j++)
            {
                dp[j] = min(dp[j], dp[j-i*i]+1);
            }
        }
        return dp.back();
    }
};

这一题和上一题就是换汤不换药哇!这里的物品其实就是隐含成1到100的一个数组,对应其重量就是i*i的平方数,目标容量就是n,最终所求的就是dp[n]。

单词拆分

[传送门]( 139. 单词拆分 - 力扣(LeetCode) )

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        vector<bool> dp(s.size()+1, false);
        dp[0] = true;

        for(int j = 1; j <= s.size(); j++)//背包
        {
            for(int i = 0; i < j; i++)//物品
            {
                string temp = s.substr(i, j-i);
                if(find(wordDict.begin(), wordDict.end(), temp) != wordDict.end() && dp[i] == true) dp[j] = true;
            }
        }
        return dp.back();
    }
};

这一题稍微有点抽象,背包比较好理解,就是dp数组,但是物品就有点难理解,这里的物品是每一次取出[i,j]范围内的字符串,然后用这一段字符串判断所给的wordDict中是否有这个字符串,并且综合dp[i]这个位置是否为true,如果为true,那么就把当前dp[j]置为true,否则还是false。

这里我认为还有个巧妙的地方在于i和j的坐标错开了,也就使得dp下标和字符串截取完美衔接,是在是太妙了!dp[0]初始化为true,我们肯定下意识就是从1开始遍历,这样恰好就能使i是字符串截取的开始值,判断dp的时候之前dp[j]所存的bool值就恰好在当前的i的位置,这我认为是这一题的精妙之处

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值