阅读视图

发现新文章,点击刷新页面。

两种方法:枚举 / 凸包+旋转卡壳(Python/Java/C++/Go)

三角形面积公式

对于平面上的三个点 $P_1,P_2,P_3$,定义 $\mathbf{a} = \overrightarrow{P_1P_2}$,$\mathbf{b} = \overrightarrow{P_1P_3}$。

根据向量叉积的定义,$|| \mathbf{a} \times \mathbf{b} ||$ 是由 $\mathbf{a}$ 和 $\mathbf{b}$ 张成的平行四边形的面积。除以 $2$ 就得到了 $\triangle P_1P_2P_3$ 的面积。

严格来说,叉积是三维概念。这里把向量 $\mathbf{a}$ 和 $\mathbf{b}$ 视作 $z$ 方向为 $0$ 的三维向量。

设 $\mathbf{a} = (x_1,y_1)$,$\mathbf{b} = (x_2,y_2)$,根据叉积的计算公式,三角形面积为

$$
\dfrac{1}{2}|x_1y_2 - y_1x_2|
$$

上式中的 $(x_1,y_1)$ 来自 $P_1,P_2$ 的横坐标之差,纵坐标之差。$(x_2,y_2)$ 来自 $P_1,P_3$ 的横坐标之差,纵坐标之差。

方法一:暴力枚举

class Solution:
    def largestTriangleArea(self, points: List[List[int]]) -> float:
        ans = 0
        for p1, p2, p3 in combinations(points, 3):
            x1, y1 = p2[0] - p1[0], p2[1] - p1[1]
            x2, y2 = p3[0] - p1[0], p3[1] - p1[1]
            ans = max(ans, abs(x1 * y2 - y1 * x2))  # 注意这里没有除以 2
        return ans / 2
class Solution {
    public double largestTriangleArea(int[][] points) {
        int n = points.length;
        int ans = 0;
        for (int i = 0; i < n - 2; i++) {
            for (int j = i + 1; j < n - 1; j++) {
                for (int k = j + 1; k < n; k++) {
                    int[] p1 = points[i], p2 = points[j], p3 = points[k];
                    int x1 = p2[0] - p1[0], y1 = p2[1] - p1[1];
                    int x2 = p3[0] - p1[0], y2 = p3[1] - p1[1];
                    ans = Math.max(ans, Math.abs(x1 * y2 - y1 * x2)); // 注意这里没有除以 2
                }
            }
        }
        return ans / 2.0;
    }
}
class Solution {
public:
    double largestTriangleArea(vector<vector<int>>& points) {
        int n = points.size();
        int ans = 0;
        for (int i = 0; i < n - 2; i++) {
            auto& p1 = points[i];
            for (int j = i + 1; j < n - 1; j++) {
                auto& p2 = points[j];
                for (int k = j + 1; k < n; k++) {
                    auto& p3 = points[k];
                    int x1 = p2[0] - p1[0], y1 = p2[1] - p1[1];
                    int x2 = p3[0] - p1[0], y2 = p3[1] - p1[1];
                    ans = max(ans, abs(x1 * y2 - y1 * x2)); // 注意这里没有除以 2
                }
            }
        }
        return ans / 2.0;
    }
};
func largestTriangleArea(points [][]int) float64 {
n := len(points)
ans := 0
for i := range n - 2 {
for j := i + 1; j < n-1; j++ {
for k := j + 1; k < n; k++ {
p1, p2, p3 := points[i], points[j], points[k]
x1, y1 := p2[0]-p1[0], p2[1]-p1[1]
x2, y2 := p3[0]-p1[0], p3[1]-p1[1]
ans = max(ans, abs(x1*y2-y1*x2)) // 注意这里没有除以 2
}
}
}
return float64(ans) / 2
}

func abs(x int) int { if x < 0 { return -x }; return x }

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^3)$,其中 $n$ 是 $\textit{points}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

方法二:凸包 + 旋转卡壳

前置题目587. 安装栅栏

若固定三角形的两个顶点,那么第三个顶点在哪?

三角形的高越长越好,所以第三个顶点相比在凸包内部,在凸包上更好(更远)。所以面积最大的三角形,三个顶点都在凸包上。

求出凸包后:

  1. 枚举凸包的顶点 $i$ 作为三角形的其中一个顶点。对于另外两个顶点,我们用旋转卡壳(同向双指针)计算。
  2. 初始化 $j=i+1$,$k=i+2$。
  3. 对于 $\triangle P_iP_jP_k$ 和 $\triangle P_iP_jP_{k+1}$ 的面积,如果后者更大,那么把 $k$ 加一。重复该过程,直到 $k+1$ 越界或者面积没有变大,跳出循环。
  4. 跳出循环后,$P_k$ 就移动到了一个在 $\overrightarrow{P_iP_j}$ 左侧且距离 $P_iP_j$ 最远的位置。计算 $\triangle P_iP_jP_k$ 的面积,更新答案的最大值。然后把 $j$ 加一,执行第三步。读者可以在纸上画画,随着 $j$ 的增大,在 $\overrightarrow{P_iP_j}$ 左侧且距离 $P_iP_j$ 最远的 $P_k$ 的下标 $k$ 也在增大,所以可以用同向双指针。

下面代码保证计算 $\mathbf{a} \times \mathbf{b}$ 时,$\mathbf{b}$ 在 $\mathbf{a}$ 的左侧,此时算出来的面积一定大于 $0$,无需取绝对值。

def det(x1: int, y1: int, x2: int, y2: int) -> int:
    return x1 * y2 - y1 * x2

def convex_hull(points: List[List[int]]) -> List[List[int]]:
    points.sort()

    # 计算下凸包(从左到右)
    q = []
    for p in points:
        while len(q) > 1 and det(q[-1][0] - q[-2][0], q[-1][1] - q[-2][1], p[0] - q[-1][0], p[1] - q[-1][1]) <= 0:
            q.pop()
        q.append(p)

    # 计算上凸包(从右到左)
    down_size = len(q)
    # 注意下凸包的最后一个点,是上凸包的右边第一个点,所以从 n-2 开始遍历
    for i in range(len(points) - 2, -1, -1):
        p = points[i]
        while len(q) > down_size and det(q[-1][0] - q[-2][0], q[-1][1] - q[-2][1], p[0] - q[-1][0], p[1] - q[-1][1]) <= 0:
            q.pop()
        q.append(p)

    # 此时首尾是同一个点 points[0],需要去掉
    q.pop()
    return q

class Solution:
    def largestTriangleArea(self, points: List[List[int]]) -> float:
        ch = convex_hull(points)

        def area(i: int, j: int, k: int) -> int:
            return det(ch[j][0] - ch[i][0], ch[j][1] - ch[i][1], ch[k][0] - ch[i][0], ch[k][1] - ch[i][1])

        m = len(ch)
        ans = 0
        # 固定三角形的其中一个顶点 ch[i]
        for i in range(m):
            # 同向双指针
            k = i + 2
            for j in range(i + 1, m - 1):
                while k + 1 < m and area(i, j, k) < area(i, j, k + 1):
                    k += 1
                # 循环结束后,ch[k] 距离 ch[i]ch[j] 最远
                ans = max(ans, area(i, j, k))  # 注意这里没有除以 2
        return ans / 2
class Solution {
    public double largestTriangleArea(int[][] points) {
        List<int[]> ch = convexHull(points);
        int m = ch.size();
        int ans = 0;
        // 固定三角形的其中一个顶点 ch[i]
        for (int i = 0; i < m; i++) {
            // 同向双指针
            int k = i + 2;
            for (int j = i + 1; j < m - 1; j++) {
                while (k + 1 < m && area(ch, i, j, k) < area(ch, i, j, k + 1)) {
                    k++;
                }
                // 循环结束后,ch[k] 距离 ch[i]ch[j] 最远
                ans = Math.max(ans, area(ch, i, j, k)); // 注意这里没有除以 2
            }
        }
        return ans / 2.0;
    }

    private List<int[]> convexHull(int[][] points) {
        Arrays.sort(points, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);

        // 计算下凸包(从左到右)
        List<int[]> q = new ArrayList<>();
        for (int[] p : points) {
            while (q.size() > 1) {
                int[] p1 = q.get(q.size() - 2);
                int[] p2 = q.getLast();
                if (det(p2[0] - p1[0], p2[1] - p1[1], p[0] - p2[0], p[1] - p2[1]) > 0) {
                    break;
                }
                q.removeLast();
            }
            q.add(p);
        }

        // 计算上凸包(从右到左)
        int downSize = q.size();
        // 注意下凸包的最后一个点,是上凸包的右边第一个点,所以从 n-2 开始遍历
        for (int i = points.length - 2; i >= 0; i--) {
            int[] p = points[i];
            while (q.size() > downSize) {
                int[] p1 = q.get(q.size() - 2);
                int[] p2 = q.getLast();
                if (det(p2[0] - p1[0], p2[1] - p1[1], p[0] - p2[0], p[1] - p2[1]) > 0) {
                    break;
                }
                q.removeLast();
            }
            q.add(p);
        }

        // 此时首尾是同一个点 points[0],需要去掉
        q.removeLast();
        return q;
    }

    private int det(int x1, int y1, int x2, int y2) {
        return x1 * y2 - y1 * x2;
    }

    private int area(List<int[]> ch, int i, int j, int k) {
        return det(ch.get(j)[0] - ch.get(i)[0], ch.get(j)[1] - ch.get(i)[1],
                ch.get(k)[0] - ch.get(i)[0], ch.get(k)[1] - ch.get(i)[1]);
    }
}
struct Vec {
    int x, y;

    Vec sub(const Vec& b) const {
        return {x - b.x, y - b.y};
    }

    int det(const Vec& b) const {
        return x * b.y - y * b.x;
    }
};

class Solution {
    vector<Vec> convexHull(vector<Vec>& points) {
        ranges::sort(points, {}, [](auto& p) { return pair(p.x, p.y); });

        vector<Vec> q;
        // 计算下凸包(从左到右)
        for (auto& p : points) {
            while (q.size() > 1 && q[q.size() - 1].sub(q[q.size() - 2]).det(p.sub(q[q.size() - 1])) <= 0) {
                q.pop_back();
            }
            q.push_back(p);
        }

        // 计算上凸包(从右到左)
        int down_size = q.size();
        // 注意下凸包的最后一个点,是上凸包的右边第一个点,所以从 n-2 开始遍历
        for (int i = (int) points.size() - 2; i >= 0; i--) {
            auto& p = points[i];
            while (q.size() > down_size && q[q.size() - 1].sub(q[q.size() - 2]).det(p.sub(q[q.size() - 1])) <= 0) {
                q.pop_back();
            }
            q.push_back(p);
        }

        // 此时首尾是同一个点 points[0],需要去掉
        q.pop_back();
        return q;
    }

public:
    double largestTriangleArea(vector<vector<int>>& points) {
        vector<Vec> a(points.size());
        for (int i = 0; i < points.size(); i++) {
            a[i] = {points[i][0], points[i][1]};
        }

        vector<Vec> ch = convexHull(a);

        auto area = [&](int i, int j, int k) -> int {
            return ch[j].sub(ch[i]).det(ch[k].sub(ch[i]));
        };

        int m = ch.size();
        int ans = 0;
        // 固定三角形的其中一个顶点 ch[i]
        for (int i = 0; i < m; i++) {
            // 同向双指针
            int k = i + 2;
            for (int j = i + 1; j < m - 1; j++) {
                while (k + 1 < m && area(i, j, k) < area(i, j, k + 1)) {
                    k++;
                }
                // 循环结束后,ch[k] 距离 ch[i]ch[j] 最远
                ans = max(ans, area(i, j, k)); // 注意这里没有除以 2
            }
        }
        return ans / 2.0;
    }
};
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 convexHull(points []vec) (q []vec) {
slices.SortFunc(points, func(a, b vec) int { return cmp.Or(a.x-b.x, a.y-b.y) })

// 计算下凸包(从左到右)
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)
}

// 计算上凸包(从右到左)
downSize := len(q)
// 注意下凸包的最后一个点,是上凸包的右边第一个点,所以从 n-2 开始遍历
for i := len(points) - 2; i >= 0; i-- {
p := points[i]
for len(q) > downSize && 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)
}

// 此时首尾是同一个点 points[0],需要去掉
q = q[:len(q)-1]
return
}

func largestTriangleArea(points [][]int) float64 {
a := make([]vec, len(points))
for i, p := range points {
a[i] = vec{p[0], p[1]}
}

ch := convexHull(a)
area := func(i, j, k int) int {
return ch[j].sub(ch[i]).det(ch[k].sub(ch[i]))
}

m := len(ch)
ans := 0
// 固定三角形的其中一个顶点 ch[i]
for i := range ch {
// 同向双指针
k := i + 2
for j := i + 1; j < m-1; j++ {
for k+1 < m && area(i, j, k) < area(i, j, k+1) {
k++
}
// 循环结束后,ch[k] 距离 ch[i]ch[j] 最远
ans = max(ans, area(i, j, k)) // 注意这里没有除以 2
}
}
return float64(ans) / 2
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^2)$,其中 $n$ 是 $\textit{points}$ 的长度。枚举 $i$ 是 $\mathcal{O}(n)$,枚举 $j$ 和 $k$ 的同向双指针也是 $\mathcal{O}(n)$。
  • 空间复杂度:$\mathcal{O}(n)$。

:本题有 $\mathcal{O}(n)$ 做法,见论文 Maximal Area Triangles in a Convex Polygon

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-最大三角形面积🟢

给你一个由 X-Y 平面上的点组成的数组 points ,其中 points[i] = [xi, yi] 。从其中取任意三个不同的点组成三角形,返回能组成的最大三角形的面积。与真实值误差在 10-5 内的答案将会视为正确答案

 

示例 1:

输入:points = [[0,0],[0,1],[1,0],[0,2],[2,0]]
输出:2.00000
解释:输入中的 5 个点如上图所示,红色的三角形面积最大。

示例 2:

输入:points = [[1,0],[0,0],[0,1]]
输出:0.50000

 

提示:

  • 3 <= points.length <= 50
  • -50 <= xi, yi <= 50
  • 给出的所有点 互不相同

【负雪明烛】小学生也能看懂的三角形面积公式

大家好,我是 @负雪明烛。点击右上方的「+关注,优质题解不间断!

题目大意

给出一些平面上点的二维坐标,求从这些点中抽取出 3 个点,能组成的最大的三角形面积。

题意很清晰。

解题方法

思路

我们把 $A(x1, y1),B(x2, y2),C(x3, y3)$分别向 $x$ 轴投影,分别得到点$E, D, F$。

可以得到 3 个梯形:以下图中的黄色的边为梯形的上下底,分别以三角形的三条边作为梯形的斜边

812. 最大三角形面积.002.png

于是:

$三角形 ABC 的面积 = 梯形 BDEA 的面积 + 梯形 AEFC 的面积 - 梯形 BDFC 的面积$
$= [(y1 + y2) * (x1 - x2)]/2 + [(y3 + y1) * (x3 - x1)]/2 - [(y2 + y3) * (x3 - x2)]/2$
$= 1/2 * [x1(y2 - y3) + x2(y3 - y1) + x3(y1 - y2)]$

我们就有了根据三角形的三点的坐标求面积的公式。

代码

三重 $for$循环,从题目给出的二维坐标中取出 3 个点,根据上面的面积公式求组成的三角形面积。

取最大面积即可。

###Python

class Solution:
    def largestTriangleArea(self, points):
        """
        :type points: List[List[int]]
        :rtype: float
        """
        res = 0
        N = len(points)
        for i in range(N - 2):
            for j in range(i + 1, N - 1):
                for k in range(i + 2, N):
                    (x1, y1), (x2, y2), (x3, y3) = points[i], points[j], points[k]
                    res = max(res, 0.5 * abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)))
        return res

###C++

class Solution {
public:
    double largestTriangleArea(vector<vector<int>>& points) {
        const int N = points.size();
        double res = 0;
        for (int i = 0; i < N - 2; i ++) {
            for (int j = i + 1; j < N - 1; j ++) {
                for (int k = j + 1; k < N; k ++) {
                    auto& point1 = points[i];
                    auto& point2 = points[j];
                    auto& point3 = points[k];
                    int x1 = point1[0], y1 = point1[1];
                    int x2 = point2[0], y2 = point2[1];
                    int x3 = point3[0], y3 = point3[1];
                    res = max(res, 0.5 * abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)));
                }
            }
        }
        return res;
    }
};

###Java

class Solution {
    public double largestTriangleArea(int[][] points) {
        int N = points.length;
        double res = 0;
        for (int i = 0; i < N - 2; i ++) {
            for (int j = i + 1; j < N - 1; j ++) {
                for (int k = j + 1; k < N; k ++) {
                    int[] point1 = points[i];
                    int[] point2 = points[j];
                    int[] point3 = points[k];
                    int x1 = point1[0], y1 = point1[1];
                    int x2 = point2[0], y2 = point2[1];
                    int x3 = point3[0], y3 = point3[1];
                    res = Math.max(res, 0.5 * Math.abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)));
                }
            }
        }
        return res;
    }
}

复杂度

  • 时间复杂度:$O(N^3)$,$N$是给出的坐标数。
  • 空间复杂度:$O(1)$,没用额外空间。

总结

  1. 求三角形的面积是两千年来的经典问题,除了死记公式以外,不妨按照我上面的做法画三条辅助线形成梯形去求。

我是 @负雪明烛 ,刷算法题 1000 多道,写了 1000 多篇算法题解,收获阅读量 300 万。
关注我,你将不会错过我的精彩动画题解、面试题分享、组队刷题活动,进入主页 @负雪明烛 右侧有刷题组织,从此刷题不再孤单。

【鞋带公式/海伦公式/三角形面积公式「枚举」】

思路分析:
因为题目给的数据量也不是太大所以可以枚举每一个三角形,计算面积并找出最大的面积值。根据三角形的三个顶点计算出面积的方法有很多种:

  • 【鞋带公式】,用于计算任意多边形的面积,可用于计算三角形的面积;
  • 【海伦公式】,用三个顶点得到三边长,并使用海伦公式计算出面积;
  • 【三角形面积公式】, S = 1/2 * a * b * sin(C),首先得到两边的长度,通过叉积算出夹角的正弦值,并使用公式计算出面积;

【公式】
B55A7665-CD28-4794-BE9D-28F039D8BB8F_1_201_a.jpeg

具体代码如下:
方法1:【鞋带公式】

###C++

class Solution {
public:
    double largestTriangleArea(vector<vector<int>> &points) {
        double ans = 0.0;
        int n = points.size();
        sort(points.begin(),points.end());
        for(int i = 0; i < n; i ++)
            for(int j = i+1; j < n; j ++)
                for(int k = j+1; k < n; k ++)
                    ans = max(ans,Triang(points[i],points[j],points[k]));
        return ans;
    }
    double Triang(vector<int>& P,vector<int>& Q,vector<int>& R){
        double k = ((P[1] - R[1])*1.0) / (P[0] - R[0]) ;
        return 0.5 * abs(P[0] - R[0]) * abs(k * (Q[0] - P[0]) + P[1] - Q[1]);
    }
};
    

方法2:【海伦公式】

###C++

class Solution {
public:
    double largestTriangleArea(vector<vector<int>>& points) {
        double maxs = 0.0;
        int n = points.size();
        for (int i = 0; i < n; i++) {
            for (int j = i+1; j < n; j++) {
                for (int s = j+1; s < n; s++) {
                    double a = sqrt(pow(abs(points[j][0] - points[i][0]),2)+pow(abs(points[j][1] - points[i][1]),2));
                    double b = sqrt(pow(abs(points[s][0] - points[j][0]),2)+pow(abs(points[s][1] - points[j][1]),2));
                    double c = sqrt(pow(abs(points[i][0] - points[s][0]),2)+pow(abs(points[i][1] - points[s][1]),2));
                    double l = (a+b+c)*0.5;
                    maxs = max(maxs,sqrt(l*(l-a)*(l-b)*(l-c)));
                }
            }
        }
        return maxs;
    }
};

方法3:【三角形面积公式】

###C++

class Solution {
public:
    double largestTriangleArea(vector<vector<int>>& points) {
        double ans = 0.0;
        int n = points.size();
         for(int i = 0; i < n; i ++)
            for(int j = i+1; j < n; j ++)
                for(int k = j+1; k < n; k ++)
                ans = max(ans, Triang(points[i], points[j], points[k]));
        return ans;
    }
    double Triang(vector<int>& P,vector<int>& Q,vector<int>& R){ 
        double a = abs(sqrt(pow((P[0]-Q[0]), 2) + pow((P[1]-Q[1]), 2)));
        double b = abs(sqrt(pow((P[0]-R[0]), 2) + pow((P[1]-R[1]), 2)));
        double c = abs(sqrt(pow((R[0]-Q[0]), 2) + pow((R[1]-Q[1]), 2)));
        double cosC = (a*a + b*b - c*c)/(2*a*b);
        double sinC = sqrt(double(1)-cosC*cosC);
        return 0.5 * a * b * abs(sinC);
    }
};

复杂度分析:

  • 时间复杂度:$O(n^3)$,其中 n 是数组 points 的长度;
  • 空间复杂度:O(1)。

每日一题-三角形最小路径和🟡

给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 ii + 1

 

示例 1:

输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
   2
  3 4
 6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

示例 2:

输入:triangle = [[-10]]
输出:-10

 

提示:

  • 1 <= triangle.length <= 200
  • triangle[0].length == 1
  • triangle[i].length == triangle[i - 1].length + 1
  • -104 <= triangle[i][j] <= 104

 

进阶:

  • 你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题吗?

教你一步步思考 DP:从记忆化搜索到递推到空间优化!(Python/Java/C++/C/Go/JS/Rust)

一、寻找子问题

由于 $\textit{triangle}$ 每排的下标都是从 $0$ 开始的,示例 1 的正确图示应该是左对齐,即

$$
\begin{aligned}
& 2 \
& 3\ 4 \
& 6\ 5\ 7 \
& 4\ 1\ 8\ 3 \
\end{aligned}
$$

我们要解决的问题(原问题)是:

  • 从最上面的 $(0,0)$ 出发,移动到 $\textit{triangle}$ 的最后一排,路径上的元素之和的最小值。

考虑下一步往哪走:

  • 走到 $(1,0)$,那么需要解决的问题为:从 $(1,0)$ 出发,移动到 $\textit{triangle}$ 最后一排,路径上的元素之和的最小值。
  • 走到 $(1,1)$,那么需要解决的问题为:从 $(1,1)$ 出发,移动到 $\textit{triangle}$ 最后一排,路径上的元素之和的最小值。

这些问题都是和原问题相似的、规模更小的子问题,可以用递归解决。

二、状态定义与状态转移方程

根据上面的讨论,定义状态为 $\textit{dfs}(i,j)$,表示从 $(i,j)$ 出发,移动到 $\textit{triangle}$ 最后一排,路径上的元素之和的最小值。

考虑下一步往哪走:

  • 走到 $(i+1,j)$,那么需要解决的问题为:从 $(i+1,j)$ 出发,移动到 $\textit{triangle}$ 最后一排,路径上的元素之和的最小值,即 $\textit{dfs}(i+1,j)$。
  • 走到 $(i+1,j+1)$,那么需要解决的问题为:从 $(i+1,j+1)$ 出发,移动到 $\textit{triangle}$ 最后一排,路径上的元素之和的最小值,即 $\textit{dfs}(i+1,j+1)$。

这两种情况取最小值,再加上当前位置的元素值 $\textit{triangle}[i][j]$ 就得到了 $\textit{dfs}(i,j)$,即

$$
\textit{dfs}(i,j) = \min(\textit{dfs}(i+1,j),\textit{dfs}(i+1,j+1)) + \textit{triangle}[i][j]
$$

递归边界:$\textit{dfs}(n-1,j)=\textit{triangle}[n-1][j]$。走到最后一排就无法再走了,路径上只有一个元素 $\textit{triangle}[n-1][j]$。

递归入口:$\textit{dfs}(0,0)$,这是原问题,也是答案。

三、递归搜索 + 保存递归返回值 = 记忆化搜索

考虑到整个递归过程中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:

  • 如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 $\textit{memo}$ 数组中。
  • 如果一个状态不是第一次遇到($\textit{memo}$ 中保存的结果不等于 $\textit{memo}$ 的初始值),那么可以直接返回 $\textit{memo}$ 中保存的结果。

注意:$\textit{memo}$ 数组的初始值一定不能等于要记忆化的值!例如初始值设置为 $0$,并且要记忆化的 $\textit{dfs}(i,j)$ 也等于 $0$,那就没法判断 $0$ 到底表示第一次遇到这个状态,还是表示之前遇到过了,从而导致记忆化失效。一般把初始值设置为 $-1$。本题由于 $\textit{triangle}[i][j]$ 可以是负数,所以改用 $-\infty$ 作为初始值。

Python 用户可以无视上面这段,直接用 @cache 装饰器。

具体请看视频讲解 动态规划入门:从记忆化搜索到递推,其中包含把记忆化搜索 1:1 翻译成递推的技巧。

###py

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        n = len(triangle)
        @cache  # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
        def dfs(i: int, j: int) -> int:
            if i == n - 1:
                return triangle[i][j]
            return min(dfs(i + 1, j), dfs(i + 1, j + 1)) + triangle[i][j]
        return dfs(0, 0)

###java

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int n = triangle.size();
        int[][] memo = new int[n][n];
        for (int[] row : memo) {
            Arrays.fill(row, Integer.MIN_VALUE); // Integer.MIN_VALUE 表示没有计算过
        }
        return dfs(triangle, 0, 0, memo);
    }

    private int dfs(List<List<Integer>> triangle, int i, int j, int[][] memo) {
        if (i == triangle.size() - 1) {
            return triangle.get(i).get(j);
        }
        if (memo[i][j] != Integer.MIN_VALUE) { // 之前计算过
            return memo[i][j];
        }
        return memo[i][j] = Math.min(dfs(triangle, i + 1, j, memo),
                dfs(triangle, i + 1, j + 1, memo)) + triangle.get(i).get(j);
    }
}

###cpp

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n = triangle.size();
        vector memo(n, vector<int>(n, INT_MIN)); // INT_MIN 表示没有计算过
        // lambda 递归
        auto dfs = [&](this auto&& dfs, int i, int j) -> int {
            if (i == n - 1) {
                return triangle[i][j];
            }
            int& res = memo[i][j]; // 注意这里是引用
            if (res != INT_MIN) { // 之前计算过
                return res;
            }
            return res = min(dfs(i + 1, j), dfs(i + 1, j + 1)) + triangle[i][j];
        };
        return dfs(0, 0);
    }
};

###c

#define MIN(a, b) ((b) < (a) ? (b) : (a))

int dfs(int** triangle, int i, int j, int n, int** memo) {
    if (i == n - 1) {
        return triangle[i][j];
    }
    if (memo[i][j] != 0x3f3f3f3f) { // 之前计算过
        return memo[i][j];
    }
    int res1 = dfs(triangle, i + 1, j, n, memo);
    int res2 = dfs(triangle, i + 1, j + 1, n, memo);
    return memo[i][j] = MIN(res1, res2) + triangle[i][j];
}

int minimumTotal(int** triangle, int triangleSize, int* triangleColSize) {
    int** memo = malloc(triangleSize * sizeof(int*));
    for (int i = 0; i < triangleSize; i++) {
        memo[i] = malloc((i + 1) * sizeof(int));
        memset(memo[i], 0x3f, triangleColSize[i] * sizeof(int));
    }

    int ans = dfs(triangle, 0, 0, triangleSize, memo);

    for (int i = 0; i < triangleSize; i++) {
        free(memo[i]);
    }
    free(memo);
    return ans;
}

###go

func minimumTotal(triangle [][]int) int {
    n := len(triangle)
    memo := make([][]int, n)
    for i := range memo {
        memo[i] = make([]int, n)
        for j := range memo[i] {
            memo[i][j] = math.MinInt // math.MinInt 表示没有计算过
        }
    }
    var dfs func(int, int) int
    dfs = func(i, j int) int {
        if i == n-1 {
            return triangle[i][j]
        }
        p := &memo[i][j]
        if *p != math.MinInt { // 之前计算过
            return *p
        }
        *p = min(dfs(i+1, j), dfs(i+1, j+1)) + triangle[i][j]
        return *p
    }
    return dfs(0, 0)
}

###js

var minimumTotal = function(triangle) {
    const n = triangle.length;
    const memo = Array.from({ length: n }, () => Array(n));
    function dfs(i, j) {
        if (i === n - 1) {
            return triangle[i][j];
        }
        if (memo[i][j] !== undefined) { // 之前计算过
            return memo[i][j];
        }
        return memo[i][j] = Math.min(dfs(i + 1, j), dfs(i + 1, j + 1)) + triangle[i][j];
    }
    return dfs(0, 0);
};

###rust

impl Solution {
    pub fn minimum_total(triangle: Vec<Vec<i32>>) -> i32 {
        fn dfs(i: usize, j: usize, triangle: &[Vec<i32>], memo: &mut [Vec<i32>]) -> i32 {
            if i == triangle.len() - 1 {
                return triangle[i][j];
            }
            if memo[i][j] != i32::MIN { // 之前计算过
                return memo[i][j];
            }
            memo[i][j] = dfs(i + 1, j, triangle, memo).min(dfs(i + 1, j + 1, triangle, memo)) + triangle[i][j];
            memo[i][j]
        }
        let n = triangle.len();
        let mut memo = vec![vec![i32::MIN; n]; n]; // i32::MIN 表示没有计算过
        dfs(0, 0, &triangle, &mut memo)
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^2)$,其中 $n$ 为 $\textit{triangle}$ 的长度。由于每个状态只会计算一次,动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题状态个数等于 $\mathcal{O}(n^2)$,单个状态的计算时间为 $\mathcal{O}(1)$,所以总的时间复杂度为 $\mathcal{O}(n^2)$。
  • 空间复杂度:$\mathcal{O}(n^2)$。保存多少状态,就需要多少空间。

四、1:1 翻译成递推

我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。

具体来说,$f[i][j]$ 的定义和 $\textit{dfs}(i,j)$ 的定义是一样的,都表示从 $(i,j)$ 出发,移动到 $\textit{triangle}$ 最后一排,路径上的元素之和的最小值。

相应的递推式(状态转移方程)也和 $\textit{dfs}$ 一样:

$$
f[i][j] = \min(f[i+1][j],f[i+1][j+1]) + \textit{triangle}[i][j]
$$

初始值 $f[n-1][j]=\textit{triangle}[n-1][j]$,翻译自递归边界 $\textit{dfs}(n-1,j)=\textit{triangle}[n-1][j]$。

答案为 $f[0][0]$,翻译自递归入口 $\textit{dfs}(0,0)$。

答疑

:如何思考循环顺序?什么时候要正序枚举,什么时候要倒序枚举?

:这里有一个通用的做法:盯着状态转移方程,想一想,要计算 $f[i][j]$,必须先把 $f[i+1][j]$ 和 $f[i+1][j+1]$ 算出来,那么只有 $i$ 从大到小枚举才能做到。对于 $j$ 来说,正序倒序都可以。

###py

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        n = len(triangle)
        f = [[0] * (i + 1) for i in range(n)]
        f[-1] = triangle[-1]
        for i in range(n - 2, -1, -1):
            for j, x in enumerate(triangle[i]):
                f[i][j] = min(f[i + 1][j], f[i + 1][j + 1]) + x
        return f[0][0]

###java

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int n = triangle.size();
        int[][] f = new int[n][n];
        for (int j = 0; j < n; j++) {
            f[n - 1][j] = triangle.get(n - 1).get(j);
        }
        for (int i = n - 2; i >= 0; i--) {
            for (int j = 0; j <= i; j++) {
                f[i][j] = Math.min(f[i + 1][j], f[i + 1][j + 1]) + triangle.get(i).get(j);
            }
        }
        return f[0][0];
    }
}

###cpp

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n = triangle.size();
        vector f(n, vector<int>(n));
        f[n - 1] = triangle[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            for (int j = 0; j <= i; j++) {
                f[i][j] = min(f[i + 1][j], f[i + 1][j + 1]) + triangle[i][j];
            }
        }
        return f[0][0];
    }
};

###c

#define MIN(a, b) ((b) < (a) ? (b) : (a))

int minimumTotal(int** triangle, int triangleSize, int* triangleColSize) {
    int** f = malloc(triangleSize * sizeof(int*));
    for (int i = 0; i < triangleSize; i++) {
        f[i] = malloc((i + 1) * sizeof(int));
    }
    f[triangleSize - 1] = triangle[triangleSize - 1];
    for (int i = triangleSize - 2; i >= 0; i--) {
        for (int j = 0; j <= i; j++) {
            f[i][j] = MIN(f[i + 1][j], f[i + 1][j + 1]) + triangle[i][j];
        }
    }
    int ans = f[0][0];
    for (int i = 0; i < triangleSize; i++) {
        free(f[i]);
    }
    free(f);
    return ans;
}

###go

func minimumTotal(triangle [][]int) int {
    n := len(triangle)
    f := make([][]int, n)
    for i := range f {
        f[i] = make([]int, i+1)
    }
    f[n-1] = triangle[n-1]
    for i := n - 2; i >= 0; i-- {
        for j, x := range triangle[i] {
            f[i][j] = min(f[i+1][j], f[i+1][j+1]) + x
        }
    }
    return f[0][0]
}

###js

var minimumTotal = function(triangle) {
    const n = triangle.length;
    const f = Array.from({ length: n }, () => Array(n));
    f[n - 1] = triangle[n - 1];
    for (let i = n - 2; i >= 0; i--) {
        for (let j = 0; j <= i; j++) {
            f[i][j] = Math.min(f[i + 1][j], f[i + 1][j + 1]) + triangle[i][j];
        }
    }
    return f[0][0];
};

###rust

impl Solution {
    pub fn minimum_total(triangle: Vec<Vec<i32>>) -> i32 {
        let n = triangle.len();
        let mut f = vec![vec![0; n]; n];
        f[n - 1] = triangle[n - 1].clone();
        for i in (0..n - 1).rev() {
            for (j, &x) in triangle[i].iter().enumerate() {
                f[i][j] = f[i + 1][j].min(f[i + 1][j + 1]) + x;
            }
        }
        f[0][0]
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^2)$,其中 $n$ 为 $\textit{triangle}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n^2)$。

五、空间优化

也可以直接把 $\textit{triangle}$ 当作 $f$ 数组。

###py

class Solution:
    def minimumTotal(self, f: List[List[int]]) -> int:
        for i in range(len(f) - 2, -1, -1):
            for j in range(i + 1):
                f[i][j] += min(f[i + 1][j], f[i + 1][j + 1])
        return f[0][0]

###java

class Solution {
    public int minimumTotal(List<List<Integer>> f) {
        for (int i = f.size() - 2; i >= 0; i--) {
            for (int j = 0; j <= i; j++) {
                f.get(i).set(j, f.get(i).get(j) + Math.min(f.get(i + 1).get(j), f.get(i + 1).get(j + 1)));
            }
        }
        return f.get(0).get(0);
    }
}

###cpp

class Solution {
public:
    int minimumTotal(vector<vector<int>>& f) {
        for (int i = f.size() - 2; i >= 0; i--) {
            for (int j = 0; j <= i; j++) {
                f[i][j] += min(f[i + 1][j], f[i + 1][j + 1]);
            }
        }
        return f[0][0];
    }
};

###c

#define MIN(a, b) ((b) < (a) ? (b) : (a))

int minimumTotal(int** f, int triangleSize, int* triangleColSize) {
    for (int i = triangleSize - 2; i >= 0; i--) {
        for (int j = 0; j <= i; j++) {
            f[i][j] += MIN(f[i + 1][j], f[i + 1][j + 1]);
        }
    }
    return f[0][0];
}

###go

func minimumTotal(f [][]int) int {
    for i := len(f) - 2; i >= 0; i-- {
        for j := range f[i] {
            f[i][j] += min(f[i+1][j], f[i+1][j+1])
        }
    }
    return f[0][0]
}

###js

var minimumTotal = function(f) {
    for (let i = f.length - 2; i >= 0; i--) {
        for (let j = 0; j <= i; j++) {
            f[i][j] += Math.min(f[i + 1][j], f[i + 1][j + 1]);
        }
    }
    return f[0][0];
};

###rust

impl Solution {
    pub fn minimum_total(mut f: Vec<Vec<i32>>) -> i32 {
        for i in (0..f.len() - 1).rev() {
            for j in 0..=i {
                f[i][j] += f[i + 1][j].min(f[i + 1][j + 1]);
            }
        }
        f[0][0]
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^2)$,其中 $n$ 为 $\textit{triangle}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

专题训练

见下面动态规划题单的「二、网格图 DP」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

递归 + 记忆化 + DP,🤷‍♀️ 必须秒懂!

🙋 今日打卡~

一、题目分析

题意
给定三角形,每次只能移动到下一行中的相邻结点,求从顶点到底边的最小路径和。

###Java

[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
相邻结点:与 `(i, j)` 点相邻的结点为 `(i + 1, j)` 和 `(i + 1, j + 1)`。

分析
若定义 $f(i, j)$ 为 $(i, j)$ 点到底边的最小路径和,则易知递归求解式为:

$f(i, j) = min(f(i + 1, j), f(i + 1, j + 1)) + triangle[i][j]$

由此,我们将任一点到底边的最小路径和,转化成了与该点相邻两点到底边的最小路径和中的较小值,再加上该点本身的值。这样本题的 递归解法(解法一) 就完成了。

二、具体实现

解法一:递归

###Java

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        return  dfs(triangle, 0, 0);
    }

    private int dfs(List<List<Integer>> triangle, int i, int j) {
        if (i == triangle.size()) {
            return 0;
        }
        return Math.min(dfs(triangle, i + 1, j), dfs(triangle, i + 1, j + 1)) + triangle.get(i).get(j);
    }
}

暴力搜索会有大量的重复计算,导致 超时,因此在 解法二 中结合记忆化数组进行优化。

解法二:递归 + 记忆化

在解法一的基础上,定义了二维数组进行记忆化。

###Java

class Solution {
    Integer[][] memo;
    public int minimumTotal(List<List<Integer>> triangle) {
        memo = new Integer[triangle.size()][triangle.size()];
        return  dfs(triangle, 0, 0);
    }

    private int dfs(List<List<Integer>> triangle, int i, int j) {
        if (i == triangle.size()) {
            return 0;
        }
        if (memo[i][j] != null) {
            return memo[i][j];
        }
        return memo[i][j] = Math.min(dfs(triangle, i + 1, j), dfs(triangle, i + 1, j + 1)) + triangle.get(i).get(j);
    }
}

时间复杂度:$O(N^2)$,$N$ 为三角形的行数。
空间复杂度:$O(N^2)$,$N$ 为三角形的行数。

解法三:动态规划

定义二维 dp 数组,将解法二中「自顶向下的递归」改为「自底向上的递推」。

1、状态定义:

$dp[i][j]$ 表示从点 $(i, j)$ 到底边的最小路径和。

2、状态转移:

$dp[i][j] = min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j]$

3、代码实现:

###Java

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int n = triangle.size();
        // dp[i][j] 表示从点 (i, j) 到底边的最小路径和。
        int[][] dp = new int[n + 1][n + 1];
        // 从三角形的最后一行开始递推。
        for (int i = n - 1; i >= 0; i--) {
            for (int j = 0; j <= i; j++) {
                dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle.get(i).get(j);
            }
        }
        return dp[0][0];
    }
}

时间复杂度:$O(N^2)$,$N$ 为三角形的行数。
空间复杂度:$O(N^2)$,$N$ 为三角形的行数。

4、空间优化

在上述代码中,我们定义了一个 $N$ 行 $N$ 列 的 $dp$ 数组($N$ 是三角形的行数)。
但是在实际递推中我们发现,计算 $dp[i][j]$ 时,只用到了下一行的 $dp[i + 1][j]$ 和 $dp[i + 1][j + 1]$。
因此 $dp$ 数组不需要定义 $N$ 行,只要定义 $1$ 行就阔以啦。
所以我们稍微修改一下上述代码,将 $i$ 所在的维度去掉(如下),就可以将 $O(N^2)$ 的空间复杂度优化成 $O(N)$ 啦~

###Java

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int n = triangle.size();
        int[] dp = new int[n + 1];
        for (int i = n - 1; i >= 0; i--) {
            for (int j = 0; j <= i; j++) {
                dp[j] = Math.min(dp[j], dp[j + 1]) + triangle.get(i).get(j);
            }
        }
        return dp[0];
    }
}

时间复杂度:$O(N^2)$,$N$ 为三角形的行数。
空间复杂度:$O(N)$,$N$ 为三角形的行数。

三角形最小路径和

前言

本题是一道非常经典且历史悠久的动态规划题,其作为算法题出现,最早可以追溯到 1994 年的 IOI(国际信息学奥林匹克竞赛)的 The Triangle。时光飞逝,经过 20 多年的沉淀,往日的国际竞赛题如今已经变成了动态规划的入门必做题,不断督促着我们学习和巩固算法。

在本题中,给定的三角形的行数为 $n$,并且第 $i$ 行(从 $0$ 开始编号)包含了 $i+1$ 个数。如果将每一行的左端对齐,那么会形成一个等腰直角三角形,如下所示:

[2]
[3,4]
[6,5,7]
[4,1,8,3]

方法一:动态规划

思路与算法

我们用 $f[i][j]$ 表示从三角形顶部走到位置 $(i, j)$ 的最小路径和。这里的位置 $(i, j)$ 指的是三角形中第 $i$ 行第 $j$ 列(均从 $0$ 开始编号)的位置。

由于每一步只能移动到下一行「相邻的节点」上,因此要想走到位置 $(i, j)$,上一步就只能在位置 $(i - 1, j - 1)$ 或者位置 $(i - 1, j)$。我们在这两个位置中选择一个路径和较小的来进行转移,状态转移方程为:

$$
f[i][j] = \min(f[i-1][j-1], f[i-1][j]) + c[i][j]
$$

其中 $c[i][j]$ 表示位置 $(i, j)$ 对应的元素值。

注意第 $i$ 行有 $i+1$ 个元素,它们对应的 $j$ 的范围为 $[0, i]$。当 $j=0$ 或 $j=i$ 时,上述状态转移方程中有一些项是没有意义的。例如当 $j=0$ 时,$f[i-1][j-1]$ 没有意义,因此状态转移方程为:

$$
f[i][0] = f[i-1][0] + c[i][0]
$$

即当我们在第 $i$ 行的最左侧时,我们只能从第 $i-1$ 行的最左侧移动过来。当 $j=i$ 时,$f[i-1][j]$ 没有意义,因此状态转移方程为:

$$
f[i][i] = f[i-1][i-1] + c[i][i]
$$

即当我们在第 $i$ 行的最右侧时,我们只能从第 $i-1$ 行的最右侧移动过来。

最终的答案即为 $f[n-1][0]$ 到 $f[n-1][n-1]$ 中的最小值,其中 $n$ 是三角形的行数。

细节

状态转移方程的边界条件是什么?由于我们已经去除了所有「没有意义」的状态,因此边界条件可以定为:

$$
f[0][0] = c[0][0]
$$

即在三角形的顶部时,最小路径和就等于对应位置的元素值。这样一来,我们从 $1$ 开始递增地枚举 $i$,并在 $[0, i]$ 的范围内递增地枚举 $j$,就可以完成所有状态的计算。

###C++

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n = triangle.size();
        vector<vector<int>> f(n, vector<int>(n));
        f[0][0] = triangle[0][0];
        for (int i = 1; i < n; ++i) {
            f[i][0] = f[i - 1][0] + triangle[i][0];
            for (int j = 1; j < i; ++j) {
                f[i][j] = min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j];
            }
            f[i][i] = f[i - 1][i - 1] + triangle[i][i];
        }
        return *min_element(f[n - 1].begin(), f[n - 1].end());
    }
};

###Java

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int n = triangle.size();
        int[][] f = new int[n][n];
        f[0][0] = triangle.get(0).get(0);
        for (int i = 1; i < n; ++i) {
            f[i][0] = f[i - 1][0] + triangle.get(i).get(0);
            for (int j = 1; j < i; ++j) {
                f[i][j] = Math.min(f[i - 1][j - 1], f[i - 1][j]) + triangle.get(i).get(j);
            }
            f[i][i] = f[i - 1][i - 1] + triangle.get(i).get(i);
        }
        int minTotal = f[n - 1][0];
        for (int i = 1; i < n; ++i) {
            minTotal = Math.min(minTotal, f[n - 1][i]);
        }
        return minTotal;
    }
}

###Python

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        n = len(triangle)
        f = [[0] * n for _ in range(n)]
        f[0][0] = triangle[0][0]

        for i in range(1, n):
            f[i][0] = f[i - 1][0] + triangle[i][0]
            for j in range(1, i):
                f[i][j] = min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j]
            f[i][i] = f[i - 1][i - 1] + triangle[i][i]
        
        return min(f[n - 1])

###Go

func minimumTotal(triangle [][]int) int {
    n := len(triangle)
    f := make([][]int, n)
    for i := 0; i < n; i++ {
        f[i] = make([]int, n)
    }
    f[0][0] = triangle[0][0]
    for i := 1; i < n; i++ {
        f[i][0] = f[i - 1][0] + triangle[i][0]
        for j := 1; j < i; j++ {
            f[i][j] = min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j]
        }
        f[i][i] = f[i - 1][i - 1] + triangle[i][i]
    }
    ans := math.MaxInt32
    for i := 0; i < n; i++ {
        ans = min(ans, f[n-1][i])
    }
    return ans
}

func min(x, y int) int {
    if x < y {
        return x
    }
    return y
}

###C

int minimumTotal(int** triangle, int triangleSize, int* triangleColSize) {
    int f[triangleSize][triangleSize];
    memset(f, 0, sizeof(f));
    f[0][0] = triangle[0][0];
    for (int i = 1; i < triangleSize; ++i) {
        f[i][0] = f[i - 1][0] + triangle[i][0];
        for (int j = 1; j < i; ++j) {
            f[i][j] = fmin(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j];
        }
        f[i][i] = f[i - 1][i - 1] + triangle[i][i];
    }
    int ret = f[triangleSize - 1][0];
    for (int i = 1; i < triangleSize; i++)
        ret = fmin(ret, f[triangleSize - 1][i]);
    return ret;
}

###C#

public class Solution {
    public int MinimumTotal(IList<IList<int>> triangle) {
        int n = triangle.Count;
        int[][] f = new int[n][];
        for (int i = 0; i < n; i++) {
            f[i] = new int[n];
        }
        f[0][0] = triangle[0][0];
        for (int i = 1; i < n; ++i) {
            f[i][0] = f[i - 1][0] + triangle[i][0];
            for (int j = 1; j < i; ++j) {
                f[i][j] = Math.Min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j];
            }
            f[i][i] = f[i - 1][i - 1] + triangle[i][i];
        }
        return f[n - 1].Min();
    }
}

###JavaScript

var minimumTotal = function(triangle) {
    const n = triangle.length;
    const f = Array(n).fill(0).map(() => Array(n).fill(0));
    f[0][0] = triangle[0][0];
    for (let i = 1; i < n; ++i) {
        f[i][0] = f[i - 1][0] + triangle[i][0];
        for (let j = 1; j < i; ++j) {
            f[i][j] = Math.min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j];
        }
        f[i][i] = f[i - 1][i - 1] + triangle[i][i];
    }
    return Math.min(...f[n - 1]);
};

###TypeScript

function minimumTotal(triangle: number[][]): number {
    const n = triangle.length;
    const f: number[][] = Array(n).fill(0).map(() => Array(n).fill(0));
    f[0][0] = triangle[0][0];
    for (let i = 1; i < n; ++i) {
        f[i][0] = f[i - 1][0] + triangle[i][0];
        for (let j = 1; j < i; ++j) {
            f[i][j] = Math.min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j];
        }
        f[i][i] = f[i - 1][i - 1] + triangle[i][i];
    }
    return Math.min(...f[n - 1]);
};

###Rust

impl Solution {
    pub fn minimum_total(triangle: Vec<Vec<i32>>) -> i32 {
        let n = triangle.len();
        let mut f = vec![vec![0; n]; n];
        f[0][0] = triangle[0][0];
        for i in 1..n {
            f[i][0] = f[i - 1][0] + triangle[i][0];
            for j in 1..i {
                f[i][j] = std::cmp::min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j];
            }
            f[i][i] = f[i - 1][i - 1] + triangle[i][i];
        }
        *f[n - 1].iter().min().unwrap()
    }
}

复杂度分析

  • 时间复杂度:$O(n^2)$,其中 $n$ 是三角形的行数。

  • 空间复杂度:$O(n^2)$。我们需要一个 $n*n$ 的二维数组存放所有的状态。

方法二:动态规划 + 空间优化

思路与算法

在题目描述中的「进阶」部分,提到了可以将空间复杂度优化至 $O(n)$。

我们回顾方法一中的状态转移方程:

$$
\begin{aligned}
f[i][j] = \begin{cases}
f[i-1][0] + c[i][0], & j=0\
f[i-1][i-1] + c[i][i], & j=i \
\min(f[i-1][j-1], f[i-1][j]) + c[i][j], & \text{otherwise}
\end{cases}
\end{aligned}
$$

可以发现,$f[i][j]$ 只与 $f[i-1][..]$ 有关,而与 $f[i-2][..]$ 及之前的状态无关,因此我们不必存储这些无关的状态。具体地,我们使用两个长度为 $n$ 的一维数组进行转移,将 $i$ 根据奇偶性映射到其中一个一维数组,那么 $i-1$ 就映射到了另一个一维数组。这样我们使用这两个一维数组,交替地进行状态转移。

###C++

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n = triangle.size();
        vector<vector<int>> f(2, vector<int>(n));
        f[0][0] = triangle[0][0];
        for (int i = 1; i < n; ++i) {
            int curr = i % 2;
            int prev = 1 - curr;
            f[curr][0] = f[prev][0] + triangle[i][0];
            for (int j = 1; j < i; ++j) {
                f[curr][j] = min(f[prev][j - 1], f[prev][j]) + triangle[i][j];
            }
            f[curr][i] = f[prev][i - 1] + triangle[i][i];
        }
        return *min_element(f[(n - 1) % 2].begin(), f[(n - 1) % 2].end());
    }
};

###Java

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int n = triangle.size();
        int[][] f = new int[2][n];
        f[0][0] = triangle.get(0).get(0);
        for (int i = 1; i < n; ++i) {
            int curr = i % 2;
            int prev = 1 - curr;
            f[curr][0] = f[prev][0] + triangle.get(i).get(0);
            for (int j = 1; j < i; ++j) {
                f[curr][j] = Math.min(f[prev][j - 1], f[prev][j]) + triangle.get(i).get(j);
            }
            f[curr][i] = f[prev][i - 1] + triangle.get(i).get(i);
        }
        int minTotal = f[(n - 1) % 2][0];
        for (int i = 1; i < n; ++i) {
            minTotal = Math.min(minTotal, f[(n - 1) % 2][i]);
        }
        return minTotal;
    }
}

###Python

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        n = len(triangle)
        f = [[0] * n for _ in range(2)]
        f[0][0] = triangle[0][0]

        for i in range(1, n):
            curr, prev = i % 2, 1 - i % 2
            f[curr][0] = f[prev][0] + triangle[i][0]
            for j in range(1, i):
                f[curr][j] = min(f[prev][j - 1], f[prev][j]) + triangle[i][j]
            f[curr][i] = f[prev][i - 1] + triangle[i][i]
        
        return min(f[(n - 1) % 2])

###Go

func minimumTotal(triangle [][]int) int {
    n := len(triangle)
    f := [2][]int{}
    for i := 0; i < 2; i++ {
        f[i] = make([]int, n)
    }
    f[0][0] = triangle[0][0]
    for i := 1; i < n; i++ {
        curr := i % 2
        prev := 1 - curr
        f[curr][0] = f[prev][0] + triangle[i][0]
        for j := 1; j < i; j++ {
            f[curr][j] = min(f[prev][j - 1], f[prev][j]) + triangle[i][j]
        }
        f[curr][i] = f[prev][i - 1] + triangle[i][i]
    }
    ans := math.MaxInt32
    for i := 0; i < n; i++ {
        ans = min(ans, f[(n-1)%2][i])
    }
    return ans
}

func min(x, y int) int {
    if x < y {
        return x
    }
    return y
}

###C

int minimumTotal(int** triangle, int triangleSize, int* triangleColSize) {
    int f[2][triangleSize];
    memset(f, 0, sizeof(f));
    f[0][0] = triangle[0][0];
    for (int i = 1; i < triangleSize; ++i) {
        int curr = i % 2;
        int prev = 1 - curr;
        f[curr][0] = f[prev][0] + triangle[i][0];
        for (int j = 1; j < i; ++j) {
            f[curr][j] = fmin(f[prev][j - 1], f[prev][j]) + triangle[i][j];
        }
        f[curr][i] = f[prev][i - 1] + triangle[i][i];
    }
    int ret = f[(triangleSize - 1) % 2][0];
    for (int i = 1; i < triangleSize; i++)
        ret = fmin(ret, f[(triangleSize - 1) % 2][i]);
    return ret;
}

###C#

public class Solution {
    public int MinimumTotal(IList<IList<int>> triangle) {
        int n = triangle.Count;
        int[][] f = new int[2][];
        f[0] = new int[n];
        f[1] = new int[n];
        f[0][0] = triangle[0][0];
        
        for (int i = 1; i < n; ++i) {
            int curr = i % 2;
            int prev = 1 - curr;
            f[curr][0] = f[prev][0] + triangle[i][0];
            for (int j = 1; j < i; ++j) {
                f[curr][j] = Math.Min(f[prev][j - 1], f[prev][j]) + triangle[i][j];
            }
            f[curr][i] = f[prev][i - 1] + triangle[i][i];
        }
        
        return f[(n - 1) % 2].Take(n).Min();
    }
}

###JavaScript

var minimumTotal = function(triangle) {
    const n = triangle.length;
    const f = Array.from({length: 2}, () => new Array(n).fill(0));
    f[0][0] = triangle[0][0];
    
    for (let i = 1; i < n; ++i) {
        const curr = i % 2;
        const prev = 1 - curr;
        f[curr][0] = f[prev][0] + triangle[i][0];
        for (let j = 1; j < i; ++j) {
            f[curr][j] = Math.min(f[prev][j - 1], f[prev][j]) + triangle[i][j];
        }
        f[curr][i] = f[prev][i - 1] + triangle[i][i];
    }
    
    return Math.min(...f[(n - 1) % 2].slice(0, n));
};

###TypeScript

function minimumTotal(triangle: number[][]): number {
    const n = triangle.length;
    const f: number[][] = Array.from({length: 2}, () => new Array(n).fill(0));
    f[0][0] = triangle[0][0];
    
    for (let i = 1; i < n; ++i) {
        const curr = i % 2;
        const prev = 1 - curr;
        f[curr][0] = f[prev][0] + triangle[i][0];
        for (let j = 1; j < i; ++j) {
            f[curr][j] = Math.min(f[prev][j - 1], f[prev][j]) + triangle[i][j];
        }
        f[curr][i] = f[prev][i - 1] + triangle[i][i];
    }
    
    return Math.min(...f[(n - 1) % 2].slice(0, n));
};

###Rust

impl Solution {
    pub fn minimum_total(triangle: Vec<Vec<i32>>) -> i32 {
        let n = triangle.len();
        let mut f = vec![vec![0; n]; 2];
        f[0][0] = triangle[0][0];
        
        for i in 1..n {
            let curr = i % 2;
            let prev = 1 - curr;
            f[curr][0] = f[prev][0] + triangle[i][0];
            for j in 1..i {
                f[curr][j] = std::cmp::min(f[prev][j - 1], f[prev][j]) + triangle[i][j];
            }
            f[curr][i] = f[prev][i - 1] + triangle[i][i];
        }
        
        *f[(n - 1) % 2][..n].iter().min().unwrap()
    }
}

上述方法的空间复杂度为 $O(n)$,使用了 $2n$ 的空间存储状态。我们还可以继续进行优化吗?

答案是可以的。我们从 $i$ 到 $0$ 递减地枚举 $j$,这样我们只需要一个长度为 $n$ 的一维数组 $f$,就可以完成状态转移。

为什么只有在递减地枚举 $j$ 时,才能省去一个一维数组?当我们在计算位置 $(i, j)$ 时,$f[j+1]$ 到 $f[i]$ 已经是第 $i$ 行的值,而 $f[0]$ 到 $f[j]$ 仍然是第 $i-1$ 行的值。此时我们直接通过

$$
f[j] = \min(f[j-1], f[j]) + c[i][j]
$$

进行转移,恰好就是在 $(i-1, j-1)$ 和 $(i-1, j)$ 中进行选择。但如果我们递增地枚举 $j$,那么在计算位置 $(i, j)$ 时,$f[0]$ 到 $f[j-1]$ 已经是第 $i$ 行的值。如果我们仍然使用上述状态转移方程,那么是在 $(i, j-1)$ 和 $(i-1, j)$ 中进行选择,就产生了错误。

这样虽然空间复杂度仍然为 $O(n)$,但我们只使用了 $n$ 的空间存储状态,减少了一半的空间消耗。

###C++

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n = triangle.size();
        vector<int> f(n);
        f[0] = triangle[0][0];
        for (int i = 1; i < n; ++i) {
            f[i] = f[i - 1] + triangle[i][i];
            for (int j = i - 1; j > 0; --j) {
                f[j] = min(f[j - 1], f[j]) + triangle[i][j];
            }
            f[0] += triangle[i][0];
        }
        return *min_element(f.begin(), f.end());
    }
};

###Java

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int n = triangle.size();
        int[] f = new int[n];
        f[0] = triangle.get(0).get(0);
        for (int i = 1; i < n; ++i) {
            f[i] = f[i - 1] + triangle.get(i).get(i);
            for (int j = i - 1; j > 0; --j) {
                f[j] = Math.min(f[j - 1], f[j]) + triangle.get(i).get(j);
            }
            f[0] += triangle.get(i).get(0);
        }
        int minTotal = f[0];
        for (int i = 1; i < n; ++i) {
            minTotal = Math.min(minTotal, f[i]);
        }
        return minTotal;
    }
}

###Python

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        n = len(triangle)
        f = [0] * n
        f[0] = triangle[0][0]

        for i in range(1, n):
            f[i] = f[i - 1] + triangle[i][i]
            for j in range(i - 1, 0, -1):
                f[j] = min(f[j - 1], f[j]) + triangle[i][j]
            f[0] += triangle[i][0]
        
        return min(f)

###Go

func minimumTotal(triangle [][]int) int {
    n := len(triangle)
    f := make([]int, n)
    f[0] = triangle[0][0]
    for i := 1; i < n; i++ {
        f[i] = f[i - 1] + triangle[i][i]
        for j := i - 1; j > 0; j-- {
            f[j] = min(f[j - 1], f[j]) + triangle[i][j]
        }
        f[0] += triangle[i][0]
    }
    ans := math.MaxInt32
    for i := 0; i < n; i++ {
        ans = min(ans, f[i])
    }
    return ans
}

func min(x, y int) int {
    if x < y {
        return x
    }
    return y
}

###C

int minimumTotal(int** triangle, int triangleSize, int* triangleColSize) {
    int f[triangleSize];
    memset(f, 0, sizeof(f));
    f[0] = triangle[0][0];
    for (int i = 1; i < triangleSize; ++i) {
        f[i] = f[i - 1] + triangle[i][i];
        for (int j = i - 1; j > 0; --j) {
            f[j] = fmin(f[j - 1], f[j]) + triangle[i][j];
        }
        f[0] += triangle[i][0];
    }
    int ret = f[0];
    for (int i = 1; i < triangleSize; i++) ret = fmin(ret, f[i]);
    return ret;
}

###C#

public class Solution {
    public int MinimumTotal(IList<IList<int>> triangle) {
        int n = triangle.Count;
        int[] f = new int[n];
        f[0] = triangle[0][0];
        
        for (int i = 1; i < n; ++i) {
            f[i] = f[i - 1] + triangle[i][i];
            for (int j = i - 1; j > 0; --j) {
                f[j] = Math.Min(f[j - 1], f[j]) + triangle[i][j];
            }
            f[0] += triangle[i][0];
        }
        
        return f.Min();
    }
}

###JavaScript

var minimumTotal = function(triangle) {
    const n = triangle.length;
    const f = new Array(n).fill(0);
    f[0] = triangle[0][0];
    
    for (let i = 1; i < n; ++i) {
        f[i] = f[i - 1] + triangle[i][i];
        for (let j = i - 1; j > 0; --j) {
            f[j] = Math.min(f[j - 1], f[j]) + triangle[i][j];
        }
        f[0] += triangle[i][0];
    }
    
    return Math.min(...f);
};

###TypeScript

function minimumTotal(triangle: number[][]): number {
    const n = triangle.length;
    const f: number[] = new Array(n).fill(0);
    f[0] = triangle[0][0];
    
    for (let i = 1; i < n; ++i) {
        f[i] = f[i - 1] + triangle[i][i];
        for (let j = i - 1; j > 0; --j) {
            f[j] = Math.min(f[j - 1], f[j]) + triangle[i][j];
        }
        f[0] += triangle[i][0];
    }
    
    return Math.min(...f);
};

###Rust

impl Solution {
    pub fn minimum_total(triangle: Vec<Vec<i32>>) -> i32 {
        let n = triangle.len();
        let mut f = vec![0; n];
        f[0] = triangle[0][0];
        
        for i in 1..n {
            f[i] = f[i - 1] + triangle[i][i];
            for j in (1..i).rev() {
                f[j] = std::cmp::min(f[j - 1], f[j]) + triangle[i][j];
            }
            f[0] += triangle[i][0];
        }
        
        *f.iter().min().unwrap()
    }
}

复杂度分析

  • 时间复杂度:$O(n^2)$,其中 $n$ 是三角形的行数。

  • 空间复杂度:$O(n)$。

结语

本题还有一些其它的动态规划方法,例如:

  • 从三角形的底部开始转移,到顶部结束;

  • 直接在给定的三角形数组上进行状态转移,不使用额外的空间。

读者可以自行尝试。如果在面试中遇到类似的题目,需要和面试官进行沟通,可以询问「是否有空间复杂度限制」「是否可以修改原数组」等问题,给出符合条件的算法。

模拟长除法(Python/Java/C++/Go/JS/Rust)

我们可以用长除法计算小数。来看两个例子。

例一:9/8 = 1.125

读者可以先在纸上算算 $9/8$,方便理解下述流程。

整数部分为 $\left\lfloor\dfrac{9}{8}\right\rfloor = 1$,初始余数为 $r = 9\bmod 8 = 1$。

  1. $r=1$。计算商 $\left\lfloor\dfrac{r\cdot 10}{8}\right\rfloor = 1$,得到小数点后第一位,更新 $r$ 为 $(r\cdot 10)\bmod 8 = 2$。
  2. $r=2$。计算商 $\left\lfloor\dfrac{r\cdot 10}{8}\right\rfloor = 2$,得到小数点后第二位,更新 $r$ 为 $(r\cdot 10)\bmod 8 = 4$。
  3. $r=4$。计算商 $\left\lfloor\dfrac{r\cdot 10}{8}\right\rfloor = 5$,得到小数点后第三位,更新 $r$ 为 $(r\cdot 10)\bmod 8 = 0$。
  4. $r=0$,说明 $9/8$ 是有限小数。

例二:3/14 = 0.2(142857)

整数部分为 $\left\lfloor\dfrac{3}{14}\right\rfloor = 0$,初始余数为 $r = 3\bmod 14 = 3$。

  1. $r=3$。计算商 $\left\lfloor\dfrac{r\cdot 10}{14}\right\rfloor = 2$,得到小数点后第一位,更新 $r$ 为 $(r\cdot 10)\bmod 14 = 2$。
  2. $r=2$。计算商 $\left\lfloor\dfrac{r\cdot 10}{14}\right\rfloor = 1$,得到小数点后第二位,更新 $r$ 为 $(r\cdot 10)\bmod 14 = 6$。
  3. $r=6$。计算商 $\left\lfloor\dfrac{r\cdot 10}{14}\right\rfloor = 4$,得到小数点后第三位,更新 $r$ 为 $(r\cdot 10)\bmod 14 = 4$。
  4. $r=4$。计算商 $\left\lfloor\dfrac{r\cdot 10}{14}\right\rfloor = 2$,得到小数点后第四位,更新 $r$ 为 $(r\cdot 10)\bmod 14 = 12$。
  5. $r=12$。计算商 $\left\lfloor\dfrac{r\cdot 10}{14}\right\rfloor = 8$,得到小数点后第五位,更新 $r$ 为 $(r\cdot 10)\bmod 14 = 8$。
  6. $r=8$。计算商 $\left\lfloor\dfrac{r\cdot 10}{14}\right\rfloor = 5$,得到小数点后第六位,更新 $r$ 为 $(r\cdot 10)\bmod 14 = 10$。
  7. $r=10$。计算商 $\left\lfloor\dfrac{r\cdot 10}{14}\right\rfloor = 7$,得到小数点后第七位,更新 $r$ 为 $(r\cdot 10)\bmod 14 = 2$。
  8. $r=2$,等于第 2 步开始时的余数。如果继续计算,我们会重复上面的第 2~7 步。这意味着我们找到了循环节。

根据 $r=2$ 首次出现的位置,可以知道循环节之前的小数为 $2$,循环节为 $142857$。

怎么知道进入循环了?

用一个哈希表记录,哈希表的 key 是余数 $r$,value 是这个余数对应着第几位小数。

计算商(添加到答案),更新 $r$ 后:

  • 如果 $r$ 在哈希表中,说明有循环节,根据哈希表中记录的小数位置,可以得到循环节之前的小数,以及循环节的内容。
  • 如果 $r$ 不在哈希表中,往哈希表中插入 $r$ 以及此时我们在算第几位小数。
  • 特别地,如果 $r=0$,说明没有循环节,退出循环。
class Solution:
    def fractionToDecimal(self, numerator: int, denominator: int) -> str:
        sign = '-' if numerator * denominator < 0 else ''
        numerator = abs(numerator)  # 保证下面的计算过程不产生负数
        denominator = abs(denominator)

        # 计算整数部分 q 和初始余数 r
        q, r = divmod(numerator, denominator)
        if r == 0:  # 没有小数部分
            return sign + str(q)

        ans = [sign + str(q) + '.']
        r_to_pos = {r: 1}  # 初始余数对应小数点后第一位
        while r:
            # 计算小数点后的数字 q,更新 r
            q, r = divmod(r * 10, denominator)
            ans.append(str(q))
            if r in r_to_pos:  # 有循环节
                pos = r_to_pos[r]  # 循环节的开始位置
                return f"{''.join(ans[:pos])}({''.join(ans[pos:])})"
            r_to_pos[r] = len(ans)  # 记录余数对应位置
        return ''.join(ans)  # 有限小数
class Solution {
    public String fractionToDecimal(int numerator, int denominator) {
        long a = numerator;
        long b = denominator;
        String sign = a * b < 0 ? "-" : "";
        a = Math.abs(a); // 保证下面的计算过程不产生负数
        b = Math.abs(b);

        // 计算整数部分 q 和初始余数 r
        long q = a / b;
        long r = a % b;
        if (r == 0) { // 没有小数部分
            return sign + q;
        }

        StringBuilder ans = new StringBuilder(sign).append(q).append('.');
        Map<Long, Integer> rToPos = new HashMap<>();
        rToPos.put(r, ans.length()); // 记录初始余数对应位置
        while (r > 0) {
            // 计算小数点后的数字 q,更新 r
            r *= 10;
            q = r / b;
            r %= b;
            ans.append(q);
            if (rToPos.containsKey(r)) { // 有循环节
                int pos = rToPos.get(r); // 循环节的开始位置
                return ans.substring(0, pos) + "(" + ans.substring(pos) + ")";
            }
            rToPos.put(r, ans.length()); // 记录余数对应位置
        }
        return ans.toString(); // 有限小数
    }
}
class Solution {
public:
    string fractionToDecimal(int numerator, int denominator) {
        long long a = numerator, b = denominator;
        string sign = a * b < 0 ? "-" : "";
        a = abs(a); // 保证下面的计算过程不产生负数
        b = abs(b);

        // 计算整数部分 q 和初始余数 r
        long long q = a / b, r = a % b;
        if (r == 0) { // 没有小数部分
            return sign + to_string(q);
        }

        string ans = sign + to_string(q) + ".";
        unordered_map<long long, int> r_to_pos = {{r, ans.size()}}; // 记录初始余数对应位置
        while (r) {
            // 计算小数点后的数字 q,更新 r
            r *= 10;
            q = r / b;
            r %= b;
            ans += '0' + q;
            if (r_to_pos.contains(r)) { // 有循环节
                int pos = r_to_pos[r]; // 循环节的开始位置
                return ans.substr(0, pos) + "(" + ans.substr(pos) + ")";
            }
            r_to_pos[r] = ans.size(); // 记录余数对应位置
        }
        return ans; // 有限小数
    }
};
func fractionToDecimal(numerator, denominator int) string {
    sign := ""
    if numerator*denominator < 0 {
        sign = "-"
    }
    numerator = abs(numerator) // 保证下面的计算过程不产生负数
    denominator = abs(denominator)

    // 计算整数部分 q 和初始余数 r
    q, r := numerator/denominator, numerator%denominator
    if r == 0 { // 没有小数部分
        return sign + strconv.Itoa(q)
    }

    ans := []byte(sign + strconv.Itoa(q) + ".")
    rToPos := map[int]int{r: len(ans)} // 记录初始余数对应位置
    for r != 0 {
        // 计算小数点后的数字 q,更新 r
        r *= 10
        q = r / denominator
        r %= denominator
        ans = append(ans, '0'+byte(q))
        if pos, ok := rToPos[r]; ok { // 有循环节,pos 为循环节的开始位置
            return string(ans[:pos]) + "(" + string(ans[pos:]) + ")"
        }
        rToPos[r] = len(ans) // 记录余数对应位置
    }
    return string(ans) // 有限小数
}

func abs(x int) int { if x < 0 { return -x }; return x }
var fractionToDecimal = function(numerator, denominator) {
    const sign = numerator * denominator < 0 ? "-" : "";
    numerator = Math.abs(numerator); // 保证下面的计算过程不产生负数
    denominator = Math.abs(denominator);

    // 计算整数部分 q 和初始余数 r
    let q = Math.floor(numerator / denominator);
    let r = numerator % denominator;
    if (r === 0) { // 没有小数部分
        return sign + String(q);
    }

    const ans = [sign + String(q) + "."];
    const r_to_pos = new Map();
    r_to_pos.set(r, 1); // 初始余数对应小数点后第一位
    while (r) {
        // 计算小数点后的数字 q,更新 r
        r *= 10;
        q = Math.floor(r / denominator);
        r = r % denominator;
        ans.push(String(q));
        if (r_to_pos.has(r)) { // 有循环节
            const pos = r_to_pos.get(r); // 循环节的开始位置
            return ans.slice(0, pos).join("") + "(" + ans.slice(pos).join("") + ")";
        }
        r_to_pos.set(r, ans.length); // 记录余数对应位置
    }
    return ans.join(""); // 有限小数
};
use std::collections::HashMap;

impl Solution {
    pub fn fraction_to_decimal(numerator: i32, denominator: i32) -> String {
        let mut a = numerator as i64;
        let mut b = denominator as i64;
        let sign = if a * b < 0 { "-" } else { "" };
        a = a.abs(); // 保证下面的计算过程不产生负数
        b = b.abs();

        // 计算整数部分 q 和初始余数 r
        let mut q = a / b;
        let mut r = a % b;
        if r == 0 { // 没有小数部分
            return format!("{}{}", sign, q);
        }

        let mut ans = format!("{}{}.", sign, q);
        let mut r_to_pos = HashMap::new();
        r_to_pos.insert(r, ans.len()); // 记录初始余数对应位置
        while r != 0 {
            // 计算小数点后的数字 q,更新 r
            r *= 10;
            q = r / b;
            r %= b;
            ans.push((b'0' + q as u8) as char);
            if let Some(&pos) = r_to_pos.get(&r) { // 有循环节,pos 为循环节的开始位置
                return format!("{}({})", &ans[..pos], &ans[pos..]);
            }
            r_to_pos.insert(r, ans.len()); // 记录余数对应位置
        }
        ans // 有限小数
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(N)$。其中 $N = \min(|\textit{denominator}|, 10^4)$。至多有 $|\textit{denominator}|$ 个不同的余数,最多循环 $\mathcal{O}(|\textit{denominator}|)$ 次。不过,本题保证答案长度小于 $10^4$。
  • 空间复杂度:$\mathcal{O}(N)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-分数到小数🟡

给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以 字符串形式返回小数

如果小数部分为循环小数,则将循环的部分括在括号内。

如果存在多个答案,只需返回 任意一个

对于所有给定的输入,保证 答案字符串的长度小于 104

 

示例 1:

输入:numerator = 1, denominator = 2
输出:"0.5"

示例 2:

输入:numerator = 2, denominator = 1
输出:"2"

示例 3:

输入:numerator = 4, denominator = 333
输出:"0.(012)"

 

提示:

  • -231 <= numerator, denominator <= 231 - 1
  • denominator != 0

【宫水三叶】模拟竖式计算(除法)

模拟

这是一道模拟 竖式计算(除法)的题目。

首先可以明确,两个数相除要么是「有限位小数」,要么是「无限循环小数」,而不可能是「无限不循环小数」。

然后考虑人工计算两数相除是如何进行:

QQ图片20211003090709.jpg

这引导我们可以在模拟竖式计算(除法)过程中,使用「哈希表」记录某个余数最早在什么位置出现过,一旦出现相同余数,则将「出现位置」到「当前结尾」之间的字符串抠出来,即是「循环小数」部分。

PS. 到这里,从人工模拟除法运算的过程,我们就可以知道「为什么不会出现“无限不循环小数”」,因为始终是对余数进行补零操作,再往下进行运算,而余数个数具有明确的上限(有限集)。所以根据抽屉原理,一直接着往下计算,最终结果要么是「出现相同余数」,要么是「余数为 $0$,运算结束」。

一些细节:

  • 一个显然的条件是,如果本身两数能够整除,直接返回即可;
  • 如果两个数有一个为“负数”,则最终答案为“负数”,因此可以起始先判断两数相乘是否小于 $0$,如果是,先往答案头部追加一个负号 -
  • 两者范围为 int,但计算结果可以会超过 int 范围,考虑 $numerator = -2^{31}$ 和 $denominator = -1$ 的情况,其结果为 $2^{31}$,超出 int 的范围 $[-2^{31}, 2^{31} - 1]$。因此起始需要先使用 long 对两个入参类型转换一下。

代码:

###Java

class Solution {
    public String fractionToDecimal(int numerator, int denominator) {
        // 转 long 计算,防止溢出
        long a = numerator, b = denominator;
        // 如果本身能够整除,直接返回计算结果
        if (a % b == 0) return String.valueOf(a / b);
        StringBuilder sb = new StringBuilder();
        // 如果其一为负数,先追加负号
        if (a * b < 0) sb.append('-');
        a = Math.abs(a); b = Math.abs(b);
        // 计算小数点前的部分,并将余数赋值给 a
        sb.append(String.valueOf(a / b) + ".");
        a %= b;
        Map<Long, Integer> map = new HashMap<>();
        while (a != 0) {
            // 记录当前余数所在答案的位置,并继续模拟除法运算
            map.put(a, sb.length());
            a *= 10;
            sb.append(a / b);
            a %= b;
            // 如果当前余数之前出现过,则将 [出现位置 到 当前位置] 的部分抠出来(循环小数部分)
            if (map.containsKey(a)) {
                int u = map.get(a);
                return String.format("%s(%s)", sb.substring(0, u), sb.substring(u));
            }
        }
        return sb.toString();
    }
}
  • 时间复杂度:复杂度取决于最终答案的长度,题目规定了最大长度不会超过 $10^4$,整体复杂度为 $O(M)$
  • 空间复杂度:复杂度取决于最终答案的长度,题目规定了最大长度不会超过 $10^4$,整体复杂度为 $O(M)$

最后

如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/

也欢迎你 关注我(公主号后台回复「送书」即可参与长期看题解学算法送实体书活动)或 加入「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。

所有题解已经加入 刷题指南,欢迎 star 哦 ~

分数到小数

方法一:长除法

题目要求根据给定的分子和分母,将分数转成整数或小数。由于给定的分子和分母的取值范围都是 $[-2^{31}, 2^{31}-1]$,为了防止计算过程中产生溢出,需要将分子和分母转成 $64$ 位整数表示。

将分数转成整数或小数,做法是计算分子和分母相除的结果。可能的结果有三种:整数、有限小数、无限循环小数。

如果分子可以被分母整除,则结果是整数,将分子除以分母的商以字符串的形式返回即可。

如果分子不能被分母整除,则结果是有限小数或无限循环小数,需要通过模拟长除法的方式计算结果。为了方便处理,首先根据分子和分母的正负决定结果的正负(注意此时分子和分母都不为 $0$),然后将分子和分母都转成正数,再计算长除法。

计算长除法时,首先计算结果的整数部分,将以下部分依次拼接到结果中:

  1. 如果结果是负数则将负号拼接到结果中,如果结果是正数则跳过这一步;

  2. 将整数部分拼接到结果中;

  3. 将小数点拼接到结果中。

完成上述拼接之后,根据余数计算小数部分。

计算小数部分时,每次将余数乘以 $10$,然后计算小数的下一位数字,并得到新的余数。重复上述操作直到余数变成 $0$ 或者找到循环节。

  • 如果余数变成 $0$,则结果是有限小数,将小数部分拼接到结果中。

  • 如果找到循环节,则找到循环节的开始位置和结束位置并加上括号,然后将小数部分拼接到结果中。

如何判断是否找到循环节?注意到对于相同的余数,计算得到的小数的下一位数字一定是相同的,因此如果计算过程中发现某一位的余数在之前已经出现过,则为找到循环节。为了记录每个余数是否已经出现过,需要使用哈希表存储每个余数在小数部分第一次出现的下标。

假设在计算小数部分的第 $i$ 位之前,余数为 $\textit{remainder}i$,则在计算小数部分的第 $i$ 位之后,余数为 $\textit{remainder}{i+1}$。

假设存在下标 $j$ 和 $k$,满足 $j \le k$ 且 $\textit{remainder}j = \textit{remainder}{k+1}$,则小数部分的第 $k+1$ 位和小数部分的第 $j$ 位相同,因此小数部分的第 $j$ 位到第 $k$ 位是一个循环节。在计算小数部分的第 $k$ 位之后就会发现这个循环节的存在,因此在小数部分的第 $j$ 位之前加上左括号,在小数部分的末尾(即第 $k$ 位之后)加上右括号。

fig1

fig2

###Java

class Solution {
    public String fractionToDecimal(int numerator, int denominator) {
        long numeratorLong = (long) numerator;
        long denominatorLong = (long) denominator;
        if (numeratorLong % denominatorLong == 0) {
            return String.valueOf(numeratorLong / denominatorLong);
        }

        StringBuffer sb = new StringBuffer();
        if (numeratorLong < 0 ^ denominatorLong < 0) {
            sb.append('-');
        }

        // 整数部分
        numeratorLong = Math.abs(numeratorLong);
        denominatorLong = Math.abs(denominatorLong);
        long integerPart = numeratorLong / denominatorLong;
        sb.append(integerPart);
        sb.append('.');

        // 小数部分
        StringBuffer fractionPart = new StringBuffer();
        Map<Long, Integer> remainderIndexMap = new HashMap<Long, Integer>();
        long remainder = numeratorLong % denominatorLong;
        int index = 0;
        while (remainder != 0 && !remainderIndexMap.containsKey(remainder)) {
            remainderIndexMap.put(remainder, index);
            remainder *= 10;
            fractionPart.append(remainder / denominatorLong);
            remainder %= denominatorLong;
            index++;
        }
        if (remainder != 0) { // 有循环节
            int insertIndex = remainderIndexMap.get(remainder);
            fractionPart.insert(insertIndex, '(');
            fractionPart.append(')');
        }
        sb.append(fractionPart.toString());

        return sb.toString();
    }
}

###C#

public class Solution {
    public string FractionToDecimal(int numerator, int denominator) {
        long numeratorLong = (long) numerator;
        long denominatorLong = (long) denominator;
        if (numeratorLong % denominatorLong == 0) {
            return (numeratorLong / denominatorLong).ToString();
        }

        StringBuilder sb = new StringBuilder();
        if (numeratorLong < 0 ^ denominatorLong < 0) {
            sb.Append('-');
        }

        // 整数部分
        numeratorLong = Math.Abs(numeratorLong);
        denominatorLong = Math.Abs(denominatorLong);
        long integerPart = numeratorLong / denominatorLong;
        sb.Append(integerPart);
        sb.Append('.');

        // 小数部分
        StringBuilder fractionPart = new StringBuilder();
        Dictionary<long, int> remainderIndexDic = new Dictionary<long, int>();
        long remainder = numeratorLong % denominatorLong;
        int index = 0;
        while (remainder != 0 && !remainderIndexDic.ContainsKey(remainder)) {
            remainderIndexDic.Add(remainder, index);
            remainder *= 10;
            fractionPart.Append(remainder / denominatorLong);
            remainder %= denominatorLong;
            index++;
        }
        if (remainder != 0) { // 有循环节
            int insertIndex = remainderIndexDic[remainder];
            fractionPart.Insert(insertIndex, '(');
            fractionPart.Append(')');
        }
        sb.Append(fractionPart.ToString());

        return sb.ToString();
    }
}

###JavaScript

var fractionToDecimal = function(numerator, denominator) {
    if (numerator % denominator == 0) {
        return '' + Math.floor(numerator / denominator);
    }

    const sb = [];
    if (numerator < 0 ^ denominator < 0) {
        sb.push('-');
    }

    // 整数部分
    numerator = Math.abs(numerator);
    denominator = Math.abs(denominator);
    const integerPart = Math.floor(numerator / denominator);
    sb.push(integerPart);
    sb.push('.');

    // 小数部分
    const fractionPart = [];
    const remainderIndexDic = new Map();
    let remainder = numerator % denominator;
    let index = 0;
    while (remainder !== 0 && !remainderIndexDic.has(remainder)) {
        remainderIndexDic.set(remainder, index);
        remainder *= 10;
        fractionPart.push(Math.floor(remainder / denominator));
        remainder %= denominator;
        index++;
    }
    if (remainder !== 0) { // 有循环节
        let insertIndex = remainderIndexDic.get(remainder);
        fractionPart.splice(insertIndex, 0, '(');
        fractionPart.push(')');
    }
    sb.push(fractionPart.join(''));

    return sb.join('');
}

###go

func fractionToDecimal(numerator, denominator int) string {
    if numerator%denominator == 0 {
        return strconv.Itoa(numerator / denominator)
    }

    s := []byte{}
    if numerator < 0 != (denominator < 0) {
        s = append(s, '-')
    }

    // 整数部分
    numerator = abs(numerator)
    denominator = abs(denominator)
    integerPart := numerator / denominator
    s = append(s, strconv.Itoa(integerPart)...)
    s = append(s, '.')

    // 小数部分
    indexMap := map[int]int{}
    remainder := numerator % denominator
    for remainder != 0 && indexMap[remainder] == 0 {
        indexMap[remainder] = len(s)
        remainder *= 10
        s = append(s, '0'+byte(remainder/denominator))
        remainder %= denominator
    }
    if remainder > 0 { // 有循环节
        insertIndex := indexMap[remainder]
        s = append(s[:insertIndex], append([]byte{'('}, s[insertIndex:]...)...)
        s = append(s, ')')
    }

    return string(s)
}

func abs(x int) int {
    if x < 0 {
        return -x
    }
    return x
}

###C++

class Solution {
public:
    string fractionToDecimal(int numerator, int denominator) {
        long numeratorLong = numerator;
        long denominatorLong = denominator;
        if (numeratorLong % denominatorLong == 0) {
            return to_string(numeratorLong / denominatorLong);
        }

        string ans;
        if (numeratorLong < 0 ^ denominatorLong < 0) {
            ans.push_back('-');
        }

        // 整数部分
        numeratorLong = abs(numeratorLong);
        denominatorLong = abs(denominatorLong);
        long integerPart = numeratorLong / denominatorLong;
        ans += to_string(integerPart);
        ans.push_back('.');

        // 小数部分
        string fractionPart;
        unordered_map<long, int> remainderIndexMap;
        long remainder = numeratorLong % denominatorLong;
        int index = 0;
        while (remainder != 0 && !remainderIndexMap.count(remainder)) {
            remainderIndexMap[remainder] = index;
            remainder *= 10;
            fractionPart += to_string(remainder / denominatorLong);
            remainder %= denominatorLong;
            index++;
        }
        if (remainder != 0) { // 有循环节
            int insertIndex = remainderIndexMap[remainder];
            fractionPart = fractionPart.substr(0,insertIndex) + '(' + fractionPart.substr(insertIndex);
            fractionPart.push_back(')');
        }
        ans += fractionPart;

        return ans;
    }
};

###Python

class Solution:
    def fractionToDecimal(self, numerator: int, denominator: int) -> str:
        if numerator % denominator == 0:
            return str(numerator // denominator)

        s = []
        if (numerator < 0) != (denominator < 0):
            s.append('-')

        # 整数部分
        numerator = abs(numerator)
        denominator = abs(denominator)
        integerPart = numerator // denominator
        s.append(str(integerPart))
        s.append('.')

        # 小数部分
        indexMap = {}
        remainder = numerator % denominator
        while remainder and remainder not in indexMap:
            indexMap[remainder] = len(s)
            remainder *= 10
            s.append(str(remainder // denominator))
            remainder %= denominator
        if remainder:  # 有循环节
            insertIndex = indexMap[remainder]
            s.insert(insertIndex, '(')
            s.append(')')

        return ''.join(s)

复杂度分析

  • 时间复杂度:$O(l)$,其中 $l$ 是答案字符串的长度,这道题中 $l \le 10^4$。对于答案字符串中的每一个字符,计算时间都是 $O(1)$。

  • 空间复杂度:$O(l)$,其中 $l$ 是答案字符串的长度,这道题中 $l \le 10^4$。空间复杂度主要取决于答案字符串和哈希表,哈希表中的每个键值对所对应的下标各不相同,因此键值对的数量不会超过 $l$。

C++:模拟题(简单易懂)

题解:
思路在注解里面,所以直接看代码就好了。

代码如下:

###cpp

class Solution {
public:
    //小数部分如果余数出现两次就表示该小数是循环小数了
    string fractionToDecimal(int numerator, int denominator) {
        if(denominator==0) return "";//边界条件,分母为0
        if(numerator==0) return "0";//边界条件,分子为0
        string result;
        
        //转换为longlong防止溢出
        long long num = static_cast<long long>(numerator);
        long long denom = static_cast<long long>(denominator);
        
        //处理正负号,一正一负取负号
        if((num>0)^(denom>0))result.push_back('-');
        
        //分子分母全部转换为正数
        num=llabs(num);denom=llabs(denom);
        
        //处理整数部分
        result.append(to_string(num/denom));
        
        //处理小数部分
        num%=denom;                         //获得余数
        if(num==0)return result;             //余数为0,表示整除了,直接返回结果
        result.push_back('.');              //余数不为0,添加小数点
        int index=result.size()-1;          //获得小数点的下标
        unordered_map<int,int> record;      //map用来记录出现重复数的下标,然后将'('插入到重复数前面就好了
        while(num&&record.count(num)==0){   //小数部分:余数不为0且余数还没有出现重复数字
            record[num]=++index;
            num*=10;                        //余数扩大10倍,然后求商,和草稿本上运算方法是一样的
            result+=to_string(num/denom);
            num%=denom;
        }
        if(record.count(num)==1){           //出现循环余数,我们直接在重复数字前面添加'(',字符串末尾添加')'
            result.insert(record[num],"(");
            result.push_back(')');
        }
        return result;
    }
};

库函数写法(Python/Java/C++/C/Go/JS/Rust)

把字符串按照 $\texttt{.}$ 分割,分割出的子串转成整数,我们可以得到两个整数列表。

示例 1 的列表为 $[1,2]$ 和 $[1,10]$。

示例 2 的列表均为 $[1,1]$。

示例 3 的列表(短的列表在末尾补 $0$)均为 $[1,0,0,0]$。

问题相当于比较这两个列表的字典序:

  • 从左到右遍历两个列表(分别记作 $a$ 和 $b$)。
  • 如果 $a[i] < b[i]$,返回 $-1$。
  • 如果 $a[i] > b[i]$,返回 $1$。
  • 否则继续向后遍历。
  • 如果遍历过程中没有返回,说明两个列表相同,返回 $0$。

###py

class Solution:
    def compareVersion(self, version1: str, version2: str) -> int:
        a = map(int, version1.split('.'))
        b = map(int, version2.split('.'))
        for ver1, ver2 in zip_longest(a, b, fillvalue=0):
            if ver1 != ver2:
                return -1 if ver1 < ver2 else 1
        return 0

###java

class Solution {
    public int compareVersion(String version1, String version2) {
        String[] a = version1.split("\\.");
        String[] b = version2.split("\\.");
        int n = a.length;
        int m = b.length;
        for (int i = 0; i < n || i < m; i++) {
            int ver1 = i < n ? Integer.parseInt(a[i]) : 0;
            int ver2 = i < m ? Integer.parseInt(b[i]) : 0;
            if (ver1 != ver2) {
                return ver1 < ver2 ? -1 : 1;
            }
        }
        return 0;
    }
}

###cpp

class Solution {
    vector<string> split(const string& s, char delim) {
        vector<string> res;
        stringstream ss(s);
        string token;
        while (getline(ss, token, delim)) {
            res.push_back(token);
        }
        return res;
    }

public:
    int compareVersion(string version1, string version2) {
        auto a = split(version1, '.');
        auto b = split(version2, '.');
        int n = a.size(), m = b.size();
        for (int i = 0; i < n || i < m; i++) {
            int ver1 = i < n ? stoi(a[i]) : 0;
            int ver2 = i < m ? stoi(b[i]) : 0;
            if (ver1 != ver2) {
                return ver1 < ver2 ? -1 : 1;
            }
        }
        return 0;
    }
};

###c

int compareVersion(char* version1, char* version2) {
    char* saveptr1;
    char* saveptr2;
    char* token1 = strtok_r(version1, ".", &saveptr1);
    char* token2 = strtok_r(version2, ".", &saveptr2);

    while (token1 != NULL || token2 != NULL) {
        int ver1 = token1 ? atoi(token1) : 0;
        int ver2 = token2 ? atoi(token2) : 0;
        if (ver1 != ver2) {
            return ver1 < ver2 ? -1 : 1;
        }
        token1 = strtok_r(NULL, ".", &saveptr1);
        token2 = strtok_r(NULL, ".", &saveptr2);
    }

    return 0;
}

###go

func compareVersion(version1, version2 string) int {
    a := strings.Split(version1, ".")
    b := strings.Split(version2, ".")
    n, m := len(a), len(b)
    for i := range max(n, m) {
        ver1 := 0
        if i < n {
            ver1, _ = strconv.Atoi(a[i])
        }
        ver2 := 0
        if i < m {
            ver2, _ = strconv.Atoi(b[i])
        }
        c := cmp.Compare(ver1, ver2)
        if c != 0 {
            return c
        }
    }
    return 0
}

###js

var compareVersion = function(version1, version2) {
    const a = version1.split(".");
    const b = version2.split(".");
    const n = a.length, m = b.length;
    for (let i = 0; i < n || i < m; i++) {
        const ver1 = i < n ? parseInt(a[i]) : 0;
        const ver2 = i < m ? parseInt(b[i]) : 0;
        if (ver1 !== ver2) {
            return ver1 < ver2 ? -1 : 1;
        }
    }
    return 0;
};

###rust

impl Solution {
    pub fn compare_version(version1: String, version2: String) -> i32 {
        let a = version1.split('.').collect::<Vec<_>>();
        let b = version2.split('.').collect::<Vec<_>>();
        let n = a.len();
        let m = b.len();
        for i in 0..n.max(m) {
            let ver1 = if i < n { a[i].parse::<i32>().unwrap() } else { 0 };
            let ver2 = if i < m { b[i].parse::<i32>().unwrap() } else { 0 };
            if ver1 != ver2 {
                return if ver1 < ver2 { -1 } else { 1 };
            }
        }
        0
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n+m)$,其中 $n$ 是 $\textit{version}_1$ 的长度,$m$ 是 $\textit{version}_2$ 的长度。
  • 空间复杂度:$\mathcal{O}(n+m)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-比较版本号🟡

给你两个 版本号字符串 version1version2 ,请你比较它们。版本号由被点 '.' 分开的修订号组成。修订号的值 是它 转换为整数 并忽略前导零。

比较版本号时,请按 从左到右的顺序 依次比较它们的修订号。如果其中一个版本字符串的修订号较少,则将缺失的修订号视为 0

返回规则如下:

  • 如果 version1 version2 返回 -1
  • 如果 version1 version2 返回 1
  • 除此之外返回 0

 

示例 1:

输入:version1 = "1.2", version2 = "1.10"

输出:-1

解释:

version1 的第二个修订号为 "2",version2 的第二个修订号为 "10":2 < 10,所以 version1 < version2。

示例 2:

输入:version1 = "1.01", version2 = "1.001"

输出:0

解释:

忽略前导零,"01" 和 "001" 都代表相同的整数 "1"。

示例 3:

输入:version1 = "1.0", version2 = "1.0.0.0"

输出:0

解释:

version1 有更少的修订号,每个缺失的修订号按 "0" 处理。

 

提示:

  • 1 <= version1.length, version2.length <= 500
  • version1version2 仅包含数字和 '.'
  • version1version2 都是 有效版本号
  • version1version2 的所有修订号都可以存储在 32 位整数

【宫水三叶】简单字符串模拟题

模拟

根据题意,对字符串进行分割,诸位比较「修订号」大小即可。

对于缺省的修订号位置,使用 $0$ 进行代指。

代码:

###Java

class Solution {
    public int compareVersion(String v1, String v2) {
        String[] ss1 = v1.split("\\."), ss2 = v2.split("\\.");
        int n = ss1.length, m = ss2.length;
        int i = 0, j = 0;
        while (i < n || j < m) {
            int a = 0, b = 0;
            if (i < n) a = Integer.parseInt(ss1[i++]);
            if (j < m) b = Integer.parseInt(ss2[j++]);
            if (a != b) return a > b ? 1 : -1;
        }
        return 0;
    }
}
  • 时间复杂度:令 v1 长度为 $n$,v2 长度为 $m$。整体复杂度为 $O(\max(n, m))$
  • 空间复杂度:$O(n + m)$

最后

如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/

也欢迎你 关注我(公主号后台回复「送书」即可参与长期看题解学算法送实体书活动)或 加入「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。

所有题解已经加入 刷题指南,欢迎 star 哦 ~

比较版本号

方法一:字符串分割

我们可以将版本号按照点号分割成修订号,然后从左到右比较两个版本号的相同下标的修订号。在比较修订号时,需要将字符串转换成整数进行比较。注意根据题目要求,如果版本号不存在某个下标处的修订号,则该修订号视为 $0$。

###Python

class Solution:
    def compareVersion(self, version1: str, version2: str) -> int:
        for v1, v2 in zip_longest(version1.split('.'), version2.split('.'), fillvalue=0):
            x, y = int(v1), int(v2)
            if x != y:
                return 1 if x > y else -1
        return 0

###Java

class Solution {
    public int compareVersion(String version1, String version2) {
        String[] v1 = version1.split("\\.");
        String[] v2 = version2.split("\\.");
        for (int i = 0; i < v1.length || i < v2.length; ++i) {
            int x = 0, y = 0;
            if (i < v1.length) {
                x = Integer.parseInt(v1[i]);
            }
            if (i < v2.length) {
                y = Integer.parseInt(v2[i]);
            }
            if (x > y) {
                return 1;
            }
            if (x < y) {
                return -1;
            }
        }
        return 0;
    }
}

###C#

public class Solution {
    public int CompareVersion(string version1, string version2) {
        string[] v1 = version1.Split('.');
        string[] v2 = version2.Split('.');
        for (int i = 0; i < v1.Length || i < v2.Length; ++i) {
            int x = 0, y = 0;
            if (i < v1.Length) {
                x = int.Parse(v1[i]);
            }
            if (i < v2.Length) {
                y = int.Parse(v2[i]);
            }
            if (x > y) {
                return 1;
            }
            if (x < y) {
                return -1;
            }
        }
        return 0;
    }
}

###go

func compareVersion(version1, version2 string) int {
    v1 := strings.Split(version1, ".")
    v2 := strings.Split(version2, ".")
    for i := 0; i < len(v1) || i < len(v2); i++ {
        x, y := 0, 0
        if i < len(v1) {
            x, _ = strconv.Atoi(v1[i])
        }
        if i < len(v2) {
            y, _ = strconv.Atoi(v2[i])
        }
        if x > y {
            return 1
        }
        if x < y {
            return -1
        }
    }
    return 0
}

###JavaScript

var compareVersion = function(version1, version2) {
    const v1 = version1.split('.');
    const v2 = version2.split('.');
    for (let i = 0; i < v1.length || i < v2.length; ++i) {
        let x = 0, y = 0;
        if (i < v1.length) {
            x = parseInt(v1[i]);
        }
        if (i < v2.length) {
            y = parseInt(v2[i]);
        }
        if (x > y) {
            return 1;
        }
        if (x < y) {
            return -1;
        }
    }
    return 0;
};
  • 时间复杂度:$O(n+m)$(或 $O(\max(n,m))$,这是等价的),其中 $n$ 是字符串 $\textit{version1}$ 的长度,$m$ 是字符串 $\textit{version2}$ 的长度。

  • 空间复杂度:$O(n+m)$,我们需要 $O(n+m)$ 的空间存储分割后的修订号列表。

方法二:双指针

方法一需要存储分割后的修订号,为了优化空间复杂度,我们可以在分割版本号的同时解析出修订号进行比较。

###Python

class Solution:
    def compareVersion(self, version1: str, version2: str) -> int:
        n, m = len(version1), len(version2)
        i, j = 0, 0
        while i < n or j < m:
            x = 0
            while i < n and version1[i] != '.':
                x = x * 10 + ord(version1[i]) - ord('0')
                i += 1
            i += 1  # 跳过点号
            y = 0
            while j < m and version2[j] != '.':
                y = y * 10 + ord(version2[j]) - ord('0')
                j += 1
            j += 1  # 跳过点号
            if x != y:
                return 1 if x > y else -1
        return 0

###C++

class Solution {
public:
    int compareVersion(string version1, string version2) {
        int n = version1.length(), m = version2.length();
        int i = 0, j = 0;
        while (i < n || j < m) {
            long long x = 0;
            for (; i < n && version1[i] != '.'; ++i) {
                x = x * 10 + version1[i] - '0';
            }
            ++i; // 跳过点号
            long long y = 0;
            for (; j < m && version2[j] != '.'; ++j) {
                y = y * 10 + version2[j] - '0';
            }
            ++j; // 跳过点号
            if (x != y) {
                return x > y ? 1 : -1;
            }
        }
        return 0;
    }
};

###Java

class Solution {
    public int compareVersion(String version1, String version2) {
        int n = version1.length(), m = version2.length();
        int i = 0, j = 0;
        while (i < n || j < m) {
            int x = 0;
            for (; i < n && version1.charAt(i) != '.'; ++i) {
                x = x * 10 + version1.charAt(i) - '0';
            }
            ++i; // 跳过点号
            int y = 0;
            for (; j < m && version2.charAt(j) != '.'; ++j) {
                y = y * 10 + version2.charAt(j) - '0';
            }
            ++j; // 跳过点号
            if (x != y) {
                return x > y ? 1 : -1;
            }
        }
        return 0;
    }
}

###C#

public class Solution {
    public int CompareVersion(string version1, string version2) {
        int n = version1.Length, m = version2.Length;
        int i = 0, j = 0;
        while (i < n || j < m) {
            int x = 0;
            for (; i < n && version1[i] != '.'; ++i) {
                x = x * 10 + version1[i] - '0';
            }
            ++i; // 跳过点号
            int y = 0;
            for (; j < m && version2[j] != '.'; ++j) {
                y = y * 10 + version2[j] - '0';
            }
            ++j; // 跳过点号
            if (x != y) {
                return x > y ? 1 : -1;
            }
        }
        return 0;
    }
}

###go

func compareVersion(version1, version2 string) int {
    n, m := len(version1), len(version2)
    i, j := 0, 0
    for i < n || j < m {
        x := 0
        for ; i < n && version1[i] != '.'; i++ {
            x = x*10 + int(version1[i]-'0')
        }
        i++ // 跳过点号
        y := 0
        for ; j < m && version2[j] != '.'; j++ {
            y = y*10 + int(version2[j]-'0')
        }
        j++ // 跳过点号
        if x > y {
            return 1
        }
        if x < y {
            return -1
        }
    }
    return 0
}

###JavaScript

var compareVersion = function(version1, version2) {
    const n = version1.length, m = version2.length;
    let i = 0, j = 0;
    while (i < n || j < m) {
        let x = 0;
        for (; i < n && version1[i] !== '.'; ++i) {
            x = x * 10 + version1[i].charCodeAt() - '0'.charCodeAt();
        }
        ++i; // 跳过点号
        let y = 0;
        for (; j < m && version2.charAt(j) !== '.'; ++j) {
            y = y * 10 + version2[j].charCodeAt() - '0'.charCodeAt();
        }
        ++j; // 跳过点号
        if (x !== y) {
            return x > y ? 1 : -1;
        }
    }
    return 0;
};

复杂度分析

  • 时间复杂度:$O(n+m)$,其中 $n$ 是字符串 $\textit{version1}$ 的长度,$m$ 是字符串 $\textit{version2}$ 的长度。

  • 空间复杂度:$O(1)$,我们只需要常数的空间保存若干变量。

[Python3/Java/C++/Go/TypeScript] 一题一解:计数(清晰题解)

方法一:计数

我们可以用一个哈希表或数组 $cnt$ 记录每个元素出现的次数。

然后我们遍历 $cnt$,找到出现次数最多的元素,记其出现次数为 $mx$,累加出现次数等于 $mx$ 的元素的出现次数,即为答案。

###python

class Solution:
    def maxFrequencyElements(self, nums: List[int]) -> int:
        cnt = Counter(nums)
        mx = max(cnt.values())
        return sum(x for x in cnt.values() if x == mx)

###java

class Solution {
    public int maxFrequencyElements(int[] nums) {
        int[] cnt = new int[101];
        for (int x : nums) {
            ++cnt[x];
        }
        int ans = 0, mx = -1;
        for (int x : cnt) {
            if (mx < x) {
                mx = x;
                ans = x;
            } else if (mx == x) {
                ans += x;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int maxFrequencyElements(vector<int>& nums) {
        int cnt[101]{};
        for (int x : nums) {
            ++cnt[x];
        }
        int ans = 0, mx = -1;
        for (int x : cnt) {
            if (mx < x) {
                mx = x;
                ans = x;
            } else if (mx == x) {
                ans += x;
            }
        }
        return ans;
    }
};

###go

func maxFrequencyElements(nums []int) (ans int) {
cnt := [101]int{}
for _, x := range nums {
cnt[x]++
}
mx := -1
for _, x := range cnt {
if mx < x {
mx, ans = x, x
} else if mx == x {
ans += x
}
}
return
}

###ts

function maxFrequencyElements(nums: number[]): number {
    const cnt: number[] = Array(101).fill(0);
    for (const x of nums) {
        ++cnt[x];
    }
    let [ans, mx] = [0, -1];
    for (const x of cnt) {
        if (mx < x) {
            mx = x;
            ans = x;
        } else if (mx === x) {
            ans += x;
        }
    }
    return ans;
}

###rust

impl Solution {
    pub fn max_frequency_elements(nums: Vec<i32>) -> i32 {
        let mut cnt = [0; 101];
        for &x in &nums {
            cnt[x as usize] += 1;
        }
        let mut ans = 0;
        let mut mx = -1;
        for &x in &cnt {
            if mx < x {
                mx = x;
                ans = x;
            } else if mx == x {
                ans += x;
            }
        }
        ans
    }
}

时间复杂度 $O(n)$,空间复杂度 $O(n)$。其中 $n$ 为数组 $nums$ 的长度。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-最大频率元素计数🟢

给你一个由 正整数 组成的数组 nums

返回数组 nums 中所有具有 最大 频率的元素的 总频率

元素的 频率 是指该元素在数组中出现的次数。

 

示例 1:

输入:nums = [1,2,2,3,1,4]
输出:4
解释:元素 1 和 2 的频率为 2 ,是数组中的最大频率。
因此具有最大频率的元素在数组中的数量是 4 。

示例 2:

输入:nums = [1,2,3,4,5]
输出:5
解释:数组中的所有元素的频率都为 1 ,是最大频率。
因此具有最大频率的元素在数组中的数量是 5 。

 

提示:

  • 1 <= nums.length <= 100
  • 1 <= nums[i] <= 100
❌