动态规划(Java)

前言

  • 动态规划的题做的太少了,刷一些题,练习一下用动态规划解题的意识 以及知道一些常用技巧。
  • 文章中每个题都写了一些对我来说,理解此题比较重要的点和思路。
  • 有问题欢迎评论哦。

1. 剑指 Offer 10- I. 斐波那契数列 [easy]

去做题
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.

斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。


输入:n = 2
输出:1

斐波那契数列是很简单很熟悉的一道题了

  1. dp[0] = 0
    dp[1] = 1
  2. dp[n] = dp[n-1] + dp[n-2]
  3. 因为 F(N) 只和 F(N-1) 、F(N-2)有关,也可以不使用数组,而利用【滚动数组思想】把空间复杂度优化成 O(1)。
public int fib(int n) {
    int[] dp = new int[n+1];
    if(n < 2)return n;
    else{
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2; i<=n; i++)
            dp[i] = (dp[i-1] + dp[i-2])%1000000007;
    }
    return dp[n];
}
//空间优化为O(1)
public int fib(int n) {
	if(n < 2) return n;
    int f0 = 0, f1 = 1, fn = 0;
    for(int i=2; i<=n; i++){
       fn = (f0 + f1)%1000000007;
       f0 = f1;
       f1 = fn; 
    }
    return fn;
}


2. 剑指 Offer 10- II. 青蛙跳台阶问题 [easy]

去做题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。


输入:n = 7
输出:21

与斐波那契数列类似。

  1. dp[n] 表示跳上n级台阶有多少种跳法
  2. dp[0] = 1
    dp[1] = 1
  3. dp[n] = 跳上(n-1)级台阶的跳法 + 跳上(n-2)级台阶的跳法
  4. 同斐波那契数列相同省去开辟数组空间。
public int numWays(int n) {
    if(n < 2) return 1;
    int f0 = 1, f1 = 1, fn = 0;
    for(int i = 2; i<=n; i++){
        fn = (f0 + f1)%1000000007;
        f0 = f1;
        f1 = fn;
    }
    return fn;
}


3. 剑指 Offer 19. 正则表达式匹配 [hard]

去做题
请实现一个函数用来匹配包含 ‘. ’ 和 ‘ * ’ 的正则表达式。模式中的字符 ‘. ’ 表示任意一个字符,而 ‘ * ’ 表示它前面的字符可以出现任意次(含0次)
在本题中,匹配是指字符串的所有字符匹配整个模式。
例如:字符串 “aaa” 与模式 “a.a”“ab*ac*a” 匹配,但与 “aa.a”“ab*a” 均不匹配。


输入:
s = “aa”
p = “a”

输出: false

  1. p为 包含 ‘. ’ 和 ‘ * ’ 的正则表达式 ,s为 字符串
  2. 布尔类型动态规划矩阵。dp[i][j] 代表字符串 s 的前 i 个字符和 p 的前 j 个字符能否匹配。
  3. dp[0][0] = true
    初始化首行,即s为空字符串时,dp[0][j] = dp[0][j - 2] && p[j - 1] = ‘*’
    例如 p = ‘a*b*’,就可以匹配空字符串,即让a、b都出现0次
  4. 由于 dp[0][0] 代表的是空字符的状态, 因此 dp[i][j] 对应的添加字符是 s[i - 1] 和 p[j - 1]
    分情况考虑新添加的字符 p[j-1] == ‘*’ 和 p[j-1] != ‘*’
class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length() + 1, n = p.length() + 1;
        boolean[][] dp = new boolean[m][n];
        dp[0][0] = true;
        // 初始化首行
        for(int j = 2; j < n; j += 2)
        	// dp[0][j] = true 条件是:dp[0][j-2] = true 且 p[j-1] = '*'
        	// 对于"a*b*"来说就是p[1]='*',p[3]='*'
            dp[0][j] = dp[0][j - 2] && p.charAt(j - 1) == '*';
        // 状态转移
        for(int i = 1; i < m; i++) {
            for(int j = 1; j < n; j++) {
                if(p.charAt(j - 1) == '*') {
                	// 1.前s[i]与p[j-2]是不是匹配, []* 匹配0次
                    if(dp[i][j - 2]) 
                    	dp[i][j] = true;
                    // 2.前s[i-1]与p[j]是不是匹配 且 新添加的s[i-1]与*前的字符是否相等
                    else if(dp[i - 1][j] && s.charAt(i - 1) == p.charAt(j - 2)) 
                    	dp[i][j] = true; 
                    // 3.前s[i-1]与p[j]是不是匹配 且 *前的字符是否为'.'
                    else if(dp[i - 1][j] && p.charAt(j - 2) == '.') 
                    	dp[i][j] = true;
                } else {
                	// 1.前s[i-1]与p[j-1]是不是匹配 且 s[i-1] == p[i-1]
                    if(dp[i - 1][j - 1] && s.charAt(i - 1) == p.charAt(j - 1)) 
                    	dp[i][j] = true;  
                    // 2.前s[i-1]与p[j-1]是不是匹配 且 s[i-1] == '.'
                    else if(dp[i - 1][j - 1] && p.charAt(j - 1) == '.') 
                    	dp[i][j] = true;         
                }
            }
        }
        return dp[m - 1][n - 1];
    }
}


4. 剑指 Offer 42. 连续子数组的最大和 [easy]

去做题
输入一个整型数组,数组中的 一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。


输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6

  1. dp[i] 表示以nums[i]结尾的子数组的最大和
  2. dp[0] = dp[0]
    dp[i] = max( 以nums[i-1]结尾的子数组最大和+nums[i],nums[i] )
  3. 只要返回最大值即可,不需要求得所有的状态,无需开辟数组空间。

在这里插入图片描述

class Solution {
    public int maxSubArray(int[] nums) {
    	// pre = 以x结尾的子数组的最大和
    	// maxAns = 最大和
        int pre = 0, maxAns = nums[0];
        for (int x : nums) {
            pre = Math.max(pre + x, x);
            maxAns = Math.max(maxAns, pre);
        }
        return maxAns;
    }
}


5. 剑指 Offer 46. 把数字翻译成字符串 [medium]

去做题
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。


输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", “bwfi”, “bczi”, “mcfi"和"mzi”

特殊的跳台阶问题,区别是能不能跳两个需要判断一下。

  1. dp[i] 表示以第 i 位结尾的前缀串翻译的方案数,考虑第 i 位单独翻译和与前一位连接起来再翻译对 dp[i] 的贡献
  2. dp[0] = 1
  3. dp[i] = dp[i-1] + dp[i-2] (10<x<25)|| dp[i-1]
//代码与 2.青蛙跳台阶问题 是相似的,只是加了判断 10<x<25
class Solution {
    public int translateNum(int num) {
    	//String.valueOf() 将【int num】转换为字符串
        String src = String.valueOf(num);
        int f0 = 1, f1 = 1;
        int fn = 1;
        for (int i = 1; i < src.length(); i++) {
        	// string.substring(from, to)
        	// from: 提取的子串的第一个字符所在位置。
        	// to: 比要提取的子串的最后一个字符位置多 1,无此参数,则截取至字符串结尾
            String subS = src.substring(i - 1, i + 1);
            //判断 
            // compareTo: 此字符串小于字符串参数,则返回一个小于 0 的值;此字符串大于字符串参数,则返回一个大于 0 的值。
            if(subS.compareTo("25") <= 0 && subS.compareTo("10") >= 0){
                fn = f0 + f1;
            }else{
                fn = f1;
            }
            f0 = f1;
            f1 = fn;
        }
        return fn;
    }
}


6. 剑指 Offer 47. 礼物的最大价值 [medium]

去做题
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?


输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]

输入: 12

全部遍历完后,返回最大值

  1. dp[ i ][ j ] 表示 到达(i,j)时能拿到的最大价值。
  2. dp[0][0] = grid[0][0]
  3. dp[ i ][ j ] = grid[ i ][ j ] + max(dp[i-1][j],dp[i][j-1])
  4. 因为只能 向下或向右 走,所以到达一个新格子后,要看是从 上面(i-1, j) 来的价值大,还是从 左面(i, j-1) 来的价值大。还要注意当在第一行时没有(i-1, j),第一列时没有(i, j-1)。
class Solution {
    public int maxValue(int[][] grid) {
        int m = grid.length, n = grid[0].length;
		// 第一行和第一列赋值
		// 直接将累加的值赋给grid[i][j], 没有再定义一个dp[][]
        for(int i=1; i<m; i++)
            grid[i][0] += grid[i-1][0];
        for(int j=1; j<n; j++)
            grid[0][j] += grid[0][j-1];
		// 遍历棋盘
        for(int i=1; i<m; i++){
            for(int j=1; j<n; j++){
            	// 取两者中大的值与原价值累加
                grid[i][j] += Math.max(grid[i-1][j], grid[i][j-1]);
            }
        }
        return grid[m-1][n-1];
    }
}


7. 剑指 Offer 48. 最长不含重复字符的子字符串 [medium]

去做题
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。


输入: “abcabcbb”
输出: 3

  1. dp[j] 代表以字符 s[j] 为结尾的 “最长不重复子字符串” 的长度
  2. dp[0] = 1
  3. dp[j] = dp[j-1] + 1 (字符 s[i] 在子字符串 dp[j−1] 区间之外) || j - i (字符 s[i] 在子字符串 dp[j-1]区间之中)
class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> dic = new HashMap<>();
        // res = 最大长度
        // tmp = 当前不重复子字符串长度
        int res = 0, tmp = 0;
        for(int j = 0; j < s.length(); j++) {
        	// 获取索引 i
        	// getOrDefault() 方法获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值。
        	// charAt()返回字符串中指定位置的字符
            int i = dic.getOrDefault(s.charAt(j), -1); 
            // 使用哈希表统计 各字符最后一次出现的索引位置
            dic.put(s.charAt(j), j); 
            // j一直往前遍历,i=上一次出现s[j]的索引位置,如果是第一次出现则等于-1
            // j-i = 两个相同字母间隔长度
            // ① tmp = tmp+1 【tmp < j - i】 字符 s[i] 在子字符串 dp[j−1] 区间之外
            // ② tmp = j-i 【tmp > j - i】 字符 s[i] 在子字符串 dp[j-1]区间之中
            tmp = tmp < j - i ? tmp + 1 : j - i; 
            // max(dp[j - 1], dp[j])
            res = Math.max(res, tmp); 
        }
        return res;
    }
}


8. 剑指 Offer 49. 丑数 [medium]

去做题
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。


输入: n = 10
输出: 12

  1. dp[n] 表示第n-1个丑数
  2. dp[0] = 1
    dp[1] = 2
  3. dp[i] = min(?×2,?×3,?×5)
  4. 一个丑数 ×2、3、5 都会产生一个新的丑数。从小到大的顺序的丑数数组中新的丑数应该等于 旧丑数×2、3、5 产生三个数再比较大小获得。
  5. 借用三个指针来指示此时的丑数是否×2、3、5
class Solution {
    public int nthUglyNumber(int n) {
        //初始化
        int[] dp = new int[n];
        dp[0] = 1;
        //借助三个指针
        int p2 = 0, p3 = 0, p5 = 0;
        //每次循环都计算出一个丑数赋给dp[n]
        for (int i = 1; i < n; i++) {
        //初始时三个指针都指向dp[0], 如果这个数加入数组,对应指针就会++
            int num2 = dp[p2] * 2, 
            	num3 = dp[p3] * 3, 
            	num5 = dp[p5] * 5;
            
            dp[i] = Math.min(Math.min(num2, num3), num5);
            
            if (dp[i] == num2) p2++;
            if (dp[i] == num3) p3++;
            if (dp[i] == num5) p5++;
        }
        return dp[n-1];
    }
}


9. 剑指 Offer 60. n个骰子的点数 [medium]

去做题
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。


输入:1
输出:[0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]

  1. 需打印出所有的点数之和出现的概率。所以输出的数组中应该有几个数呢?有6n-(n-1) = 5n+1 个 【随着n的增加,最大能出现的数就是6n,再减去n-1(如果是6个骰子扔在地上,那和最小也是6即6个1,不可能出现[1, 2, 3, 4, 5],所以能出现的和要减去 n-1】
  2. dp[n] 表示 n 个骰子所有可能的值出现的概率
  3. dp[1] = [1/6, 1/6, 1/6, 1/6, 1/6, 1/6]
  4. dp[n] 数组中对应和的概率 = 原本为这个和的概率 + 新组合可为这个和的概率

以n=2为例
求dp[2],遍历dp[1]的各点数概率对dp[2]数组的影响。
在这里插入图片描述
图来自:力扣Krahets题解

class Solution {
    public double[] dicesProbability(int n) {
    	// dp初始化,一个骰子值全为1/6
        double[] dp = new double[6];
        Arrays.fill(dp, 1.0 / 6.0);
        // 循环,直到求到n个骰子的情况
        for (int i = 2; i <= n; i++) {
            double[] tmp = new double[5 * i + 1];
            // 遍历dp数组, j<dp.length
            for (int j = 0; j < dp.length; j++) {
                for (int k = 0; k < 6; k++) {
                	// 新加入的骰子可能投出的值为[1~6],对原来dp数组产生影响。
                    tmp[j + k] = tmp[j + k] + dp[j] *(1.0/6.0);
                }
            }
            //dp和tmp交替赋值,tmp为当前所求,dp为n-1,最后将tmp赋给dp
            dp = tmp;
            
        }
        return dp;
    }
}


10. 剑指 Offer 63. 股票的最大利润 [medium]

去做题
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。


输入:[7,1,5,3,6,4]
输出:5

  1. 定义状态数组 dp
    dp[i] 表示以prices[i]为结尾的子数组的最大利润(即前 i 日的最大利润)
  2. dp[0] = 0
    dp[i] = max( 前(i-1)日的最大利润,第i日 - 前i日最低价格 )
  3. 由于只需要求得最大得利润,其实是不需要将所有状态都储存的,所有可以利用滚动数组思想优化空间。
class Solution {
    public int maxProfit(int[] prices) {
    	//cost = 前i日最低价格
    	//profit = 最大利润
        int cost = Integer.MAX_VALUE, profit = 0;
        
        for(int price : prices) {
            cost = Math.min(cost, price);
            profit = Math.max(profit, price - cost);
        }
        return profit;
    }
}


感谢阅读,点个赞呗😉😉
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值