目录
5. 最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
示例 3:
输入:s = “a”
输出:“a”
示例 4:
输入:s = “ac”
输出:“a”
解:
1.子串substring:原始字符串的一个连续子集
子序列subsequence:原始字符串的一个子集
2.回文串天生具有动态规划的性质 因为一个回文串去掉两头字符以后中间位置依旧是回文串
3.
4.
5.由于dp[i][j]参考他左下方的值 所以升序填列 再升序填行 如在填dp[0][3]时 需要参考dp[1][2] 由于列已经填好 可以方便的计算出dp[0][3]的结果
代码如下:
JS:
var longestPalindrome = function(s) {
if (s.trim().length === 0) return "";// s.trim()去除字符串两头空格
let dp = Array.from(new Array(s.length + 1),() => new Array(s.length + 1).fill(0));// 初始化一个二维数组dp[][]里面全是0
let max = 1;
let res = s.charAt(0);// s.charAt(0):取字符串s的第0个字符
for (let j = 0; j < s.length; j++) {
for (let i = 0; i <= j; i++) {
if (s[i] === s[j]) {
dp[i][j] = j - i < 2 ? true : dp[i + 1][j - 1];
if (dp[i][j] === true && j - i + 1 > max) {
max = j - i + 1;
res = s.slice(i, j + 1);// s.slice(i, j + 1):截取字符串s的[i,j]字符串
}
} else {
dp[i][j] = false;
}
}
}
return res;
};
JAVA:
class Solution {
public String longestPalindrome(String s) {
boolean[][] dp = new boolean[s.length()][s.length()];
int begin = 0;
int end = 0;
int max = 0;
for (int j= 0; j < s.length(); j++) {
for (int i = j; i >= 0; i--) {
dp[i][j] = s.charAt(i) == s.charAt(j);
if (j - i + 1 > 2) {
dp[i][j] = dp[i][j] && dp[i + 1][j - 1];
}
if (dp[i][j] && max < j - i + 1){
max = j - i + 1;
begin = i;
end = j;
}
}
}
return s.substring(begin,end + 1);
}
}
53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [0]
输出:0
示例 4:
输入:nums = [-1]
输出:-1
示例 5:
输入:nums = [-100000]
输出:-100000
解:
1.动态规划
f(i)代表以第i个数结尾的「连续子数组的最大和」动态转移方程:f(i)=max{f(i−1)+nums[i],nums[i]}
代码如下:
JS:
var maxSubArray = function(nums) {
let res = nums[0];
let arr = new Array(nums.length);
arr[0] = nums[0];
for (let i = 1; i < nums.length; i++) {
arr[i] = Math.max(nums[i], arr[i - 1] + nums[i]);
res = Math.max(arr[i], res);
}
return res;
};
上述代码可以改进:因为我们计算f(i)时 只需要f(i−1) 所以我们可以用滑动数组 定义两个变量pre now分别用来存储f(i - 1) f(i) 当我们计算f(i)时 先将now中的值存在pre 然后计算now
var maxSubArray2 = function(nums) {
let now = nums[0];
let pre;
let max = now;
for (let i = 1; i < nums.length; i++) {
pre = now;
now = Math.max(nums[i], nums[i] + pre);
max = Math.max(now, max);
}
return max;
};
2.分治
将一个序列从中间分开 分成左右两个子序列 然后分别计算两个子序列中的最大连续子序列的和 那么序列的最大连续子序列的和有三种可能:(1)存在于左子序列(2)存在于右子序列(3)序列的最大连续子序列的和的begin位于左子序列 end位于右子序列。由于左右序列的最大连续子序列的和都已经找出来了 所以第三种情况只可能出现在左右序列的最大连续子序列的和相加的情况
代码如下:
JAVA:
static int maxSubarray3(int[] nums){
if(nums == null || nums.length == 0) return 0;
return maxSubarray3(nums,0,nums.length);
}
/*
求解数组nums的[begin,end)中最大连续子序列的和
*/
static int maxSubarray3(int[] nums,int begin,int end) {
// if (end - begin < 2) return nums[begin];
// int mid = (begin + end) >> 1;
// int leftMax = Integer.MIN_VALUE;
// int leftSum = 0;
// for (int i = mid - 1; i >= begin; i--) {
// leftSum += nums[i];
// leftMax = Math.max(leftMax, leftSum);
// }
//
// int rightMax = Integer.MIN_VALUE;
// int rightSum = 0;
// for (int i = mid; i < end; i++) {
// rightSum += nums[i];
// rightMax = Math.max(rightMax, rightSum);
// }
//以下是第二种写法
if (end - begin < 2) return nums[begin];//数组只有一个元素
int mid = (begin + end) >> 1;//数组和/2 求数组的中间数 将数组从中间分开 分为左右两个子序列
/*
因为本方法求解的是[begin,end)中最大连续子序列的和 所以从mid切分后 左子序列[begin,mid)
也就是[begin,mid - 1] 右子序列[mid,end)leftMax leftSum指向左子序列的最后一个元素mid - 1
然后从mid - 2开始从中间向头(begin)遍历 leftSum存储最后一个元素和倒数第二个元素的和
然后比较leftSum leftMax 也就是最后一个元素和倒数第二个元素的和与最后一个元素的大小
把大的存储在leftMax中 然后leftSum存储最后一个元素 倒数第二个元素 倒数第三个元素的和
然后比较leftSum leftMax
也就是最后一个元素 倒数第二个元素 倒数第三个元素的和与最后一个元素和倒数第二个元素的和的大小
把大的存储在leftMax中……如此循环 直到找出左子序列中的最大连续子序列的和
*/
int leftMax = nums[mid - 1];
int leftSum = leftMax;
for (int i = mid - 2; i >= begin; i--) {
leftSum += nums[i];
leftMax = Math.max(leftMax,leftSum);
}
/*
右子序列的范围是[mid,end) rightMax rightSum都指向nums[mid] 然后从中间向尾(end)进行遍历
查找方法同上
*/
int rightMax = nums[mid];
int rightSum = rightMax;
for (int i = mid + 1; i < end; i++) {
rightSum += nums[i];
rightMax = Math.max(rightMax,rightSum);
}
//比较左右序列的最大连续子序列的和 左右序列的最大连续子序列的和相加 三者的大小
return Math.max(leftMax + rightMax,Math.max(maxSubarray3(nums,begin,mid),maxSubarray3(nums,mid,end)));
}
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1 阶 + 1 阶
2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1 阶 + 1 阶 + 1 阶
1 阶 + 2 阶
2 阶 + 1 阶
解:dp[i]表示i阶台阶有dp[i]种方法爬到
dp[i]=dp[i - 1] + dp[i - 2] 即爬1阶和爬2阶两种情况的总和
代码如下:
JS:
var climbStairs = function(n) {
let dp = [];
dp[0] = 0
dp[1] = 1;
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
JAVA:
public int climbStairs(int n) {
if (n < 0)return 0;
if (n == 1)return 1;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i < n + 1; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
剑指 Offer 10- II. 青蛙跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
解:这道题和上一题一样 只不过要对答案取模 代码如下:
JS:
var numWays = function(n) {
let dp = [];
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
for (let i = 3; i <= n; i++) {
dp[i] = (dp[i - 1] + dp[i - 2]) % 1000000007;
}
return dp[n];
};
跳台阶扩展问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
解:
方法一:因为n级台阶,第一步有n种跳法:跳1级、跳2级、到跳n级
跳1级,剩下n-1级,则剩下跳法是f(n-1)
跳2级,剩下n-2级,则剩下跳法是f(n-2)
所以f(n)=f(n-1)+f(n-2)+…+f(1)
因为f(n-1)=f(n-2)+f(n-3)+…+f(1)
所以f(n)=2*f(n-1)
代码如下:
JS:
function jumpFloorII(number){
if(number == 1) return 1;
return jumpFloorII(number - 1) * 2;
}
方法二:每个台阶都有跳与不跳两种情况(除了最后一个台阶),最后一个台阶必须跳。所以共用2^(n-1)中情况
代码如下:
JS:
function jumpFloorII(number){
if(number === 0)return 0
return Math.pow(2, number - 1)
}
121. 买卖股票的最佳时机
剑指 Offer 63. 股票的最大利润
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
例1:输入: [7,1,5,3,6,4] 输出: 5
解:dp[i]表示第i天卖出的最大收益 第i天卖出的最大收益dp[i]应该等于前一天卖出的最大收益(dp[i - 1])、第i天卖出的收益(今天的价格-今天之前的最低价) 二者中最大的那个值 即dp[i]=max(dp[i−1],prices[i]−前面的最低价格)
优化:用一个变量保存今天之前的最低价 由于dp[i]只与dp[i-1]有关 定义一个变量保存dp[i-1]就行
代码如下
JS
let min = prices[0];
let maxProfit = 0;
for (let i = 1; i < prices.length; i++) {
min = Math.min(min, prices[i]);
maxProfit = Math.max(maxProfit, prices[i] - min);
}
264. 丑数 II
给你一个整数 n ,请你找出并返回第 n 个 丑数 。丑数 就是只包含质因数 2、3 和/或 5 的正整数。
示例 1:
输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。
示例 2:
输入:n = 1
输出:1
解释:1 通常被视为丑数。
解:dp[i]表示第i个丑数。定义三个指针p2,p3,p5指向该乘2/3/5的元素。dp[i]=min(dp[p2]×2,dp[p3]×3,dp[p5]×5)
相当于3个数组,分别是能被2、3、5整除的递增数组,且每个数组的第一个数都为1。然后维护三个指针p2,p3,p5,将三个数组合并为一个严格递增的数组。
动态规划就是标识 3 个元素,通过比较他们的 2 倍、3 倍、5 倍的大小,来一个一个存
代码如下:
JS:
var nthUglyNumber = function(n) {
const dp = new Array(n + 1).fill(0);
dp[1] = 1;
let p2 = 1, p3 = 1, p5 = 1;
for (let i = 2; i <= n; i++) {
const 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];
};
300. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
解:
1.dp[i] 代表前i个数字的最长子序列长度。
2.设 j∈[0,i),每次计算dp[i]时,遍历[0,i) 区间,做以下判断:当nums[i] > nums[j]时 nums[i] 可以接在 nums[j]之后,此情况下最长上升子序列长度为 dp[j] + 1;
3.所以状态转移方程:dp[i] = max(dp[i], dp[j] + 1)
代码如下:
322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
解:假设有25分 20分 5分 1分的硬币 现要凑41分的硬币 如何办到硬币个数最少?
假设dp(n)是凑到n分硬币需要的最少硬币个数
如果第一次选择25分的硬币 那么dp(n)=dp(n - 25) + 1 加的这个1代表你已经选了一枚25分的硬币了
如果第一次选择20分的硬币 那么dp(n)=dp(n - 20) + 1
如果第一次选择5分的硬币 那么dp(n)=dp(n - 5) + 1
如果第一次选择1分的硬币 那么dp(n)=dp(n - 1) + 1
所以dp(n)=min{dp(n - 25),dp(n - 20),dp(n - 5),dp(n - 1)} + 1
所以动态转移方程:dp[i] = Math.min(dp[i - coin[0]], dp[i - coin[1]], …) + 1
代码如下:
JAVA:
static int coins6(int n, int[] faces) {
if (n < 1 || faces == null || faces.length == 0) return -1;
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
int min = Integer.MAX_VALUE;
for (int face : faces) {
if (i < face) continue;
int v = dp[i - face];//想凑的钱-面值=剩余的钱v
if (v < 0 || v >= min) continue;//若剩余的钱凑不齐 还是凑不齐 直接退出本次循环
min = v;
}
if (min == Integer.MAX_VALUE) {//想凑的钱i小于面值 凑不齐 dp[i] = -1;
dp[i] = -1;
} else {//凑得齐 正常处理
dp[i] = min + 1;
}
}
return dp[n];
}
JS:
var coinChange = function(coins, amount) {
let dp = new Array( amount + 1 ).fill( Infinity );
dp[0] = 0;
for (let i = 1; i <= amount; i++) {
for (let coin of coins) {
if (i - coin >= 0) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
矩形覆盖
我们可以用2x1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2x1的小矩形无重叠地覆盖一个2xn的大矩形,从同一个方向看总共有多少种不同的方法?比如n=3时,2x3的矩形块有3种不同的覆盖方法(从同一个方向看):
输入描述:
2x1的小矩形的总个数n
返回值描述:
覆盖一个2xn的大矩形总共有多少种不同的方法(从同一个方向看)
示例1:输入:0 返回值:0
示例2:输入:1 返回值:1
示例3:输入:4 返回值:5
解:dp[i]代表2xi的大矩阵需要dp[i]个2x1的小矩阵来覆盖 由上图可知:
(1)2xi的矩阵可以由两个2x1的矩阵放在最左边覆盖(第1列的第2个图那样) 此时覆盖大矩阵的方法主要由剩下的部分来决定 即dp[i] = dp[i - 2]
(2)2xi的矩阵可以由1个2x1的矩阵放在最左边覆盖(第1列的2个图那样) 此时覆盖大矩阵的方法主要由剩下的部分来决定 即dp[i] = dp[i - 1]
所以dp[i] = dp[i - 1] + dp[i - 2]
代码如下:
JS
function rectCover(number){
let dp = [];
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
for (let i = 3; i <= number; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[number];
}
343. 整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
解:dp[i]表示将i拆分成至少两个正整数的和之后,这些正整数的最大乘积。
假设对正整数i拆分出的第一个正整数是j(1<=j<i),则有以下两种可能:
- 将i拆分成j和i−j,且i−j不再拆分成多个正整数,此时的乘积是j×(i−j);
- 将i拆分成j和i−j,且i−j继续拆分成多个正整数,此时的乘积是j×dp[i−j]
因此状态转移方程为dp[i]=max(j×(i−j),j×dp[i−j])。
稍微优化一下:因为将10拆分成4和6、6和4是一样的 所以j的范围可以缩小到i/2
代码如下:
JS
var cuttingRope = function(n) {
let dp = new Array(n + 1);
dp[0] = 0;
dp[1] = 0;
dp[2] = 1;
let max = 1;
let temp;
for (let i = 3; i < n + 1; i++) {
for (let j = 1; j <= i >> 1; j++) {
temp = Math.max(dp[i - j] * j, (i - j) * j);
max = Math.max(max, temp);
}
dp[i] = max;
}
return dp[n];
};
剑指 Offer 14- I. 剪绳子
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
剑指 Offer 14- II. 剪绳子 II
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m - 1] 。请问 k[0]k[1]…*k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
解:本题和上面一题一样 只不过数值变大了 导致了大数越界情况的出现 需要取模。因此本题需要用BigInt进行大数计算 Math.max()方法不能求BigInt的最值 所以我们还需要重新写一个max函数
代码如下:
JS
var cuttingRope = function(n) {
let dp = new Array(n + 1).fill(BigInt(1));
let max = BigInt(1);
let temp;
function maxBigInt (a, b) {
return a > b ? a : b;
}
for (let i = 3; i < n + 1; i++) {
for (let j = 1; j <= i >> 1; j++) {
temp = maxBigInt(dp[i - j] * BigInt(j), BigInt((i - j) * j));
max = maxBigInt(max, temp);
}
dp[i] = max;
}
return dp[n] % 1000000007n;
};
剑指 Offer 47. 礼物的最大价值
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例 1:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
解:dp[i][j]表示i,j位置上可以拿到的最多价值的礼物
dp[i][j] = grid[i][j] + Math.max(dp[i - 1][j], dp[i][j - 1])
var maxValue = function(grid) {
if (grid.length === 0 || grid[0].length === 0) return 0;
let dp = Array.from(new Array(grid.length), () => new Array(grid[0].length).fill(0));
dp[0][0] = grid[0][0];
for (let col = 1; col < grid[0].length; col++) {
dp[0][col] = dp[0][col - 1] + grid[0][col];
console.log(dp[0][col]);
}
for (let row = 1; row < grid.length; row++) {
dp[row][0] = dp[row - 1][0] + grid[row][0];
console.log(dp[row][0]);
}
for (let row = 1; row < grid.length; row++) {
for (let col = 1; col < grid[0].length; col++) {
dp[row][col] = grid[row][col] + Math.max(dp[row - 1][col], dp[row][col - 1])
}
}
return dp[grid.length - 1][grid[0].length - 1];
};
剑指 Offer 46. 把数字翻译成字符串
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
例:输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", “bwfi”, “bczi”, “mcfi"和"mzi”
解:dp[i]表示以第i个字母结尾有多少种不同的方法
以第i个字母结尾有两种选择 一种是只选择i 一种是选择i和i-1的组合(只有10 <= num.charAt(0) * 10 + num.charAt(1) <= 25
才可以选择这一种) 所以dp[i]= dp[i-1]或dp[i]= dp[i-1] + dp[i - 2](当10 <= num.charAt(0) * 10 + num.charAt(1) <= 25)
var translateNum = function(num) {
num += "";
let dp = [num.length];// dp[i]表示以第i个字母结尾有多少种不同的方法
dp[0] = 1;
if (parseInt(num.charAt(0)) * 10 + parseInt(num.charAt(1)) <= 25) dp[1] = 2;
else dp[1] = 1;
for (let i = 2; i < num.length; i++) {
dp[i] = dp[i - 1];
if ((parseInt(num.charAt(i - 1)) !== 0) && (parseInt(num.charAt(i - 1)) * 10 + parseInt(num.charAt(i)) <= 25)) {
dp[i] += dp[i - 2];
}
}
return dp[num.length - 1];
};
优化:因为这里只用了dp[i] dp[i-1]与dp[i-2] 所以可以用滚动数组 即用三个变量来维护 而不用dp数组 假设a a_1 a_2分别表示dp[i] dp[i-1] dp[i-2] 代码如下:
var translateNum = function(num) {
num += "";
let a = 1, a_1 = 1, a_2 = 1;
for (let i = 1; i < num.length; i++) {
if ((parseInt(num.charAt(i - 1)) * 10 + parseInt(num.charAt(i)) >= 10) && (parseInt(num.charAt(i - 1)) * 10 + parseInt(num.charAt(i)) <= 25)) {
a += a_2;
}
a_2 = a_1;
a_1 = a;
}
return a;
};