3243.新增道路查询后的最短距离I

目标

给你一个整数 n 和一个二维整数数组 queries。

有 n 个城市,编号从 0 到 n - 1。初始时,每个城市 i 都有一条单向道路通往城市 i + 1( 0 <= i < n - 1)。

queries[i] = [ui, vi] 表示新建一条从城市 ui 到城市 vi 的单向道路。每次查询后,你需要找到从城市 0 到城市 n - 1 的最短路径的长度。

返回一个数组 answer,对于范围 [0, queries.length - 1] 中的每个 i,answer[i] 是处理完前 i + 1 个查询后,从城市 0 到城市 n - 1 的最短路径的长度。

示例 1:

输入: n = 5, queries = [[2, 4], [0, 2], [0, 4]]
输出: [3, 2, 1]
解释:
新增一条从 2 到 4 的道路后,从 0 到 4 的最短路径长度为 3。
新增一条从 0 到 2 的道路后,从 0 到 4 的最短路径长度为 2。
新增一条从 0 到 4 的道路后,从 0 到 4 的最短路径长度为 1。

示例 2:

输入: n = 4, queries = [[0, 3], [0, 2]]
输出: [1, 1]
解释:
新增一条从 0 到 3 的道路后,从 0 到 3 的最短路径长度为 1。
新增一条从 0 到 2 的道路后,从 0 到 3 的最短路径长度仍为 1。

说明:

  • 3 <= n <= 500
  • 1 <= queries.length <= 500
  • queries[i].length == 2
  • 0 <= queries[i][0] < queries[i][1] < n
  • 1 < queries[i][1] - queries[i][0]
  • 查询中没有重复的道路。

思路

n 个城市,刚开始每一个城市 i 有一条单项道路通向城市 i + 1,有一个二维数组 queriesqueries[i] 表示增加一条从 queries[i][0]queries[i][1] 的单项道路,返回 answer 数组,answer[i] 表示增加了 queries[i] 之后从城市 0 到城市 n - 1 的最短路径。

建图,每次增加路径之后使用BFS计算最短路径。注意过滤重复元素,否则过不了。

也可以使用动态规划来做。

定义 dp[i] 表示从城市 i 到城市 n - 1 的最短路径,每添加一条路径,受影响的只有包括起点在内前面的城市。当增加一条道路 [l, r],状态转移方程为 dp[l] = Math.min(dp[r] + 1, dp[l]),表示从 r 到达终点加一步 与 原来值的最小值。我们需要同步更新 l 之前的 dp 值。我们可以倒序遍历 [0, l)dp[j] = Math.min(dp[j], dp[j + 1] + 1) 取其自身与 后面元素 dp 值加一的较小值,同时还要考虑区间内有前面查询新增的道路,比如前面有以 j 为起点的查询,还要再取一个较小值 Math.min(dp[j], dp[end] + 1)end 表示之前增加的道路的终点。

代码


/**
 * @date 2024-11-19 0:47
 */
public class ShortestDistanceAfterQueries3243 {

    public int[] shortestDistanceAfterQueries_v2(int n, int[][] queries) {
        int[] dp = new int[n];
        int ql = queries.length;
        int[] res = new int[ql];
        for (int i = 0; i < n; i++) {
            dp[i] = n - i - 1;
        }
        Map<Integer, List<Integer>> map = new HashMap<>(ql);
        for (int i = 0; i < ql; i++) {
            int l = queries[i][0];
            int r = queries[i][1];
            dp[l] = Math.min(dp[r] + 1, dp[l]);
            for (int j = l - 1; j >= 0; j--) {
                dp[j] = Math.min(dp[j], dp[j + 1] + 1);
                if (map.containsKey(j)) {
                    for (Integer end : map.get(j)) {
                        dp[j] = Math.min(dp[j], dp[end] + 1);
                    }
                }
            }
            res[i] = dp[0];
            map.putIfAbsent(l, new ArrayList<>());
            map.get(l).add(r);
        }
        return res;
    }

    public int[] shortestDistanceAfterQueries_v1(int n, int[][] queries) {
        List<Integer>[] g = new ArrayList[n];
        for (int i = 0; i < g.length; i++) {
            g[i] = new ArrayList<>();
            g[i].add(i + 1);
        }
        int ql = queries.length;
        int[] res = new int[ql];
        for (int i = 0; i < ql; i++) {
            g[queries[i][0]].add(queries[i][1]);
            res[i] = bfs_v1(g);
        }
        return res;
    }

    public int bfs_v1(List<Integer>[] g) {
        int n = g.length;
        List<Integer> list = new ArrayList<>();
        boolean[] visited = new boolean[n];
        list.add(0);
        for (int res = 1; ; res++) {
            List<Integer> tmp = list;
            int size = tmp.size();
            list = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                Integer cur = tmp.get(i);
                for (Integer next : g[cur]) {
                    if (next == n - 1) {
                        return res;
                    }
                    if (!visited[next]) {
                        visited[next] = true;
                        list.add(next);
                    }
                }
            }
        }
    }

    public int[] shortestDistanceAfterQueries(int n, int[][] queries) {
        List<Integer>[] g = new ArrayList[n];
        for (int i = 0; i < g.length; i++) {
            g[i] = new ArrayList<>();
            g[i].add(i + 1);
        }
        int ql = queries.length;
        int[] res = new int[ql];
        for (int i = 0; i < ql; i++) {
            g[queries[i][0]].add(queries[i][1]);
            res[i] = bfs(g);
        }
        return res;
    }

    public int bfs(List<Integer>[] g) {
        int res = 0, n = g.length;
        Queue<Integer> q = new ArrayDeque<>();
        q.offer(0);
        here:
        while (!q.isEmpty()) {
            int size = q.size();
            Set<Integer> set = new HashSet<>();
            for (int i = 0; i < size; i++) {
                int cur = q.poll();
                if (cur == n - 1) {
                    break here;
                }
                set.addAll(g[cur]);
            }
            q.addAll(set);
            res++;
        }
        return res;
    }

}

性能

1547.切棍子的最小成本

目标

有一根长度为 n 个单位的木棍,棍上从 0 到 n 标记了若干位置。例如,长度为 6 的棍子可以标记如下:

给你一个整数数组 cuts ,其中 cuts[i] 表示你需要将棍子切开的位置。

你可以按顺序完成切割,也可以根据需要更改切割的顺序。

每次切割的成本都是当前要切割的棍子的长度,切棍子的总成本是历次切割成本的总和。对棍子进行切割将会把一根木棍分成两根较小的木棍(这两根木棍的长度和就是切割前木棍的长度)。请参阅第一个示例以获得更直观的解释。

返回切棍子的 最小总成本 。

示例 1:

输入:n = 7, cuts = [1,3,4,5]
输出:16
解释:按 [1, 3, 4, 5] 的顺序切割的情况如下所示:
第一次切割长度为 7 的棍子,成本为 7 。第二次切割长度为 6 的棍子(即第一次切割得到的第二根棍子),第三次切割为长度 4 的棍子,最后切割长度为 3 的棍子。总成本为 7 + 6 + 4 + 3 = 20 。
而将切割顺序重新排列为 [3, 5, 1, 4] 后,总成本 = 16(如示例图中 7 + 4 + 3 + 2 = 16)。

示例 2:

输入:n = 9, cuts = [5,6,1,4,2]
输出:22
解释:如果按给定的顺序切割,则总成本为 25 。总成本 <= 25 的切割顺序很多,例如,[4, 6, 5, 2, 1] 的总成本 = 22,是所有可能方案中成本最小的。

说明:

  • 2 <= n <= 10^6
  • 1 <= cuts.length <= min(n - 1, 100)
  • 1 <= cuts[i] <= n - 1
  • cuts 数组中的所有整数都 互不相同

提示:

  • Build a dp array where dp[i][j] is the minimum cost to achieve all the cuts between i and j.
  • When you try to get the minimum cost between i and j, try all possible cuts k between them, dp[i][j] = min(dp[i][k] + dp[k][j]) + (j - i) for all possible cuts k between them.

思路

有一个长度为 n 的木棍,刻度从 0 ~ n,有一个整数数组 cutscuts[i] 表示需要在刻度 i 处进行切割,切割的成本为该刻度所在棍子的长度,求切割棍子的最小成本。

许多算法书上引入动态规划经常举的一个例子是钢条切割问题。已知特定长度钢条的价值,问怎样切可以使价值最大。而本题是给出必须要切的点,问按照什么顺序切成本最小。

定义 dp[i][j] 表示完成 (i, j) 之间所有切割点所需要的最小成本,dp[0][n] 就是答案。状态转移方程为 dp[i][j] = min(dp[i][k] + dp[k][j]) + j - i)。根据定义 dp 数组应该初始化为 0,因为无法切割时成本为 0。

但是由于切割点的范围太大 2 ~ 10^6,如果直接定义的话会超出内存限制。可以先将切割点排序,定义 ij 为切点的下标,切点个数 m 最大为 100,时间复杂度为 O(m^3)

考虑到切点本身不包含木棍的两个端点 0n,我们定义端点 endpoint 数组,将这两个端点加进来,dp[0][m - 1] 即为所求。状态转移方程为 dp[i][j] = min(dp[i][k] + dp[k][j]) + endpoint[j] - endpoint[i]

特别需要注意的是 dp 数组的遍历的顺序。当我们计算 dp[i][j] 时需要已经计算出 dp[k][j],枚举起点 i 应该倒序,因为 k > i,同理还需要计算出 dp[i][k],枚举终点 j 应该正序,因为 k > y。枚举 k 正序倒序都可以。

枚举 i,j 的先后顺序也是可以交换的。

代码


/**
 * @date 2024-11-11 10:07
 */
public class MinCost1547 {

    public int minCost(int n, int[] cuts) {
        Arrays.sort(cuts);
        int m = cuts.length;
        int[] endpoint = new int[m + 2];
        System.arraycopy(cuts, 0, endpoint, 1, m);
        endpoint[m + 1] = n;

        m = endpoint.length;
        int[][] dp = new int[m][m];

        for (int i = m - 3; i >= 0; i--) {
            for (int j = i + 2; j < m; j++) {
                int min = Integer.MAX_VALUE;
                for (int k = i + 1; k < j; k++) {
                    min = Math.min(dp[i][k] + dp[k][j], min);
                }
                dp[i][j] = min + endpoint[j] - endpoint[i];
            }
        }
        return dp[0][m - 1];
    }

}

性能

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

代码

性能

3259.超级饮料的最大强化能量

目标

来自未来的体育科学家给你两个整数数组 energyDrinkA 和 energyDrinkB,数组长度都等于 n。这两个数组分别代表 A、B 两种不同能量饮料每小时所能提供的强化能量。

你需要每小时饮用一种能量饮料来 最大化 你的总强化能量。然而,如果从一种能量饮料切换到另一种,你需要等待一小时来梳理身体的能量体系(在那个小时里你将不会获得任何强化能量)。

返回在接下来的 n 小时内你能获得的 最大 总强化能量。

注意 你可以选择从饮用任意一种能量饮料开始。

示例 1:

输入:energyDrinkA = [1,3,1], energyDrinkB = [3,1,1]
输出:5
解释:
要想获得 5 点强化能量,需要选择只饮用能量饮料 A(或者只饮用 B)。

示例 2:

输入:energyDrinkA = [4,1,1], energyDrinkB = [1,1,3]
输出:7
解释:
第一个小时饮用能量饮料 A。
切换到能量饮料 B ,在第二个小时无法获得强化能量。
第三个小时饮用能量饮料 B ,并获得强化能量。

说明:

  • n == energyDrinkA.length == energyDrinkB.length
  • 3 <= n <= 10^5
  • 1 <= energyDrinkA[i], energyDrinkB[i] <= 10^5

思路

energyDrinkAenergyDrinkB 两个数组,表示饮料 AB 在第 i 小时可以提供的能量,现在需要每一小时饮用饮料 AB 来获取能量,可以暂停一小时来切换饮料,求能够获得的最大能量。

定义 dp[i][j] 表示第 i 小时选择饮料 j 所能获取的最大能量,假设 j = 0 表示饮料 Aj = 1 表示饮料 B,那么状态转移方程为 dp[i][0] = Math.max(dp[i - 1][1], dp[i - 1][0] + energyDrinkA)dp[i][1] = Math.max(dp[i - 1][0], dp[i - 1][1] + energyDrinkB),最终返回 Math.max(dp[n - 1][0], dp[n - 1][1])。初始条件 dp[0][0] = energyDrinkA[0] dp[0][1] = energyDrinkA[1]

由于只与前面的状态有关,因此可以进行存储优化,使用两个变量保存前一个小时饮用 A B 的最大能量即可。

代码


/**
 * @date 2024-11-01 0:37
 */
public class MaxEnergyBoost3259 {

    public long maxEnergyBoost_v1(int[] energyDrinkA, int[] energyDrinkB) {
        int n = energyDrinkA.length;
        long prevA = energyDrinkA[0];
        long prevB = energyDrinkB[0];
        for (int i = 1; i < n; i++) {
            long curA = Math.max(prevB,  prevA + energyDrinkA[i]);
            long curB = Math.max(prevA,  prevB + energyDrinkB[i]);
            prevA = curA;
            prevB = curB;
        }
        return Math.max(prevA, prevB);
    }

}

性能

3181.执行操作可获得的最大总奖励II

目标

给你一个整数数组 rewardValues,长度为 n,代表奖励的值。

最初,你的总奖励 x 为 0,所有下标都是 未标记 的。你可以执行以下操作 任意次 :

  • 从区间 [0, n - 1] 中选择一个 未标记 的下标 i。
  • 如果 rewardValues[i] 大于 你当前的总奖励 x,则将 rewardValues[i] 加到 x 上(即 x = x + rewardValues[i]),并 标记 下标 i。

以整数形式返回执行最优操作能够获得的 最大 总奖励。

示例 1:

输入:rewardValues = [1,1,3,3]
输出:4
解释:
依次标记下标 0 和 2,总奖励为 4,这是可获得的最大值。

示例 2:

输入:rewardValues = [1,6,4,3,2]
输出:11
解释:
依次标记下标 0、2 和 1。总奖励为 11,这是可获得的最大值。

说明:

  • 1 <= rewardValues.length <= 5 * 10^4
  • 1 <= rewardValues[i] <= 5 * 10^4

思路

与昨天的题相比,数据范围变大了。

// todo

代码

性能

3180.执行操作可获得的最大总奖励I

目标

给你一个整数数组 rewardValues,长度为 n,代表奖励的值。

最初,你的总奖励 x 为 0,所有下标都是 未标记 的。你可以执行以下操作 任意次 :

  • 从区间 [0, n - 1] 中选择一个 未标记 的下标 i。
  • 如果 rewardValues[i] 大于 你当前的总奖励 x,则将 rewardValues[i] 加到 x 上(即 x = x + rewardValues[i]),并 标记 下标 i。

以整数形式返回执行最优操作能够获得的 最大 总奖励。

示例 1:

输入:rewardValues = [1,1,3,3]
输出:4
解释:
依次标记下标 0 和 2,总奖励为 4,这是可获得的最大值。

示例 2:

输入:rewardValues = [1,6,4,3,2]
输出:11
解释:
依次标记下标 0、2 和 1。总奖励为 11,这是可获得的最大值。

说明:

  • 1 <= rewardValues.length <= 2000
  • 1 <= rewardValues[i] <= 2000

思路

有一个长度为 n 的整数数组 rewardValues 和一个变量 x 初始为 0,可以执行任意次操作,每次操作可以从 [0, n - 1] 中选一个未标记的下标 i,如果 rewardValues[i] > xx += rewardValues[i],并标记 i。求 x 的最大值。

// todo

代码

性能

1884.鸡蛋掉落-两枚鸡蛋

目标

给你 2 枚相同 的鸡蛋,和一栋从第 1 层到第 n 层共有 n 层楼的建筑。

已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都 会碎 ,从 f 楼层或比它低 的楼层落下的鸡蛋都 不会碎 。

每次操作,你可以取一枚 没有碎 的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。

请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?

示例 1:

输入:n = 2
输出:2
解释:我们可以将第一枚鸡蛋从 1 楼扔下,然后将第二枚从 2 楼扔下。
如果第一枚鸡蛋碎了,可知 f = 0;
如果第二枚鸡蛋碎了,但第一枚没碎,可知 f = 1;
否则,当两个鸡蛋都没碎时,可知 f = 2。

示例 2:

输入:n = 100
输出:14
解释:
一种最优的策略是:
- 将第一枚鸡蛋从 9 楼扔下。如果碎了,那么 f 在 0 和 8 之间。将第二枚从 1 楼扔下,然后每扔一次上一层楼,在 8 次内找到 f 。总操作次数 = 1 + 8 = 9 。
- 如果第一枚鸡蛋没有碎,那么再把第一枚鸡蛋从 22 层扔下。如果碎了,那么 f 在 9 和 21 之间。将第二枚鸡蛋从 10 楼扔下,然后每扔一次上一层楼,在 12 次内找到 f 。总操作次数 = 2 + 12 = 14 。
- 如果第一枚鸡蛋没有再次碎掉,则按照类似的方法从 34, 45, 55, 64, 72, 79, 85, 90, 94, 97, 99 和 100 楼分别扔下第一枚鸡蛋。
不管结果如何,最多需要扔 14 次来确定 f 。

说明:

  • 1 <= n <= 1000

思路

有一个 1 ~ n 层楼的建筑,存在一个楼层 f,任何大于 f 层落下的鸡蛋都会摔碎。现在有两个鸡蛋,每次操作可以从任意楼层向下扔鸡蛋,如果鸡蛋碎了则无法再使用,求确定 f 值的最小操作次数。

为了确保能够找到 f,如果第一个尝试的鸡蛋碎了,那么另一个鸡蛋只能从已知的安全楼层一层一层向上尝试。

观察示例2,可以从 n 开始 减 1 2 3 …… i 直到小于等于零,返回 i - 1即可。

看了题解,这样做可行的逻辑是这样的:

假设已知最小操作次数 k,我们扔第一枚鸡蛋选第几层?显然,应该选第 k 层,因为如果第一枚鸡蛋碎了,只需要从 1 ~ k - 1 枚举即可。

如果第一枚鸡蛋没碎,那么下一次选第几层?现在还剩下 k - 1 次尝试,所以应该选 k + 1 + (k - 2) = k + (k - 1) 层,因为如果在该层扔鸡蛋碎了,只需从 k + 1 ~ k + k - 2 枚举即可,共 k - 2 次,再加上前面尝试的 2 次,总次数为 k

以此类推,我们可以确定总层数 n = k + (k - 1) + (k - 2) + …… + 2 + 1 = k * (k + 1)/2,解方程得 k = (sqrt(1+8*n) - 1)/2,结果需要向上取整。

代码


/**
 * @date 2024-10-13 19:30
 */
public class TwoEggDrop1884 {

    public int twoEggDrop_v1(int n) {
        return (int) Math.ceil((Math.sqrt(1 + 8 * n) - 1) / 2);
    }

    public int twoEggDrop(int n) {
        int i = 1;
        while (n > 0){
            n -= i++;
        }
        return i - 1;
    }
}

性能

1928.规定时间内到达终点的最小花费

目标

一个国家有 n 个城市,城市编号为 0 到 n - 1 ,题目保证 所有城市 都由双向道路 连接在一起 。道路由二维整数数组 edges 表示,其中 edges[i] = [xi, yi, timei] 表示城市 xi 和 yi 之间有一条双向道路,耗费时间为 timei 分钟。两个城市之间可能会有多条耗费时间不同的道路,但是不会有道路两头连接着同一座城市。

每次经过一个城市时,你需要付通行费。通行费用一个长度为 n 且下标从 0 开始的整数数组 passingFees 表示,其中 passingFees[j] 是你经过城市 j 需要支付的费用。

一开始,你在城市 0 ,你想要在 maxTime 分钟以内 (包含 maxTime 分钟)到达城市 n - 1 。旅行的 费用 为你经过的所有城市 通行费之和 (包括 起点和终点城市的通行费)。

给你 maxTime,edges 和 passingFees ,请你返回完成旅行的 最小费用 ,如果无法在 maxTime 分钟以内完成旅行,请你返回 -1 。

示例 1:

输入:maxTime = 30, edges = [[0,1,10],[1,2,10],[2,5,10],[0,3,1],[3,4,10],[4,5,15]], passingFees = [5,1,2,20,20,3]
输出:11
解释:最优路径为 0 -> 1 -> 2 -> 5 ,总共需要耗费 30 分钟,需要支付 11 的通行费。

示例 2:

输入:maxTime = 29, edges = [[0,1,10],[1,2,10],[2,5,10],[0,3,1],[3,4,10],[4,5,15]], passingFees = [5,1,2,20,20,3]
输出:48
解释:最优路径为 0 -> 3 -> 4 -> 5 ,总共需要耗费 26 分钟,需要支付 48 的通行费。
你不能选择路径 0 -> 1 -> 2 -> 5 ,因为这条路径耗费的时间太长。

示例 3:

输入:maxTime = 25, edges = [[0,1,10],[1,2,10],[2,5,10],[0,3,1],[3,4,10],[4,5,15]], passingFees = [5,1,2,20,20,3]
输出:-1
解释:无法在 25 分钟以内从城市 0 到达城市 5 。

说明:

  • 1 <= maxTime <= 1000
  • n == passingFees.length
  • 2 <= n <= 1000
  • n - 1 <= edges.length <= 1000
  • 0 <= xi, yi <= n - 1
  • 1 <= timei <= 1000
  • 1 <= passingFees[j] <= 1000
  • 图中两个节点之间可能有多条路径。
  • 图中不含有自环。

思路

// todo

代码

性能

983.最低票价

目标

在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 days 的数组给出。每一项是一个从 1 到 365 的整数。

火车票有 三种不同的销售方式 :

  • 一张 为期一天 的通行证售价为 costs[0] 美元;
  • 一张 为期七天 的通行证售价为 costs[1] 美元;
  • 一张 为期三十天 的通行证售价为 costs[2] 美元。

通行证允许数天无限制的旅行。 例如,如果我们在第 2 天获得一张 为期 7 天 的通行证,那么我们可以连着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。

返回 你想要完成在给定的列表 days 中列出的每一天的旅行所需要的最低消费 。

示例 1:

输入:days = [1,4,6,7,8,20], costs = [2,7,15]
输出:11
解释: 
例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划:
在第 1 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 1 天生效。
在第 3 天,你花了 costs[1] = $7 买了一张为期 7 天的通行证,它将在第 3, 4, ..., 9 天生效。
在第 20 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 20 天生效。
你总共花了 $11,并完成了你计划的每一天旅行。

示例 2:

输入:days = [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15]
输出:17
解释:
例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划: 
在第 1 天,你花了 costs[2] = $15 买了一张为期 30 天的通行证,它将在第 1, 2, ..., 30 天生效。
在第 31 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 31 天生效。 
你总共花了 $17,并完成了你计划的每一天旅行。

说明:

  • 1 <= days.length <= 365
  • 1 <= days[i] <= 365
  • days 按顺序严格递增
  • costs.length == 3
  • 1 <= costs[i] <= 1000

思路

有一个严格递增的出行计划表 daysdays[i] 表示计划在这一天出行。出行需要持有通行证,通行证有三种,1 天有效期,7 天有效期,30 天有效期,价格各不相同。求完成出行计划所需的最低花费。

定义 dp[i] 表示截止到第 i 天的最小花费,初始化数组大小为 days[n - 1] + 1

  • 如果第 i 天需要出行,dp[i] = Math.min(dp[i - 1] + cost[0], dp[i - 7] + cost[1], dp[i - 30] + cost[2])
  • 否则,dp[i] = dp[i - 1]

网友最快题解定义 dp[i] 为旅行了 i 天的最小花费,这样与 days[i] 的数据范围无关,仅与出行天数 days.length 有关。

代码


/**
 * @date 2024-10-01 20:43
 */
public class MincostTickets983 {

    /**
     * 针对 前面方法 的优化
     * 去掉初始化 dp[i] 为截止到第i天 使用一天票的总花费,
     * 使用数组记录是否出行
     */
    public int mincostTickets_v1(int[] days, int[] costs) {
        int n = days.length;
        int end = days[n - 1];
        int[] dp = new int[end + 1];
        boolean[] isTravel = new boolean[end + 1];
        for (int day : days) {
            isTravel[day] = true;
        }
        for (int i = 1; i <= end; i++) {
            int tmp7 = Math.max(0, i - 7);
            int tmp30 = Math.max(0, i - 30);
            if (isTravel[i]) {
                dp[i] = Math.min(dp[i - 1] + costs[0], dp[tmp7] + costs[1]);
                dp[i] = Math.min(dp[i], dp[tmp30] + costs[2]);
            } else {
                dp[i] = dp[i - 1];
            }
        }
        return dp[end];
    }

    public int mincostTickets(int[] days, int[] costs) {
        int n = days.length;
        int end = days[n - 1];
        int[] dp = new int[end + 1];
        int last = 0;
        int index = 0;
        Set<Integer> set = new HashSet<>(n);
        for (int day : days) {
            set.add(day);
            while (index < day) {
                dp[index++] = last;
            }
            dp[day] += last + costs[0];
            last = dp[day];
        }
        for (int i = 1; i <= end; i++) {
            int tmp7 = Math.max(0, i - 7);
            int tmp30 = Math.max(0, i - 30);
            if (!set.contains(i)) {
                dp[i] = dp[i - 1];
            } else {
                dp[i] = Math.min(dp[i - 1] + costs[0], dp[tmp7] + costs[1]);
                dp[i] = Math.min(dp[i], dp[tmp30] + costs[2]);
            }
        }
        return dp[end];
    }

}

性能

  • 去掉 dp[i] 的初始化,刚开始写的是将其初始化为截止到第 i 天使用一天票的总花费
  • 使用数组记录是否出行

1014.最佳观光组合

目标

给你一个正整数数组 values,其中 values[i] 表示第 i 个观光景点的评分,并且两个景点 i 和 j 之间的 距离 为 j - i。

一对景点(i < j)组成的观光组合的得分为 values[i] + values[j] + i - j ,也就是景点的评分之和 减去 它们两者之间的距离。

返回一对观光景点能取得的最高分。

示例 1:

输入:values = [8,1,5,2,6]
输出:11
解释:i = 0, j = 2, values[i] + values[j] + i - j = 8 + 5 + 0 - 2 = 11

示例 2:

输入:values = [1,2]
输出:2

说明:

  • 2 <= values.length <= 5 * 10^4
  • 1 <= values[i] <= 1000

思路

从数组 values 中选两个下标,计算 values[i] + values[j] + i - j 的最大值。

遍历可能组合的复杂度为 O(n^2),暴力求解不可行。很自然地想动态规划,考虑重复子问题是什么?如果是累加 i ~ j 范围内的 value,然后再加上 i - j,由于 value[i] >= 1,当 j 固定的时候,i 应该尽可能的小,因为累加 value[i] 抵消了 i 的减少。但这里并不是累加所有景点的评分,而是选两个景点,然后再考虑它们之间的距离。

注意到,当 j 固定时,评分的最大值即为 value[i] + i 的最大值。当 i 固定时,评分最大值为 values[j] - j 的最大值。但是我们不能直接取这两个最大值相加,需要保证 i 取得最大值时,i < j。枚举右边界,计算之前的最大值。

定义 dp[i] 表示 [0, i] 范围内, value[i] + i 的最大值。那么评分的最大值即为 value[j] - j + dp[j - 1] 的最大值。由于只与 dp[j - 1] 有关,可以进行空间优化,用一个变量保存截止到前一个元素的最大值。

这里面有一个小技巧是将 maxi 放到后面更新,这样就不用维护 i = j - 1 这个指针了。

代码


/**
 * @date 2024-09-23 9:26
 */
public class MaxScoreSightseeingPair1014 {

    public int maxScoreSightseeingPair(int[] values) {
        int n = values.length;
        int maxi = values[0];
        int res = 0;
        for (int j = 1; j < n; j++) {
            res = Math.max(res, values[j] - j + maxi);
            maxi = Math.max(maxi, values[j] + j);
        }
        return res;
    }

}

性能