《代码随想录(Carl)》 数据结构与算法
二算法类题目
2.1 动态规划
1.509. 斐波那契数【如果没有限定0<=n<=30,大数问题,要增加if( dp[i-1]<(INT_MAX/2) ) 判断,dp[i]=dp[i-1]+dp[i-2]中两个int整型数相加防止越界】
2.70. 爬楼梯【n为正整数,不考虑dp[0]的初始化,dp[0]在本题没有意义;使用滚动数组的思想,将空间复杂度由O(n)优化到O(1)】
3.746. 使用最小花费爬楼梯【题目描述的理解同LeetCode官方题解,题目要求到达楼顶的最小花费,动规算法设计直接求出dp[cost.size()];】
4.62. 不同路径【初始化0行dp[0][j]和0列dp[i][0]为1,同时也方便两层for循环从1开始递推计算,从递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1]可知,dp[i][j]都是从其上⽅和左⽅推导⽽来,那么从左到右从上到下⼀层⼀层遍历就可以了。这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]⼀定是有数值的;使用滚动数组的思想,将空间复杂度由二维向量O(m*n)优化到一维向量O(n);图论=>二叉树叶子节点的个数,递归后序遍历时间复杂度O(2^(m+n-1) - 1);数论=>转成求组合:给你m + n - 2个不同的数,随便取m - 1个数,有⼏种取法。求组合的时候,要防⽌两个int相乘溢出!要在计算分⼦的时候,不断除以分⺟。】
5.63. 不同路径 II【初始化0行dp[0][j]和0列dp[i][0]为1时,如果(i, 0) 这条边有了障碍,障碍之后(包括障碍)都是⾛不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0,循环继续条件i < m && obstacleGrid[0][0] == 0,初始化的部分,很容易忽略了障碍之后应该都是0的情况。该初始化方式也包括了起点是障碍物obstacleGrid[0][0] == 1的情况;】
6.343. 整数拆分【2<=n<=58动态规划,“递推公式决定dp数组要如何初始化”的一个案例-不同递推公式决定不同dp数组初始化;59<=n<=1000贪心=>大数幂求余=>快速/循环幂求余[剑指 Offer 14- II. 剪绳子 II]】
7.96. 不同的二叉搜索树【1举例,2画图,3大问题拆成小问题 进行复杂问题分析,根据动规适合解决的问题特点确定用动态规划方法;】
8.二维dp数组01背包理论基础【初始化三个角度:首先从dp[i][j]数组的定义出发,如果背包容量j为0的话,即dp[i][0],⽆论是选取哪些物品,背包价值总和⼀定为0,即dp[i][0]=0;其次从递推公式出发dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]),可以看出i 是由 i-1 推导出来,那么i为0的时候就⼀定要初始化,dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最⼤价值,倒序遍历保证物品0只被放一次;最后初始化其他下标元素,dp[i][j]在推导的时候⼀定是取价值最⼤的数,如果题⽬给的价值都是正整数那么⾮0下标都初始化为0就可以了,因为0就是最⼩的了,不会影响取最⼤价值的结果。如果题⽬给的价值有负数,那么⾮0下标就要初始化为负⽆穷了。这样才能让dp数组在递推公式的过程中取最⼤的价值,⽽不是被初始值覆盖了。】
9.一维dp数组(滚动数组)01背包理论基础【使用滚动数组将dp二维数组降到一维,需要满⾜的条件:上⼀层可以重复利⽤,直接拷⻉到当前层;初始化时由于递推公式dp[j]不依赖i-1行,所以不对i=0行dp[0][j]专门初始化;⼀维dp数组遍历背包是从⼤到⼩的顺序,倒叙遍历是为了保证物品i只被放⼊⼀次!两个嵌套for循环的顺序只能是先遍历物品后遍历背包容量,背包容量⼀定是要倒序遍历的,如果遍历背包容量放在上⼀层,那么每个dp[j]就只会放⼊⼀个物品,即:背包⾥只放⼊了⼀个物品。】
10.416. 分割等和子集【⾸先,本题要求集合⾥能否出现总和为 sum / 2 的⼦集。其次满足如下四点才能把01背包问题套到本题上:1背包中每⼀个元素不可重复放⼊;2背包的体积为sum / 2;3背包要放⼊的物品是nums[i] 集合⾥的元素,重量是nums[i]元素的数值,价值也是nums[i]元素的数值;4背包如果正好装满,说明找到了总和为sum / 2的⼦集,即dp[sum/2]==sum/2集合中的⼦集总和正好可以凑成sum/2。二维dp数组递推公式中,if (j < weight[i]) dp[i][j] = dp[i - 1][j];不能省略,计算中要用到;】
11.1049. 最后一块石头的重量 II【⾸先,尽量让⽯头分成重量相同的两堆,相撞之后剩下的⽯头最⼩,这样就化解成01背包问题了;其次满足如下四点:1背包中每⼀个元素不可重复放⼊;2初始化:把⽯头遍历⼀遍,计算出⽯头总重量sum,然后除2得到背包的体积为target=sum / 2,dp数组的⼤⼩target+1;3背包要放⼊的物品是nums[i] 集合⾥的元素,重量是nums[i]元素的数值,价值也是nums[i]元素的数值;4那么分成两堆⽯头,⼀堆⽯头的总重量是dp[target],另⼀堆就是sum - dp[target]。在计算target = sum / 2时因为是向下取整,所以sum - dp[target] ⼀定是⼤于等于dp[target]的,那么相撞之后剩下的最⼩⽯头重量就是 (sum - dp[target]) - dp[target]。】
12.494. 目标和【⾸先,本题要如何使表达式结果为S,根据题意有 left组合 - right组合 = S,同时left + right = sum,公式推导left - (sum - left) = S -> left = (S + sum)/2。S是固定的,数组元素和sum是固定的,可以求出left。在集合nums中找出和为left的组合有⼏种方法,也就是装满容量为W = (S + sum) / 2的背包有⼏种⽅法;其次:1背包中每⼀个元素不可重复放⼊;2求装满背包有⼏种⽅法(求组合类问题)时递推公式⼀般为:dp[j] += dp[j - nums[i]];3初始化时从定义和递推公式出发dp[0]⼀定要初始化为1;看到(S + sum) / 2 应该担⼼计算的过程中向下取整有没有影响,应该本能的反应两个int相加数值可能溢出的问题;如果仅仅是求个数的话,就可以⽤dp,如果要求的是把所有组合列出来,还是要使⽤回溯法暴力搜索的。】
13.474. 一和零【首先,本质是有两个维度的01背包;其次:1三维dp数组01背包,dp[i][j][k]为三维向量;2背包容量约束变成2个,分别为字符0的最多个数m和字符1的最多个数n,且要同时满足;3背包要放⼊的物品是strs[i] 集合⾥的元素字符串,每个字符串重量weight_0为字符0的个数,重量weight_1为字符1的个数,价值为字符串的个数1;本题中strs数组⾥的元素就是物品,每个物品都是⼀个!⽽m 和 n相当于是⼀个背包的两个容量维度,两个维度的背包。多重背包是每个物品,数量不同的情况。理解成多重背包的同学主要是把m和n混淆为物品了,感觉这是不同数量的物品。】
14.一维dp数组(滚动数组)完全背包理论基础【完全背包和01背包问题唯⼀不同的地⽅就是,每种物品有⽆限件。完全背包和01背包唯⼀不同就体现在遍历顺序上:1一个物品是可以添加多次的,所以正序遍历背包容量(从⼩到⼤去遍历);2对于⼀维dp数组的纯完全背包问题来说,其实两个for循环嵌套顺序同样⽆所谓!因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。只要保证下标j之前的dp[j]都是经过计算的就可以了。】
15.518. 零钱兑换 II【纯完全背包问题求得是能否凑成总和,和凑成总和的元素有没有顺序没关系,有顺序也⾏,没有顺序也⾏!本题要求装满背包有⼏种⽅式,即凑成总和的组合数,元素之间要求没有顺序。如果求组合数就是外层for循环遍历物品,内层for遍历背包。如果求排列数就是外层for循环遍历背包,内层for遍历物品。】
16.377. 组合总和 Ⅳ【本题求的是排列总和的个数,元素之间要求有顺序;如果求排列数就是外层for循环遍历背包,内层for遍历物品。本题仅仅是求排列总和的个数,并不是把所有的排列都列出来。如果本题要把排列都列出来的话,只能使⽤回溯算法暴力搜索。C++测试⽤例存在使得递推公式dp[j] += dp[j - nums[i]]两个数相加超过int的数据,所以需要在if⾥加上dp[i] < INT_MAX - dp[i - num],if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]])。】
17.70. 爬楼梯【首先,本质是完全背包问题:⼀步⼀个台阶,两个台阶,三个台阶,…,直到m个台阶。问有多少种不同的⽅法可以爬到楼顶呢?其次,1. ⼀步爬1阶,2阶,… m阶就是物品,需要爬n阶到楼顶就是背包。2.每⼀阶可以重复使⽤,例如跳了1阶,还可以继续跳1阶。3.问跳到楼顶有⼏种⽅法其实就是问装满背包有⼏种⽅法,求排列个数。】
18.322. 零钱兑换【本题是要求最少硬币数量,硬币是组合数还是排列数都⽆所谓!所以两个for循环先后顺序怎样都可以!初始化:下标⾮0的元素都是应该是最⼤值INT_MAX;amount=0,返回0而不是-1;dp[j - coins[i]] != INT_MAX时说明当前coins [i]可以凑成j;dp[amount]==INT_MAX时判定“没有任何一种硬币组合能组成总金额,返回 -1”;】
19.279. 完全平方数【首先,本质是完全背包问题:完全平⽅数就是物品(可以⽆限件使⽤),凑个正整数n就是背包容量,问凑满这个背包最少有多少物品?具体求解和[322. 零钱兑换]一样。】
20.139. 单词拆分【1本质是完全背包问题,装满背包有⼏种⽅法,求排列个数;字符串字符匹配时下标从0开始,要与背包容量和字典字符串长度进行区分;C++测试⽤例存在使得递推公式dp[j] += dp[j - nums[i]]两个数相加超过int的数据,所以需要在if⾥加上dp[i] < INT_MAX - dp[i - num],if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]])。Carl完全背包,⽽且是求能否组成背包;本题还有特殊性,因为是要求⼦串,最好是遍历背包放在外循环,将遍历物品放在内循环;如果要是外层for循环遍历物品,内层for遍历背包,就需要把所有的⼦串都预先放在⼀个容器⾥。】
21.多重背包【1每件物品最多有Mi件可⽤,把Mi件摊开放到物品集合中,其实就是⼀个01背包问题了;2在01背包⾥⾯再加⼀个for循环遍历每种商品的数量,把不同数量的该种商品都当成一个物品进行计算,计算Mi次。】
22.背包问题总结【确定递推公式和确定遍历顺序都具有规律性和代表性;对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了。】
感谢Carl对于LeetCode中的数据结构与算法题目的系统讲解工作,可查看网页 linkhttps://programmercarl.com
和 linkhttps://github.com/youngyangyang04/leetcode-master 中的讲解文档。
按照类型从基础到提高循序渐进的过程选择经典题目高频面试题来讲解,大大提高了学习数据结构与算法知识和刷题效率,解决了浪费的时间主要三个问题点:1找题;2找到了不应该现阶段做的题;3没有全套的优质题解可以参考。
按照如下类型来练习:数组-> 链表-> 哈希表->字符串->栈与队列->树->回溯->贪心->动态规划->图论->高级数据结构,从简单刷起,做了几个类型题目之后,再慢慢做中等题目、困难题目。
经过系统的知识学习和技能练习现对整个知识体系的掌握有了一个质的飞跃。