198. 打家劫舍
/*
本题思考过程如下。很多题都可以【暴力递归---记忆化搜索---动态规划】这条路搞。
思路1.极端回溯法,每个元素可选可不选,所有不相邻情况,求出最大偷取的钱数。
思路2.暴力递归,如果选i就从i+2开始嘛,不选i就从i+1开始嘛,递归此过程。
思路3.把暴力递归改成记忆化搜索。
思路4.改成动态规划。
思路5.再优化。
*/
【代码一】超时—暴力递归
class Solution {
public int rob(int[] nums) {
return f(nums, 0);
}
private int f(int[] nums, int i){
if(i >= nums.length){
return 0;
}
return Math.max(f(nums, i+1), nums[i] + f(nums, i+2));
}
}
【代码二】通过—记忆化搜索
class Solution {
public int rob(int[] nums) {
int[] help = new int[nums.length];
Arrays.fill(help, -1);
return f(nums, 0, help);
}
private int f(int[] nums, int i, int[] help){
if(i >= nums.length){
return 0;
}
if(help[i] == -1){
help[i] = Math.max(f(nums, i+1, help), nums[i] + f(nums, i+2, help));
}
return help[i];
}
}
【代码三】通过—动态规划
class Solution {
public int rob(int[] nums) {
if(nums == null || nums.length == 0){
return 0;
}
if(nums.length == 1){
return nums[0];
}
if(nums.length == 2){
return Math.max(nums[0], nums[1]);
}
int n = nums.length;
int[] dp = new int[n];
dp[n-1] = nums[n-1];
dp[n-2] = Math.max(nums[n-1], nums[n-2]);
for(int i = n - 3; i >= 0; i--){
dp[i] = Math.max(dp[i+1], nums[i]+dp[i+2]);
}
return dp[0];
}
}
【代码四】通过—动态规划优化(状态压缩)
class Solution {
public int rob(int[] nums) {
if(nums == null || nums.length == 0){
return 0;
}
if(nums.length == 1){
return nums[0];
}
if(nums.length == 2){
return Math.max(nums[0], nums[1]);
}
int n = nums.length;
int dp = 0;
int dp1 = nums[n-1];
int dp2 = Math.max(nums[n-1], nums[n-2]);
for(int i = n - 3; i >= 0; i--){
dp = Math.max(dp2, nums[i]+dp1);
dp1 = dp2;
dp2 = dp;
}
return dp;
}
}
继续分析,上述代码可以简化如下:
class Solution {
public int rob(int[] nums) {
int dp1 = 0;
int dp2 = 0;
int dp = 0;
for(int i = nums.length - 1; i >= 0; i--){
dp = Math.max(dp2, nums[i]+dp1);
dp1 = dp2;
dp2 = dp;
}
return dp;
}
}
338. 比特位计数
【代码一】
class Solution {
public int[] countBits(int num) {
int[] res = new int[num+1];
for(int i = 0; i <= num; i++){
res[i] = f(i);
}
return res;
}
// 计算数n有多少个1
private int f(int n){
int i = 1; // 不断左移来确定n的每一位是1还是0
int sum = 0;
while(i != 0){
if((n & i) != 0){
sum++;
}
i <<= 1;
}
return sum;
}
}
可直接调用库函数:
class Solution {
public int[] countBits(int num) {
int[] res = new int[num+1];
for(int i = 0; i <= num; i++){
res[i] = Integer.bitCount(i);
}
return res;
}
}
【代码二】通过—动态规划
class Solution {
// 寻找关系。。。从移位操作联想,某数n和n/2的关系
// 注意这样一个事实:
// (1)如果n%2==0,n的最后一位是0,右移1位二进制1的个数不变。
// (非最后一位的1转化到十进制都能整除2,最后一位的1转化为十进制是1,不能整除2。)
// (2)如果n%2==1,n的最后一位是1,右移1位二进制1的个数减少1。
// dp[n] = dp[n/2] + (n%2)
public int[] countBits(int num) {
int[] dp = new int[num+1];
for(int i = 1; i <= num; i++){
dp[i] = dp[i/2] + (i%2);
}
return dp;
}
}
877. 石子游戏
【代码一】超时—暴力
class Solution {
public boolean stoneGame(int[] piles) {
return f(piles, 0, piles.length-1) > s(piles, 0, piles.length-1);
}
// 先手
private int f(int[] piles, int i, int j){
if(i == j){ // 只有一个元素,作为先手,直接拿走
return piles[i];
}
// 先手取后,就变成了后手,后面获得之和就调用s了
return Math.max(piles[i]+s(piles, i+1, j), piles[j] + s(piles, i, j-1));
}
// 后手
private int s(int[] piles, int i, int j){
if(i == j){ // 只有一个元素,作为后手,这个元素要让先手拿走,后手得到0
return 0;
}
// 由于是后手,先手拿走之后,才能拿,而先手拿走之后,后手就变成了先手,于是调用f
// 由于先手是精明的,先手必定是留下较小的给后手,所以用min
return Math.min(f(piles, i+1, j), f(piles, i, j-1));
}
}
【代码二】通过—动态规划
class Solution {
public boolean stoneGame(int[] piles) {
int n = piles.length;
int[][] f = new int[n][n]; // 根据暴力递归知道,搞个二维表格填表
int[][] s = new int[n][n];
for(int i = 0; i < n; i++){
f[i][i] = piles[i];
s[i][i] = 0;
}
// i<=j
for(int i = 0; i < n-1; i++){
for(int j = i+1; j < n; j++){
f[i][j] = Math.max(piles[i] + s[i+1][j], piles[j]+s[i][j-1]);
s[i][j] = Math.min(f[i+1][j], f[i][j-1]);
}
}
return f[0][n-1] > s[0][n-1];
}
}
【代码三】通过—数学
class Solution {
public boolean stoneGame(int[] piles) {
/*数学
n=piles.length,根源就在于n是偶数,并且piles各个元素之和是奇数,必无平局
n=2,显然必胜
n=4,设piles={a,b,c,d},必定可拿到a+c或d+b,两者的最大值必能拿到,必胜
……
推广到一般情况。。。假设数组piles={a,b,a,b,a,b,……,a,b,a,b}
必定可以拿到所有的a或所有的b,可以拿到n/2*a或n/2*b,必胜
。。。竟然是必胜。。。
*/
return true;
}
}
96. 不同的二叉搜索树
【代码一】通过—暴力递归
【代码二】通过—动态规划
class Solution {
// 关键还是暴力尝试。。。
public int numTrees(int n) {
return f3(n);
}
// 暴力递归,从st到en能够产生的树种类数=分别以[st,en]为根的种类数之和
private int f1(int st, int en){
if(st >= en){
return 1;
}
int ans = 0;
for(int i = st; i <= en; i++){
int left = f1(st, i-1);
int right = f1(i+1, en);
ans += left*right;
}
return ans;
}
// 暴力递归,由于[st,en]等价于[1, en-st+1],故f1可改为f2
private int f2(int n){ // [1,n]
if(n <= 1){
return 1;
}
int ans = 0;
for(int i = 1; i <= n; i++){
int left = f2(i-1); // [1, i-1]
int right = f2(n-i); // [i+1, n] ---> [1, n-(i+1)+1] ---> [1, n-i]
ans += left*right;
}
return ans;
}
// 动态规划
private int f3(int n){
if(n <= 1){
return 1;
}
int[] dp = new int[n+1];
dp[0] = dp[1] = 1;
for(int i = 2; i <= n; i++){ // dp[n]取决于dp[1,n-1],故依次递推
for(int j = 1; j <= i; j++){ // 长度为i时,树根可能是[1, i]
dp[i] += dp[j-1] * dp[i-j];
}
}
return dp[n];
}
}
95. 不同的二叉搜索树 II
【代码一】通过—暴力递归
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
// 结合【96. 不同的二叉搜索树】解法,来做。要想想为啥就能保证是二叉搜索树!
// (以某个点为根,左侧必定小于该点,右侧必定大于该点,递归下去。。。)
public List<TreeNode> generateTrees(int n) {
if(n == 0){
return new ArrayList<TreeNode>();
}
return f(1, n);
}
// [st, en]
private List<TreeNode> f(int st, int en){
List<TreeNode> res = new ArrayList<>();
if(st > en){ // 递归结束条件
res.add(null);
return res;
}
for(int i = st; i <= en; i++){ // 枚举所有的i为根
List<TreeNode> left = f(st, i-1);
List<TreeNode> right = f(i+1, en);
for(TreeNode l : left){
for(TreeNode r : right){
TreeNode root = new TreeNode(i);
root.left = l;
root.right = r;
res.add(root);
}
}
}
return res;
}
}
712. 两个字符串的最小ASCII删除和
【代码一】通过—动态规划
class Solution {
// 积累解题经验!
// s1 = "delete", s2 = "leet"
// s1 = "zzdelete", s2 = "leetzz"
// s2 = "ffdelete", s2 = "leetff"
// dp[i][j]: s1[i:]和s2[j:]达到要求,然后递推到dp[0][0],显然是递归。只写dp
public int minimumDeleteSum(String s1, String s2) {
// 根据递归思路,画出二维表格,就知道怎么写了
int len1 = s1.length();
int len2 = s2.length();
int[][] dp = new int[len1+1][len2+1];
for(int i = len2-1; i >= 0; i--){
dp[len1][i] = dp[len1][i+1] + s2.charAt(i);
}
for(int i = len1-1; i >= 0; i--){
dp[i][len2] = dp[i+1][len2] + s1.charAt(i);
}
for(int i = len1-1; i >= 0; i--){
for(int j = len2-1; j >= 0; j--){
if(s1.charAt(i) == s2.charAt(j)){
dp[i][j] = dp[i+1][j+1];
}else{
dp[i][j] = Math.min(s1.charAt(i)+dp[i+1][j], s2.charAt(j)+dp[i][j+1]);
}
}
}
return dp[0][0];
}
}
647. 回文子串
【代码一】通过—暴力
【代码二】通过—中心扩散
【代码三】通过—动态规划
class Solution {
public int countSubstrings(String s) {
return f3(s);
}
// 1.暴力
private int f1(String s){
int n = s.length();
int sum = 0;
for(int i = 0; i < n; i++){
for(int j = i; j < n; j++){
sum += check(s, i, j);
}
}
return sum;
}
private int check(String s, int i, int j){
while(i <= j){
if(s.charAt(i) != s.charAt(j)){
return 0;
}
i++;
j--;
}
return 1;
}
// 2.中心扩展,每个字符都可能成为中心,暴力枚举
private int f2(String s){
int n = s.length();
int sum = 0;
for(int i = 0; i < n; i++){
sum += process(s, i, i); // 奇数中心扩展
sum += process(s, i, i+1); // 偶数中心扩展
}
return sum;
}
private int process(String s, int p, int q){
int sum = 0;
while(p >= 0 && q < s.length() && s.charAt(p) == s.charAt(q)){
sum++;
p--;
q++;
}
return sum;
}
// 3.动态规划 dp[i][j]表示[i,j]是否是回文串
private int f3(String s){
int n = s.length();
boolean[][] dp = new boolean[n][n];
int sum = n; // 先把单独字符是回文串的情况加上
for(int i = n-1; i >= 0; i--){
dp[i][i] = true;
for(int j = i+1; j < n; j++){
// 这里处理了i+1==j和i+1<j-1两种情况,合二为一
if(s.charAt(i) == s.charAt(j) && (i+1 >= j || dp[i+1][j-1])){
dp[i][j] = true;
sum++;
}else{
dp[i][j] = false;
}
}
}
return sum;
}
}
121. 买卖股票的最佳时机
【代码一】通过—动态规划
class Solution {
// dp 状态+状态转移(其实就是暴力递归的尝试)
// i从1开始,j从1开始
// dp[i][j][k] --> 第i天,至多进行了j次交易,当前是否持有股票(k=1持有,k=0不持有),的最大利润
// dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1]+prices[i]) 前天无股票或卖了股票->今天无股票
// dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]) 前天有股票或买了股票->今天有股票
// dp[i][0][0] = 0 至多进行0次交易,利润是0
// dp[i][0][1] = 负无穷 至多进行0次交易,持有股票是不可能的
// dp[0][j][0] = 0
// dp[0][j][1] = 负无穷 (不可能)
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
int[][][] dp = new int[n+1][2][2];
for(int i = 0; i <= n; i++){
dp[i][0][1] = Integer.MIN_VALUE;
}
for(int j = 0; j <= 1; j++){
dp[0][j][1] = Integer.MIN_VALUE;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= 1; j++){
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i-1]);
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i-1]);
}
}
return dp[n][1][0];
}
}
【代码二】通过—动态规划(状态压缩)
class Solution {
public int maxProfit(int[] prices) {
// 化简状态转移方程得到(由j<=1优化得到)
// dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i-1])
// dp[i][1] = Math.max(dp[i-1][1], -prices[i-1])
// dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1]+prices[i-1]) (j不变,和j无关)
// -> dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i-1])
// dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i])
// = Math.max(dp[i-1][j][1], -prices[i-1]) (j不变,和j无关)
// -> dp[i][1] = Math.max(dp[i-1][1], -prices[i-1])
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
int[][] dp = new int[n+1][2];
dp[0][1] = Integer.MIN_VALUE;
for(int i = 1; i <= n; i++){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i-1]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i-1]) ;
}
return dp[n][0];
}
}
【代码三】通过—动态规划(状态压缩)
class Solution {
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
int a = 0;
int b = Integer.MIN_VALUE;
for(int i = 1; i <= n; i++){
a = Math.max(a, b + prices[i-1]); // 改成?:运算符,可大大提升速度
b = Math.max(b, -prices[i-1]);
}
return a;
}
}
122. 买卖股票的最佳时机 II
参考[121. 买卖股票的最佳时机]解法
【代码一】超时—动态规划
class Solution {
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
int[][][] dp = new int[n+1][n+1][2];
for(int i = 0; i <= n; i++){
dp[i][0][1] = Integer.MIN_VALUE;
}
for(int j = 0; j <= n; j++){
dp[0][j][1] = Integer.MIN_VALUE;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i-1]);
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i-1]);
}
}
return dp[n][n][0];
}
}
【代码二】通过----动态规划(状态压缩)
class Solution {
// 状态压缩 既然j尽可能大,那就和j无关了,可以把j视作正无穷不变就OK了
/*
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i-1])
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i-1])
*/
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
int[][] dp = new int[n+1][2];
dp[0][1] = Integer.MIN_VALUE;
for(int i = 1; i <= n; i++){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i-1]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i-1]);
}
return dp[n][0];
}
}
【代码三】通过----动态规划(状态压缩)
class Solution {
// 状态压缩 既然j尽可能大,那就和j无关了,可以把j视作正无穷不变就OK了
/*
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i-1])
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i-1]);
*/
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
int[][] dp = new int[n+1][2];
int a = 0;
int b = Integer.MIN_VALUE;
int tmp = a;
for(int i = 1; i <= n; i++){
a = Math.max(tmp, b + prices[i-1]);
b = Math.max(b, tmp - prices[i-1]);
tmp = a;
}
return a;
}
}
123. 买卖股票的最佳时机 III
参考[121. 买卖股票的最佳时机]解法
【代码一】通过—动态规划
class Solution {
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
int[][][] dp = new int[n+1][3][2];
for(int i = 0; i <= n; i++){
dp[i][0][1] = Integer.MIN_VALUE;
}
for(int j = 0; j <= 2; j++){
dp[0][j][1] = Integer.MIN_VALUE;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= 2; j++){
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i-1]);
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i-1]);
}
}
return dp[n][2][0];
}
}
188. 买卖股票的最佳时机 IV
参考[121. 买卖股票的最佳时机]解法
【代码一】通过—动态规划
class Solution {
public int maxProfit(int k, int[] prices) {
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
if(k > n/2){ // 关键。最多只能n/2次交易,如果k>n/2,情况同122题
return maxProfit0(prices);
}
int[][][] dp = new int[n+1][k+1][2];
for(int i = 0; i <= n; i++){
dp[i][0][1] = Integer.MIN_VALUE;
}
for(int j = 0; j <= k; j++){
dp[0][j][1] = Integer.MIN_VALUE;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= k; j++){
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i-1]);
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i-1]);
}
}
return dp[n][k][0];
}
private int maxProfit0(int[] prices) {
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
int[][] dp = new int[n+1][2];
int a = 0;
int b = Integer.MIN_VALUE;
int tmp = a;
for(int i = 1; i <= n; i++){
a = Math.max(tmp, b + prices[i-1]);
b = Math.max(b, tmp - prices[i-1]);
tmp = a;
}
return a;
}
}
309. 最佳买卖股票时机含冷冻期
【代码一】通过—动态规划(状态压缩)
class Solution {
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
int[][] dp = new int[n+1][2];
dp[0][1] = Integer.MIN_VALUE;
// dp[1][0] = 0;
dp[1][1] = -prices[0];
for(int i = 2; i <= n; i++){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i-1]);
// 注意下面的i-2,从i-2转移过来肯定是对的,所以就从i-2转移过来
dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0] - prices[i-1]);
}
return dp[n][0];
}
}
714. 买卖股票的最佳时机含手续费
【代码一】通过—动态规划(状态压缩)
class Solution {
// 状态压缩 既然j尽可能大,那就和j无关了,可以把j视作正无穷不变就OK了
/*
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i-1])
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i-1] - fee)
*/
public int maxProfit(int[] prices, int fee) {
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
int[][] dp = new int[n+1][2];
dp[0][1] = Integer.MIN_VALUE;
for(int i = 1; i <= n; i++){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i-1]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i-1] -fee);
}
return dp[n][0];
}
}
【代码二】通过—动态规划(状态压缩)
class Solution {
// 状态压缩 既然j尽可能大,那就和j无关了,可以把j视作正无穷不变就OK了
/*
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i-1])
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i-1] - fee)
*/
public int maxProfit(int[] prices, int fee) {
if(prices == null || prices.length == 0){
return 0;
}
int n = prices.length;
int a = 0; // dp[0][0]
int b = Integer.MIN_VALUE; // dp[0][1]
int tmp = a;
for(int i = 1; i <= n; i++){
a = Math.max(tmp, b+prices[i-1]);
b = Math.max(b, tmp-prices[i-1]-fee);
tmp = a;
}
return a;
}
}
413. 等差数列划分
【代码一】通过—暴力尝试
class Solution {
public int numberOfArithmeticSlices(int[] A) {
int n = A.length;
int sum = 0;
for(int i = 0; i < n - 2; i++){
int d = A[i+1]-A[i];
if(A[i+2]-A[i+1] == d){
sum++;
for(int j = i+3; j < n; j++){
if(A[j] - A[j-1] == d){
sum++;
}else{
break;
}
}
}
}
return sum;
}
}
【代码二】通过—暴力递归
class Solution {
private int sum;
public int numberOfArithmeticSlices(int[] A) {
f(A, 0);
return sum;
}
private int f(int[] A, int i){
if(i >= A.length-2){
return 0;
}
int cnt = 0;
if(A[i+2]-A[i+1] == A[i+1]-A[i]){
cnt = 1 + f(A, i+1);
sum += cnt;
}else{
f(A, i+1);
}
return cnt;
}
}
【代码三】通过—动态规划
class Solution {
public int numberOfArithmeticSlices(int[] A) {
int n = A.length;
if(n <= 2){
return 0;
}
int[] dp = new int[n];
int sum = 0;
for(int i = n-3; i >= 0; i--){
if(A[i+2]-A[i+1] == A[i+1]-A[i]){
dp[i] = 1 + dp[i+1];
sum += dp[i];
}
}
return sum;
}
}
【代码四】通过—动态规划(状态压缩)
class Solution {
public int numberOfArithmeticSlices(int[] A) {
int n = A.length;
if(n <= 2){
return 0;
}
int dp = 0;
int sum = 0;
for(int i = n-3; i >= 0; i--){
if(A[i+2]-A[i+1] == A[i+1]-A[i]){
dp = 1 + dp;
sum += dp;
}else{
dp = 0;
}
}
return sum;
}
}
特别说明
本文参考leetcode官方网站题库及相关讨论区解答。链接地址