剑指 Offer | 动态规划 [14-I,14-II,42,47,63,19,]

42.连续子数组的最大和

class Solution {
    // 用tmp[i]表示[0,i]中包括i的最大子数组的和。
    // 最后返回的是max i tmp[i]
    public int maxSubArray(int[] nums) {
        int l = nums.length;
        int tmp = nums[0];
        int res = tmp;
        for(int i=1;i<l;i++){
            tmp = Math.max(tmp+nums[i],nums[i]);
            res = Math.max(res,tmp); 
        }
        return res;
    }
}

63. 股票的最大利润

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length==0)
            return 0;
        int historyMin = prices[0];
        int res = 0;

        for(int i=1;i<prices.length;i++){
            historyMin = Math.min(historyMin,prices[i]);
            res = Math.max(res,prices[i]-historyMin);
        }
        return res;
    }
}

14- I. 剪绳子 | 递归+Memoization

思路一:每个最大元素,要么是自己分裂成2块,要么自己分裂一块+别人分裂一块,要么全靠别人分裂。

public int cuttingRope(int n) {
        int[] res = new int[n+1]; //res用于记录剪长度为n的绳子最大可能乘积
        res[0] = 0;
        res[1] = 1;
        res[2] = 1;
        for(int i=3;i<=n;i++){
            res[i] = ((int)(i+1)/2)*((int)i/2); //拆成2个元素,这里用了均值不等式
            for(int j=0;j<=i;j++){
                res[i] = Math.max(res[i],j*res[i-j]);
                res[i] = Math.max(res[i],res[i-j]*res[j]);
            }
        }
        // for(int i=0;i<=n;i++)
            // System.out.print(i+"\t");
        // System.out.print("\n");
        // for(int i=0;i<=n;i++){
            // System.out.print(res[i]+"\t");
        // }
        return res[n];
}

思路二:递归+Memoization

class Solution {
    public int memo(int[] m, int n){
        // 如果我们已经计算过这个元素了
        if(m[n]!=0){
            return m[n];
        }
        for(int i=1;i<n;i++){
            m[n] = Math.max(m[n],Math.max(memo(m,n-i)*i,(n-i)*i)); //在分成2块,还是>=3中最大的结果。
        }
        return m[n];
    }
    public int cuttingRope(int n) {
        int[] m = new int[n+1]; // m[i]用来存储长度为i的绳子的最优解。
        m[0] = 0;
        m[1] = 1;
        m[2] = 1;
        memo(m,n);
        return m[n];
    }
}

14- II. 剪绳子 II 🌟

数学证明参考https://leetcode-cn.com/problems/jian-sheng-zi-lcof/solution/mian-shi-ti-14-i-jian-sheng-zi-tan-xin-si-xiang-by/

class Solution {
        public int cuttingRope(int n) {
            if(n==2)
                return 1;
            if(n==3)
                return 2;
            if(n==4)
                return 4;
            // 否则,尽可能分成3的小段
            long res = 1;
            while(n>4){
                n -=3;
                res = (res*3) % 1000000007;
            }
            //最后分成 n = 2,3,4三种情况讨论
            return (int)(n*res%1000000007);
        }
}

47. 礼物的最大价值

这道题感觉和不同路径那一题类似

class Solution {
    public int maxValue(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[] res = new int[n];
        // 初始化第0行
        res[0] = grid[0][0];
        for(int i=1;i<n;i++)
            res[i]=grid[0][i]+res[i-1];
        // 更新第1行到最后
        for(int i=1;i<m;i++){
            // for(int k=0;k<n;k++){
            //     System.out.print(res[k]+"\t");
            // }
            // System.out.print("\n");
            res[0] += grid[i][0];
            for(int j=1;j<n;j++)
                res[j] = Math.max(res[j],res[j-1])+grid[i][j];
        }
        // for(int k=0;k<n;k++){
        //     System.out.print(res[k]+"\t");
        // }
        return res[n-1];
    }
}

19. 正则表达式匹配 | 倒序思考问题

模式中的字符’.‘表示任意一个字符,而’*'表示它前面的字符可以出现任意次(含0次)。
在这里插入图片描述

class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length()+1;
        int n = p.length()+1;
        // dp[i,j]判断 s[0,i-1]和p[0,j-1]是否匹配。 dp[0,0]表示空字符串
        boolean[][] dp = new boolean[m][n]; 
        // 初始化dp[0][j]
        dp[0][0] = true; // 其余默认false
        // 只需要检查偶数位,因为*只可能出现在
        for(int j=2;j<n;j++){
            dp[0][j] = dp[0][j-2] && (p.charAt(j-1)=='*');
        }
        // 状态转移
        // dp[i,0] = False i>0 因为p此时为空字符串
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                // 这里不用担心j-2数组越界,因为不可能在j=1的时候,即p中第一个元素就出现*
                if(p.charAt(j-1)=='*'){
                    dp[i][j] = dp[i][j-2] || dp[i-1][j] && ( s.charAt(i-1)==p.charAt(j-2) || p.charAt(j-2)=='.');
                }
                else{
                    dp[i][j] = dp[i-1][j-1] && (p.charAt(j-1)=='.' || s.charAt(i-1)==p.charAt(j-1));
                }
            }
        }
        return dp[m-1][n-1];
    }
}

46. 把数字翻译成字符串

dp[i+1] = dp[i] + dp[i-1] (若10*i+(i-1))能构成数字。

需要注意的测试样例 507
如果直接看07=10*0+7是可以转化的,但实际上不行。

class Solution {

    // 每一个数字占一位
    public int[] intToArray(int num){
        int tmp = num;
        int l = 0;
        while(tmp!=0){
            tmp/=10;
            l ++;
        }
        int[] res = new int[l];
        for(int i=l-1;i>=0;i--){
            res[i] = num %10;
            num /= 10;
        }
        return res;
    }
    public int translateNum(int num) {
        if(num==0)
            return 1;
        int[] nums = intToArray(num);
        int[] dp = new int[nums.length]; // dp[i]表示[0,i]最多翻译的方法,字符串从1开始
        dp[0] = 1;
        for(int i=1;i<dp.length;i++){
            dp[i] = dp[i-1];
            if(nums[i-1]!=0 && 0<=(10*nums[i-1]+nums[i]) && (10*nums[i-1]+nums[i])<26){
                if(i==1)
                    dp[i]+=1;
                else
                    dp[i]+= dp[i-2];
            }
        }

        return dp[dp.length-1];
    }
}

别人精简的解法:

class Solution {
    public int translateNum(int num) {
        String s = String.valueOf(num);
        int a = 1, b = 1; // i,i-1
        for(int i = s.length() - 2; i > -1; i--) {
            String tmp = s.substring(i, i + 2);
            //若可以组成就返回i-2和i-1的和
            int c = tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0 ? a + b : a;
            b = a;
            a = c;
        }
        return a;
    }
}

48. 最长不含重复字符的子字符串

思路一:

  • dp[j]表示以s.charAt(j)结尾的最长不含重复字符的子字符串。
  • 我们分为两种情况:
    • s.charAt(j) 在之前没有出现过,则dp[j]=dp[j-1]+1。(在上一次结果后面直接添加)
    • s.charAt(j) 在之前第i位出现过,则dp[j]=Math.min(dp[j-1]+1,j-i)。新的解决一定比dp[j-1]小的原因是如果超过dp[j-1]则按照dp[j-1]的定义,它一定会包含重复字符。
class Solution {
    HashMap<Character,Integer> map = new HashMap<>(); //用于记录上一次这个元素何时出现。
    public int lengthOfLongestSubstring(String s) {
        if(s.length()==0)  return 0;
        int l = s.length();
        int[] dp = new int[l]; // dp[i]表示 [0,i] 中以i为结尾的最长不含重复字符的子字符串数量
        dp[0] = 1;
        map.put(s.charAt(0),0);
        int res = 1; // max(dp[i])

        for(int i = 1; i<l;i++){
            // 如果这个元素之前出现过1次,则我们不一定能取到dp[i-1]+1,除非上次出现的位置在上一个之前
            if(map.containsKey(s.charAt(i))){
                // System.out.println("char "+s.charAt(i)+" i = "+i+" map = "+map.get(s.charAt(i)));
                dp[i] = Math.min(dp[i-1]+1,i-map.get(s.charAt(i)));
            }
            else{
                dp[i] = dp[i-1]+1; //因为这个元素之前没有出现过,所以可以添加到上一次dp的结果中。
            }
            map.put(s.charAt(i),i); // 更新这个元素出现的下标
            // 每次取最大的值
            res = Math.max(res,dp[i]);
        }
        return res;
    }
}

我们注意到我们每次dp的时候只和之前1个元素有关,因此,我们可以用一个tmp代替dp[]


class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> dic = new HashMap<>();
        int res = 0, tmp = 0, len = s.length();
        for(int j = 0; j < len; j++) {
            int i = dic.getOrDefault(s.charAt(j), -1); // 获取索引 i
            dic.put(s.charAt(j), j); // 更新哈希表
            tmp = tmp < j - i ? tmp + 1 : j - i; // dp[j - 1] -> dp[j]
            res = Math.max(res, tmp); // max(dp[j - 1], dp[j])
        }
        return res;
    }
}

思路二:双指针
每次确保[i+1,j]之间没有重复字符串,如果有重复字符串,就同时将i,j向右移动。因为之前出现过[i+1,j]的结果,所以存在这样长的子字符串没有重复字符。

class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> dic = new HashMap<>();
        int i = -1, res = 0, len = s.length();
        for(int j = 0; j < len; j++) {
            if(dic.containsKey(s.charAt(j)))
                i = Math.max(i, dic.get(s.charAt(j))); // 更新左指针 i
            dic.put(s.charAt(j), j); // 哈希表记录
            res = Math.max(res, j - i); // 更新结果
        }
        return res;
    }
}

49. 丑数

思路一:暴力打表

class Solution {
    public int nthUglyNumber(int n) {
        Queue<Long> pq = new PriorityQueue<>();
        HashSet<Long> set = new HashSet<>();
        pq.add(1L);
        set.add(1L);
        long res=1;
        while(n>0){
            res = pq.poll();
            if(!set.contains(2*res)){
                pq.add(2*res);
                set.add(2*res);
            }
            if(!set.contains(3*res)){
                pq.add(3*res);
                set.add(3*res);
            }
            if(!set.contains(5*res)){
                pq.add(5*res);
                set.add(5*res);
            }
            n--;
        }
        return (int)res;
    }
}

思路二:

  • dp[i] 代表第 i + 1i+1 个丑数;
  • dp[a]首个乘以2后大于 x n x_n xn的丑数,所以如果 d p [ a ] ∗ 2 = = x n + 1 dp[a]*2==x_{n+1} dp[a]2==xn+1,要a+1切换到下一个丑数。
  • dp[b] 3, dp[c] 4。
  • 换言之,我们每次都要保证要下一个要诞生的处于离他最近的*2,*3,*5的元素范围之内。
    x n + 1 = { x a ∗ 2 , a ∈ [ 1 , n ] x b ∗ 3 , b ∈ [ 1 , n ] x c ∗ 5 , c ∈ [ 1 , n ] x_{n+1}=\left\{\begin{matrix} x_a*2, a\in[1,n] \\ x_b*3, b\in[1,n]\\ x_c*5, c\in[1,n] \end{matrix}\right. xn+1=xa2,a[1,n]xb3,b[1,n]xc5,c[1,n]
    x n + 1 = m i n ( x a ∗ 2 , x b ∗ 3 , x c ∗ 5 ) x_{n+1}=min(x_a*2,x_b*3,x_c*5) xn+1=min(xa2,xb3,xc5)
    即下一个丑数由之前某一个丑数的2/3/5倍得到。
    另一方面,我们要保证 x a ∗ 2 x_a*2 xa2能包含新的丑数,即
    在这里插入图片描述
    所以若 x a ∗ 2 = = x n x_a*2==x_n xa2==xn我们要a++。
class Solution {
    public int nthUglyNumber(int n) {
        int a = 0, b = 0, c = 0;
        int[] dp = new int[n];
        dp[0] = 1;
        for(int i = 1; i < n; i++) {
            int n2 = dp[a] * 2, n3 = dp[b] * 3, n5 = dp[c] * 5;
            dp[i] = Math.min(Math.min(n2, n3), n5);
            if(dp[i] == n2) a++;
            if(dp[i] == n3) b++;
            if(dp[i] == n5) c++;
        }
        return dp[n - 1];
    }
}

注意

(int)(nres%1000000007) ≠ \neq = (int)(nres)%1000000007 后者先计算(int)(n*res)。

Memoization模版

经常利用默认的初始化,如int[] tmp=new int [10]则tmp中10个元素都是0。

public int memo(int[] m, ...){
	// 如果已经有记录了,直接返回
	if(m[n]!=0)
		return m[n];
	// 否则,按照更新方式遍历,取最优的,其中的递归如果已经考虑过了就可以看成是一个常数。
	for(int i=1;i<n;i++)
		m[n] = Math.max(m[n],Math.max(memo(m,n-i)*i,(n-i)*i)); //在分成2块,还是>=3中最大的结果。
	// 返回最优的结果
	return m[n];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值