算法-动态规划(Java实现)

本文探讨了多种算法问题的解决方案,包括使用动态规划解决加减目标值、最少硬币数目、最长递增子序列等问题,以及使用线段树处理连续子数组的最大和。此外,还介绍了如何运用贪心策略解决股票买卖问题,并讲解了如何寻找三角形最小路径和、乘积最大子数组等。文章深入浅出地展示了多种算法在实际问题中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

加减的目标值

https://leetcode-cn.com/problems/YaVDxD/

剑指 Offer II 102. 加减的目标值
给定一个正整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

class Solution {
    // dfs() 搜索
    public int findTargetSumWays(int[] nums, int target) {
        if (nums == null) {
            return -1;
        }
        int len = nums.length;
        if (len <= 0) {
            return -1;
        }
        goalLen = len;
        dfs(0, nums, nums[0], target);
        dfs(0, nums, -nums[0], target);
        return res;
    }

    private int res = 0;
    private int goalLen = 0;

    private void dfs(int index, int[] nums, int sum, int target) {
        if (index == (goalLen - 1)) {
            if (sum == target) {
                res++;
                return;
            }
        } else {
            dfs(index + 1, nums, sum + nums[index + 1], target);
            dfs(index + 1, nums, sum - nums[index + 1], target);
        }
    }
}

class Solution02 {
    // DP 动态规划(二维的动态规划问题!!!)
    class Solution {
        /**
         * 记数组的元素和为 sum,添加 - 号的元素之和为 neg,则其余添加 + 的元素之和为 sum−neg ,得到的表达式的结果为
         * (sum−neg)−neg = sum−2⋅neg =  target
         * neg = (sum-target) / 2
         */
        public int findTargetSumWays(int[] nums, int target) {
            int sum = 0;
            for (int num : nums) {
                sum += num;
            }
            int diff = sum - target;
            if (diff < 0 || diff % 2 != 0) {
                return 0;
            }
            int n = nums.length, neg = diff / 2;
            /**
             dp[i][j] 表示在数组 nums 的前 i 个数中选取元素,使得这些元素之和等于 j 的方案数
             假设数组 nums 的长度为 n,则最终答案为 dp[n][neg]
             动态规划方程是:
             */
            int[][] dp = new int[n + 1][neg + 1];
            dp[0][0] = 1;
            for (int i = 1; i <= n; i++) {
                int num = nums[i - 1];
                for (int j = 0; j <= neg; j++) {
                    dp[i][j] = dp[i - 1][j];
                    if (j >= num) {
                        dp[i][j] += dp[i - 1][j - num];
                    }
                }
            }
            return dp[n][neg];
        }
    }
}

剑指 Offer II 103. 最少的硬币数目

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。
https://leetcode-cn.com/problems/gaM7Ch/
在这里插入图片描述

public class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = amount + 1;
        // dp[i] 表示:组成金额 i 所需最少的硬币数量
        // 那么 dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, max);
        dp[0] = 0;
        for (int i = 1; i <= amount; i++) {
            for (int j = 0; j < coins.length; j++) {
                if (coins[j] <= i) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
}
  • 时间:O(n^2)
  • 空间:O(n)

剑指 Offer 42. 连续子数组的最大和

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

思路:DP 或者 线段树(这个感觉挺有意思的,与时间玩玩)

class Solution {
    public int maxSubArray(int[] nums) {
        if (nums == null) {
            return 0;
        }
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int res = dp[0];
        for (int i = 1; i < nums.length; i++) {
            dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]);
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}
// 滚动数组优化后
    public int maxSubArray(int[] nums) {
        int currentMax = nums[0];
        int max = nums[0];
        for (int i = 1; i < nums.length; i++) {
            int previousMax = currentMax;
            currentMax = Math.max(previousMax + nums[i], nums[i]);
            max = Math.max(max, currentMax);
        }
        return max;
    }
  • 时间:O(n)
  • 空间:O(n),这里可以直接使用滚动数组来优化为 O(1)

64. 最小路径和

https://leetcode-cn.com/problems/minimum-path-sum/
在这里插入图片描述

class Solution {
    public int minPathSum(int[][] grid) {
        if (grid == null || grid.length <= 0) {
            return 0;
        }
        int row = grid.length;
        int col = grid[0].length;

        int[][] dp = new int[row][col];
        dp[0][0] = grid[0][0];

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (i == 0 && j > 0) {
                    // 第一行 DP:只能从 左边 过来
                    dp[i][j] = dp[0][j - 1] + grid[0][j];
                } else if (j == 0 && i > 0) {
                    // 第一列 DP:只能从 上边 过来
                    dp[i][j] = dp[i - 1][0] + grid[i][0];
                } else if (j > 0 && i > 0) {
                    dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
                }
            }
        }
        return dp[row - 1][col - 1];
    }
}
  • 时间:O(mn)
  • 空间:O(mn)

剑指 Offer 46. 把数字翻译成字符串

https://leetcode-cn.com/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

class Solution01 {
    public int translateNum(int num) {
        String str = String.valueOf(num);
        if (str.length() <= 1) {
            return 1;
        }
        int[] dp = new int[str.length()];
        dp[0] = 1;
        dp[1] = 1;
        if (str.substring(0, 2).compareTo("25") <= 0 && str.substring(0, 2).compareTo("10") >= 0) {
            dp[1] = 2;
        }
        for (int i = 2; i < str.length(); i++) {
            dp[i] = dp[i - 1];
            if (str.substring(i - 1, i + 1).compareTo("25") <= 0 && str.substring(i - 1, i + 1).compareTo("10") >= 0) {
                dp[i] = Math.max(dp[i - 1] + dp[i - 2], dp[i]);
            }
        }
        return dp[str.length() - 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;
        for(int i = 2; i <= s.length(); i ++){
            String temp = s.substring(i-2, i);
            if(temp.compareTo("10") >= 0 && temp.compareTo("25") <= 0)
                dp[i] = dp[i-1] + dp[i-2];
            else
                dp[i] = dp[i-1];
        }
        return dp[s.length()];
    }
}
  • 时间:O(n)
  • 空间:O(n)

823. 带因子的二叉树

https://leetcode-cn.com/problems/binary-trees-with-factors/

思路:

// 设 dp(v) 是以 v 为根节点的树种类数。
// 其子节点就是 x*y = v
// dp[v] = dp(x) * dp(y)
// 为了快速的计算,我们使用 index 数组快速查找:如果 A[k] = A[i] / A[j],那么 index[A[i] / A[j]] = k

class Solution {
    public int numFactoredBinaryTrees(int[] arr) {
        if (arr == null) {
            return 0;
        }
        // 设 dp(v) 是以 v 为根节点的树种类数。
        // 其子节点就是 x*y = v
        // dp[v] =  dp(x) * dp(y)
        // 为了快速的计算,我们使用 index 数组快速查找:如果 A[k] = A[i] / A[j],那么 index[A[i] / A[j]] = k
        long[] dp = new long[arr.length];
        Arrays.fill(dp, 1);

        //在上面的例子中我们知道 x < v 和 y < v,我们可以用动态规划的方法按照升序值计算 dp[i] 的值
        Arrays.sort(arr);

        // arr 数值:index
        HashMap<Integer, Integer> index = new HashMap<>();
        for (int i = 0; i < arr.length; i++) {
            index.put(arr[i], i);
        }

        int MOD = 1_000_000_007;

        for (int i = 0; i < arr.length; i++) {
            // 在数组中 找 i 的所有因子
            for (int j = 0; j < i; j++) {
                if (arr[i] % arr[j] == 0) {
                    int right = arr[i] / arr[j];
                    if (index.containsKey(right)) {
                        dp[i] = (dp[i] + dp[j] * dp[index.get(right)]) % MOD;
                    }
                }
            }
        }
        long res = 0;
        for (long l : dp) {
            res += l;
        }
        return (int) (res % MOD);
    }
}
  • 时间:O(n^2)
  • 空间:O(n)

剑指 Offer 49. 丑数

https://leetcode-cn.com/problems/chou-shu-lcof/

思路:

最小堆思路

既然是求:第n个元素,那么我们就考虑使用堆的数据结构即可。

初始时堆为空。首先将最小的丑数 11 加入堆。

每次取出堆顶元素 xx,则 xx 是堆中最小的丑数,由于 2x, 3x, 5x2x,3x,5x 也是丑数,因此将 2x, 3x, 5x2x,3x,5x 加入堆。

上述做法会导致堆中出现重复元素的情况。为了避免重复元素,可以使用哈希集合去重,避免相同元素多次加入堆。

此题中:要求是 最小堆+不重复。所以直接可以使用 TreeSet 数据结构,避免:PriorityQueue+Set 数据结构

// 刚开始写出来的,运行不对是因为最小堆中有重复元素,所以会导致结果不对。需要去重。
 class Solution {
        public int nthUglyNumber(int n) {
            if (n <= 0) {
                return 0;
            }
            PriorityQueue<Long> queue = new PriorityQueue<>();
            Long res = null;
            queue.add(1L);
            for (int i = 1; i <= n; i++) {
                res = queue.poll();
                queue.add(res * 2);
                queue.add(res * 3);
                queue.add(res * 5);
            }
            return res.intValue();
        }
    }
// AC
class Solution {
    public int nthUglyNumber(int n) {
        if (n <= 0) {
            return 0;
        }
        TreeSet<Long> set = new TreeSet<>();
        Long res = null;
        set.add(1L);
        for (int i = 1; i <= n; i++) {
            res = set.pollFirst();
            set.add(res * 2);
            set.add(res * 3);
            set.add(res * 5);
        }
        return res.intValue();
    }
}    
  • 时间:O(nlogN)
  • 空间:O(n)

动态规划

// 我写的:通过 500 / 596 个通过测试用例(不太明白时间浪费在哪里了呐?)
class Solution {
        public int nthUglyNumber(int n) {
            if (n <= 0) {
                return 0;
            }
            //下标:对应的丑数
            //使用 HashMap 不使用 int[] ,主要是为了避免 Java JVM 自己默认的数组初始化,有程序员自定义
            HashMap<Integer, Integer> map = new HashMap<>();
            map.put(1, 1);
            map.put(2, 2);
            map.put(3, 3);
            map.put(4, 4);
            map.put(5, 5);
            int index = n;
            for (int i = 6; map.size() < n; i++) {
                index = i;
                // continue 之间有相应顺序
                if (i % 2 == 0 && map.containsKey(i / 2)) {
                    map.put(i, map.get(i / 2) * 2);
                    continue;
                }
                if (i % 3 == 0 && map.containsKey(i / 3)) {
                    map.put(i, map.get(i / 3) * 3);
                    continue;
                }
                if (i % 5 == 0 && map.containsKey(i / 5)) {
                    map.put(i, map.get(i / 5) * 5);
                    continue;
                }
            }
            return map.get(index).intValue();
        }
    }

70. 爬楼梯

https://leetcode-cn.com/problems/climbing-stairs/

思路

我们用 f(x) 表示爬到第 x 级台阶的方案数,考虑最后一步可能跨了一级台阶,也可能跨了两级台阶,所以我们可以列出如下式子:
f(x) = f(x - 1) + f(x - 2)

它意味着爬到第 xx 级台阶的方案数是爬到第 x - 1 级台阶的方案数和爬到第 x - 2 级台阶的方案数的和。很好理解,因为每次只能爬 1级或 2级,所以 f(x) 只能从 f(x - 1) 和 f(x - 2) 转移过来,而这里要统计方案总数,我们就需要对这两项的贡献求和。

   class Solution {
        public int climbStairs(int n) {
            if (n == 0 || n == 1 || n == 2) {
                return n;
            }
            int[] dp = new int[n + 1];
            dp[1] = 1;
            dp[2] = 2;
            for (int i = 3; i <= n; i++) {
                dp[i] = dp[i - 1] + dp[i - 2];
            }
            return dp[n];
        }
    }
  • 时间:O(n)
  • 空间:O(n) 这里其实可以搞为O(1)的, 因为只保存上一步和上两步的方法数即可。

120. 三角形最小路径和

https://leetcode-cn.com/problems/triangle/

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        /**
         先下向上进行状态递推。
         对应的数值表示:到达该点的最小路径和
         从 倒数第二行 开始,因为倒数第一行就是它本身
         */
        for (int i = triangle.size() - 2; i >= 0; i--) {
            for (int j = 0; j < triangle.get(i).size(); j++) {
                Integer current = triangle.get(i).get(j);
                // 正下方
                Integer xiaFang = triangle.get(i + 1).get(j);
                // 斜下方
                Integer xieXiafang = triangle.get(i + 1).get(j + 1);
                // 状态转移方程
                current = current + Math.min(xieXiafang, xiaFang);

                List<Integer> tmp = triangle.get(i);
                tmp.set(j, current);
                triangle.set(i, tmp);
            }
        }
        return triangle.get(0).get(0);
    }
}
  • 时间:O(行*列)
  • 空间:(O(1))

152. 乘积最大子数组

https://leetcode-cn.com/problems/maximum-product-subarray/
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

子数组 是数组的连续子序列

思路:

dp[i] 表示从0到i最大子数组乘积,状态方程是
dp[i] = dp[i-1]*nums[i]
但是这里需要注意可能存在 -2,3,3,3,3,-9 的情况,所以还需要一个dp[]状态来保存其当前的最小值。

  class Solution {
        private int threeMax(int a, int b, int c) {
            int tmp = Math.max(a, b);
            return tmp > c ? tmp : c;
        }

        private int threeMin(int a, int b, int c) {
            int tmp = Math.min(a, b);
            return tmp < c ? tmp : c;
        }

        public int maxProduct(int[] nums) {
            if (nums == null) {
                return 0;
            }
            int maxRes = nums[0];
            // 二维数组:因为可能存在 -2,3,3,3,3,-9 的情况
            //正的最大值
            int[] positiveMax = new int[nums.length];
            //负的最大值
            int[] negativeMax = new int[nums.length];
            
            positiveMax[0] = nums[0];
            negativeMax[0] = nums[0];

            for (int i = 1; i < nums.length; i++) {
                int x = positiveMax[i - 1] * nums[i];
                int y = negativeMax[i - 1] * nums[i];
                positiveMax[i] = this.threeMax(x, y, nums[i]);
                negativeMax[i] = this.threeMin(x, y, nums[i]);
                maxRes = positiveMax[i] > maxRes ? positiveMax[i] : maxRes;
            }
            return maxRes;
        }
    }
  • 时间:O(n)
  • 空间:O(n),空间还可优化,见leetcode 题解即可,这里为了不破坏dp的模板,就不展示出来了!

121 股票买卖序列

算法-贪心算法-买卖股票的最佳时机 II

121,122,123,309,188,714 题
暂时只做:
121 和 122 系列即可,因为工作中也应该很久很久不会用到 三维的DP定义,面试自然也是用不啦~

121 从始至终只能买卖一次股票系列

思路:

  1. 对于第 i 个数而言,保存之前的最小值,然后判断当前 i 处卖出,利益是否最大。
  2. dp[i][2] 表示到 i 处所获得的最大利润。
   class Solution {
        public int maxProfit(int[] prices) {
            if (prices == null) {
                return 0;
            }
            // dp[i][2] i 表示到 i 处所获得的最大利润, i的范围是: 0->n
            // 另外两个维度表示:0:持股 1:不持股
            int[][] maxPro = new int[prices.length][2];

            // 持股话利润就是 负的 prices[0]
            maxPro[0][0] = -prices[0];
            // 不持股的话利润就是 0
            maxPro[0][1] = 0;

            int maxRes = 0;
            for (int i = 1; i < prices.length; i++) {
                // 求今天 需持股时的最大利润(两种选择:买入 或 等待)
                // 坑点:只允许交易一次,所以 手上的现金数就是当天的股价的相反数)。
                maxPro[i][0] = Math.max(-prices[i], maxPro[i - 1][0]);
                // 求今天 不持股时的最大利润(两种选择:卖出 或 等待)
                maxPro[i][1] = Math.max(maxPro[i - 1][0] + prices[i], maxPro[i - 1][1]);
                maxRes = maxPro[i][0] > maxPro[i][1] ? maxPro[i][0] : maxPro[i][1];
            }
            return maxRes;
            //直接写为这样也可以~~~
            return maxPro[prices.length - 1][1];
        }
    }

//因为这个是个简单问题,所以有一个简单解法如下:
public class Solution {
    public int maxProfit(int prices[]) {
        int minprice = Integer.MAX_VALUE;
        int maxprofit = 0;
        for (int i = 0; i < prices.length; i++) {
            if (prices[i] < minprice) {
                minprice = prices[i];
            } else if (prices[i] - minprice > maxprofit) {
                maxprofit = prices[i] - minprice;
            }
        }
        return maxprofit;
    }
}
- 时间:O(n)
- 空间:O(1)
  • 时间:O(n)
  • 空间:O(n)

122 可同天,可以交易无数次的情况

思路与121相同,只是121坑点那里不同而已。

 // 坑点:只允许交易一次,所以 手上的现金数就是当天的股价的相反数)。
 maxPro[i][0] = Math.max(-prices[i], maxPro[i - 1][0]);
 换为:
 maxPro[i][0] = Math.max(maxPro[i - 1][1] - prices[i], maxPro[i - 1][0]);
 即可!!!

300. 最长递增子序列

https://leetcode-cn.com/problems/longest-increasing-subsequence/

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

思路1:动态规划

  1. 在这里插入图片描述定义 dp[i] 为考虑前 i 个元素,以第 i个数字结尾的最长上升子序列的长度
class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int[] dp = new int[nums.length];
        // 初始化应该都是 1
        int res = 1;
        dp[0] = 1;
        for (int i = 1; i < nums.length; i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[j] + 1, dp[i]);
                }
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}
  • 时间:O(n^2)
  • 空间:O(n)

思路2

class Solution {
    int search(int[] res, int left, int right, int target) {
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (res[mid] == target) {
                //找到真正的想等值,那就直接返回。否则就返回 left 的值
                return mid;
            } else if (res[mid] < target) {
                left = mid + 1;
            } else if (res[mid] > target) {
                right = mid - 1;
            }
        }
        return left;
    }

    public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        // 最大的值永远在:最后一个数字
        int[] res = new int[nums.length];
        // 两个初始化 还蛮重要的 !
        res[0] = nums[0];
        int len = 0;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > res[len]) {
                len++;
                res[len] = nums[i];
            } else {
                //std::lower_bound() 的实现
                int index = search(res, 0, len, nums[i]);
                res[index] = nums[i];
            }
        }
        return len + 1;
    }
}
  • 时间:O(nlogn)
  • 空间:O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值