方法一:正反两次扫描
为了计算酿造药水的时间,定义 $\textit{lastFinish}[i]$ 表示巫师 $i$ 完成上一瓶药水的时间。
示例 1 在处理完 $\textit{mana}[0]$ 后,有
$$
\textit{lastFinish} = [5,30,40,60]
$$
如果接着 $\textit{lastFinish}$ 继续酿造下一瓶药水 $\textit{mana}[1]=1$,完成时间是多少?注意开始酿造的时间不能早于 $\textit{lastFinish}[i]$。
| $i$ | 
$\textit{skill}[i]$ | 
$\textit{lastFinish}[i]$ | 
完成时间 | 
| $0$ | 
$1$ | 
$5$ | 
$5+1=6$ | 
| $1$ | 
$5$ | 
$30$ | 
$\max(6,30)+5=35$ | 
| $2$ | 
$2$ | 
$40$ | 
$\max(35,40)+2=42$ | 
| $3$ | 
$4$ | 
$60$ | 
$\max(42,60)+4=64$ | 
题目要求「药水在当前巫师完成工作后必须立即传递给下一个巫师并开始处理」,也就是说,酿造药水的过程中是不能有停顿的。
从 $64$ 开始倒推,可以得到每名巫师的实际完成时间。比如倒数第二位巫师的完成时间,就是 $64$ 减去最后一名巫师花费的时间 $4\cdot 1$,得到 $60$。
| $i$ | 
$\textit{skill}[i+1]$ | 
实际完成时间 | 
| $3$ | 
- | 
$64$ | 
| $2$ | 
$4$ | 
$64-4\cdot 1=60$ | 
| $1$ | 
$2$ | 
$60-2\cdot 1=58$ | 
| $0$ | 
$5$ | 
$58-5\cdot 1=53$ | 
按照上述过程处理每瓶药水,最终答案为 $\textit{lastFinish}[n-1]$。
本题视频讲解,欢迎点赞关注~
###py
class Solution:
    def minTime(self, skill: List[int], mana: List[int]) -> int:
        n = len(skill)
        last_finish = [0] * n  # 第 i 名巫师完成上一瓶药水的时间
        for m in mana:
            # 按题意模拟
            sum_t = 0
            for x, last in zip(skill, last_finish):
                if last > sum_t: sum_t = last  # 手写 max
                sum_t += x * m
            # 倒推:如果酿造药水的过程中没有停顿,那么 last_finish[i] 应该是多少
            last_finish[-1] = sum_t
            for i in range(n - 2, -1, -1):
                last_finish[i] = last_finish[i + 1] - skill[i + 1] * m
        return last_finish[-1]
###java
class Solution {
    public long minTime(int[] skill, int[] mana) {
        int n = skill.length;
        long[] lastFinish = new long[n]; // 第 i 名巫师完成上一瓶药水的时间
        for (int m : mana) {
            // 按题意模拟
            long sumT = 0;
            for (int i = 0; i < n; i++) {
                sumT = Math.max(sumT, lastFinish[i]) + skill[i] * m;
            }
            // 倒推:如果酿造药水的过程中没有停顿,那么 lastFinish[i] 应该是多少
            lastFinish[n - 1] = sumT;
            for (int i = n - 2; i >= 0; i--) {
                lastFinish[i] = lastFinish[i + 1] - skill[i + 1] * m;
            }
        }
        return lastFinish[n - 1];
    }
}
###cpp
class Solution {
public:
    long long minTime(vector<int>& skill, vector<int>& mana) {
        int n = skill.size();
        vector<long long> last_finish(n); // 第 i 名巫师完成上一瓶药水的时间
        for (int m : mana) {
            // 按题意模拟
            long long sum_t = 0;
            for (int i = 0; i < n; i++) {
                sum_t = max(sum_t, last_finish[i]) + skill[i] * m;
            }
            // 倒推:如果酿造药水的过程中没有停顿,那么 lastFinish[i] 应该是多少
            last_finish[n - 1] = sum_t;
            for (int i = n - 2; i >= 0; i--) {
                last_finish[i] = last_finish[i + 1] - skill[i + 1] * m;
            }
        }
        return last_finish[n - 1];
    }
};
###go
func minTime(skill, mana []int) int64 {
n := len(skill)
lastFinish := make([]int, n) // 第 i 名巫师完成上一瓶药水的时间
for _, m := range mana {
// 按题意模拟
sumT := 0
for i, x := range skill {
sumT = max(sumT, lastFinish[i]) + x*m
}
// 倒推:如果酿造药水的过程中没有停顿,那么 lastFinish[i] 应该是多少
lastFinish[n-1] = sumT
for i := n - 2; i >= 0; i-- {
lastFinish[i] = lastFinish[i+1] - skill[i+1]*m
}
}
return int64(lastFinish[n-1])
}
复杂度分析
- 时间复杂度:$\mathcal{O}(nm)$,其中 $n$ 是 $\textit{skill}$ 的长度,$m$ 是 $\textit{mana}$ 的长度。
 
- 空间复杂度:$\mathcal{O}(n)$。
 
方法二:递推开始时间
由于酿造药水的过程是连续的,所以知道了开始时间(或者完成时间)就能知道每个 $\textit{lastFinish}[i]$。所以 $\textit{lastFinish}$ 数组是多余的。
设开始酿造 $\textit{mana}[j]$ 的时间为 $\textit{start}_j$,那么有
$$
\textit{lastFinish}_j[i] = \textit{start}j + \textit{mana}[j]\cdot \sum{k=0}^{i} \textit{skill}[k]
$$
在已知 $\textit{start}_{j-1}$ 的前提下,能否递推算出 $\textit{start}_j$?
哪位巫师决定了开始时间?假设第 $i$ 位巫师决定了开始时间,那么这位巫师完成 $\textit{mana}[j-1]$ 的时间,同时也是他开始 $\textit{mana}[j]$ 的时间。
所以有
$$
\textit{lastFinish}_{j-1}[i] + \textit{mana}[j]\cdot \textit{skill}[i] = \textit{lastFinish}_j[i]
$$
两边代入 $\textit{lastFinish}_j[i]$ 的式子,得
$$
\textit{start}{j-1} + \textit{mana}[j-1]\cdot \sum{k=0}^{i} \textit{skill}[k] + \textit{mana}[j]\cdot \textit{skill}[i] = \textit{start}j + \textit{mana}[j]\cdot \sum{k=0}^{i} \textit{skill}[k]
$$
移项得
$$
\textit{start}j = \textit{start}{j-1} + \textit{mana}[j-1]\cdot \sum_{k=0}^{i} \textit{skill}[k] - \textit{mana}[j]\cdot \sum_{k=0}^{i-1} \textit{skill}[k]
$$
计算 $\textit{skill}$ 的 前缀和 数组 $s$,上式为
$$
\textit{start}j = \textit{start}{j-1} + \textit{mana}[j-1]\cdot s[i+1] - \textit{mana}[j]\cdot s[i]
$$
枚举 $i$,取最大值,得
$$
\textit{start}j = \textit{start}{j-1} + \max_{i=0}^{n-1} \left{\textit{mana}[j-1]\cdot s[i+1] - \textit{mana}[j]\cdot s[i]\right}
$$
初始值 $\textit{start}_0 = 0$。
答案为 $\textit{lastFinish}{m-1}[n-1] = \textit{start}{m-1} + \textit{mana}[m-1]\cdot s[n]$。
###py
class Solution:
    def minTime(self, skill: List[int], mana: List[int]) -> int:
        n = len(skill)
        s = list(accumulate(skill, initial=0))  # skill 的前缀和
        start = 0
        for pre, cur in pairwise(mana):
            start += max(pre * s[i + 1] - cur * s[i] for i in range(n))
        return start + mana[-1] * s[-1]
###java
class Solution {
    public long minTime(int[] skill, int[] mana) {
        int n = skill.length;
        int[] s = new int[n + 1]; // skill 的前缀和
        for (int i = 0; i < n; i++) {
            s[i + 1] = s[i] + skill[i];
        }
        int m = mana.length;
        long start = 0;
        for (int j = 1; j < m; j++) {
            long mx = 0;
            for (int i = 0; i < n; i++) {
                mx = Math.max(mx, (long) mana[j - 1] * s[i + 1] - (long) mana[j] * s[i]);
            }
            start += mx;
        }
        return start + (long) mana[m - 1] * s[n];
    }
}
###cpp
class Solution {
public:
    long long minTime(vector<int>& skill, vector<int>& mana) {
        int n = skill.size(), m = mana.size();
        vector<int> s(n + 1); // skill 的前缀和
        partial_sum(skill.begin(), skill.end(), s.begin() + 1);
        long long start = 0;
        for (int j = 1; j < m; j++) {
            long long mx = 0;
            for (int i = 0; i < n; i++) {
                mx = max(mx, 1LL * mana[j - 1] * s[i + 1] - 1LL * mana[j] * s[i]);
            }
            start += mx;
        }
        return start + 1LL * mana[m - 1] * s[n];
    }
};
###go
func minTime(skill, mana []int) int64 {
n, m := len(skill), len(mana)
s := make([]int, n+1) // skill 的前缀和
for i, x := range skill {
s[i+1] = s[i] + x
}
start := 0
for j := 1; j < m; j++ {
mx := 0
for i := range n {
mx = max(mx, mana[j-1]*s[i+1]-mana[j]*s[i])
}
start += mx
}
return int64(start + mana[m-1]*s[n])
}
复杂度分析
- 时间复杂度:$\mathcal{O}(nm)$,其中 $n$ 是 $\textit{skill}$ 的长度,$m$ 是 $\textit{mana}$ 的长度。
 
- 空间复杂度:$\mathcal{O}(n)$。如果在遍历的同时计算前缀和,则可以做到 $\mathcal{O}(1)$ 空间。
 
方法三:record 优化
将递推式
$$
\textit{start}j = \textit{start}{j-1} + \max_{i=0}^{n-1} \left{\textit{mana}[j-1]\cdot s[i+1] - \textit{mana}[j]\cdot s[i]\right}
$$
变形为
$$
\textit{start}j = \textit{start}{j-1} + \max_{i=0}^{n-1} \left{(\textit{mana}[j-1]-\textit{mana}[j])\cdot s[i]  + \textit{mana}[j-1]\cdot \textit{skill}[i] \right}
$$
设 $d = \textit{mana}[j-1]-\textit{mana}[j]$。分类讨论:
- 如果 $d > 0$。由于 $s$ 是单调递增数组,如果 $\textit{skill}[3] < \textit{skill}[5]$,那么 $i=3$ 绝对不会算出最大值;但如果 $\textit{skill}[3] > \textit{skill}[5]$,谁会算出最大值就不一定了。所以我们只需要考虑 $\textit{skill}$ 的逆序 record,这才是可能成为最大值的数据。其中逆序 record 的意思是,倒序遍历 $\textit{skill}$,每次遍历到更大的数,就记录下标。
 
- 如果 $d < 0$。由于 $s$ 是单调递增数组,如果 $\textit{skill}[5] < \textit{skill}[3]$,那么 $i=5$ 绝对不会算出最大值;但如果 $\textit{skill}[5] > \textit{skill}[3]$,谁会算出最大值就不一定了。所以我们只需要考虑 $\textit{skill}$ 的正序 record,这才是可能成为最大值的数据。其中正序 record 的意思是,正序遍历 $\textit{skill}$,每次遍历到更大的数,就记录下标。
 
- $d = 0$ 的情况可以并入 $d>0$ 的情况。
 
###py
class Solution:
    def minTime(self, skill: List[int], mana: List[int]) -> int:
        n = len(skill)
        s = list(accumulate(skill, initial=0))
        suf_record = [n - 1]
        for i in range(n - 2, -1, -1):
            if skill[i] > skill[suf_record[-1]]:
                suf_record.append(i)
        pre_record = [0]
        for i in range(1, n):
            if skill[i] > skill[pre_record[-1]]:
                pre_record.append(i)
        start = 0
        for pre, cur in pairwise(mana):
            record = pre_record if pre < cur else suf_record
            start += max(pre * s[i + 1] - cur * s[i] for i in record)
        return start + mana[-1] * s[-1]
###java
class Solution {
    public long minTime(int[] skill, int[] mana) {
        int n = skill.length;
        int[] s = new int[n + 1];
        for (int i = 0; i < n; i++) {
            s[i + 1] = s[i] + skill[i];
        }
        List<Integer> suf = new ArrayList<>();
        suf.add(n - 1);
        for (int i = n - 2; i >= 0; i--) {
            if (skill[i] > skill[suf.getLast()]) {
                suf.add(i);
            }
        }
        List<Integer> pre = new ArrayList<>();
        pre.add(0);
        for (int i = 1; i < n; i++) {
            if (skill[i] > skill[pre.getLast()]) {
                pre.add(i);
            }
        }
        int m = mana.length;
        long start = 0;
        for (int j = 1; j < m; j++) {
            List<Integer> record = mana[j - 1] < mana[j] ? pre : suf;
            long mx = 0;
            for (int i : record) {
                mx = Math.max(mx, (long) mana[j - 1] * s[i + 1] - (long) mana[j] * s[i]);
            }
            start += mx;
        }
        return start + (long) mana[m - 1] * s[n];
    }
}
###java
class Solution {
    public long minTime(int[] skill, int[] mana) {
        int n = skill.length;
        int[] s = new int[n + 1];
        for (int i = 0; i < n; i++) {
            s[i + 1] = s[i] + skill[i];
        }
        int[] suf = new int[n];
        int sufLen = 0;
        suf[sufLen++] = n - 1;
        for (int i = n - 2; i >= 0; i--) {
            if (skill[i] > skill[suf[sufLen - 1]]) {
                suf[sufLen++] = i;
            }
        }
        int[] pre = new int[n];
        int preLen = 0;
        pre[preLen++] = 0;
        for (int i = 1; i < n; i++) {
            if (skill[i] > skill[pre[preLen - 1]]) {
                pre[preLen++] = i;
            }
        }
        int m = mana.length;
        long start = 0;
        for (int j = 1; j < m; j++) {
            int[] record = mana[j - 1] < mana[j] ? pre : suf;
            int recordLen = mana[j - 1] < mana[j] ? preLen : sufLen;
            long mx = 0;
            for (int k = 0; k < recordLen; k++) {
                int i = record[k];
                mx = Math.max(mx, (long) mana[j - 1] * s[i + 1] - (long) mana[j] * s[i]);
            }
            start += mx;
        }
        return start + (long) mana[m - 1] * s[n];
    }
}
###cpp
class Solution {
public:
    long long minTime(vector<int>& skill, vector<int>& mana) {
        int n = skill.size(), m = mana.size();
        vector<int> s(n + 1);
        partial_sum(skill.begin(), skill.end(), s.begin() + 1);
        vector<int> suf = {n - 1};
        for (int i = n - 2; i >= 0; i--) {
            if (skill[i] > skill[suf.back()]) {
                suf.push_back(i);
            }
        }
        vector<int> pre = {0};
        for (int i = 1; i < n; i++) {
            if (skill[i] > skill[pre.back()]) {
                pre.push_back(i);
            }
        }
        long long start = 0;
        for (int j = 1; j < m; j++) {
            auto& record = mana[j - 1] < mana[j] ? pre : suf;
            long long mx = 0;
            for (int i : record) {
                mx = max(mx, 1LL * mana[j - 1] * s[i + 1] - 1LL * mana[j] * s[i]);
            }
            start += mx;
        }
        return start + 1LL * mana[m - 1] * s[n];
    }
};
###go
func minTime(skill, mana []int) int64 {
n, m := len(skill), len(mana)
s := make([]int, n+1)
for i, x := range skill {
s[i+1] = s[i] + x
}
suf := []int{n - 1}
for i := n - 2; i >= 0; i-- {
if skill[i] > skill[suf[len(suf)-1]] {
suf = append(suf, i)
}
}
pre := []int{0}
for i := 1; i < n; i++ {
if skill[i] > skill[pre[len(pre)-1]] {
pre = append(pre, i)
}
}
start := 0
for j := 1; j < m; j++ {
record := suf
if mana[j-1] < mana[j] {
record = pre
}
mx := 0
for _, i := range record {
mx = max(mx, mana[j-1]*s[i+1]-mana[j]*s[i])
}
start += mx
}
return int64(start + mana[m-1]*s[n])
}
复杂度分析(最坏情况)
- 时间复杂度:$\mathcal{O}(nm)$,其中 $n$ 是 $\textit{skill}$ 的长度,$m$ 是 $\textit{mana}$ 的长度。
 
- 空间复杂度:$\mathcal{O}(n)$。
 
复杂度分析(平均情况)
力扣喜欢出随机数据,上述算法在随机数据下的性能如何?
换句话说,在随机数据下,record 的期望长度是多少?
为方便分析,假设 $\textit{skill}$ 是一个随机的 $[1,n]$ 的排列。
$\textit{skill}[i]$ 如果是一个新的最大值,那么它是 $[0,i]$ 中的最大值。在随机排列的情况下,$[0,i]$ 中的排列也是随机的,所以这等价于该排列的最后一个数是最大值的概率,即
$$
\dfrac{1}{i+1}
$$
record 的期望长度,等于「每个位置能否成为新的最大值」之和,能就贡献 $1$,不能就贡献 $0$。
所以 $\textit{skill}[i]$ 给期望的贡献是 $\dfrac{1}{i+1}$。所以 record 的期望长度为
$$
\sum_{i=0}^{n-1} \dfrac{1}{i+1}
$$
由调和级数可知,record 的期望长度为 $\Theta(\log n)$。
- 时间复杂度:$\Theta(n + m\log n)$,其中 $n$ 是 $\textit{skill}$ 的长度,$m$ 是 $\textit{mana}$ 的长度。
 
- 空间复杂度:$\mathcal{O}(n)$。
 
方法四:凸包 + 二分
前置知识:二维计算几何,凸包,Andrew 算法。
把递推式
$$
\textit{start}j = \textit{start}{j-1} + \max_{i=0}^{n-1} \left{(\textit{mana}[j-1]-\textit{mana}[j])\cdot s[i]  + \textit{mana}[j-1]\cdot \textit{skill}[i] \right}
$$
中的
$$
(\textit{mana}[j-1]-\textit{mana}[j])\cdot s[i]  + \textit{mana}[j-1]\cdot \textit{skill}[i]
$$
改成点积的形式,这样我们能得到来自几何意义上的观察。
设向量 $\mathbf{v}_i = (s[i],\textit{skill}[i])$。
设向量 $\mathbf{p} = (\textit{mana}[j-1]-\textit{mana}[j], \textit{mana}[j-1])$。
那么我们求的是
$$
\max_{i=0}^{n-1} \mathbf{p}\cdot \mathbf{v}_i
$$
根据点积的几何意义,我们求的是 $\mathbf{v}_i$ 在 $\mathbf{p}$ 方向上的投影长度,再乘以 $\mathbf{p}$ 的模长 $||\mathbf{p}||$。由于 $||\mathbf{p}||$ 是个定值,所以要最大化投影长度。
考虑 $\mathbf{v}_i$ 的上凸包(用 Andrew 算法计算),在凸包内的点,就像是山坳,比凸包顶点的投影长度短。所以只需考虑凸包顶点。
这样有一个很好的性质:顺时针(或者逆时针)遍历凸包顶点,$\mathbf{p}\cdot \mathbf{v}_i$ 会先变大再变小(单峰函数)。那么要计算最大值,就类似 852. 山脉数组的峰顶索引,二分首个「下坡」的位置,具体见 我的题解。
###py
class Vec:
    __slots__ = 'x', 'y'
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y
    def __sub__(self, b: "Vec") -> "Vec":
        return Vec(self.x - b.x, self.y - b.y)
    def det(self, b: "Vec") -> int:
        return self.x * b.y - self.y * b.x
    def dot(self, b: "Vec") -> int:
        return self.x * b.x + self.y * b.y
class Solution:
    # Andrew 算法,计算 points 的上凸包
    # 由于横坐标(前缀和)是严格递增的,所以无需排序
    def convex_hull(self, points: List[Vec]) -> List[Vec]:
        q = []
        for p in points:
            while len(q) > 1 and (q[-1] - q[-2]).det(p - q[-1]) >= 0:
                q.pop()
            q.append(p)
        return q
    def minTime(self, skill: List[int], mana: List[int]) -> int:
        s = list(accumulate(skill, initial=0))
        vs = [Vec(pre_sum, x) for pre_sum, x in zip(s, skill)]
        vs = self.convex_hull(vs)  # 去掉无用数据
        start = 0
        for pre, cur in pairwise(mana):
            p = Vec(pre - cur, pre)
            # p.dot(vs[i]) 是个单峰函数,二分找最大值
            check = lambda i: p.dot(vs[i]) > p.dot(vs[i + 1])
            i = bisect_left(range(len(vs) - 1), True, key=check)
            start += p.dot(vs[i])
        return start + mana[-1] * s[-1]
###java
class Solution {
    private record Vec(int x, int y) {
        Vec sub(Vec b) {
            return new Vec(x - b.x, y - b.y);
        }
        long det(Vec b) {
            return (long) x * b.y - (long) y * b.x;
        }
        long dot(Vec b) {
            return (long) x * b.x + (long) y * b.y;
        }
    }
    // Andrew 算法,计算 points 的上凸包
    // 由于横坐标(前缀和)是严格递增的,所以无需排序
    private List<Vec> convexHull(Vec[] points) {
        List<Vec> q = new ArrayList<>();
        for (Vec p : points) {
            while (q.size() > 1 && q.getLast().sub(q.get(q.size() - 2)).det(p.sub(q.getLast())) >= 0) {
                q.removeLast();
            }
            q.add(p);
        }
        return q;
    }
    public long minTime(int[] skill, int[] mana) {
        int n = skill.length;
        int[] s = new int[n + 1];
        Vec[] points = new Vec[n];
        for (int i = 0; i < n; i++) {
            s[i + 1] = s[i] + skill[i];
            points[i] = new Vec(s[i], skill[i]);
        }
        List<Vec> vs = convexHull(points); // 去掉无用数据
        int m = mana.length;
        long start = 0;
        for (int j = 1; j < m; j++) {
            Vec p = new Vec(mana[j - 1] - mana[j], mana[j - 1]);
            // p.dot(vs[i]) 是个单峰函数,二分找最大值
            int l = -1, r = vs.size() - 1;
            while (l + 1 < r) {
                int mid = (l + r) >>> 1;
                if (p.dot(vs.get(mid)) > p.dot(vs.get(mid + 1))) {
                    r = mid;
                } else {
                    l = mid;
                }
            }
            start += p.dot(vs.get(r));
        }
        return start + (long) mana[m - 1] * s[n];
    }
}
###cpp
struct Vec {
    int x, y;
    Vec operator-(const Vec& b) { return {x - b.x, y - b.y}; }
    long long det(const Vec& b) { return 1LL * x * b.y - 1LL * y * b.x; }
    long long dot(const Vec& b) { return 1LL * x * b.x + 1LL * y * b.y; }
};
class Solution {
    // Andrew 算法,计算 points 的上凸包
    // 由于横坐标(前缀和)是严格递增的,所以无需排序
    vector<Vec> convex_hull(vector<Vec>& points) {
        vector<Vec> q;
        for (auto& p : points) {
            while (q.size() > 1 && (q.back() - q[q.size() - 2]).det(p - q.back()) >= 0) {
                q.pop_back();
            }
            q.push_back(p);
        }
        return q;
    }
public:
    long long minTime(vector<int>& skill, vector<int>& mana) {
        int n = skill.size(), m = mana.size();
        vector<int> s(n + 1);
        vector<Vec> vs(n);
        for (int i = 0; i < n; i++) {
            s[i + 1] = s[i] + skill[i];
            vs[i] = {s[i], skill[i]};
        }
        vs = convex_hull(vs); // 去掉无用数据
        long long start = 0;
        for (int j = 1; j < m; j++) {
            Vec p = {mana[j - 1] - mana[j], mana[j - 1]};
            // p.dot(vs[i]) 是个单峰函数,二分找最大值
            int l = -1, r = vs.size() - 1;
            while (l + 1 < r) {
                int mid = l + (r - l) / 2;
                (p.dot(vs[mid]) > p.dot(vs[mid + 1]) ? r : l) = mid;
            }
            start += p.dot(vs[r]);
        }
        return start + 1LL * mana[m - 1] * s[n];
    }
};
###go
type vec struct{ x, y int }
func (a vec) sub(b vec) vec { return vec{a.x - b.x, a.y - b.y} }
func (a vec) det(b vec) int { return a.x*b.y - a.y*b.x }
func (a vec) dot(b vec) int { return a.x*b.x + a.y*b.y }
// Andrew 算法,计算 points 的上凸包
// 由于横坐标(前缀和)是严格递增的,所以无需排序
func convexHull(points []vec) (q []vec) {
for _, p := range points {
for len(q) > 1 && q[len(q)-1].sub(q[len(q)-2]).det(p.sub(q[len(q)-1])) >= 0 {
q = q[:len(q)-1]
}
q = append(q, p)
}
return
}
func minTime(skill, mana []int) int64 {
n, m := len(skill), len(mana)
s := make([]int, n+1)
vs := make([]vec, n)
for i, x := range skill {
s[i+1] = s[i] + x
vs[i] = vec{s[i], x}
}
vs = convexHull(vs) // 去掉无用数据
start := 0
for j := 1; j < m; j++ {
p := vec{mana[j-1] - mana[j], mana[j-1]}
// p.dot(vs[i]) 是个单峰函数,二分找最大值
i := sort.Search(len(vs)-1, func(i int) bool { return p.dot(vs[i]) > p.dot(vs[i+1]) })
start += p.dot(vs[i])
}
return int64(start + mana[m-1]*s[n])
}
复杂度分析
- 时间复杂度:$\mathcal{O}(n + m\log n)$,其中 $n$ 是 $\textit{skill}$ 的长度,$m$ 是 $\textit{mana}$ 的长度。
 
- 空间复杂度:$\mathcal{O}(n)$。
 
分类题单
如何科学刷题?
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
 
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
 
- 单调栈(基础/矩形面积/贡献法/最小字典序)
 
- 网格图(DFS/BFS/综合应用)
 
- 位运算(基础/性质/拆位/试填/恒等式/思维)
 
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
 
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
 
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
 
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
 
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
 
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
 
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
 
我的题解精选(已分类)
欢迎关注 B站@灵茶山艾府