找规律之动态规划系列

前言

模拟作为暴力解并不是解决问题的唯一解,而找到问题里的规律,可以避免很多无意义的暴力。把大问题拆成很多相关联的小问题,其中的关联就是规律,规律是一种规划,规律在小问题之前传递就是动态的,即动态规划。

一、案例

在这里插入图片描述

二、题解

每次都只能走一步或两步,所以到达每一个台阶都是由前两个台阶的最小值通过跳一步或者跳两步到达当前台阶,如此反复,直到所有台阶跳完,这个时候取上一个台阶所需花费和上两个台阶所需花费的最小值,即跳完的最低消费。

//找其中的规律,用动态规划解题。
    public int minCostClimbingStairs2(int[] cost) {
        int dp1 = 0, dp2 = 0;
        for (int i : cost) {
            int dp = dp1 < dp2 ? dp1 + i : dp2 + i;
            dp1 = dp2;
            dp2 = dp;
        }
        return dp1 < dp2 ? dp1 : dp2;
    }

当然,我们来看看暴力法,可以不断DFS,选花费最低的那条路径的和,但是很多路径是没有意义的。

//DFS快速解题
    public int minCostClimbingStairs(int[] cost) {
        dfs(cost, -1, 0);
        return min;
    }

    private void dfs(int[] cost, int cur, int sum) {
        if (cur >= cost.length) {
            min = min < sum ? min : sum;
            return;
        }
        dfs(cost, cur + 1, sum + (cur == -1 ? 0 : cost[cur]));
        dfs(cost, cur + 2, sum + (cur == -1 ? 0 : cost[cur]));
    }

    int min = Integer.MAX_VALUE;

总结

1)如果能把题分解成多个前后关联的小问题,那么就知道怎么去实现动态性,找到具体的关联是什么,就知道怎么实现规划。

参考文献

[1] LeetCode 爬楼梯的最少成本

附录

1、房屋偷盗

在这里插入图片描述

//动态规划,偷一个房间时存的金额大小只跟前面第2个和第3个有关,中间必须间隔一个或两个。看最后一个和倒数第二个谁大取谁。
    public int rob(int[] nums) {
        int dp1 = 0, dp2 = 0, dp3 = 0;

        for (int num : nums) {
            int dp = Math.max(dp1, dp2) + num;
            dp1 = dp2;
            dp2 = dp3;
            dp3 = dp;
        }
        return dp2 < dp3 ? dp3 : dp2;
    }

2、房屋偷盗之环形房

在这里插入图片描述

	//环形房子
    //吃什么受什么苦不是关键,成长核心在于一点一点的学习和思考去积累。
    public int rob2(int[] nums) {
        if (nums.length == 1) return 0;
        int dp1 = 0, dp2 = 0, dp3 = 0;

        for (int i = 0; i < nums.length - 1; i++) {
            int dp = Math.max(dp1, dp2) + nums[i];
            dp1 = dp2;
            dp2 = dp3;
            dp3 = dp;
        }
        int m1 = Math.max(dp2, dp3);

        dp1 = dp2 = dp3 = 0;
        for (int i = 1; i < nums.length; i++) {
            int dp = Math.max(dp1, dp2) + nums[i];
            dp1 = dp2;
            dp2 = dp3;
            dp3 = dp;
        }
        int m2 = Math.max(dp2, dp3);
        return m1 < m2 ? m2 : m1;
    }

3、粉刷房子

在这里插入图片描述

package com.xhu.offer.offerII;

//粉刷房子
public class MinCost {
    //动态规划,给房子刷成什么颜色,要看前面一个房子除了现在颜色之外的最低花费。
    //换句话说,就是当前选什么颜色之和前一个选什么颜色所花费的开销有关。
    public int minCost(int[][] costs) {
        int[][] dp = new int[3][2];
        for (int[] cost : costs) {
            int[] newDp = new int[]{Math.min(dp[1][1], dp[2][1]), Math.min(dp[0][1], dp[2][1]), Math.min(dp[0][1], dp[1][1])};

            for (int i = 0; i < 3; i++) dp[i][1] = newDp[i] + cost[i];
        }
        return Math.min(dp[0][1], Math.min(dp[1][1], dp[2][1]));
    }

    //优化,减少一点空间
    public int minCost2(int[][] costs) {
        int[] dp = new int[3];//让下标代替三种颜色
        for (int[] cost : costs) {
            int[] newDp = new int[]{dp[1] < dp[2] ? dp[1] : dp[2], dp[0] < dp[2] ? dp[0] : dp[2], dp[0] < dp[1] ? dp[0] : dp[1]};

            for (int i = 0; i < 3; i++) dp[i] = newDp[i] + cost[i];
        }
        return dp[0] < dp[1] ? dp[0] < dp[2] ? dp[0] : dp[2] : dp[1] < dp[2] ? dp[1] : dp[2];
    }
}

4、翻转字符

在这里插入图片描述

package com.xhu.offer.offerII;

//翻转字符
public class MinFlipsMonoIncr {
	
    //从0到每个字符所行成的子字符串需翻转的次数之和前一个字符有关。翻转之后有序。
    public int minFlipsMonoIncr(String s) {
        //以0结尾最少翻转次数为dp0,以1结尾最少翻转次数为dp1;都是在有序的基础上。
        int len = s.length();
        int dp0 = 0, dp1 = 0;
        for (int i = 0; i < len; i++) {
            int v = s.charAt(i) - '0';
            if (v == 1) {
                dp1 = Math.min(dp0, dp1);
                dp0++;
                continue;
            }
            dp1 = Math.min(dp0, dp1) + 1;
        }
        return Math.min(dp0, dp1);
    }
}

总结

1)动态规划的状态定义很重要,如该题,dp0表示从0到当前字符的以0结尾的最小翻转数,dp1表示以0到当前字符的以1结尾的最小翻转数。

5、最长斐波那契数列

在这里插入图片描述

package com.xhu.offer.offerII;

import java.util.HashMap;
import java.util.Map;

//最长斐波拉契数列
public class LenLongestFibSubseq {
    //hashmap处理+动态规划(往后遍历,每加一个元素,都要看前面那两个元素符合,所以需要固定一个元素,所以双循环,另一元素确定用map快速查找)
    public int lenLongestFibSubseq(int[] arr) {
        int len = arr.length, ans = 0;
        Map<Integer, Integer> cache = new HashMap<>();//map解决前面可能符合条件太多的状态,以O(1)快速找到,而不是一个一个去试。
        int[][] dp = new int[len][len];//i到j的最长斐波拉契数列
        for (int i = 0; i < len; i++) {
            for (int j = i + 1; j < len; j++) {
                dp[i][j] = 2;
                int gap = arr[j] - arr[i];
                if (cache.containsKey(gap)) {
                    int l = cache.get(gap);
                    dp[i][j] = dp[l][i] + 1;
                    ans = Math.max(ans, dp[i][j]);
                }
                cache.put(arr[i], i);
            }
        }
        return ans < 3 ? 0 : ans;
    }
}

6、最少回文分割

在这里插入图片描述

//dp1 + dp2
    //dp1:int[][] dp1 = new int[][];dp1[i][j]状态定义:[j,i]区间的字符串是否为回文。状态转移:dp[i-1][j+1]为回文且ch[i] == ch[j]时,为1.
    //dp2:int[] dp2 = new int[];dp2[i]状态定义:[0,i]的最小回文个数。
    public int minCut(String s) {
        int len = s.length();
        int[][] dp1 = new int[len][len];
        for (int i = 0; i < len; i++) {
            for (int j = i; j >= 0; j--) {
                if (s.charAt(i) == s.charAt(j) && (i == j || i - 1 == j || dp1[i - 1][j + 1] == 1)) dp1[i][j] = 1;
            }
        }
        int[] dp2 = new int[len + 1];
        for (int i = 1; i <= len; i++) {
            dp2[i] = i;
            for (int j = 0; j < i; j++) {
                if (dp1[i - 1][j] == 1) {
                    dp2[i] = Math.min(dp2[i], dp2[j] + 1);
                }
            }
        }
        return dp2[len] - 1;
    }

7、最长公共子序列(经典二维动规)

在这里插入图片描述

package com.xhu.offer.offerII;

public class LongestCommonSubsequence {
    //二维动态规划
    public int longestCommonSubsequence(String text1, String text2) {
        //状态定义:dp[i][j]表示text1[0,i]和text2[0,j]的公共子串。
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m + 1][n + 1];

        for (int i = 1; i <= m; i++) {
            char chT1 = text1.charAt(i - 1);
            for (int j = 1; j <= n && j <= i; j++) {
                char chT2 = text2.charAt(j - 1);
                if (chT1 == chT2) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[m][n];
    }
    //思路总结:
    //拿text1的第0到i所行成的子字符串去和text2匹配,一共匹配text1.len次。
    //每次新来的text1[i]去和text[j]匹配,根据两字符是否相等来确定当前两子字符串的最长公共子串。
}

8、字符串交织

在这里插入图片描述

package com.xhu.offer.offerII;

//字符串交织
public class IsInterleave {
    //二维dp
    //dp[i][j]表示s1[0,i]和s2[0,j]能否交织成s3[0,i+j+1];当前状态与dp[i-1][j] 或dp[i][j - 1]有关;
    public boolean isInterleave(String s1, String s2, String s3) {
        int m = s1.length(), n = s2.length(), k = s3.length();
        if (m + n != k) return false;

        boolean[][] dp = new boolean[m + 1][n + 1];
        //初始状态为false,因为字符串为空肯定无法和其它交织。但两个空串可以
        dp[0][0] = true;
        //只有一个字符串不为空
        for (int i = 1; i <= m; i++) {
            if (s1.charAt(i - 1) == s3.charAt(i - 1))
                dp[i][0] = dp[i - 1][0];
            else break;
        }
        for (int i = 1; i <= n; i++) {
            if (s2.charAt(i - 1) == s3.charAt(i - 1))
                dp[0][i] = dp[0][i - 1];
            else break;
        }
        //都有字符时。
        for (int i = 1; i <= m; i++) {
            char chS1 = s1.charAt(i - 1);
            for (int j = 1; j <= n; j++) {
                char chS2 = s2.charAt(j - 1);
                char chS3 = s3.charAt(i + j - 1);
                dp[i][j] = (chS1 == chS3 && dp[i - 1][j]) || (chS2 == chS3 && dp[i][j - 1]);
            }
        }
        return dp[m][n];
    }
}
//空间降维
    public boolean isInterleave2(String s1, String s2, String s3) {
        int m = s1.length(), n = s2.length(), k = s3.length();
        if (m + n != k) return false;

        boolean[] dp = new boolean[n + 1];
        //初始状态为false,因为字符串为空肯定无法和其它交织。但两个空串可以
        dp[0] = true;

        for (int i = 1; i <= n; i++) {
            if (s2.charAt(i - 1) == s3.charAt(i - 1))
                dp[i] = dp[i - 1];
            else break;
        }
        //都有字符时。
        for (int i = 1; i <= m; i++) {
            char chS1 = s1.charAt(i - 1);
            dp[0] = dp[0] && (chS1 == s3.charAt(i - 1));//对于每一个s1的字符,都有一个新的dp[0]
            for (int j = 1; j <= n; j++) {
                char chS2 = s2.charAt(j - 1);
                char chS3 = s3.charAt(i + j - 1);
                dp[j] = (chS1 == chS3 && dp[j]) || (chS2 == chS3 && dp[j - 1]);
            }
        }
        return dp[n];
    }

9、子序列数目

在这里插入图片描述

package com.xhu.offer.offerII;

//子序列的数目
public class NumDistinct {
    //动态规划
    //问题分析及转换:s中有多少个t子串?== s[0,m)中有多少个t[0,n)子串。往动态规划上分析,所以转化为二维动态规划问题。
    //初始判断:如果s的长度小于t的长度,就没有子串的说法了。
    //状态描述:dp[i][j]:s[0,i)中有多少个t[0,j)的子串。
    //初始状态:空串是空串的子串,所以dp[0][0] = 1;t[0,0)为空串,是s任意子串的子串,所以dp[i][0] = 1;反之则不成立。
    //状态转移:当s[i] == t[j]时,当前状态应该为s[0,i - 1)中t[0,j - 1)子串的多少 + s[0,i - 1)中t[0,j)子串的多少,即dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
    //        当s[i] != t[j]时,当前状态只能是s[0,i - 1)中有多少t[0,j)子串,只能当字符不匹配处理,即dp[i][j] = dp[i - 1][j];
    public int numDistinct(String s, String t) {
        int m = s.length(), n = t.length();
        if (m < n) return 0;

        //状态初始化
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 0; i <= m; i++) dp[i][0] = 1;

        //状态转移
        for (int i = 1; i <= m; i++) {
            char c1 = s.charAt(i - 1);
            for (int j = 1; j <= i && j <= n; j++) {
                char c2 = t.charAt(j - 1);

                dp[i][j] = dp[i - 1][j];
                dp[i][j] += c1 == c2 ? dp[i - 1][j - 1] : 0;
            }
        }
        //最终状态
        return dp[m][n];
    }
}
//空间优化,状态压缩
    public int numDistinct2(String s, String t) {
        int m = s.length(), n = t.length();
        if (m < n) return 0;

        //状态初始化
        int[] dp = new int[n + 1];
        dp[0] = 1;

        //状态转移
        for (int i = 1; i <= m; i++) {
            char c1 = s.charAt(i - 1);
            int pre = dp[0];
            for (int j = 1; j <= i && j <= n; j++) {
                char c2 = t.charAt(j - 1);

                int next = dp[j];
                dp[j] += c1 == c2 ? pre : 0;

                //防止上一个值被覆盖。
                pre = next;
            }
        }
        //最终状态
        return dp[n];
    }

10、路径的数目

在这里插入图片描述

package com.xhu.offer.offerII;

//路径的数目
public class UniquePaths {
    //动态规划
    //问题的分析与转化:机器人只能向右和向下走,那么到达每个格子的路径数 == 到达上格子的路径数 + 到达左格子的路径数,归类为动态规划。
    //初始判断:若m < 0 || n < 0则返回0,没有路径。
    //状态描述:dp[i][j]表示从位置(0,0)到位置(i,j)的路径数。
    //初始状态,dp[0][0] = 1,表示只有一条路径到达初始位置。
    //状态转移:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
    //最终状态:dp[m][n],表示从位置(0,0)到达位置(m,n)的路径数
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];

        //状态初始化
        dp[0][0] = 1;

        //状态转移
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                dp[i][j] += i - 1 == -1 ? 0 : dp[i - 1][j];
                dp[i][j] += j - 1 == -1 ? 0 : dp[i][j - 1];
            }
        }
        //最终状态
        return dp[m - 1][n - 1];
    }
    //优化,状态压缩
    public int uniquePaths(int m, int n) {
        int[] dp = new int[n];

        //状态初始化
        dp[0] = 1;

        //状态转移
        for (int i = 0; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[j] += i - 1 == -1 ? -dp[j] : 0;
                dp[j] += j - 1 == -1 ? 0 : dp[j - 1];
            }
        }
        //最终状态
        return dp[n - 1];
    }
}

11、最小路径之和

在这里插入图片描述

package com.xhu.offer.offerII;

//最小路径之和
public class MinPathSum {
    //动态规划
    //分析问题并转化:机器人只能往右走或是往下走,所以到达每个格子的最小路径和来自于左边和上边的最小的那个和,归类动态规划。
    //初始判断:当gird == null时,返回0,即没有路径和。
    //状态描述:dp[i][j],从位置(0,0)到位置(i,j)的最小路径和。
    //初始状态:dp[0][0] = grid[0][0],表示只需走这一个格子。
    //状态转移:dp[i][j] = Math.min(dp[i - 1][j - 1],dp[i][j - 1])
    //最终状态:dp[gird.len][gird[0].len]
    public int minPathSum(int[][] grid) {
        //初始判断
        if (grid == null || grid.length == 0 || grid[0].length == 0) return 0;

        int m = grid.length, n = grid[0].length;

        int[][] dp = new int[m][n];

        //初始状态,
        dp[0][0] = grid[0][0];
        for (int i = 1; i < m; i++) dp[i][0] = dp[i - 1][0] + grid[i][0];
        for (int i = 1; i < n; i++) dp[0][i] = dp[0][i - 1] + grid[0][i];

        //状态转移
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }
        //最终状态
        return dp[m - 1][n - 1];
    }

    //改进,状态压缩
    public int minPathSum2(int[][] grid) {
        //初始判断
        if (grid == null || grid.length == 0 || grid[0].length == 0) return 0;

        int m = grid.length, n = grid[0].length;

        int[] dp = new int[n];

        //初始状态,
        dp[0] = grid[0][0];
        for (int i = 1; i < n; i++) dp[i] = dp[i - 1] + grid[0][i];

        //for (int i = 1; i < n; i++) dp[0][i] = dp[0][i - 1] + grid[0][i];

        //状态转移
        for (int i = 1; i < m; i++) {
            dp[0] += grid[i][0];
            for (int j = 1; j < n; j++) {
                dp[j] = Math.min(dp[j], dp[j - 1]) + grid[i][j];
            }
        }
        //最终状态
        return dp[n - 1];
    }

    //调试
    //p1:应该初始化第一行,而且for循环条件应为i < n而不是i < m
    public static void main(String[] args) {
        new MinPathSum().minPathSum2(new int[][]{{1, 2, 3}, {4, 5, 6}});
    }
}

12、三角形中最小路径之和

在这里插入图片描述

package com.xhu.offer.offerII;

import java.util.List;

//三角形中最小路径之和
public class MinimumTotal {
    //问题分析与转化:求路径和意味着要走到最后一层才结束;最小意味着每到达下一个节点要选root到达上一个节点的最短路径。归类为动态规划
    //初始判断:triangle==null || triangle.size() == 0 || triangle.get(0).size() == 0,则return 0;本题有限制。
    //状态描述:dp[i][j]:从root到第i+1行j+1列的最短路径
    //初始状态:dp[0][0]:初始时,从root出发,dp[0][0] = triangle.get(0).get(0);
    //状态转移:dp[i][j]选择从上一层的dp[i-1][j-1]、dp[i-1][j]、dp[i-1][j+1]中选最小的一个,仅当都不越界时。
    //最终状态:应该选择dp[n][0,n]最小的一个
    public int minimumTotal(List<List<Integer>> triangle) {
        //初始判断无需操作,题目限制

        //二维dp
        int[][] dp = new int[200][200];

        //初始状态
        dp[0][0] = triangle.get(0).get(0);

        //状态转移
        int size = triangle.size();
        for (int i = 1; i < size; i++) {
            int s = triangle.get(i).size();
            for (int j = 0; j < s; j++) {
                //int left = j - 1 == -1 ? Integer.MAX_VALUE : dp[i - 1][j - 1];
                int mid = j < s - 1 ? dp[i - 1][j] : Integer.MAX_VALUE;
                int right = j + 1 < s - 1 ? dp[i - 1][j + 1] : Integer.MAX_VALUE;

                dp[i][j] = Math.min(mid, right) + triangle.get(i).get(j);
            }
        }
        //最终状态
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < size; i++) min = Math.min(dp[size - 1][i], min);
        return min;
    }

    //优化,状态压缩
    public int minimumTotal2(List<List<Integer>> triangle) {
        //初始判断无需操作,题目限制

        //二维dp
        int size = triangle.size();
        int[] dp = new int[size];

        //初始状态
        dp[0] = triangle.get(0).get(0);

        //状态转移
        for (int i = 1; i < size; i++) {
            List<Integer> t = triangle.get(i);
            int s = t.size();
            int pre = dp[1];
            for (int j = 0; j < s; j++) {
                int left = j - 1 == -1 ? Integer.MAX_VALUE : pre;
                int mid = j < s - 1 ? dp[j] : Integer.MAX_VALUE;
                int next = dp[j];

                dp[j] = Math.min(left, mid) + triangle.get(i).get(j);

                pre = next;//防止覆盖。
            }
        }
        //最终状态
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < size; i++) min = Math.min(dp[i], min);
        return min;
    }
}

13、分割等和子集

在这里插入图片描述

package com.xhu.offer.offerII;

//分割等和子集
public class CanPartition {
    //总结:细心看题(也许就是自己对自己的状态要求不严格),不要用我以为就开始做题。
    //问题分析和转化:能否找到数组内元素之和为总元素的一半,
    //该数组内是否能抽出和一半的子数组和前n-1的子数组能否抽出 和为刚才的一半 - nums[n - 1] || 和为一半有关。所以规划为动态规划问题。
    //初始判断:如果和为奇数,则返回false。
    //状态描述:dp[i][j],表示nums[0,i)子数组里是否有和为j的再分子数组。
    //状态转移:dp[i][j] = dp[i - 1][j] || (j >= nums[i - 1] && dp[i - 1][j - nums[i - 1]])
    //最终状态:dp[len][target],表示整个数组中是否有子数组和为target。
    public boolean canPartition(int[] nums) {
        int total = 0, len = nums.length;
        for (int num : nums) total += num;

        if ((total & 1) == 1) return false;

        int target = total >>> 1;
        boolean[][] dp = new boolean[len + 1][target + 1];

        dp[0][0] = true;
        for (int i = 1; i <= len; i++) {
            for (int j = 0; j <= target; j++) {
                dp[i][j] = dp[i - 1][j] || (j >= nums[i - 1] && dp[i - 1][j - nums[i - 1]]);
            }
        }
        return dp[len][target];
    }

    //优化,状态压缩
    public boolean canPartition2(int[] nums) {
        int total = 0, len = nums.length;
        for (int num : nums) total += num;

        if ((total & 1) == 1) return false;

        int target = total >>> 1;
        boolean[] dp = new boolean[target + 1];

        for (int i = 1; i <= len; i++) {
            dp[0] = i == 1;
            for (int j = target; j >= 0; j--) {
                dp[j] = dp[j] || (j >= nums[i - 1] && dp[j - nums[i - 1]]);
            }
        }
        return dp[target];
    }
}

14、加减的目标值

在这里插入图片描述

package com.xhu.offer.offerII;

//加减的目标值
public class FindTargetSumWays {
    //DFS
    public int findTargetSumWays(int[] nums, int target) {
        dfs(nums, 0, 0, target);
        return count;
    }

    int count = 0;

    private void dfs(int[] nums, int cur, int sum, int target) {
        int len = nums.length;
        if (cur == len) {
            if (sum == target) count++;
            return;
        }
        dfs(nums, cur + 1, sum + nums[cur], target);
        dfs(nums, cur + 1, sum - nums[cur], target);
    }

    //动态规划
    //问题分析与转化:nums数组的和 == target    等于
    //     nums[0,len - 1)数组的和 == target - nums[len - 1]
    //   + nums[0,len - 1)数组的和 == target + nums[len - 1],所以归类为(显,无需转换)-动态规划。
    //初始判断:nums == null || nums.length = 0时,return 0 == target
    //初始状态:当数组为空时,和为0,则有一个。
    //状态描述:dp[i][j]:nums[0,i)子数组和为j的个数;数组和都小于target则return 0;
    //状态转移:dp[i][j] = dp[i - 1][j - nums[i - 1]] + dp[i - 1][j + nums[i - 1]]
    //最终状态:dp[len][target]:表示nums的和为target的有多少个。
    public int findTargetSumWays2(int[] nums, int target) {
        int len = nums.length, total = 0;

        for (int n : nums) total += n;

        //初始判断
        if (total < target) return 0;

        int[][] dp = new int[len + 1][2001];
        //初始状态
        dp[0][1000] = 1;

        //状态转移
        for (int i = 1; i <= len; i++) {
            for (int j = 0; j <= 2000; j++) {
                if (j - nums[i - 1] >= 0) dp[i][j] = dp[i - 1][j - nums[i - 1]];
                if (j + nums[i - 1] <= 2000) dp[i][j] += dp[i - 1][j + nums[i - 1]];
            }
        }
        return dp[len][target + 1000];
    }

    //优化,状态压缩
    public int findTargetSumWays3(int[] nums, int target) {
        int len = nums.length, total = 0;

        for (int n : nums) total += n;

        //初始判断
        if (total < target) return 0;

        int[] dp = new int[2001];
        int[] res = new int[2001];
        //初始状态
        dp[1000] = 1;

        //状态转移
        for (int i = 1; i <= len; i++) {
            for (int j = 0; j <= 2000; j++) {
                if (j - nums[i - 1] >= 0) res[j] = dp[j - nums[i - 1]];
                if (j + nums[i - 1] <= 2000) res[j] += dp[j + nums[i - 1]];
            }
            int[] t = dp;
            dp = res;
            res = t;
        }
        return dp[target + 1000];
    }

    //优化,问题转换,数学知识,代码简化
    //负数和为neg,正数和为pos = total - neg;target == pos - neg = total - 2 * neg = target;neg = total - target >>> 1;
    //初始判断:total - target为偶数 且 target <= total;
    public int findTargetSumWays4(int[] nums, int target) {
        int len = nums.length, total = 0;

        for (int n : nums) total += n;

        //初始判断
        if (total < target || (total - target & 1) == 1) return 0;

        int neg = total - target >>> 1;
        int[][] dp = new int[len + 1][neg + 1];
        //初始状态
        dp[0][0] = 1;

        //状态转移
        for (int i = 1; i <= len; i++) {
            for (int j = 0; j <= neg; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j - nums[i - 1] >= 0) dp[i][j] += dp[i - 1][j - nums[i - 1]];
            }
        }
        return dp[len][neg];
    }

    //状态优化
    public int findTargetSumWays5(int[] nums, int target) {
        int len = nums.length, total = 0;

        for (int n : nums) total += n;

        //初始判断
        if (total < target || (total - target & 1) == 1) return 0;

        int neg = total - target >>> 1;
        int[] dp = new int[neg + 1];
        //初始状态
        dp[0] = 1;

        //状态转移
        for (int i = 1; i <= len; i++) {
            for (int j = neg; j >= 0; j--) {//注1:细节易忽视,忽视之后还很难查到,毕竟思维固化,下一次会误以为,需调试。需要倒着更新dp从而防止覆盖。
                if (j - nums[i - 1] >= 0) dp[j] += dp[j - nums[i - 1]];
            }
        }
        return dp[neg];
    }
}

总结

1)遇到问题,若理清思路快速检查一遍,还是出错,多半是思维固化,到bug处会出现误以为的情况,需调试。

15、最少的硬币数目

在这里插入图片描述

package com.xhu.offer.offerII;

//最少的硬币数目
public class CoinChange {
    //DFS(total<=amount)
    public int coinChange(int[] coins, int amount) {
        dfs(coins, amount, 0, 0);
        return min == Integer.MAX_VALUE ? -1 : min;
    }

    int min = Integer.MAX_VALUE;//p1:没有考虑找不到的情况。

    private void dfs(int[] coins, int amount, long total, int num) {//p2:没有考虑integer.Max + n 可能越界。
        if (amount <= total) {
            if (amount == total) min = num < min ? num : min;

            return;
        }
        for (int coin : coins) dfs(coins, amount, total + coin, num + 1);
    }

    //优化,动态规划(DFS-component + DP/记忆化数组-plugin == 减少不必要的计算)
    public int coinChange2(int[] coins, int amount) {
        if (amount < 1) return 0;
        //记忆化搜索
        int[] dp = new int[amount];
        return dfs(coins, amount, dp);
    }

    private int dfs(int[] coins, int money, int[] dp) {
        if (money < 0) return -1;
        if (money == 0) return 0;
        if (dp[money - 1] != 0) return dp[money - 1];

        //找该money额,所需要的最少硬币数
        int min = Integer.MAX_VALUE;
        for (int coin : coins) {
            int n = dfs(coins, money - coin, dp);
            if (n >= 0 && n < min) min = n + 1;
        }
        dp[money - 1] = min == Integer.MAX_VALUE ? -1 : min;
        return dp[money - 1];
    }
}

16、排列的数目

在这里插入图片描述

package com.xhu.offer.offerII;

//排列的数目
public class CombinationSum4 {
    //DFS快速解题
    public int combinationSum4(int[] nums, int target) {
        dfs(nums, target, 0);
        return count;
    }

    int count = 0;

    private void dfs(int[] nums, int target, int sum) {
        if (sum >= target) {
            count += sum == target ? 1 : 0;
            return;
        }
        for (int num : nums) dfs(nums, target, sum + num);
    }

    //DFS超时,优化,DFS-component + DP-plugin
    //总结:再看DFS,有点暴力模拟,但凡题中所展现的问题有所规律或是可拆分,都不能直接暴力DFS。
    //bug:p1-2
    public int combinationSum42(int[] nums, int target) {
        int[] dp = new int[target];

        int r = dfs(nums, target, dp);
        return r == -1 ? 0 : r;//p2:应题目需要,没有和为target时,应该返回0而不是-1
    }

    private int dfs(int[] nums, int target, int[] dp) {
        if (target < 0) return -1;
        if (target == 0) return 1;
        if (dp[target - 1] != 0) return dp[target - 1];
        int n = 0;
        for (int num : nums) {
            int r = dfs(nums, target - num, dp);
            n += r != -1 ? r : 0;
        }
        dp[target - 1] = n == 0 ? -1 : n;//p1:判断和为该数是否成立,不成立要用-1标记。
        return dp[target - 1];
    }

    //DP-component
    public int combinationSum43(int[] nums, int target) {
        int[] dp = new int[target + 1];
        dp[0] = 1;
        int len = nums.length;

        for (int i = 1; i <= target; i++) {
            for (int j = 0; j < len; j++) {
                dp[i] += i >= nums[j] ? dp[i - nums[j]] : 0;
            }
        }
        return dp[target];
    }
}

总结

1)再看DFS,有点暴力模拟,但凡题中所展现的问题有所规律或是可拆分,都不能直接暴力DFS。
2)细节边界问题,都是因为自己不足够清晰问题所致,有时没有错误案例根本找不到,所以多训练,争取不需要错误案例,只有训练到一种境界时,对问题有很清晰的认识才行。
3)不管是自己分析还是看题解,拆解问题+把握问题整体观都很重要。

17、双DP-component

在这里插入图片描述

//component-DP
    public int[][] updateMatrix3(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        int[][] dp = new int[m][n];

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (mat[i][j] != 0) {
                    int left = j == 0 ? Integer.MAX_VALUE >>> 1 : dp[i][j - 1];
                    int up = i == 0 ? Integer.MAX_VALUE >>> 1 : dp[i - 1][j];

                    int r = Math.min(left, up);
                    dp[i][j] = r + 1;
                }
            }
        }
        for (int i = m - 1; i >= 0; i--) {
            for (int j = n - 1; j >= 0; j--) {
                if (mat[i][j] != 0) {
                    int right = j == n - 1 ? Integer.MAX_VALUE >>> 1 : dp[i][j + 1];
                    int down = i == m - 1 ? Integer.MAX_VALUE >>> 1 : dp[i + 1][j];

                    int r = Math.min(right, down);
                    dp[i][j] = Math.min(dp[i][j], r + 1);
                    mat[i][j] = dp[i][j];
                }
            }
        }

        return mat;
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值