1️⃣1️⃣ 普通动态规划(基础 DP)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
题解:
-
动态规划Dynamic Programming ,在观察动态中找到如何规划解题的步骤
-
本题中, n=1,sov=1,n=2,sov=2;n=3时, 3=1+2=2+1,也就是说台阶为3时, 在第一阶就还需要爬两节,在第二阶就还需要爬一节, n=4=3+1=2+2,以此类推
-
dp[i]=dp[i-1]+dp[i-2]即为解
python
public int climbStairs(int n){
if(n==1) return 1;
if(n==2) return 2;
int[] dp = new int[n+1];
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
题解:
-
每一行第i个数由上一行的i.val+(i-1).val组成,即是题解
java
public List<List<Integer>> generate(int numRows) {
// if(numRows==1){return new ArrayList<>(List.of(1));}
// if(numRows==2){return new ArrayList<>(List.of(1,1));}
List<List<Integer>> res = new ArrayList<List<Integer>>();
List<Integer> org = new ArrayList<>(List.of(1));
res.add(org);
if(numRows==1){
return res;
}
for(int i=2;i<=numRows;i++){
List<Integer> temp = new ArrayList<>();
temp.add(1);
for(int j=1;j<org.size();j++){
temp.add(org.get(j)+org.get(j-1));
}
temp.add(1);
org=temp;
res.add(org);
}
return res;
}
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
题解:
-
动态规划就是计算共i间房屋, 到了第i间房屋能够偷到的最大金额,用dp[]数组标识
-
dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1])
java
class Solution {
public int rob(int[] nums) {
if(nums==null||nums.length==0){
return 0;
}
int n = nums.length;
if(n==1){
return nums[0];
}
int[] dp = new int[n];
dp[0]=nums[0];
dp[1]=Math.max(nums[0],nums[1]);
for(int i=2;i<n;i++){
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[n-1];
}
}
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
题解:
-
对于一个数nums,它的最小完全平方数dp[nums],
-
从1~num这个过程, 我们可以逐渐建立一个dp[i]为num=i时的最小完全平方数, 建立过程如下:
- 1~i存在一个j, 这个j使得dp[i-j*j]是最小的解, 那么dp[i]=dp[i-j*j]+1
- 那么在j*j<=i的循环内, 采用min记录找到该j即可
java
class Solution {
public int numSquares(int n) {
int[] dp=new int[n+1];
for(int i=1;i<=n;i++){
int minn=Integer.MAX_VALUE;
for(int j=1;j*j<=i;j++){
minn=Math.min(minn,dp[i-j*j]);
}
dp[i]=minn+1;
}
return dp[n];
}
}
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。你可以认为每种硬币的数量是无限的。
题解:
-
跟上题是一样的思路, 动态维持从1~amount的最小零钱数
java
class Solution {
public int coinChange(int[] coins, int amount) {
int n=coins.length;
int[] dp = new int[amount+1];
for(int i=1;i<=amount;i++){
int minn=Integer.MAX_VALUE;
int find=0;
for(int j=n-1;j>=0;j--){
if(coins[j]<=i&&dp[i-coins[j]]!=-1){
find=1;
minn=Math.min(minn,dp[i-coins[j]]);
}
}
if(find==1){
dp[i]=minn+1;
}else{
dp[i]=-1;
}
}
return dp[amount];
}
}
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
题解:
-
为了更好的判断s中是否存在字典中的单词, 将字典转化为集合Set, 基于哈希表实现的set能够直接判断是否存在/contains
-
存一个布尔数组, 判断在s的第i个位置, 该子串是否合法
-
从s头遍历到尾部, 遍历到i时, 如果0~i存在一个j使得s[0,j]在字典中且s[j,i]在字典中, 那么说明s[0,i]在字典中,即dp[i]=true,那么此处就存在一个DP,每次步入一个新的i,判断其中是否存在j使得上述成立, 直至i=s.length()
java
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet = new HashSet(wordDict);
boolean[] dp = new boolean[s.length()+1];
dp[0]=true;
for(int i=1;i<=s.length();i++){
for(int j=0;j<i;j++){
if(dp[j]&&wordDictSet.contains(s.substring(j,i))){
dp[i]=true;
break;
}
}
}
return dp[s.length()];
}
}
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
题解:
-
最初想到用进出栈的方式维护一个递增子序列, 但是 进出栈维护的是递增的连续子序列,而不是子序列, 给出一个例子就能明白:
-
10,20,10,30,40\],最大的递增连续子序列是3-\[10,30,40\],而最大递增子序列是4-\[10,20,30,40
-
-
上法不通,采用动态规划: 维护一个长度为n的dp数组,dp[i]代表的是nums索引从0到i的最长递增子序列的长度, 那么就能得到:
dp[i] = max(dp[j])+1, 0<j<i, 前提是nums[j] < nums[i]
- 此法从头遍历, 每个i再遍历0~ i-1 即可
- 本方法的时间复杂度是n^2
-
是否可以优化一下, 上法可以看出 ,每次循环遍历查找max(dp[j])存在很大浪费的, 如果能够存储当前最大长度所在的最小nums值就好了, 那么能否实现 维护一个d[]数组, 下表i为最长递增子序列的大小, 那么d[i]即为当前序列能够维持的最小num
-
至于d[i]如果再现在更新为一个比之前小的值, 那么原本存储在该位置的值会存在哪里, 可以把d[i]看作一个桶, 越早存的数据移动到里面
javaclass Solution { public int lengthOfLIS(int[] nums) { int n = nums.length; if(n==0||n==1) return n; int[] d = new int[n+1]; int len = 1; d[len] = nums[0]; for(int i=1;i<n;i++){ if(nums[i]>d[len]){ d[++len] = nums[i]; }else{ int l=1,r=len,pos=0; while(l<=r){ int mid = (l+r) >>1; if(d[mid]<nums[i]){ pos = mid; l=mid+1; }else{ r=mid-1; } } d[pos+1] = nums[i]; } } return len; } }//pos设置为0且最后是d[pos+1]=nums[i]的原因在于二分查找中, pos+1能够定位到最后一个小于nums[i]的d[i], pos+1就能定位到第一个大于nums[i]的d[i]
-
-
与本题有关的是一道阿里的笔试题:
- 也是需要维护一个LIS/最长连续子序列
- 读取数据后根据x属性升序排序, 然后对于y值维护一个最长连续子序列即可
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。测试用例的答案是一个 32-位 整数。
题解:
-
索引i的最大值不再能够依赖索引i-1的最大值来判断, 即,当前位置的最优解未必是由前一个位置的最优解转移得到的
-
关键的就是负数会使当前最大值变为最小值, 如果后面再次出现负数又会使当前最小值变为最大值, 关键在于维护当前的最小值和最大值
-
如果出现负数, 那么前一位的最大值会变成最小值, 前一位的最小值会变成最大值, 那么当前的最大值=Math.max(前面的最大值*当前值,当前值),当前的最小值=Math.min(前面的最小值*当前值, 当前值)
java
class Solution {
public int maxProduct(int[] nums) {
int n = nums.length;
int res = nums[0];
int[] max = new int[n];
int[] min = new int[n];
max[0] = min[0] = nums[0];
for(int i=1;i< n;i++){
if(nums[i]<=0){
int[] temp = max;
max = min;
min = temp;
}
max[i] = Math.max(max[i-1]*nums[i],nums[i]);
min[i] = Math.min(min[i-1]*nums[i],nums[i]);
res = Math.max(max[i],res);
}
return res;
}
}
最无法理解的一题...谁爱做谁做去...看都看不懂...
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
题解:
-
对于这种求最值的题目, 可能需要用到的就是动态规划了, 研究最优策略, 根据子问题的可递归性得到转移方程
-
本题的子问题就是, 对于一个')'来说, 找出与它配对的'('
-
维护一个dp数组, 对于i索引来说, dp[i]表述以nums[i]为结尾字符的最长有效字符长度
-
如果s[i] = '(' 那么无法与其之前的元素组成有效的括号对, 所以dp[i]=0;如果s[i] = ')' 那么分情况考虑
- s[i-1]='(' : 则该二者组成有效括号, 有效括号长度新增2,即dp[i] = dp[i-2]+2 --此处还需判断i-2是否有效即是否在边界值内
- s[i-1]=')': 则该二者的左括号必定在前面, 需要先找到s[i-1]的左括号在哪, dp[i-1]的长度就是s[i-1]左右括号匹配的长度, 那么与s[i]匹配的左括号的位置就是i-dp[i-1]-1; 那么dp[i] = dp[i-1]+2+dp[i-dp[i-1]-1-1](就是还得加上s[i]匹配的左括号的前面部分的有效括号长度,不排除为0的可能)
java
class Solution {
public int longestValidParentheses(String s) {
int n = s.length();
int maxAns = 0;
int[] dp = new int[n];
for(int i = 1;i<n;i++){
if(s.charAt(i)==')'){
if(s.charAt(i-1)=='('){
dp[i] = (i>=2 ? dp[i-2] : 0) +2;
}else if(i-dp[i-1]> 0&&s.charAt(i-1-dp[i-1])=='('){
dp[i] = dp[i-1]+((i-dp[i-1]) >= 2? dp[i-dp[i-1]-2] : 0) +2;
}
maxAns = Math.max(maxAns,dp[i]);
}
}
return maxAns;
}
}