目录
- 前言
- 简单
-
- [1. 1025. 除数博弈](https://leetcode-cn.com/problems/divisor-game/)
- [2. 303. 区域和检索 - 数组不可变](https://leetcode-cn.com/problems/range-sum-query-immutable/)
- [3. 剑指 Offer 42. 连续子数组的最大和](https://leetcode-cn.com/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/)
- [4. 面试题 16.17. 连续数列](https://leetcode-cn.com/problems/contiguous-sequence-lcci/)
- [5. 746. 使用最小花费爬楼梯](https://leetcode-cn.com/problems/min-cost-climbing-stairs/)
- [6.面试题 17.16. 按摩师](https://leetcode-cn.com/problems/the-masseuse-lcci/)
- [7. 392. 判断子序列](https://leetcode-cn.com/problems/is-subsequence/)
- [8.面试题 08.01. 三步问题](https://leetcode-cn.com/problems/three-steps-problem-lcci)
- 中等
-
- [1. 338. 比特位计数](https://leetcode-cn.com/problems/counting-bits/)
- [2.1641. 统计字典序元音字符串的数目](https://leetcode-cn.com/problems/count-sorted-vowel-strings/)
- [3. 877. 石子游戏](https://leetcode-cn.com/problems/stone-game/)
- [4.1314. 矩阵区域和](https://leetcode-cn.com/problems/matrix-block-sum/)
- [5. 1277. 统计全为 1 的正方形子矩阵](https://leetcode-cn.com/problems/count-square-submatrices-with-all-ones/)
- [6. 714. 买卖股票的最佳时机含手续费](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/)
- [7. 96. 不同的二叉搜索树](https://leetcode-cn.com/problems/unique-binary-search-trees/)
- [8. 剑指 Offer 47. 礼物的最大价值](https://leetcode-cn.com/problems/li-wu-de-zui-da-jie-zhi-lcof/)
- [9. 64. 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/)
- [10. 1043. 分隔数组以得到最大和](https://leetcode-cn.com/problems/partition-array-for-maximum-sum/)
- [11.95. 不同的二叉搜索树 II](https://leetcode-cn.com/problems/unique-binary-search-trees-ii/)
前言
妈的,深搜都刷不会.
简单
1. 1025. 除数博弈
题目描述:
爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。
最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作:
选出任一 x,满足 0 < x < N 且 N % x == 0 。
用 N - x 替换黑板上的数字 N 。
如果玩家无法执行这些操作,就会输掉游戏。
只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 False。假设两个玩家都以最佳状态参与游戏。
思路1:数学
当N=1时,输
当N=2时,赢
当N=3时,输
当N=4时,赢
当N=5时,输
猜想结论:奇数输,偶数赢
证明:
N=1和N=2时结论成立
N>2时,假设N<=k结论成立,则当N=k+1时
1, 如果k是奇数,那么k+1是偶数,此时的x可以是奇数也可以是偶数,那么x可以选择奇数,让k+1-x是奇数,让bob输. 偶数赢的结论成立.
2. 如果k是偶数,那么k+1是奇数,此时的x只能是奇数,选择x之后,k+1-x是偶数,此时bob必赢. 奇数输的结论也成立.
代码1:
class Solution {
public boolean divisorGame(int N) {
return N % 2 == 0;
}
}
思路2:动态规划(重点)
当N时,我们选择一个x,那么f(N)就取决于f(N-x)了. 具体的,当 存在1<=x<N,f(N-x) = false,则f(N)=true, 否则f(N) = false
代码2:
class Solution {
public boolean divisorGame(int N) {
boolean[] f = new boolean[1001];
f[1] = false;
f[2] = true;
for(int i=3;i<=N;i++){
for(int j=1;j<i;j++){
if(i%j==0&&f[i-j]==false){
f[i] = true;
break;
}
}
}
return f[N];
}
}
2. 303. 区域和检索 - 数组不可变
题目描述:
给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。
实现 NumArray 类:
NumArray(int[] nums) 使用数组 nums 初始化对象
int sumRange(int i, int j) 返回数组 nums 从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点(也就是 sum(nums[i], nums[i + 1], … , nums[j]))
思路:啊,果断又看了题解,是一种"前缀和"的题目.状态转移方程是:sum(i,j) = pre_sum(j+1) pre_sum(i),即,pre_sum(i)中存储了0到i-1的和.
代码:
class NumArray {
int[] pre_sum;
public NumArray(int[] nums) {
pre_sum = new int[nums.length+1];
for(int i = 1;i<=nums.length;i++){
pre_sum[i] = pre_sum[i-1] + nums[i-1];
}
}
public int sumRange(int i, int j) {
return pre_sum[j+1] - pre_sum[i];
}
}
3. 剑指 Offer 42. 连续子数组的最大和
题目描述:
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
思路:max[i] = Math.max(max[i], max[i]+max[i-1])(max[i]表示以第i个元素为结尾的最大连续和)
代码:
class Solution {
public int maxSubArray(int[] nums) {
int ans = nums[0];
for(int i = 1;i<nums.length;i++){
nums[i] = Math.max(nums[i],nums[i]+nums[i-1]);
ans = Math.max(ans, nums[i]);
}
return ans;
}
}
4. 面试题 16.17. 连续数列
思路1:同3. 剑指 Offer 42. 连续子数组的最大和
思路2:分治
5. 746. 使用最小花费爬楼梯
题目描述:
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。
请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
思路:维护一个dp数组,dp[i]表示在第i个台阶的最低花费.由于第i个台阶只能从i-1或者i-2过来,所以dp[i]=Math.min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]).
代码:
class Solution {
public int minCostClimbingStairs(int[] cost) {
int[] dp = new int[1001];
dp[0] = dp[1] = 0;
for(int i = 2; i <=cost.length; i++){
dp[i] = Math.min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]);
}
return dp[cost.length];
}
}
6.面试题 17.16. 按摩师
题目描述:
一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
注意:本题相对原题稍作改动
思路:dp[i][0] = max(dp[i-1][0], dp[i-1][1]); dp[i][1] = nums[i] + dp[i-1][0];
代码:
class Solution {
public int massage(int[] nums) {
if(nums.length==0) return 0;
int dp0 = 0;
int dp1 = nums[0];
for(int i = 1; i < nums.length; i++){
int ndp0 = Math.max(dp0, dp1);
int ndp1 = nums[i] + dp0;
dp0 = ndp0;
dp1 = ndp1;
}
return Math.max(dp0, dp1);
}
}
7. 392. 判断子序列
题目描述:
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
思路1:双指针
代码1:
class Solution {
public boolean isSubsequence(String s, String t) {
int s_len = s.length();
int t_len = t.length();
int i = 0;
int j = 0;
while(i<s_len && j<t_len){
if(s.charAt(i) == t.charAt(j)) i++;
j++;
}
return i == s_len;
}
}
思路2:动态规划,dp[i][j]=i if j对应的字符正好第一次出现在字符串t的i这个位置上;dp[i][j]=dp[i+1][j] if 前面的条件不成立.即,dp[idx][j]保存了:在字符串t中,第idx位及其后面的子串中,dp[j]+'a’所对应的字符,第一次出现的位置. 所以每一次得到一个新的ss = s.charAt(i),只要去查看dp[idx][ss-‘a’]对应的转移下标.参考:https://leetcode-cn.com/problems/is-subsequence/solution/pan-duan-zi-xu-lie-by-leetcode-solution/
代码2:
class Solution {
public boolean isSubsequence(String s, String t) {
int s_len = s.length();
int t_len = t.length();
int[][] dp = new int[t_len+1][26];
for(int j = 0; j < 26;j++){
dp[t_len][j] = t_len;
}
for(int i = t_len - 1; i >= 0 ; i--){
for(int j = 0; j < 26; j++){
if(t.charAt(i) - 'a' == j){
dp[i][j] = i;
}else{
dp[i][j] = dp[i+1][j];
}
}
}
int idx = 0;
for(int i = 0; i < s_len; i++){
if(dp[idx][s.charAt(i) - 'a']==t_len){
return false;
}
idx = dp[idx][s.charAt(i) - 'a'] + 1;
}
return true;
}
}
8.面试题 08.01. 三步问题
题目描述:
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。
思路:
f(n) = f(n-1) + f(n-2) + f(n-3)
代码:
class Solution {
public int waysToStep(int n) {
int ans = 0;
int a = 1;
int b = 2;
int c = 4;
if(n == 1){
return a;
}else if(n==2){
return b;
}else if(n==3){
return c;
}else{
for(int i = 4; i <= n; i++){
ans = ((a + b ) % 1000000007 + c) % 1000000007;
a = b;
b = c;
c = ans;
}
}
return ans;
}
}
中等
1. 338. 比特位计数
题目描述:
给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
思路1:硬算
代码1:
class Solution {
public int[] countBits(int num) {
int[] all_ans = new int[num+1];
for(int j = 0; j <=num; j++){
all_ans[j] = cal(j);
}
return all_ans;
}
public int cal(int i){
int ans = 0;
while(i > 0){
i = i&(i-1);
ans ++;
}
return ans;
}
}
思路2:bit(n) = bit(n - 最高有效数) + 1 主要的思想就是,对于bit(n),如何用bit(比n更小的数)来表示,方法一:保存最高有效位,比如8的二进制数是1000,9的二进制是1001,那么bit(9) = bit(8) + 1,10的二进制是1010,因为 10 - 8 = 2,2的二进制,正好是10的二进制减去了最高有效数8的最高位,所以bit(10) = bit(2) + 1。如何得到最高有效数?—— if i&(i-1) == 0 then i是最高有效数.
代码2:
class Solution {
public int[] countBits(int num) {
int[] ans = new int[num+1];
int top = 0;
for(int i = 1; i <= num; i++){
if((i&(i-1))==0) top = i;
ans[i] = ans[i-top] + 1;
}
return ans;
}
}
思路3:bit(n) = bit(n >> 1) + n % 2 或者 bit(n) = bit(n >> 1) + n & 1 对于数字n,把n右移一位得到数字n’,分两种情况:1. n是偶数,则bit(n) = bit(n’) 2. n是奇数,则bit(n) = bit(n’) + 1。其中,右移可以使用n>>1来实现.n>>1 小于 n 一定成立.
代码3:
class Solution {
public int[] countBits(int num) {
int[] ans = new int[num+1];
for(int i = 1; i <= num; i++){
ans[i] = ans[i>>1] + i % 2;
}
return ans;
}
}
思路4:bit(n) = bit(n&(n-1)) + 1对于数n,n&(n-1)表示把n的最低为1的那一位,变成0,n&(n-1)小于n一定成立.
代码4:
class Solution {
public int[] countBits(int num) {
int[] ans = new int[num+1];
for(int i = 1; i <= num; i++){
ans[i] = ans[i & (i-1)] + 1;
}
return ans;
}
}
2.1641. 统计字典序元音字符串的数目
题目描述:
给你一个整数 n,请返回长度为 n 、仅由元音 (a, e, i, o, u) 组成且按 字典序排列 的字符串数量。
字符串 s 按 字典序排列 需要满足:对于所有有效的 i,s[i] 在字母表中的位置总是与 s[i+1] 相同或在 s[i+1] 之前。
思路:
定义aeiou数组,aeiou[0]表示以’a’为结尾的情况数,aeiou[1]表示以’e’为结尾的情况数,aeiou[2]表示以’i’为结尾的情况数,aeiou[3]表示以’o’为结尾的情况数,aeiou[4]表示以’u’为结尾的情况数. 以aeiou[0]为例,它一定是上一个长度的aeiou[0],再在末尾加上一个’a’,得到的.
代码:
class Solution {
public int countVowelStrings(int n) {
int[] aeiou = new int[5];
for(int i = 0;i < 5;i++){
aeiou[i] = 1;
}
for (int j=2;j<=n;j++){
aeiou[4] = aeiou[4] + aeiou[3] + aeiou[2] + aeiou[1] + aeiou[0];
aeiou[3] = aeiou[3] + aeiou[2] + aeiou[1] + aeiou[0];
aeiou[2] = aeiou[2] + aeiou[1] + aeiou[0];
aeiou[1] = aeiou[1] + aeiou[0];
aeiou[0] = aeiou[0];
}
return aeiou[0] + aeiou[1] + aeiou[2] + aeiou[3] + aeiou[4];
}
}
3. 877. 石子游戏
题目描述:
亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。
游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。
亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。
假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。
思路1:
用i和j定义石子堆的范围,dp[i][j]表示,当还剩i~j堆石头时,当前玩家与另外一位玩家的差值最大,这符合常理:dp[i][j]表示一位玩家,那么dp[i-1][j]和dp[i+1][j]等就表示的是另外一位玩家;考虑dp[0][length-1]表示最终两个人的最大差值,这与题目中"发挥出最佳水平"一致.状态转移方程dp[i][j]
= max(piles[i] - dp[i+1][j],piles[j] - dp[i][j-1]).特别的,1. i要从大往前 2. j要从小往后.且只需要填满i<=j的部分.
代码1:
class Solution {
public boolean stoneGame(int[] piles) {
int[][] dp = new int[piles.length][piles.length];
for(int i = 0; i < piles.length; i++){
dp[i][i] = piles[i]; // 只有一堆石头
}
for(int i = piles.length - 2; i >= 0; i--){
for(int j = i + 1; j < piles.length; j ++){
dp[i][j] = Math.max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1]);
}
}
return dp[0][piles.length-1] > 0;
}
}
思路2:
一维数组的版本
代码2:
class Solution {
public boolean stoneGame(int[] piles) {
int[] dp = new int[piles.length];
for(int i = 0; i < piles.length; i++){
dp[i] = piles[i]; // 只有一堆石头
}
for(int i = piles.length - 2; i >= 0; i--){
for(int j = i + 1; j < piles.length; j ++){
dp[j] = Math.max(piles[i] - dp[j], piles[j] - dp[j-1]);
}
}
return dp[piles.length-1] > 0;
}
}
思路3:
假设一共n堆石头,将其下标标为0,1,2,3…n-1,奇数下标为一组(1),偶数下标为一组(2).初始化后,第0堆属于第二组,第n-1堆属于第一组. 1. 如果亚历克斯选择第0堆,那么剩下的下标为:1,2,3…n-1,此时可以发现,李要么选第1堆,要么选第n-1堆,两者都属于第一组. 选完之后,剩下的可能是:2(第二组),3,…n-1 (第一组)或者 1(第一组),2,…n-2(第二组),那么,亚历克斯又可以有选择第一组和第二组两种选择. 所以,在一开始我们计算是第一组的和更大,还是第二组,然后从一开始,亚历克斯就选择和更大的那一组中的石头堆,就一定可以取得胜利.
代码3:
class Solution {
public boolean stoneGame(int[] piles) {
return true;
}
}
4.1314. 矩阵区域和
题目描述:
给你一个 m * n 的矩阵 mat 和一个整数 K ,请你返回一个矩阵 answer ,其中每个 answer[i][j] 是所有满足下述条件的元素 mat[r][c] 的和:
i - K <= r <= i + K, j - K <= c <= j + K
(r, c) 在矩阵内。
预备知识:
设二维数组 A 的大小为 m * n,行下标的范围为 [1, m],列下标的范围为 [1, n]。
数组 P 是 A 的前缀和数组,等价于 P 中的每个元素 P[i][j]:
如果 i 和 j 均大于 0,那么 P[i][j] 表示 A 中以 (1, 1) 为左上角,(i, j) 为右下角的矩形区域的元素之和;
如果 i 和 j 中至少有一个等于 0,那么 P[i][j] 也等于 0。
数组 P 可以帮助我们在 O(1)O(1) 的时间内求出任意一个矩形区域的元素之和。具体地,设我们需要求和的矩形区域的左上角为 (x1, y1),右下角为 (x2, y2),则该矩形区域的元素之和可以表示为:
sum = A[x1…x2][y1…y2] = P[x2][y2] - P[x1 - 1][y2] - P[x2][y1 - 1] + P[x1 - 1][y1 - 1]
思路:左上角(x1,y1)和右下角(x2,y2)区域和的状态公式: sum = p(x2,y2)-p(x2,y1-1)-p(x1-1,y2)+p(x1-1,y1-1).特别的:mat[i][j](左上角(i,j),右下角(i,j)) = p(i,j) - p(i, j-1) - p(i-1, j) + p(i-1, j-1). 所以p(i,j)的转移公式为: p(i,j) = mat[i][j] + p(i, j-1) + p(i-1, j) - p(i-1, j-1). 所以,我们只需要正常的从小到大遍历就可以了.
代码:
class Solution {
public int[][] matrixBlockSum(int[][] mat, int K) {
// p(i,j) = mat[i][j] + p(i, j-1) + p(i-1, j) - p(i-1, j-1)
int N = mat.length;
int M = mat[0].length;
int[][] p = new int[N+1][M+1];
for(int i = 1; i <= N; i++){
for(int j = 1; j <= M; j++){
p[i][j] = mat[i-1][j-1] + p[i][j-1] + p[i-1][j] - p[i-1][j-1];
}
}
int[][] answer = new int[N][M];
for(int i = 0; i < N; i ++){
for(int j = 0; j < M; j++){
int x1 = Math.max(i - K, 0) + 1;
int y1 = Math.max(j - K, 0) + 1;
int x2 = Math.min(i + K, N-1) + 1;
int y2 = Math.min(j + K, M-1) + 1;
// sum = p(x2,y2)-p(x2,y1-1)-p(x1-1,y2)+p(x1-1,y1-1).
answer[i][j] = p[x2][y2] - p[x2][y1-1] - p[x1-1][y2] + p[x1-1][y1-1];
}
}
return answer;
}
}
5. 1277. 统计全为 1 的正方形子矩阵
题目描述:
给你一个 m * n 的矩阵,矩阵中的元素不是 0 就是 1,请你统计并返回其中完全由 1 组成的 正方形 子矩阵的个数。
思路1:
按照**p(i,j) = mat[i][j] + p(i, j-1) + p(i-1, j) - p(i-1, j-1)**的公式构建前缀和数组p,按照 **sum = p(x2,y2)-p(x2,y1-1)-p(x1-1,y2)+p(x1-1,y1-1)**的公式,计算所有正方形的和. 如果和=(x2-x1)的平方,说明元素全都是1. 如何遍历所有正方形?——三重循环,变量1:c(1到min(M,N)),变量2:i(0到M-c),变量3:j(0到N-c),特别注意: int x2 = i + c - 1 + 1; int y2 = j + c - 1 + 1;
代码1:
class Solution {
public int countSquares(int[][] matrix) {
//p(i,j) = mat[i][j] + p(i, j-1) + p(i-1, j) - p(i-1, j-1)
int N = matrix.length;
int M = matrix[0].length;
int[][] p = new int[N+1][M+1];
for(int i = 1; i <= N; i++){
for(int j = 1; j <= M; j++){
p[i][j] = matrix[i-1][j-1] + p[i][j-1] + p[i-1][j] - p[i-1][j-1];
}
}
int ans = 0;
//sum = p(x2,y2)-p(x2,y1-1)-p(x1-1,y2)+p(x1-1,y1-1)
int max_c = Math.min(N,M);
for(int c = 1; c <= max_c; c++){
for(int i = 0; i <= N-c; i++){
for(int j = 0; j <= M-c; j++){
int x1 = i + 1;
int y1 = j + 1;
int x2 = i + c - 1 + 1;
int y2 = j + c - 1 + 1;
int sum = p[x2][y2] - p[x2][y1-1] - p[x1-1][y2] + p[x1-1][y1-1];
if(sum == c*c){
ans++;
}
}
}
}
return ans;
}
}
思路2:https://leetcode-cn.com/problems/count-square-submatrices-with-all-ones/solution/tong-ji-quan-wei-1-de-zheng-fang-xing-zi-ju-zhen-f/
6. 714. 买卖股票的最佳时机含手续费
题目描述:
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
思路:
dp[i][0] 表示第i天结束后,手里没有股票的最大利润
没有股票,1.前一天也没有dp[i-1][0] 2.前一天有,今天卖掉了dp[i-1][1] + prices[i] - fee
dp[i][1] 表示第i天结束后,手里有股票的最大利润
有股票,1.前一天也有dp[i-1][1] 2.前一天没有,今天刚买dp[i-1][0] - prices[i]
代码:
class Solution {
public int maxProfit(int[] prices, int fee) {
int[][] dp = new int[prices.length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i = 1; i < prices.length; i++){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i] - fee);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
}
return dp[prices.length-1][0];
}
}
7. 96. 不同的二叉搜索树
题目描述:
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
思路:
//F[i][n]:以i为根,长度为n的序列对应的二叉搜索树个数
//G[n]:长度为n的序列对应的二叉搜索树个数
//G[n] = /sum_i=1_n(F[i][n])
//F[i][n] = G[i-1] * G[n-i]
// G[n] = /sum_i=1_n(G[i-1] * G[n-i])
// G[i] = /sum_j=1_i(G[j-1] * G[i-j])
代码:
class Solution {
public int numTrees(int n) {
int[] G = new int[n+1];
G[0] = 1;
G[1] = 1;
for(int i = 2; i <= n ; i++){
for(int j = 1; j <= i; j++){
G[i] += G[j - 1] * G[i - j];
}
}
return G[n];
}
}
思路2:
G[n] = /sum_i=1_n(G[i-1] * G[n-i])是卡塔兰数
得到: G[n+1] = G(n) * (4*n+2) / (n+2)
代码2:
class Solution {
public int numTrees(int n) {
long C = 1;
for(int i = 0; i < n; i++){
C = C * (4 * i + 2) / (i + 2);
}
return (int)C;
}
}
8. 剑指 Offer 47. 礼物的最大价值
题目描述:
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
思路:1
还是没有理解动态规划,我是这样想:对于每一个位置,可以从左边一个位置向右到达,可以从上面一个位置向下到达。然后我又傻逼地考虑到:左边或者上面那个位置,也可能有两种被到达的方式,所以我定义了dp[i][j][2]。
ans[i][j][0] 表示从上一个位置向右移动到达 = max(ans[i][j-1][0], ans[i][j-1][1]) + grid[i][j];ans[i][j][1] 表示从上一个位置向下移动到达 = max(ans[i-1][j][0], ans[i-1][j][1]) + grid[i][j];特殊位置:1. 上一个位置无法向右移动: 没有左一个位置,你自己的j就是0–>初始化 2. 上一个位置无法向下移动: 没有上一个位置,你自己的i就是0–>初始化。答案: max0([0],[1]);
代码1:
class Solution {
public int maxValue(int[][] grid) {
int[][][] ans = new int[grid.length][grid[0].length][2];
ans[0][0][0] = grid[0][0];
ans[0][0][1] = grid[0][0];
// 一直向右:
for(int j = 1; j < grid[0].length; j++){
ans[0][j][0] = ans[0][j-1][0] + grid[0][j];
ans[0][j][1] = ans[0][j][0];
}
// 一直向下:
for(int i = 1; i < grid.length; i++){
ans[i][0][0] = ans[i-1][0][1] + grid[i][0];
ans[i][0][1] = ans[i][0][0];
}
for(int i = 1; i < grid.length; i++){
for(int j = 1; j < grid[0].length; j++){
ans[i][j][0] = Math.max(ans[i][j-1][0], ans[i][j-1][1]) + grid[i][j];
ans[i][j][1] = Math.max(ans[i-1][j][0], ans[i-1][j][1]) + grid[i][j];
}
}
return Math.max(ans[grid.length-1][grid[0].length-1][0], ans[grid.length-1][grid[0].length-1][1]);
}
}
思路2:
事实上,很简单。如果i,j是从左边一个位置:i-1,j到达,那么dp[i-1][j]本身就一定是一个最优解,而不存在说:还要去考虑dp[i-1][j]是从它的上还是它的左进一步到达的。这是正确的最优子结构。
状态转移方程:dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + grid[i][j]
代码2:
class Solution {
public int maxValue(int[][] grid) {
for(int j = 1; j < grid[0].length; j++){
grid[0][j] += grid[0][j-1];
}
for(int i = 1; i < grid.length; i++){
grid[i][0] += grid[i-1][0];
}
for(int i = 1; i < grid.length; i++){
for(int j = 1; j < grid[0].length; j++){
grid[i][j] += Math.max(grid[i-1][j], grid[i][j-1]);
}
}
return grid[grid.length-1][grid[0].length-1];
}
}
9. 64. 最小路径和
题目描述:
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
思路:
和上一题一摸一样~
代码:
class Solution {
public int minPathSum(int[][] grid) {
for(int i = 1; i < grid.length; i++){
grid[i][0] += grid[i-1][0];
}
for(int j = 1; j < grid[0].length; j++){
grid[0][j] += grid[0][j-1];
}
for(int i = 1; i < grid.length; i++){
for(int j = 1; j < grid[0].length; j++){
grid[i][j] += Math.min(grid[i-1][j], grid[i][j-1]);
}
}
return grid[grid.length-1][grid[0].length-1];
}
}
10. 1043. 分隔数组以得到最大和
题目描述:
给你一个整数数组 arr,请你将该数组分隔为长度最多为 k 的一些(连续)子数组。分隔完成后,每个子数组的中的所有值都会变为该子数组中的最大值。
返回将数组分隔变换后能够得到的元素最大和。
注意,原数组和分隔后的数组对应顺序应当一致,也就是说,你只能选择分隔数组的位置而不能调整数组中的顺序。
思路:
状态转移方程:dp[i] = max_j=i_j=i-k+1(dp[j] + max(A[j:i+1]*(i-j+1))), dp[i]代表以第i个位置为结尾的数组,它的最大值,如果我们把A[i]单独拎出来,那么前面的最优解是dp[i-1],如果把A[i-1]和A[i]这两个拎出来组成一个子数组,那么前面的最优解是dp[i-2]…另外,我们要一直计算A[i-…]到A[i]这些元素中的最大值。
代码:
class Solution {
public int maxSumAfterPartitioning(int[] A, int K) {
int n = A.length;
int[] dp = new int[n+1];
for(int i = 0; i <= n; i++){
int j = i - 1;
int max = 0;
while(i-j <= K && j>=0){
max = Math.max(max, A[j]);
dp[i] = Math.max(dp[i], dp[j] + max * (i-j));
j --;
}
}
return dp[n];
}
}
11.95. 不同的二叉搜索树 II
题目描述:
给定一个整数 n,生成所有由 1 … n 为节点所组成的 二叉搜索树 。
思路1:
递归?额,怎么又叫回溯?反正不叫DP。
代码1:
class Solution {
public List<TreeNode> generateTrees(int n) {
if(n==0){
return new ArrayList<TreeNode>();
}else{
return dfs(1, n);
}
}
public List<TreeNode> dfs(int start, int end){
List<TreeNode> tmp_result = new ArrayList<TreeNode>();
if(start > end){
tmp_result.add(null);
return tmp_result;
}
for(int root = start ; root <= end; root ++){
List<TreeNode> left_result = dfs(start, root-1);
List<TreeNode> right_result = dfs(root+1, end);
for(TreeNode left:left_result){
for(TreeNode right:right_result){
TreeNode tmp_root = new TreeNode(root);
tmp_root.left = left;
tmp_root.right = right;
tmp_result.add(tmp_root);
}
}
}
return tmp_result;
}
}
思路2: