算法学习day13(动态规划)

一、打家劫舍III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

除了 root 之外,每栋房子有且只有一个"父"房子与之相连。一番侦察之后,聪明的小偷意识到"这个地方的所有房屋的排列类似于一棵二叉树"。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

父节点和子节点不能同时偷(因为是直接相连的房子)

dp0/dp1:分别代表该节点上不偷和偷的最大值

思路:在树形结构中,采用后序遍历,左右中的顺序,从下往上求每一个节点的最大价值。

**如果该节点不偷,**那么下一节点可以偷,也可以不偷,dp0=下一节点偷或不偷的最大值;

**如果该节点偷,**那么下一节点就不可以偷,dp1=root.val+left0+right0;

代码:

复制代码
class Solution {
    public int rob(TreeNode root) {
        int[] res=robTree(root);
        return Math.max(res[0],res[1]);
    }
    //递归函数
    public int[] robTree(TreeNode root){
        int[] result=new int[2];
        //终止条件        
        if(root==null)return result;
        //单层递归逻辑
        int[] left=robTree(root.left);
        int[] right=robTree(root.right);
        //该节点不偷 孩子节点就可以偷
        result[0]=Math.max(left[0],left[1])+Math.max(right[0],right[1]);
        //该节点偷,孩子节点就不能偷
        result[1]=root.val+left[0]+right[0];
        return result;
    }
}

股票问题

二、买卖股票的最佳时机I(一次遍历/动态规划)

一次遍历(贪心):

如果pricei<minprice,更新minprice;

如果pricei>=minprice,看pricei-minprice是否大于maxprofit,大于的话就更新

代码:

复制代码
class Solution {
    public int maxProfit(int[] prices) {
        int minprice = Integer.MAX_VALUE;
        int maxprofit = 0;
        for (int i = 0; i < prices.length; i++) {
            if (prices[i] < minprice) {
                minprice = prices[i];
            } else if (prices[i] - minprice > maxprofit) {
                maxprofit = prices[i] - minprice;
            }
        }
        return maxprofit;
    }
}
动态规划:

注意:题目要求是买入和卖出只能一次,也就是只能在某一天买入或者某一天卖出

1.dpi0/1:0代表该天不持有股票,1代表该天持有股票。dpi0/1:代表该天可以获得最大利润

2.递推公式:

2.1 dpi0可能是延续上一天的状态,也可能是在这一天把股票卖了。

dpi0(第i天不持有股票)=Math.max(dpi-10,dpi-11+pricesi);

2.2 dpi1可能是延续上一天的状态,也可能是在这一天买入股票

dpi1=Math.max(dpi-11,-pricesi);

3.初始化:dp00:0 dp01:-prices;

4.遍历:从1开始到prices.length

代码:

复制代码
class Solution {
    public int maxProfit(int[] prices) {
        if(prices==null||prices.length==0)return 0;
        // dp[x][0]:第x天不持有股票;dp[x][1]:第x天持有股票
        int[][] dp = new int[prices.length][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];

        //注意dp[i][1]的递推公式 只能买入/卖出一次和每天买入/卖出的区别
        for (int i = 1; i < prices.length; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], - prices[i]);
        }
        return Math.max(dp[prices.length - 1][0], dp[prices.length - 1][1]);
    }
}

三、买卖股票的最佳时机II(贪心/动态规划)

可以在某一天买入,同一天卖出。买入卖出允许多次。

贪心:如果pricesi>pricesi-1 直接加利润
复制代码
class Solution {
    public int maxProfit(int[] prices) {
        int sumProfit=0;
        for(int i=1;i<prices.length;i++){
            if(prices[i]-prices[i-1]>0){
                sumProfit+=prices[i]-prices[i-1];
            }
        }
        return sumProfit;
    }
}
动态规划:

这个题和I的区别在于:上一道题只能一天买入,一天卖出。买入卖出的次数仅是1;而这一道题买入卖出的次数可以是多次。因此在状态方程上要发生变化

递推公式:

dpi0可能是延续上一天的状态,也可能是在这一天把股票卖了。

dpi0(第i天不持有股票)=Math.max(dpi-10,dpi-11+pricesi);

dpi1可能是延续上一天的状态,也可能是在这一天买入股票

dpi1=Math.max(dpi-11,dpi-10-pricesi);

代码:

复制代码
class Solution {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0)
            return 0;
        // 定义dp数组
        int[][] dp = new int[prices.length][2];
        // 初始化dp数组
        dp[0][0] = 0;// 不占有股票
        dp[0][1] = -prices[0];// 占有股票
        for (int i = 1; i < prices.length; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return Math.max(dp[prices.length - 1][0], dp[prices.length - 1][1]);
    }
}

四、买卖股票的最佳时机III(只可以买卖两次)

dpprices.length5:dp数组的五个状态

不操作:dpi0=dpi-10;

第一次持有:dpi1=Math.max(dpi-11,dpi-10-pricesi);

第一次不持有:dpi2=Math.max(dpi-12,dpi-11+pricesi);

第二次持有:dpi3=Math.max(dpi-13,dpi-12-pricesi);

第二次不持有:dpi4=Math.max(dpi-14,dpi-13+pricesi);

初始化:

dp00 = 0;

dp01 = -prices0;

dp02 = 0;

dp03 = -prices0;

dp04 = 0;

代码:

复制代码
class Solution {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0)
            return 0;
        // 只能买卖两次 dp五种状态 不操作 第一次持有 第一次不持有 第二次持有 第二次不持有
        int[][] dp = new int[prices.length][5];
        // dp数组初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        dp[0][3] = -prices[0];
        dp[0][4] = 0;
        // 遍历数组
        for (int i = 1; i < prices.length; i++) {
            dp[i][0] = dp[i - 1][0];
            dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
            dp[i][2] = Math.max(dp[i-1][2],dp[i-1][1]+prices[i]);
            dp[i][3] = Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
            dp[i][4] = Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
        }
        return dp[prices.length-1][4];
    }
}

五、买卖股票的最佳时机IV(k次交易)

从III找出规律来,两次交易,dp状态会有五次。k次交易,dp状态就会有2k+1次。

定义dp数组:dpprices.length2\*k+1;

状态分别是:第一次不操作;第一次占有/第一次不占有;第二次占有/第二次不占有;.....;第k次占有/第k次不占有

dp数组初始化:

dp00 = 0;

dp01 = -prices0; dp02 = 0; 第一次占有/不占有

dp03 = -prices0; dp04 = 0; 第二次占有/不占有

dp02k-1=-prices0; dp02k=0; 第k次占有/不占有

dp数组遍历:

不操作:dpi0=dpi-10;

第一次持有:dpi1=Math.max(dpi-11,dpi-10-pricesi);

第一次不持有:dpi2=Math.max(dpi-12,dpi-11+pricesi);

第二次持有:dpi3=Math.max(dpi-13,dpi-12-pricesi);

第二次不持有:dpi4=Math.max(dpi-14,dpi-13+pricesi);

找规律:

等于0, =dpi-10;

不等于0,并且是偶数:+pricesi;

不等于0,并且是奇数: -pricesi;

代码:

复制代码
class Solution {
    public int maxProfit(int k, int[] prices) {
        if(prices==null||prices.length==0)return 0;
        //k次交易 定义dp数组
        int[][] dp=new int[prices.length][1+2*k];
        //初始化
        dp[0][0]=0;//不操作
        for(int i=1;i<1+2*k;i++){
            if(i%2==0){
                dp[0][i]=0;
            }else{
                dp[0][i]=-prices[0];
            }
        }
        //进行遍历dp数组
        for(int i=1;i<prices.length;i++){
            for(int j=0;j<1+2*k;j++){
                if(j==0){
                    dp[i][0]=dp[i-1][0];
                }else if(j%2!=0){
                    dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
                }else if(j%2==0){
                     dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-1]+prices[i]);
                }
            }
        }
        return dp[prices.length-1][1+2*k-1];
    }
}

六、买卖股票的最佳时机(包含冷冻期)

冷冻期:在卖出股票后,无法在第二天在进行买入/卖出(冷冻期为一天)

dpi4:四种状态 表示某一天某种状态下的最大利润

**占有股票:**dpi0=Math.max(dpi-10,dpi-13-pricesi,dpi-11-pricesi)

延续上一天占有股票的状态;冷冻期后的买入股票;不占有股票后的买入股票

**不占有股票:**dpi1=Math.max(dpi-11,dpi-13);

延续上一天不占股票的状态;上一天卖出股票

**卖出股票:**dpi2=dpi-10+pricesi;

上一天为占有股票

**冷冻期:**dpi3=dpi-12;

上一天为卖出股票

代码:

复制代码
class Solution {
    public int maxProfit(int[] prices) {
        //剪枝?
        if(prices==null||prices.length==0)return 0;
        /**
        创建dp数组 0:持有股票 1:不持有股票 2:卖出股票 3:冷冻期
         */
        int[][] dp=new int[prices.length][4];
        //初始化dp数组
        dp[0][0]=-prices[0];
        dp[0][1]=0;
        dp[0][2]=0;
        dp[0][3]=0;
        //遍历数组
        for(int i=1;i<prices.length;i++){
            //延续上一天占有股票的状态;不占有股票后的买入股票;冷冻期后的买入股票
            dp[i][0]=Math.max(dp[i-1][0],Math.max(dp[i-1][1]-prices[i],dp[i-1][3]-prices[i]));
            //延续上一天的不持有股票;冷冻期后的不持有股票
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][3]);
            //上一天持有股票
            dp[i][2]=dp[i-1][0]+prices[i];
            //上一天为卖出股票
            dp[i][3]=dp[i-1][2];
        }
        return Math.max(dp[prices.length-1][1],Math.max(dp[prices.length-1][2],dp[prices.length-1][3]));
    }
}

返回最大利润:要进行比较,因为最大利润可能出现在不持有股票/卖出股票/冷冻期三种情况中。

七、买卖股票的最佳时机含手续费

思路:其实跟II差不多,但是多了一个手续费,这里我们规定在每次卖出的时候需要付手续费。因为卖出肯定是买入之后的,就是买入卖出整个操作需要付手续费。

改变:在dp公式需要作出改变;

不占有股票:dpi0=Math.max(dpi-10,dpi-11+pricesi-fee);

占有股票:dpi1=Math.max(dpi-11,dpi-10-pricesi);

初始化:dp00=0;dp01=-prices0;

代码:

复制代码
class Solution {
    public int maxProfit(int[] prices, int fee) {
        //含有手续费 就会限制你进行买卖
        int[][] dp=new int[prices.length][2];
        //dp[i][0]:不占有股票 dp[i][1]:占有股票 买的时候扣手续费
        dp[0][0]=0;
        dp[0][1]=0-prices[0];
        //初始化dp数组
        for(int i=1;i<prices.length;i++){
            dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]-fee);
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
        }
        return dp[prices.length-1][0]; 
    }
}

买卖股票的最佳时机总结:

I:只能有一次买入/一次卖出

不持有股票:dpi0=Math.max(dpi-10,dpi-11+pricesi);

持有股票:dpi1=Math.max(dpi-11,-pricesi);

II:可以进行多次买入/卖出

不持有股票:dpi0=Math.max(dpi-10,dpi-11+pricesi);

持有股票:dpi1=Math.max(dpi-11,dpi-10-pricesi);

III:只能进行两次买入/卖出

状态分为五种:不操作;第一次占有/第一次不占有;第二次占有/第二次不占有

dpi0=dpi-10;

dpi1=dpi-10-pricesi;

dpi2=dpi-11+pricesi;

dpi3=dpi-12-pricesi;

dpi4=dpi-13+pricesi;

有规律的。

IV:只能进行k次买入/卖出

状态共有:2k+1次。

dpij=dpi-1j-1±pricesi; j为偶数+;j为奇数-;

V:包含冰冻期

状态共有四种:

占有股票:dpi0=Math.max(dpi-10,Math.max(dpi-11-pricesi,dpi3-pricesi));

不占有股票:dpi1=Math.max(dpi-11,dpi-13);

卖出股票:dpi2=dpi-10+pricesi

冰冻期:dpi3=dpi-12;

VI:含有手续费

在II的基础上,添加手续费。每次在交易完成的时候要付出手续费,在卖出股票时候。

不持有股票:dpi0=Math.max(dpi-10,dpi-11+pricesi-fee);

持有股票:dpi1=Math.max(dpi-11,dpi-10-pricesi);

子序列问题:

八、最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

复制代码
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 

思路:

1.dpi:表示从0到i,最长子序列的长度

2.dpi=Math.max(dpi,dpj+1);

递推公式如何来的:根据dpi的含义,要找到numsj<numsi的元素,然后在dpj的基础上+1;

但是numsj不仅仅有一个,因此每次取dpi,和dpj+1的较大的。

3.初始化:所有元素初始化为1

4.遍历顺序从1开始。双层for循环寻找numsj<numsi的元素,并且更新dpi

注意:最大值不是dpnums.length-1,而是dp数组中的最大值。

代码:

复制代码
class Solution {
    public int lengthOfLIS(int[] nums) {
        //定义dp数组
        int[] dp=new int[nums.length];
        //初始化
        Arrays.fill(dp,1);
        //双层for循环遍历
        int max=1;
        for(int i=1;i<nums.length;i++){
            for(int j=0;j<i;j++){
                if(nums[j]<nums[i]){
                    dp[i]=Math.max(dp[i],dp[j]+1);
                    max=Math.max(max,dp[i]);
                }
            }
        }
        return max;
    }
}

九、最长连续递增序列(比较简单)

不需要双层for循环寻找比numsi小的元素了。因为题目要求子序列必须是连续的。

代码:

复制代码
class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int[] dp=new int[nums.length];
        Arrays.fill(dp,1);
        int max=1;
        for(int i=1;i<nums.length;i++){
            if(nums[i]>nums[i-1]){
                dp[i]=dp[i-1]+1;
                max=Math.max(max,dp[i]);
            }
        }
        return max;
    }
}

十、最长重复子数组

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

思路:

1.dpij:遍历到i-1,j-1的最长公共子数组的长度

2.if(numsi-1==numsj-0)dpij=dpi-1j-1+1;

3.初始化都为0,因为是从i-1,j-1(-1 -1开始的)。相当于多加了一行一列,这样就不用多用两个循环去初始化原本的第一行第一列。让所有工作交给遍历

4.从i和j分别从1,1开始到nums1.length,nums2.length;

代码:

复制代码
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int len1=nums1.length;
        int len2=nums2.length;
        int max=0;
        int[][] dp=new int[len1+1][len2+1];
        for(int i=1;i<=len1;i++){
            for(int j=1;j<=len2;j++){
                if(nums1[i-1]==nums2[j-1]){
                    dp[i][j]=dp[i-1][j-1]+1;
                    max=Math.max(max,dp[i][j]);
                }
            }
        }
        return max;
    }
}

十一、最长公共子序列(和上一题类似也有不同)

不同:上一道题要求的是子数组是连续的,因此如果nums1i==nums2j的时候,dpij=dpi-1j-1+1;况且只能由i-1j-1而来;

而这一道题不要求序列是连续的,因此当text1.charAt(i-1)==text2.charAt(j-1) 的时候,dpij=dpi-1j-1+1;如果不相等的话,可以由dpi-1j和dpij-1得到。

类似:和上一题一样,为了减少初始化时for循环的复杂,多加一行一列,dplen1+1len2+1

然后初始化由遍历帮助我们完成

代码:

复制代码
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int len1=text1.length();
        int len2=text2.length();
        int[][] dp=new int[len1+1][len2+1];
        
        int result=0;
        for(int i=1;i<=len1;i++){
            for(int j=1;j<=len2;j++){
                if(text1.charAt(i-1)==text2.charAt(j-1)){
                    dp[i][j]=dp[i-1][j-1]+1;
                }else{
                    dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
                }
                result=Math.max(result,dp[i][j]);
            }
        }
        printfDp(dp);
        return result;
    }
    public void printfDp(int[][] dp){
        for(int[] arr:dp){
            for(int i:arr){
                System.out.print(i+" ");
            }
             System.out.println("");
        }
    }
}

十二、最大子序和(贪心/动态规划)

贪心:

如果前面的值是负的,那么就不加(加上拖后腿)

如果前面的值是正的,再加(加上更大)

代码:

复制代码
    public int maxSubArray(int[] nums) {
        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.dpi:从0->i,最大的子序列的和

2.if(dpi-1>0)dpi=dpi-1+numsi;else dpi=numsi

3.初始化:dp0=nums0;

4.从i=1开始遍历

十三、判断子序列(双指针/动态规划)

双指针:

定义slow,fast指针。slow指向s串,fast指向t串。如果fast遍历到末尾以及之前,slow也可以遍历到末尾,那么就包含。

代码:

复制代码
class Solution {
    public boolean isSubsequence(String s, String t) {
        int sLen=s.length();
        int tLen=t.length();
        if(sLen>tLen)return false;
        int slow = 0;
        int fast = 0;

        while(fast<tLen){
            if(slow==sLen)return true;
            else{
                if(t.charAt(fast)==s.charAt(slow)){
                    slow++;
                }
                fast++;
            }
        }
        return slow==sLen;
    }
}

注意:如果是slow和fast同时到达末尾的话,那么while(fast<tLen)最后一次就没进去,但此时slow==sLen,就无法判断到。因此返回的时候应该写:return slow==sLen;

动态规划(类似于求最长公共子序列的长度):

如果最长子序列的长度==s串的长度,那么就返回true。

复制代码
class Solution {
    public boolean isSubsequence(String s, String t) {
        int sLen = s.length();
        int tLen = t.length();
        if (sLen > tLen)
            return false;
        // 定义dp数组
        int[][] dp = new int[sLen + 1][tLen + 1];
        // 遍历dp数组(初始化dp数组放到遍历中)
        int max = 0;
        for (int i = 1; i <= sLen; i++) {
            for (int j = 1; j <= tLen; j++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
                }
                max = Math.max(dp[i][j], max);
            }
        }
        printfDp(dp);
        System.out.println(max);
        return max == sLen;
    }
    public void printfDp(int[][] dp){
        for(int[] arr:dp){
            for(int i:arr){
                System.out.print(i+" ");
            }
            System.out.println(" ");
        }
    }

十四、不同的子序列(回溯法/动态规划)

回溯法:超时..

代码:

复制代码
class Solution {
    StringBuilder sb = new StringBuilder();
    int count = 0;

    public int numDistinct(String s, String t) {
        if(s.length()<t.length())return 0;
        backTracking(s, t, 0);
        return count;
    }

    public void backTracking(String s, String t, int startIndex) {
        // 终止条件
        if (sb.toString().equals(t)) {
            count++;
            return;
        }
        if (startIndex >= s.length())
            return;
        // 单层递归逻辑
        for (int i = startIndex; i < s.length(); i++) {
            sb.append(s.charAt(i));
            backTracking(s, t, i + 1);
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}
动态规划:

1.dpij:以i-1为结尾的s中有多少个以j-1为结尾的t (方便操作 不用初始化了)

2.递推公式(难以理解)

if(s.charAt(i)==t.charAt(j))dpij==dpi-1j-1+dpi-1dpj;

else dpij=dpi-1j;

如何理解:

s[i-1]t[j-1] 不相等时
  • dp[i][j] = dp[i-1][j]
  • 解释:此时我们不能用 s[i-1] 来匹配 t[j-1],所以只能看 s[0...i-2] 中有多少个 t[0...j-1] 的子序列。
s[i-1]t[j-1] 相等时
  • dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
  • 解释:我们有两种选择:
    1. s[i-1] 来匹配 t[j-1]:此时我们需要看 s[0...i-2] 中有多少个 t[0...j-2] 的子序列,这就是 dp[i-1][j-1]
    2. 不用 s[i-1] 来匹配 t[j-1]:此时我们需要看 s[0...i-2] 中有多少个 t[0...j-1] 的子序列,这就是 dp[i-1][j]

3.初始化:dpi0=1;dp0j=0;

4.遍历顺序 1 1

代码:

复制代码
class Solution {
    public int numDistinct(String s, String t) {
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        for (int i = 0; i < s.length() + 1; i++) {
            dp[i][0] = 1;
        }
        
        for (int i = 1; i < s.length() + 1; i++) {
            for (int j = 1; j < t.length() + 1; j++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                }else{
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        
        return dp[s.length()][t.length()];
    }
}
相关推荐
KaMeidebaby1 小时前
卡梅德生物技术快报|PD1 单克隆抗体定制配套 N 糖全谱质控开发
前端·人工智能·算法·数据挖掘·数据分析
我叫唧唧波1 小时前
Python+AI 全栈学习笔记
人工智能·python·学习
8Qi82 小时前
LeetCode 235. 二叉搜索树的最近公共祖先(LCA)
算法·leetcode·二叉树·递归·二叉搜索树·lca·迭代
bIo7lyA8v2 小时前
算法稳定性分析中的随机扰动建模的技术8
算法
城北徐宫2 小时前
Linux信号深度解剖:5种产生、3张表、4次切换
linux·c++·学习
科研online2 小时前
基于多源数据和XGBoost-SHAP分析中国大陆绿地碳汇空间变异影响因素的非线性相关性与尺度差异
算法·学习方法
Cthy_hy3 小时前
拓扑排序超详解:原理 + Kahn 贪心算法
python·算法·贪心算法
三品吉他手会点灯3 小时前
C语言学习笔记 - 43.运算符与表达式 - 运算符1 - 运算符的分类和简单介绍
c语言·笔记·学习·算法
VkN2X2X4b3 小时前
算法复杂度的实验验证与误差分析的技术8
算法
其利天下技术3 小时前
风扇灯无刷电机自适应算法实战指南
算法·cocos2d·无刷电机自适应算法·bldc驱动自适应算法·其利无刷电机驱动算法