Bootstrap

算法-动态规划

动态规划

基础知识

  • 包含了分治思想、空间换时间、最优解等多种基石算法;
  • 动态规划是通过组合子问题的解得到原问题的解;
  • 适合动态规划的问题具有重叠子问题和最优子结构两大特性;
    重叠子问题:即各个子问题中包含重复的更小子问题;使用暴力枚举,求解相同的子问题会产生大量的重复计算;而动态规划会将子问题的解保存,后续迭代查表即可,保证每个独立子问题只被计算一次;
    最优子结构:如果一个问题的最优解可以由其子问题的最优解组合构成,并且这些子问题可以独立求解,那么称此问题具有最优子结构。

重叠子问题
记忆递归 = 从顶至低;
动态规划 = 从低至顶;
斐波那契数列问题并不包含最优子结构问题,只需要计算每个子问题的解,避免重复计算即可,并不需要从子问题组合中选择最优组合。

最优子结构
一个问题的最优解可以由其子问题最优解组合构成,那么称此问题具有最优子结构;

蛋糕售价最高问题:

不同重量的蛋糕售价不一样,已知总的蛋糕重量,求蛋糕的最大收益?
状态:
f(x)表示x重量的蛋糕的最高售价,其中f(0=0 f(1= p(1= 2
p(x) 表示x重量的蛋糕价格;

分析:重量为n的蛋糕总价可切分为n种组合,即01,2...n-1 蛋糕的最高售价 加上 n,n-1,n-2....1剩余蛋糕的售价,组合中取最大值。

转移方程:
f(n) = f(i) + p(n-i)的最大值,i属于0到n之间

分治与动态规划的区别

区别动态规划分治方法
子问题划分将问题划分成子问题有重叠的情况
不同的子问题具有公共的子子问题
将问题划分成互不相交的子问题
是否会反复求解公共子子问题

解题步骤

  • 1 确定状态
    解动态规划需要创建一个数组,数组的每个元素f[i] 或者 f[i][j] 表示什么;
    步骤:研究最优策略的最后一步;化为子问题;

  • 2 初始条件、边界情况
    初始条件:f[0] f[1]
    边界条件:数组的边界、越不越界问题

  • 3 转移方程
    根据子问题定义直接得到

  • 4 计算顺序
    利用之前的计算结果

分割等和子集(leecode416 )

描述
给你一个 只包含正整数 的 非空数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

案例
输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。
输入:nums = [1,2,3,5] 输出:false 解释:数组不能分割成两个元素和相等的子集。

分析
①自己
将数组分成两个数组,比较两个数组的和是否一样。这种思路;
②参考1
提取数组元素子集,看子集之和是否是整个数组元素之和的一半。

动态规划1

图解分析
在这里插入图片描述
在这里插入图片描述

class Solution {
    public boolean canPartition(int[] nums) {
		// 分析
		int n = nums.length;
		// 求数组元素之和 与 最大值
		int sums = 0,maxNum = 0;
		for(int num : nums){
			sums += num;
			maxNum = Math.max(num,maxNum);
		}
		// 如果数组长度小于2 返回false,因为无法分成两个集合
		if(n < 2)
			return false;
		// 如果sums结果为奇数,没有办法分成两个数组 且两者元素之和相等
		if(sums %2 != 0)
			return false;
		
		// 目标值target
		int target = sums / 2;
		// 如果元素的最大值 大于 目标值 说明数组无法分成两个元素值之和相等的 情况
		if(maxNum > target)
			return false;

		// 确定状态 dp[i][j] 表示 从数组下标0-i中选取若干个元素,是否可以等于j
		boolean[][] dp = new boolean[n][target+1];
		
		// 特殊值
		// 不选值的时候,且目标值是0 则结果全是true
		// 另一种解释:背包容量为0,一定为true,主要不选择任何元素,就能填满背包;
		for(int i = 0 ; i < n; i++){
			dp[i][0] = true;
		}
		// 只有一个值的时候,j等于它自己本身 则结果为true
		dp[0][nums[0]] = true;

		// 转移方程
		for(int i = 1 ; i < n ; i++){
			int num = nums[i];
			for(int j = 1 ; j <= target; j++){
				if(j > num){
					dp[i][j] = dp[i-1][j-num] | dp[i-1][j];
				}else{
					dp[i][j] = dp[i-1][j];
				}
			}
		}
		return dp[n-1][target];
    }
}

时间复杂度:O(NC) N 数组的个数;C数组元素的和的一半
嵌套的for循环,外层循环判断条件i<N 内层循环判断条件 j < C
空间复杂度:O(NC)

动态规划2 优化空间

因为本行的dp只和上一行的dp有关系,所以可以将状态从二维转为一维。

class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        int sums = 0 , maxNum = 0;
        for(int num : nums){
            sums += num;
            if(num > maxNum) maxNum = num;
        }
        int target = sums / 2 ;
        if(n < 2 || sums %2 != 0 || maxNum > target)
            return false;

        boolean[] dp = new boolean[target+1];
        // 不选元素 
        dp[0] = true;
        // 只有一个元素,
        dp[nums[0]] = true;
        for(int i = 1 ; i < n ; i++){
            for(int j = target ; j > 0 ; --j){
                if(j >= nums[i])
                    dp[j] = dp[j] | dp[j-nums[i]];
            }
        }
        return dp[target];
    }
}

时间复杂度:O(NC) N 数组的个数;C数组元素的和的一半
嵌套的for循环,外层循环判断条件i<N 内层循环判断条件 j < C
空间复杂度:O© 减少了物品的维度,无论多少个,都使用一行表示状态。

一和零(leetcode474)

需求
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3 输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。
其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3。

输入:strs = [“10”, “0”, “1”], m = 1, n = 1 输出:2
解释:最大的子集是 {“0”, “1”} ,所以答案是 2 。

动态规划(三维空间)

分析
经典的背包问题只有一种容量不同。之前是两维表示状态,该问题现在就是三维。
dp[i][j][k] = 表示前i个字符串中使用j个0和k个1的情况下最多可以得到的字符串数量。
字符串长度为n,则dp数组定义为dp[n][j][k]

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        // 数组长度
        int len = strs.length;

        // 分析状态
        // 创建三维数组,表示前i个字符串中 使用j个0和k个1的情况下最多有几个字符串;
        int[][][] dp = new int[len+1][m+1][n+1];

        // 字符串中0 和 1 的个数
        int zeros = 0;
        int ones = 0;

        // 特殊值:没有字符串可以使用的时候,即i=0的情况,则dp = 0
        for(int j = 0;j<m+1;j++){
            for(int k =0; k <n+1; k++){
                dp[0][j][k] = 0;
            }
        }

        // 转移方程
        for(int i = 1 ; i <=len ; i++){
        // 小于等于len 因为字符串数组长度为len 需要对len个数据进行处理
            zeros = sum(strs[i-1])[0];
            ones = sum(strs[i-1])[1];
            for(int j = 0; j < m+1; j++){
                for(int k = 0 ; k < n+1; k++){
                    if(j < zeros || k < ones)
                        dp[i][j][k] = dp[i-1][j][k];
                    else if(j >= zeros && k >= ones)
                        dp[i][j][k] = Math.max(dp[i-1][j][k],dp[i-1][j-zeros][k-ones]+1);
                }
            }
        }
        return dp[len][m][n];

    }
    // 统计字符串
    public int[] sum(String str){
        int[] arr = new int[2];
        char[] cha = str.toCharArray();
        for(char ch:cha){
            if(ch == '0')
                arr[0]++;
            else if(ch == '1')
                arr[1]++;
        }
        return arr;
    }

}

空间复杂度是:O(lenmn)

动态规划(二维空间)

分析
由于dp[i][][] 只和 dp[i-1][][]有关系,所以可以将状态数组的维度降低一维。

前者:
for(int j = 0; j < m+1 ; j++){
	for(int k = 0 ; k < n+1; k++){
	}
}
后者:
for(int j = m ; j >= zeros ; j--){
	for(int k = n ; k >= ones ; k --){
	}
}

对于for循环j、k的部分,需要从m和n开始,而不是之前的0和0开始,
因为从前往后 dp[j][k] = Math.max(dp[j][k], dp[j - zeros][k - ones] + 1)
这个语句中的dp[j-zeros][k-ones]在前面已经作为dp当前值更新过了,这里相当于使用一个当前行的值更新当前行的值。

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        // 压缩空间存储
        int[][] dp = new int[m+1][n+1];

        int len = strs.length;
        int zeros = 0,ones = 0;

        for(int i = 1;i <len+1;i++){
            zeros = sum(strs[i-1])[0];
            ones = sum(strs[i-1])[1];
            for(int j = m; j >= zeros ; j--){
                for(int k = n ; k >= ones ; k--){
                    dp[j][k] = Math.max(dp[j][k],dp[j-zeros][k-ones]+1);
                }
            }
        }
        return dp[m][n];
    }
    public int[] sum(String str){
        int[] arr = new int[2];
        char[] charr = str.toCharArray();
        for(char ch:charr){
            if(ch == '0')   
                arr[0]++;
            else if(ch == '1')
                arr[1]++;
        }
        return arr;
    }
}

目标和(leetcode494)

需求
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例
输入:nums = [1,1,1,1,1], target = 3 输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

输入:nums = [1], target = 1 输出:1

分析
动态规划 = 计算数组中所有元素之和,添加负号的元素之和,其余就是添加正号的元素之和,
(sum- neg) - neg = target 计算

动态规划
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        // 计算数组的元素的和
        int sum = 0;
        for(int num : nums)
            sum += num;
        // 假定添加负号的元素之和为neg,那么正号之和为(sum-neg),target = (sum-neg)-neg,即sum-2*neg = target。
        int neg = (sum-target)/2;
        
        // neg必须是非负数整数,则sum-target就必须是非负偶数
        if(neg < 0 || sum-target <0 || (sum - target) % 2 != 0)
            return 0;
        // 数组长度
        int n = nums.length;

        // 定义状态数组,表示数组nums的前i个元素里选取元素,使之等于j的方案数
        int[][] dp = new int[n+1][neg+1];

        // 特殊值
        // 当没有元素可以选择的时候,则sum = 0 dp[0][0] = 1 其余的dp[0][j]等于0
        dp[0][0] = 1;
        for(int j = 1 ; j < neg+1 ; j++)
            dp[0][j] = 0;

        // 转移方程
        int num = 0;
        for(int i = 1 ; i < n+1 ; i++){
            // 注意此处可能会产生索引越界问题。
            num = nums[i-1];
            for(int j = 0; j < neg+1 ; j++){
                if(j < num){
                    dp[i][j] = dp[i-1][j];
                }
                else{
                    dp[i][j] = dp[i-1][j] + dp[i-1][j-num];
                }   
            }
        }
        return dp[n][neg];
    }
}

空间复杂度:O(n * neg)

动态规划(优化空间)
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        // 定义数组长度、计算数组元素之和
        int n = nums.length;
        int sum = 0;
        for(int num:nums)
            sum += num;
        // 假设分配负号的元素之和 neg,和target sum存在这样的关系
        // (sum-neg)-neg = target
        int neg = (sum - target)/2;

        // 异常处理
        if(neg < 0 || (sum-target) % 2 != 0)
            return 0;
        
        // 定义状态
        int[] dp = new int[neg+1];
        // 没有元素可以选择 则j的值等于0 对应的结果是1
        dp[0] = 1;
        // 数组元素
        int num = 0;
        // 状态转移方程
        for(int i = 1; i < n+1; i++){
            num = nums[i-1];
            for(int j = neg; j >= num; j--){
                dp[j] += dp[j-num];
            }
        }
        return dp[neg];
    }
}

空间复杂度:O(neg)

回溯

分析
数组nums的每个元素都可以添加符号+ 或者- 因此每个元素有两种添加符号的方法,n个元素共有2n种添加符号的方法,对应的表达式的个数为2n种。
如果表达式的结果等于目标数target,则该表达式即为符合要求的表达式。
可以使用回溯的方法遍历所有的表达式,过程中维护一个计数器count,遇到表达式的值为target时,count的值加1。

图示
在这里插入图片描述
代码

class Solution {

    // 定义变量count 用于统计表达式等于target的数量
    int count = 0 ;

    public int findTargetSumWays(int[] nums, int target) {
        backrack(nums,target,0,0);
        return count;
    }

    public void backrack(int[] nums,int target,int index,int sum){
        //回溯点
        if(index == nums.length){
            if(sum == target){
                count++;
            }
        }else{
            backrack(nums,target,index+1,sum+nums[index]);
            backrack(nums,target,index+1,sum-nums[index]);
        }
    }
}

爬楼梯问题

需求
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

案例
输入:n = 2 输出:2 分析两种情况:1+1 ;2
输入:n = 3 输出:3 分析三种情况:1+1 +1;1+ 2 ; 2+1

动态规划

分析

  • 定义状态
    dp[i] 表示爬第x阶台阶的方案数;即第x阶可能是一步跨上来的,也可能是两步跨上来的。
  • 状态方程
    dp[i] = dp[i-1] + dp[i-2]
  • 边界问题
    0阶台阶 可以看做只有一种方案dp[0] = 1
    1阶台阶 只有一种方案dp[1] = 1

代码

class Solution {
    public int climbStairs(int n) {
        // 定义状态,表示跨越第i阶台阶
        int[] dp = new int[n+1];

        // 状态边界值
        dp[0] = 1;
        dp[1] = 1;

        // 转移方程
        for(int i = 2 ; i < n+1 ; i++){
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }
}

时间复杂度:O(n)
空间复杂度:O(n)

动态规划(空间复杂度降级)

分析
由于当前值dp[i] 只和 dp[i-1] 和 dp[i-2] 有关,所以定义三个元素即可,dp one two
这样空间复杂度就变成O(1)

代码

class Solution {
    public int climbStairs(int n) {
        // 滚动数组 所需三个值
        // dp = 表示爬第i个台阶 one 表示一步迈上第i个台阶 two 表示两步迈上第i个台阶
        int dp = 0,one = 0, two = 1;
        // 状态转移方程
        for(int i = 1 ; i < n+1 ; i++){
            dp = one + two;
            one = two;
            two = dp;
        }
        return dp;
    }
}

时间复杂度:O(n)
空间复杂度:O(1)

矩阵快速幂

图示
在这里插入图片描述

class Solution {
    public int climbStairs(int n) {
        // 矩阵快速幂
        int[][] w = {{1,1},{1,0}};
        
        // 结果矩阵
        int[][] ret = pow(w,n);
        return ret[0][0];
    }

    public int[][] pow(int[][] a,int n){
        int[][] ret = {{1,0},{0,1}};
        while(n > 0){
            if((n & 1 ) == 1)
                ret = multiply(ret,a);
            n >>= 1;
            a = multiply(a,a);
        }
        return ret;
    }

    public int[][] multiply(int[][] a,int[][] b){
        int[][] c = {{0,0},{0,0}};
        for(int i = 0; i < 2; i ++){
            for(int j = 0 ; j < 2; j++){
                c[i][j] = a[i][0]*b[0][j] + a[i][1]*b[1][j];
            }
        }
        return c;
    }
}
通项公式

图示
在这里插入图片描述
在这里插入图片描述

public class Solution {
    public int climbStairs(int n) {
        double sqrt5 = Math.sqrt(5);
        // 求有n阶台阶 由于数列中有0阶 所以第n阶是数列中第n+1个元素
        double f = Math.pow((1+sqrt5)/2,n+1) - Math.pow((1-sqrt5)/2,n+1);
        return (int)Math.round(f/sqrt5);
        // 采用四舍五入的形式。
    }
}

offer63买股票的最佳时机

需求
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

只能买卖该股票一次。

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

方法1:动态规划(有问题)

分析

price[] 表示当前股票的价格
定义状态:
dp[i][0] 表示第i+1天手上没有股票的最大收益;
dp[i][1] 表示第i+1天手上有股票的最大收益;

临界值判断:
第1天的所有值都是0 即dp[0][0] = 0; dp[0][1] = -price[0];

转移方程:
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+price[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-price[i]);
class Solution {
    public int maxProfit(int[] prices) {
        //动态规划

        // 特殊情况
        // 价格数组长度小于2 则怎么都是亏 或者 不亏不赚。
        if(prices.length < 2 || prices == null)
            return 0;
        
        int len  = prices.length;
        // 定义状态
        int[][] dp = new int[len][2];

        // 临界值定义
        dp[0][0] = 0;
        dp[0][1] = -prices[0];

        // 状态转移方程
        for(int i = 1 ; i < len ; i++){
            // 第i+1天没有股票的最大利润;
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
            // 第i+1天有股票的最大利润;
            dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0] - prices[i]);
        }
        // 返回的值是最后一天手中没有股票的情况。
        return dp[len-1][0];
    }
}

分析
结果有问题,不知道哪里出现错误了。
股票只能卖出一次 买入一次。问题出在这里了。

方法2:暴力解法

分析
循环遍历,找到最大值利润,卖出的序号必须在买入序号的后面。

class Solution {
    public int maxProfit(int[] prices) {
        // 暴力解法
        int profit = 0;
        for(int i = 0 ; i < prices.length-1; i++){
            for(int j = i+1; j < prices.length;j++){
                if(prices[j]-prices[i] > profit)
                    profit = prices[j] - prices[i];
            }
        }
        return profit;
    }
}
方法3:一次遍历

分析
找价格最低的点,然后在每一天都判断获利情况,找到获利最多的值。

class Solution {
    public int maxProfit(int[] prices) {
        // 找最小值
        int minPrices = Integer.MAX_VALUE;
        int maxProfit = 0;

        // 特殊值判断
        if(prices.length < 2)
            return 0;
        // 遍历
        for( int i = 0; i < prices.length ; i++){
            if(minPrices > prices[i])
                minPrices = prices[i];
            else if(prices[i] - minPrices > maxProfit)
                maxProfit = prices[i] - minPrices ; 
        }
        return maxProfit;
    }
}

时间复杂度:遍历一次O(N)
空间复杂度:常数个存储空间O(1)

方法4:方法3的简化版本
class Solution {
    public int maxProfit(int[] prices) {
        // 特殊值判断
        if(prices.length <2)
            return 0;
        
        // 定义变量存利润最大值和价格最小值
        int maxProfit = 0, minPrices= Integer.MAX_VALUE;

        for(int price:prices){
            minPrices = Math.min(price,minPrices);
            maxProfit = Math.max(price-minPrices,maxProfit);
        }
        return maxProfit;
    }
}

offer10 I斐波那契数列

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

方法1:暴力递归
class Solution {
    public int fib(int n) {
        if(n == 0 || n == 1) return n;

        return fib(n-1) + fib(n-2); 
    }
}

分析
该方法会超时,当n=44的时候;
执行一次fib的时间复杂度是O(1)、二叉树节点数为指数级O(2n)

方法2:记忆化递归
class Solution {
    public int fibonacci(int n, int[] dp) {
        if (n == 0) return 0;           // 返回 f(0)
        if (n == 1) return 1;           // 返回 f(1)
        if (dp[n] != 0) return dp[n];   // 若 f(n) 以前已经计算过,则直接返回记录的解
        dp[n] = fibonacci(n - 1, dp) + fibonacci(n - 2, dp); // 将 f(n) 则记录至 dp
        return dp[n];
    }

// 求第 n 个斐波那契数
    public int fib(int n) {
    	int MOD = 1000000007 ;
        int[] dp = new int[n + 1]; // 用于保存 f(0) 至 f(n) 问题的解
        return fibonacci(n, dp) % MOD;
    }
}
方法3:动态规划
class Solution {
    public int fib(int n) {
        // 动态规划
        int MOD = 1000000007;
        // 定义状态
        int[] dp = new int[n+1];

        if(n == 0) return 0;

        // 边界值
        dp[1] = 1;

        // 状态转移方程
        for(int i = 2 ; i < n+1 ; i++)
            dp[i] = (dp[i-1] + dp[i-2]) %MOD;
        return dp[n];
    }
}

offer62 连续子数组的最大和

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

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

方法1:动态规划

分析
在这里插入图片描述

class Solution {
    public int maxSubArray(int[] nums) {
        if(nums.length == 0 || nums == null)
            return 0;
        // 定义状态:nums[i] 表示以nums[i] 结尾的数组的子数组的最大和的值
        int max = nums[0];
        if(nums.length <2)
            return nums[0];
        for(int i = 1 ; i < nums.length;i++){
            nums[i] = Math.max(nums[i-1]+nums[i],nums[i]);
            max = Math.max(nums[i],max);
        }
        return max;
    }
}
方法2:暴力解法(超时)
class Solution {
    public int maxSubArray(int[] nums) {
        // 暴力解法
        int max = Integer.MIN_VALUE;
        int sum ;
        // i 表示数组的起点,j表示数组的终点;
        for(int i = 0 ; i < nums.length ; i ++){
            sum = 0 ;
            for(int j = i ;j <nums.length ; j++){
                sum += nums[j];
                if(max < sum)
                    max = sum;
            }
        }
        return max;
    }
}

offer47 礼物的最大价值

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

示例

输入: 
[[1,3,1],
[1,5,1],
[4,2,1]]
输出: 12
方法1:动态规划

分析
状态是二维数组。

class Solution {
    public int maxValue(int[][] grid) {
        // 动态规划 二维数组表示
        int m = grid.length,n = grid[0].length;
        int[][] dp = new int[m][n];

        // 初始值
        dp[0][0] = grid[0][0];
        // 转移方程
        for(int i = 0 ; i < m ; i++){
            for(int j = 0 ; j < n; j++){
                if(i == 0 && j == 0)
                    continue;
                else if(i == 0 && j != 0)
                    dp[i][j] = grid[i][j] + dp[i][j-1];
                else if(i != 0 && j == 0)
                    dp[i][j] = grid[i][j] + dp[i-1][j];
                else if(i != 0 && j != 0)
                    dp[i][j] = grid[i][j] + Math.max(dp[i][j-1],dp[i-1][j]);
            }
        }
        return dp[m-1][n-1];
    }
}
方法2:动态规划(+初始化)
class Solution {
    public int maxValue(int[][] grid) {
        int m  = grid.length,n = grid[0].length;

        // 状态
        int[][] dp = new int[m][n];
        dp[0][0] = grid[0][0];
        // 状态方程
        for(int j = 1 ; j < n ; j++)
            dp[0][j] = grid[0][j] + dp[0][j-1];
        for(int i = 1 ; i < m ; i ++)
            dp[i][0] = grid[i][0] + dp[i-1][0];
        for(int i = 1; i < m; i++){
            for( int j = 1 ; j < n ; j++){
                 dp[i][j] = grid[i][j] + Math.max(dp[i-1][j],dp[i][j-1]);
            }
        }
        return dp[m-1][n-1];
    }
}
方法3:动态规划(空间压缩)

不使用额外的状态存储空间dp

class Solution {
    public int maxValue(int[][] grid) {
        //动态规划
        // grid[i][j] 表示从00点到ij点 可以得到的最大礼物值

        for(int i = 0 ; i < grid.length ; i++){
            for(int j = 0 ; j < grid[i].length;j++){
                if(i == 0 && j == 0) continue;
                else if(i == 0 && j != 0) grid[i][j] +=  grid[i][j-1];
                else if(i != 0 && j == 0) grid[i][j] += grid[i-1][j];
                else grid[i][j] += Math.max(grid[i-1][j],grid[i][j-1]);
            }
        }
        return grid[grid.length-1][grid[0].length-1];
    }
}

offer46 把数字翻译成字符串

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

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

方法1:动态规划

class Solution {
    public int translateNum(int num) {
        String s = String.valueOf(num);
        // 动态规划
        int[] dp = new int[s.length()+1];

        // 边界条件
        dp[0] = 1;
        dp[1] = 1;

        // 状态转移方程
        String temp;
        for(int i = 2 ; i <= s.length(); i++){
            temp = s.substring(i-2,i);
            if(temp.compareTo("25") <= 0 && temp.compareTo("10") >= 0)
                dp[i] = dp[i-1] + dp[i-2];
            else
                dp[i] = dp[i-1];
        }
        return dp[s.length()];
    }
}

方法2:动态规划(滚动数组)

class Solution {
    public int translateNum(int num) {
        // 滚动数组
        String s = String.valueOf(num);

        // 特殊值
        if(s.length() == 1 || s.length() == 0)
            return 1;
        int p1 = 1 , p2 = 1, r = 0;
        String temp ;
        for(int i = 2 ; i <= s.length() ; i++){
            temp = s.substring(i-2,i);
            if(temp.compareTo("25") <= 0 && temp.compareTo("10") >=0)
                r = p1 + p2;
            else r = p1;
            p2 = p1;
            p1 = r;
        }
        return r;
    }
}

方法3:动态规划(滚动数组+三元表达式)

class Solution {
    public int translateNum(int num) {
        // 动态规划滚动数组
        String s = String.valueOf(num);

        // p2 表示没有元素的翻译可能性数;p1 表示以第1个元素结尾数组的翻译可能性数
        int p1 = 1,p2 = 1,r = 0;

        if(s.length() < 2)
            return 1;
        
        String temp;
        
        for(int i = 2 ; i <= s.length(); i++){
            temp = s.substring(i-2,i);
            r = (temp.compareTo("10") >= 0 && temp.compareTo("25") <= 0 ) ? p1+p2:p1;
            p2 = p1;
            p1 = r;
        }
        return r;
    }
}

方法4:数字取余

分析
利用数字其余和求整,实现从右向左的动态规划计算。使用滚动数组

class Solution {
    public int translateNum(int num) {
        if(num <= 9)
            return 1;
        // 数字求余 从右向左计算
        int a = 1 , b = 1 , r = 0 ;
        
        int intx = 0,inty = 0;
        int temp = 0 ;
        while(num > 9){
            inty = num % 10;
            num /= 10;
            intx = num % 10;
            temp = intx * 10 + inty;
            r = (temp >= 10 && temp <= 25)? a+b : a;
            b = a;
            a = r;
        }
        return r;

    }
}

offer48 最长不含重复字符的子字符串

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

示例
输入: “abcabcbb” 输出: 3
分析:因为无重复字符的最长子串是 “abc”,所以其长度为 3。
输入: “bbbbb” 输出: 1
分析:因为无重复字符的最长子串是 “b”,所以其长度为 1。
输入: “pwwkew” 输出: 3
分析:因为无重复字符的最长子串是 “wke”,所以其长度为 3。

方法1:动态规划+哈希表

分析
在这里插入图片描述

class Solution {
    public int lengthOfLongestSubstring(String s) {
        // j 表示遍历字符串 i表示子串右侧元素 相同的左侧元素索引
        int j,i;

        int[] dp = new int[s.length()+1];

        // 定义哈希表存储每个元素最后一次出现的位置
        HashMap<Character,Integer> map = new HashMap<Character,Integer>();

        if(s.length() <2)
            return s.length();

        dp[0] = 1; // 表示第i个字符结尾的子串最长子串长度
        map.put(s.charAt(0),0);

        for(j = 1 ; j < s.length() ; j++){
            if(map.get(s.charAt(j))!= null){
                i = map.get(s.charAt(j));
                if(dp[j-1] < j-i)
                    // 说明 s【i】在dp【j】子串的外面,则s【j】可以加入子串
                    dp[j] = dp[j-1] + 1;
                else
                    dp[j] = j-i;
            }
            else dp[j] = dp[j-1] +1;
            map.put(s.charAt(j),j);
        }
        int max = -1 ;
        for(int dpi:dp)
            if(max < dpi) max = dpi;
        
        return max;
    }
}

方法2:动态规划+哈希表(滚动数组)

class Solution {
    public int lengthOfLongestSubstring(String s) {
        // 动态规划 + 哈希表 (滚动数组)
        int temp = 0, res = 0;

        if(s.length() < 2)
            return s.length();
        
        HashMap<Character,Integer> map = new HashMap<Character,Integer>();

        int  i;
        for(int j = 0 ; j < s.length() ; j++ ){
            i = map.getOrDefault(s.charAt(j),-1);
            if(temp < j-i) temp = temp + 1;
            else temp = j-i;
            map.put(s.charAt(j),j);
            res = Math.max(res,temp);
        }
        return res;

    }
}

方法3:动态规划+索引(substring、lastIndexOf)+滚动数组

class Solution {
    public int lengthOfLongestSubstring(String s) {
        // 动态规划 + 索引查找(滚动数组)
        if(s.length() <2)
            return s.length();
        int temp=0,res =0;

        for(int j = 0 ; j<s.length() ; j++){
            String ss = s.substring(0,j);
            int i = ss.lastIndexOf(s.charAt(j));
            if(temp < j-i) temp = temp+1;
            else temp = j-i;
            res = Math.max(temp,res);
        }
        return res;

    }
}

方法4:动态规划+线性遍历

class Solution {
    public int lengthOfLongestSubstring(String s) {
        // 动态规划 + 索引查找(滚动数组)
        if(s.length() <2)
            return s.length();
        int temp=0,res =0;

        int  i ; 
        for(int j = 0 ; j<s.length() ; j++){
            // 线性查找 i
            i = j-1;
            while(i >=0 && s.charAt(i) != s.charAt(j)) i--;
            if(temp < j-i) temp = temp+1;
            else temp = j-i;
            res = Math.max(temp,res);
        }
        return res;

    }
}

方法5:双指针+哈希表

class Solution {
    public int lengthOfLongestSubstring(String s) {
        // 哈希表
        HashMap<Character,Integer> map = new HashMap<Character,Integer>();

        if(s.length() < 2)
            return s.length();
        
        int i = -1,res = 0 ;
        for(int j = 0 ; j < s.length() ; j++){
            i = Math.max(map.getOrDefault(s.charAt(j),-1),i);
            res = Math.max(res,j-i);  
            map.put(s.charAt(j),j);         
        }
        return res;
    }
}

offer60 n个骰子的点数

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

示例
输入: 1 输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
输入: 2 输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]

完全背包问题

零钱兑换(leetcode322)

零钱兑换(leetcode518)

;