文章目录
动态规划
基础知识
- 包含了分治思想、空间换时间、最优解等多种基石算法;
- 动态规划是通过组合子问题的解得到原问题的解;
- 适合动态规划的问题具有重叠子问题和最优子结构两大特性;
重叠子问题:即各个子问题中包含重复的更小子问题;使用暴力枚举,求解相同的子问题会产生大量的重复计算;而动态规划会将子问题的解保存,后续迭代查表即可,保证每个独立子问题只被计算一次;
最优子结构:如果一个问题的最优解可以由其子问题的最优解组合构成,并且这些子问题可以独立求解,那么称此问题具有最优子结构。
重叠子问题
记忆递归 = 从顶至低;
动态规划 = 从低至顶;
斐波那契数列问题并不包含最优子结构问题,只需要计算每个子问题的解,避免重复计算即可,并不需要从子问题组合中选择最优组合。
最优子结构
一个问题的最优解可以由其子问题最优解组合构成,那么称此问题具有最优子结构;
蛋糕售价最高问题:
不同重量的蛋糕售价不一样,已知总的蛋糕重量,求蛋糕的最大收益?
状态:
f(x)表示x重量的蛋糕的最高售价,其中f(0)=0 f(1) = p(1) = 2
p(x) 表示x重量的蛋糕价格;
分析:重量为n的蛋糕总价可切分为n种组合,即0,1,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]