2218.从栈中取出K个硬币的最大面值和

目标

一张桌子上总共有 n 个硬币 栈 。每个栈有 正整数 个带面值的硬币。

每一次操作中,你可以从任意一个栈的 顶部 取出 1 个硬币,从栈中移除它,并放入你的钱包里。

给你一个列表 piles ,其中 piles[i] 是一个整数数组,分别表示第 i 个栈里 从顶到底 的硬币面值。同时给你一个正整数 k ,请你返回在 恰好 进行 k 次操作的前提下,你钱包里硬币面值之和 最大为多少 。

示例 1:

输入:piles = [[1,100,3],[7,8,9]], k = 2
输出:101
解释:
上图展示了几种选择 k 个硬币的不同方法。
我们可以得到的最大面值为 101 。

示例 2:

输入:piles = [[100],[100],[100],[100],[100],[100],[1,1,1,1,1,1,700]], k = 7
输出:706
解释:
如果我们所有硬币都从最后一个栈中取,可以得到最大面值和。

说明:

  • n == piles.length
  • 1 <= n <= 1000
  • 1 <= piles[i][j] <= 10^5
  • 1 <= k <= sum(piles[i].length) <= 2000

思路

有 n 个栈,栈中有 piles[i].length 个硬币,每次操作可以从任意栈顶取一个硬币,求 k 次操作取得的硬币和的最大值。

定义 dp[i][j] 表示 从前 i + 1 个栈中最多取 j 个 所能得到的最大值,与 0 - 1 背包不同的是我们需要枚举从当前栈取 1 至 x 枚硬币的最大值,状态转移方程为 dp[i][j] = Math.max(dp[i][j], Math.max(dp[i - 1][j], dp[i - 1][j - x] + prefix[i][x])),外层 max 求的是当前栈每种取法的最大值,第二个参数的 max 求的是当前栈 不取 或者 取 x 枚硬币对应的最大值。由于是从栈顶取,我们使用前缀和 prefix 记录每个栈从栈顶到底的硬币和。

代码


/**
 * @date 2025-01-21 13:46
 */
public class MaxValueOfCoins2218 {

    public int maxValueOfCoins(List<List<Integer>> piles, int k) {
        int n = piles.size();
        int[][] prefix = new int[n][];
        // 计算前缀和
        for (int i = 0; i < n; i++) {
            List<Integer> pile = piles.get(i);
            prefix[i] = new int[pile.size() + 1];
            for (int j = 1; j <= pile.size(); j++) {
                prefix[i][j] = prefix[i][j - 1] + pile.get(j - 1);
            }
        }
        // 定义dp[i][j] 表示 从前 i + 1 个栈中最多取 j 个 所能得到的最大值
        int[][] dp = new int[n][k + 1];
        // 初始化,从第一个栈最多取 k 个
        for (int j = 1; j <= k; j++) {
            int length = piles.get(0).size();
            if (j <= length) {
                dp[0][j] = prefix[0][j];
            } else {
                dp[0][j] = dp[0][length];
            }
        }
        for (int i = 1; i < n; i++) {
            // 枚举右边界
            int length = piles.get(i).size();
            for (int j = 1; j <= k; j++) {
                // j 表示从前 i + 1 个栈中总共取 j 个
                for (int x = 1; x <= length && j >= x; x++) {
                    // 表示从当前栈中取 x 个
                    dp[i][j] = Math.max(dp[i][j], Math.max(dp[i - 1][j], dp[i - 1][j - x] + prefix[i][x]));
                }
            }
        }
        return dp[n - 1][k];
    }

}

性能

638.大礼包

目标

在 LeetCode 商店中, 有 n 件在售的物品。每件物品都有对应的价格。然而,也有一些大礼包,每个大礼包以优惠的价格捆绑销售一组物品。

给你一个整数数组 price 表示物品价格,其中 price[i] 是第 i 件物品的价格。另有一个整数数组 needs 表示购物清单,其中 needs[i] 是需要购买第 i 件物品的数量。

还有一个数组 special 表示大礼包,special[i] 的长度为 n + 1 ,其中 special[i][j] 表示第 i 个大礼包中内含第 j 件物品的数量,且 special[i][n] (也就是数组中的最后一个整数)为第 i 个大礼包的价格。

返回 确切 满足购物清单所需花费的最低价格,你可以充分利用大礼包的优惠活动。你不能购买超出购物清单指定数量的物品,即使那样会降低整体价格。任意大礼包可无限次购买。

示例 1:

输入:price = [2,5], special = [[3,0,5],[1,2,10]], needs = [3,2]
输出:14
解释:有 A 和 B 两种物品,价格分别为 ¥2 和 ¥5 。 
大礼包 1 ,你可以以 ¥5 的价格购买 3A 和 0B 。 
大礼包 2 ,你可以以 ¥10 的价格购买 1A 和 2B 。 
需要购买 3 个 A 和 2 个 B , 所以付 ¥10 购买 1A 和 2B(大礼包 2),以及 ¥4 购买 2A 。

示例 2:

输入:price = [2,3,4], special = [[1,1,0,4],[2,2,1,9]], needs = [1,2,1]
输出:11
解释:A ,B ,C 的价格分别为 ¥2 ,¥3 ,¥4 。
可以用 ¥4 购买 1A 和 1B ,也可以用 ¥9 购买 2A ,2B 和 1C 。 
需要买 1A ,2B 和 1C ,所以付 ¥4 买 1A 和 1B(大礼包 1),以及 ¥3 购买 1B , ¥4 购买 1C 。 
不可以购买超出待购清单的物品,尽管购买大礼包 2 更加便宜。

说明:

  • n == price.length == needs.length
  • 1 <= n <= 6
  • 0 <= price[i], needs[i] <= 10
  • 1 <= special.length <= 100
  • special[i].length == n + 1
  • 0 <= special[i][j] <= 50
  • 生成的输入对于 0 <= j <= n - 1 至少有一个 special[i][j] 非零。

思路

有一个购物清单 needneed[i] 表示需要购买商品 i 的数量,price[i] 表示商品 i 的单价,此外还有一组大礼包 specialspecial[j][i] 表示大礼包 j 中包含的第 i 件商品的数量,并且 specal[j][n] 表示该大礼包的价格。求购买 need 清单中的商品最少花费多少钱,我们可以购买大礼包任意次,但是购买的总数量不能超过需求的数量,尽管可能价格更低。

完全背包问题是物品有无限个,背包容量有限,求能装下的最大价值/最小价值。如果将题目中的清单视为多个背包容量,单买物品 i,以及购买大礼包 j 中的商品 i 视为不同的商品,那么我们求的是装满所有背包的最小价值。问题在于,大礼包不光有商品 i,还有其它商品,如何处理?

网友题解将单买也看成大礼包,只不过其它商品数量为 0,这样可以统一处理大礼包。

// todo

代码

性能

2028.找出缺失的观测数据

目标

现有一份 n + m 次投掷单个 六面 骰子的观测数据,骰子的每个面从 1 到 6 编号。观测数据中缺失了 n 份,你手上只拿到剩余 m 次投掷的数据。幸好你有之前计算过的这 n + m 次投掷数据的 平均值 。

给你一个长度为 m 的整数数组 rolls ,其中 rolls[i] 是第 i 次观测的值。同时给你两个整数 mean 和 n 。

返回一个长度为 n 的数组,包含所有缺失的观测数据,且满足这 n + m 次投掷的 平均值 是 mean 。如果存在多组符合要求的答案,只需要返回其中任意一组即可。如果不存在答案,返回一个空数组。

k 个数字的 平均值 为这些数字求和后再除以 k 。

注意 mean 是一个整数,所以 n + m 次投掷的总和需要被 n + m 整除。

示例 1:

输入:rolls = [3,2,4,3], mean = 4, n = 2
输出:[6,6]
解释:所有 n + m 次投掷的平均值是 (3 + 2 + 4 + 3 + 6 + 6) / 6 = 4 。

示例 2:

输入:rolls = [1,5,6], mean = 3, n = 4
输出:[2,3,2,2]
解释:所有 n + m 次投掷的平均值是 (1 + 5 + 6 + 2 + 3 + 2 + 2) / 7 = 3 。

示例 3:

输入:rolls = [1,2,3,4], mean = 6, n = 4
输出:[]
解释:无论丢失的 4 次数据是什么,平均值都不可能是 6 。

示例 4:

输入:rolls = [1], mean = 3, n = 1
输出:[5]
解释:所有 n + m 次投掷的平均值是 (1 + 5) / 2 = 3 。

说明:

  • m == rolls.length
  • 1 <= n, m <= 10^5
  • 1 <= rolls[i], mean <= 6

思路

已知 m+n 个投骰子的观测数据的均值 mean,以及其中 m 个观察数据 rolls,返回缺失的观测数据,如果存在多个只返回其中一组,如果不存在答案返回空数组。

我们可以很容易计算出观测数据的总和 mean * (m + n),用它减去已知的观测数据和 sum,得到 diff

  • 如果 diff > n * 6 说明 剩余的 n 次都得到 6 点也不够,返回空数组。
  • 如果 diff < n 说明 剩余的 n 次都得到 1 点也多余,返回空数组。
  • 否则,问题变成选 n 个数字使其和等于 diff,每个数的取值范围是 1 ~ 6

这让我想起了背包问题还有之前做过的硬币找零,组合总和等问题。这里只需要返回一种可能就行了,不需要动态规划。

可以先计算 val = diff / n,如果有剩余 r,就为 r 个值加1。

代码

/**
 * @date 2024-05-27 9:20
 */
public class MissingRolls2028 {

    public int[] missingRolls(int[] rolls, int mean, int n) {
        int m = rolls.length;
        int sum = 0;
        for (int roll : rolls) {
            sum += roll;
        }
        int diff = mean * (m + n) - sum;
        if (diff > n * 6 || diff < n) {
            return new int[0];
        }
        int[] res = new int[n];
        int val = diff / n;
        diff = diff - val * n;
        Arrays.fill(res, val);
        for (int i = 0; i < diff; i++) {
            res[i]++;
        }
        return res;
    }
}

性能

39.组合总和

目标

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1
输出: []

提示:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • candidates 的所有元素 互不相同
  • 1 <= target <= 40

思路

一看到这道题就想到要用动态规划,但是昨天看了回溯算法的视频,所以就试图使用dfs去写。

先从target开始,循环减去可选数字,然后递归。想法是好的,但是这种集合嵌套集合的操作一会就把我搞晕了,向下传递什么,返回什么?有机会再想想吧。

还是用动态规划吧,难点在于去重。刚开始甚至写了hash函数,但是它不能处理2, 5(2 3)4(2 2), 3的情况,dp[2] + dp[5] 与 dp[4] + dp[3] 得到的组合是相同的 [2, 2, 3]

这让我想到了518.零钱兑换II,这两道题本质是一样的。那个只让返回组合数,这个需要返回具体的组合。

去重的精髓就在于不能提前初始化dp,只能在第一次访问到候选值的时候初始化。

代码

/**
 * @date 2024-04-20 10:20
 */
public class CombinationSum39 {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>>[] dp = new List[target + 1];
        for (int i = 0; i <= target; i++) {
            dp[i] = new ArrayList<>();
        }
        for (int candidate : candidates) {
            if (candidate <= target) {
                List<Integer> list = new ArrayList<>();
                list.add(candidate);
                dp[candidate].add(list);
            }
            for (int i = candidate; i <= target; i++) {
                for (List<Integer> lj : dp[i - candidate]) {
                    List<Integer> tmp = new ArrayList<>();
                    tmp.add(candidate);
                    tmp.addAll(lj);
                    dp[i].add(tmp);
                }
            }
        }
        return dp[target];
    }
}

性能

518.零钱兑换II

目标

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

示例 1:

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:

输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。

示例 3:

输入:amount = 10, coins = [10] 
输出:1

说明:

  • 1 <= coins.length <= 300
  • 1 <= coins[i] <= 5000
  • coins 中的所有值 互不相同
  • 0 <= amount <= 5000

思路

// todo

代码

/**
 * @date 2024-03-25 8:31
 */
public class CoinChange {

    public int change(int amount, int[] coins) {
        int[] dp = new int[amount + 1];
        dp[0] = 1;
        for (int coin : coins) {
            for (int j = coin; j <= amount; j++) {
                dp[j] += dp[j - coin];
            }
        }
        return dp[amount];
    }

    public static void main(String[] args) {
        CoinChange main = new CoinChange();
//        int amount = 5;
        int amount = 500;
        int[] coins = new int[]{1, 2, 5};
//        System.out.println(main.change(amount, coins));
//        System.out.println(main.change(amount, coins));
        System.out.println(main.change(amount, coins));
    }
}

性能

// todo

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

说明:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4

思路

// todo

思考的方向错了,试图用贪心算法枚举可能的组合。做法是优先选面值最大的,取得余数,再计算下一个面值的余数,直到余数为0。但是这样得到的不一定是最优解。尝试将最大面值的个数减一,然后余数加上最大面值,重新计算。但是还是一样的,如果要调整的话,所有面值的个数都要调整,不能只调整最大的,后面的还用贪心,这样问题就不可解了。

代码

/**
 * @date 2024-03-24 0:04
 */
public class CoinChange {

    public int coinChange(int[] coins, int amount) {
        int max = amount + 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];
    }

    public static void main(String[] args) {
        CoinChange main = new CoinChange();
//        int[] coins = new int[]{3, 7};
        int[] coins = new int[]{186, 419, 83, 408};
//        System.out.println(main.coinChange(coins, 9));
        System.out.println(main.coinChange(coins, 6249));
    }
}

性能

// todo