https://leetcode.cn/problems/minimum-skips-to-arrive-at-meeting-on-time/description/?envType=daily-question&envId=2024-04-11

DP

通过 $n$ 条路跳过休息次数最少,显然最优解中通过 $n - 1$​ 条路的跳过次数也是最少的。走过的路对后面路的选择显然没有影响,因此考虑使用 DP 求解。

令 $T[i][j]$ 为在跳过了 $j$ 次休息后走完 $i$ 条路所花费的时间,有如下两种情况:

$$ \begin{equation} T[i][j]= \begin{cases} \lceil T[i - 1][j] + dist[i - 1] / speed \rceil &\mbox{if not skip} \\ T[i - 1][j - 1] + dist[i - 1] / speed &\mbox{if skip} \end{cases} \end{equation} $$

其中 $\lceil x \rceil$ 为对 $x$ 向上取整。

题目要求要在规定时间内到达,所以 $T[i][j]$ 需要尽可能小,可以写出递推公式如下:

$$ T[i][j] = min \{ \lceil T[i - 1][j] + dist[i - 1] / speed \rceil, T[i - 1][j - 1] + dist[i - 1] / speed \} $$

其中有几个注意点:

  • $j = 0$ 的时候不能进行跳过。第 $i$ 步都没有跳过第 $i - 1$ 步肯定没有跳过。
  • $j = i$ 的时候必须进行跳过。前面每一步都进行跳过的情况下 $j$ 才可能等于 $i$ 。
  • $j > i$ 的情况不存在。每段路程最多进行一次跳过,所以 $j$ 不可能大于 $i$ 。

这里有个坑,根据 IEEE 754 标准,浮点数在计算机中存储的精度是有限的,而本题中我们不可避免的会使用「浮点数运算」以及「向上取整」运算,如果强行忽略产生的计算误差,会得到错误的结果。

举一个简单的例子,假设使用的语言中「向上取整」运算的函数为 $ceil$,下面的运算:

$$ ceil(8.0 + 1.0 / 3 + 1.0 / 3 + 1.0 / 3) $$

结果应当是 $9$ ,而计算机会给出 $10$ 。这是因为浮点数误差导致:

$8.0 + 1.0 / 3 + 1.0 / 3 + 1.0 / 3$ 计算出的结果约为:$9.000000000000002$ ,向上取整后会得到 $10$ ,产生了错误的答案。

因此需要引入常量 $eps$ 表示一个极小值,例如 $10^{-8}$ 。在进行「向上取整」运算前,将待取整的浮点数减去 $eps$​ 再进行取整,就可以避免上述的误差。

class Solution {
public:
    int minSkips(std::vector<int>& dist, int speed, int hoursBefore) {
        const double EPS = 1e-8, INF = 1e18;
        const int n = dist.size();

        double** timeCost = new double* [n + 1];
        for (int i = 0; i <= n; ++i) {
            timeCost[i] = new double [n + 1];
            std::fill(timeCost[i], timeCost[i] + n + 1, INF);
        }

        timeCost[0][0] = 0.;
        for (int i = 1; i <= n; ++i) {
            for (int j = 0; j <= i; ++j) {
                if (j != i)
                    timeCost[i][j] = std::min(timeCost[i][j], ceil(timeCost[i - 1][j] + (double)dist[i - 1] / (double)speed - EPS));

                if (j != 0)
                    timeCost[i][j] = std::min(timeCost[i][j], timeCost[i - 1][j - 1] + (double)dist[i - 1] / (double)speed);
            }
        }

        for (int j = 0; j <= n; ++j) {
            if (timeCost[n][j] < hoursBefore + EPS)
                return j;
        }
        return -1;
    }
};

时间复杂度 $O(n^2)$ ,空间复杂度 $O(n^2)$ 。其中 $n$ 为 $dist$ 长度。

压缩DP

观察上述状态转移方程,计算 $T[i]$ 的时候只需要用到 $T[i - 1]$ 的值,所以可以去掉一个维度,复用一个长度为 $n$ 的数组即可。

为了防止状态被覆盖,遍历 $j$ 的时候要从后往前遍历。

初始值 $T[0]$ 需要单独处理。

class Solution {
public:
    int minSkips(std::vector<int>& dist, int speed, int hoursBefore) {
        const double EPS = 1e-8, INF = 1e18;
        const int n = dist.size();

        double timeCost[n + 1];
        std::fill(timeCost + 2, timeCost + n + 1, INF);

        timeCost[0] = ceil((double)dist[0] / (double)speed - EPS);
        timeCost[1] = (double)dist[0] / (double)speed;
        for (int i = 2; i <= n; ++i) {
            for (int j = i; j >= 0; --j) {
                if (j != i)
                    timeCost[j] = ceil(timeCost[j] + (double)dist[i - 1] / (double)speed - EPS);
                if (j != 0)
                    timeCost[j] = std::min(timeCost[j], timeCost[j - 1] + (double)dist[i - 1] / (double)speed);
            }
        }

        for (int j = 0; j <= n; ++j) {
            if (timeCost[j] < hoursBefore + EPS)
                return j;
        }
        return -1;
    }
};

时间复杂度 $O(n^2)$ ,空间复杂度 $O(n)$ 。其中 $n$ 为 $dist$ 长度。

最后修改:2024 年 04 月 21 日
如果觉得我的文章对你有用,请随意赞赏