阅读视图

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

每日一题-完成所有任务的最少初始能量🔴

给你一个任务数组 tasks ,其中 tasks[i] = [actuali, minimumi] :

  • actuali 是完成第 i 个任务 需要耗费 的实际能量。
  • minimumi 是开始第 i 个任务前需要达到的最低能量。

比方说,如果任务为 [10, 12] 且你当前的能量为 11 ,那么你不能开始这个任务。如果你当前的能量为 13 ,你可以完成这个任务,且完成它后剩余能量为 3 。

你可以按照 任意顺序 完成任务。

请你返回完成所有任务的 最少 初始能量。

 

示例 1:

输入:tasks = [[1,2],[2,4],[4,8]]
输出:8
解释:
一开始有 8 能量,我们按照如下顺序完成任务:
    - 完成第 3 个任务,剩余能量为 8 - 4 = 4 。
    - 完成第 2 个任务,剩余能量为 4 - 2 = 2 。
    - 完成第 1 个任务,剩余能量为 2 - 1 = 1 。
注意到尽管我们有能量剩余,但是如果一开始只有 7 能量是不能完成所有任务的,因为我们无法开始第 3 个任务。

示例 2:

输入:tasks = [[1,3],[2,4],[10,11],[10,12],[8,9]]
输出:32
解释:
一开始有 32 能量,我们按照如下顺序完成任务:
    - 完成第 1 个任务,剩余能量为 32 - 1 = 31 。
    - 完成第 2 个任务,剩余能量为 31 - 2 = 29 。
    - 完成第 3 个任务,剩余能量为 29 - 10 = 19 。
    - 完成第 4 个任务,剩余能量为 19 - 10 = 9 。
    - 完成第 5 个任务,剩余能量为 9 - 8 = 1 。

示例 3:

输入:tasks = [[1,7],[2,8],[3,9],[4,10],[5,11],[6,12]]
输出:27
解释:
一开始有 27 能量,我们按照如下顺序完成任务:
    - 完成第 5 个任务,剩余能量为 27 - 5 = 22 。
    - 完成第 2 个任务,剩余能量为 22 - 2 = 20 。
    - 完成第 3 个任务,剩余能量为 20 - 3 = 17 。
    - 完成第 1 个任务,剩余能量为 17 - 1 = 16 。
    - 完成第 4 个任务,剩余能量为 16 - 4 = 12 。
    - 完成第 6 个任务,剩余能量为 12 - 6 = 6 。

 

提示:

  • 1 <= tasks.length <= 105
  • 1 <= actuali <= minimumi <= 104

交换论证法(Python/Java/C++/C/Go/JS/Rust)

按照什么规则排序?

本文把 $\textit{actual}$ 简称为 $a$,把 $\textit{minimum}$ 简称为 $m$。

如果 $\textit{tasks}$ 的某个排列 $T$ 是最优的(完成所有任务所需的初始能量最少),那么 $T$ 中的相邻任务满足什么性质?

设 $T$ 中的一对相邻任务为 $t_1 = (a_1,m_1)$ 和 $t_2 = (a_2,m_2)$。设初始能量为 $E_0$,完成这两个任务之前的能量为 $E$。由于从一开始到完成这两个任务之前,所消耗的能量之和是固定的 $S$,所以有 $E = E_0-S$。根据该式,$E$ 越小,初始能量 $E_0$ 也越小。

先完成哪个任务更好?

  • 如果先完成 $t_1$ 再完成 $t_2$,那么完成 $t_1$ 之前,必须满足 $E\ge m_1$;完成 $t_2$ 之前,必须满足 $E-a_1\ge m_2$。联立得 $E\ge \max(m_1,m_2+a_1)$。
  • 如果先完成 $t_2$ 再完成 $t_1$,那么完成 $t_2$ 之前,必须满足 $E\ge m_2$;完成 $t_1$ 之前,必须满足 $E-a_2\ge m_1$。联立得 $E\ge \max(m_2,m_1+a_2)$。

如果先完成 $t_1$ 再完成 $t_2$ 更优(或者相同),则有

$$
\max(m_1,m_2+a_1)\le \max(m_2,m_1+a_2)
$$

两边同时减去 $a_1+a_2$,得

$$
\max(m_1-a_1-a_2,m_2-a_2)\le \max(m_2-a_1-a_2,m_1-a_1)
$$

为方便阅读,令 $X=m_1-a_1$,$Y = m_2-a_2$,上式化简为

$$
\max(X-a_2,Y)\le \max(Y-a_1,X)
$$

  • 如果 $X<Y$,那么上式左侧等于 $Y$,右侧严格小于 $Y$(题目保证 $a_1 > 0$),此时 $\max(X-a_2,Y) > \max(Y-a_1,X)$。
  • 如果 $X\ge Y$,那么上式左侧两个值都 $\le X$,右侧等于 $X$,此时 $\max(X-a_2,Y) \le \max(Y-a_1,X)$。

所以当且仅当 $X\ge Y$ 成立时,$\max(X-a_2,Y)\le \max(Y-a_1,X)$ 成立。

所以当且仅当 $m_1-a_1 \ge m_2-a_2$ 成立时,先完成 $t_1$ 再完成 $t_2$ 更优(或者相同)。

这意味着,对于 $\textit{tasks}$ 中的相邻任务,设左边的任务为 $(a_1,m_1)$,右边的任务为 $(a_2,m_2)$,如果 $m_1-a_1 < m_2-a_2$,那么交换这两个任务可以让初始能量变得更小(或者不变)。于是通过交换 $\textit{tasks}$ 的相邻任务(类似冒泡排序的过程),把 $\textit{tasks}$ 按照 $m_i-a_i$ 从大到小排序,可以得到 $\textit{tasks}$ 的最优排列。

计算初始能量(正序)

设初始能量为 $E_0$,设执行 $\textit{tasks}[i] = (a_i,m_i)$ 之前,累计耗费的能量为 $S$,那么当前能量为 $E_0 - S$,必须满足

$$
E_0 - S \ge m_i
$$

$$
E_0 \ge S + m_i
$$

由此可以得到 $n$ 个关于 $E_0$ 的下界,所有下界取最大值,即为答案。

###py

class Solution:
    def minimumEffort(self, tasks: List[List[int]]) -> int:
        tasks.sort(key=lambda t: t[0] - t[1])  # 按照 minimum - actual 从大到小排序

        ans = 0
        s = 0  # 累计耗费的能量
        for actual, minimum in tasks:
            # 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
            # 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
            ans = max(ans, s + minimum)
            s += actual
        return ans

###java

class Solution {
    public int minimumEffort(int[][] tasks) {
        // 按照 minimum - actual 从大到小排序
        Arrays.sort(tasks, (a, b) -> (b[1] - b[0]) - (a[1] - a[0]));

        int ans = 0;
        int s = 0; // 累计耗费的能量
        for (int[] t : tasks) {
            int actual = t[0];
            int minimum = t[1];
            // 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
            // 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
            ans = Math.max(ans, s + minimum);
            s += actual;
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int minimumEffort(vector<vector<int>>& tasks) {
        ranges::sort(tasks, {}, [](auto& t) { return t[0] - t[1]; }); // 按照 minimum - actual 从大到小排序

        int ans = 0;
        int s = 0; // 累计耗费的能量
        for (auto& t : tasks) {
            int actual = t[0], minimum = t[1];
            // 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
            // 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
            ans = max(ans, s + minimum);
            s += actual;
        }
        return ans;
    }
};

###c

int cmp(const void* p, const void* q) {
    int* a = *(int**)p;
    int* b = *(int**)q;
    return (b[1] - b[0]) - (a[1] - a[0]); // 按照 minimum - actual 从大到小排序
}

int minimumEffort(int** tasks, int tasksSize, int* tasksColSize) {
    qsort(tasks, tasksSize, sizeof(int*), cmp);

    int ans = 0;
    int s = 0; // 累计耗费的能量
    for (int i = 0; i < tasksSize; i++) {
        int actual = tasks[i][0];
        int minimum = tasks[i][1];
        // 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
        // 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
        ans = MAX(ans, s + minimum);
        s += actual;
    }
    return ans;
}

###go

func minimumEffort(tasks [][]int) (ans int) {
slices.SortFunc(tasks, func(a, b []int) int {
return (b[1] - b[0]) - (a[1] - a[0]) // 按照 minimum - actual 从大到小排序
})

s := 0 // 累计耗费的能量
for _, t := range tasks {
actual, minimum := t[0], t[1]
// 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
// 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
ans = max(ans, s+minimum)
s += actual
}
return
}

###js

var minimumEffort = function(tasks) {
    tasks.sort((a, b) => (b[1] - b[0]) - (a[1] - a[0])); // 按照 minimum - actual 从大到小排序

    let ans = 0;
    let s = 0; // 累计耗费的能量
    for (const [actual, minimum] of tasks) {
        // 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
        // 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
        ans = Math.max(ans, s + minimum);
        s += actual;
    }
    return ans;
};

###rust

impl Solution {
    pub fn minimum_effort(mut tasks: Vec<Vec<i32>>) -> i32 {
        tasks.sort_unstable_by_key(|t| t[0] - t[1]); // 按照 minimum - actual 从大到小排序

        let mut ans = 0;
        let mut s = 0; // 累计耗费的能量
        for t in tasks {
            let actual = t[0];
            let minimum = t[1];
            // 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
            // 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
            ans = ans.max(s + minimum);
            s += actual;
        }
        ans
    }
}

计算初始能量(倒序)

也可以从右到左计算。

设完成任务 $t_i = (a_i,m_i)$ 之后的能量为 $E$,那么完成 $t_i$ 之前的能量为 $E+a_i$,但这个能量又必须 $\ge m_i$,所以完成 $t_i$ 之前的能量至少为

$$
\max(E+a_i, m_i)
$$

为了最小化初始能量,完成最后一个任务后的能量应当为 $0$,作为 $E$ 的初始值。

代码实现时,可以改成按照 $m_i-a_i$ 从小到大排序,这样可以正序遍历数组,更方便。

###py

class Solution:
    def minimumEffort(self, tasks: list[list[int]]) -> int:
        tasks.sort(key=lambda t: t[1] - t[0])  # 按照 minimum - actual 从小到大排序

        e = 0
        for actual, minimum in tasks:
            # 完成该任务之后的能量为 e,那么完成该任务之前的能量为 e+actual,同时该能量必须至少为 minimum
            e = max(e + actual, minimum)
        return e

###java

class Solution {
    public int minimumEffort(int[][] tasks) {
        // 按照 minimum - actual 从小到大排序
        Arrays.sort(tasks, (a, b) -> (a[1] - a[0]) - (b[1] - b[0]));

        int e = 0;
        for (int[] t : tasks) {
            int actual = t[0];
            int minimum = t[1];
            // 完成 t 之后的能量为 e,那么完成 t 之前的能量为 e+actual,同时该能量必须至少为 minimum
            e = Math.max(e + actual, minimum);
        }
        return e;
    }
}

###cpp

class Solution {
public:
    int minimumEffort(vector<vector<int>>& tasks) {
        ranges::sort(tasks, {}, [](auto& t) { return t[1] - t[0]; }); // 按照 minimum - actual 从小到大排序

        int e = 0;
        for (auto& t : tasks) {
            int actual = t[0], minimum = t[1];
            // 完成 t 之后的能量为 e,那么完成 t 之前的能量为 e+actual,同时该能量必须至少为 minimum
            e = max(e + actual, minimum);
        }
        return e;
    }
};

###c

int cmp(const void* p, const void* q) {
    int* a = *(int**)p;
    int* b = *(int**)q;
    return (a[1] - a[0]) - (b[1] - b[0]); // 按照 minimum - actual 从小到大排序
}

int minimumEffort(int** tasks, int tasksSize, int* tasksColSize) {
    qsort(tasks, tasksSize, sizeof(int*), cmp);

    int e = 0;
    for (int i = 0; i < tasksSize; i++) {
        int actual = tasks[i][0];
        int minimum = tasks[i][1];
        // 完成 tasks[i] 之后的能量为 e,那么完成 tasks[i] 之前的能量为 e+actual,同时该能量必须至少为 minimum
        e = MAX(e + actual, minimum);
    }
    return e;
}

###go

func minimumEffort(tasks [][]int) (e int) {
slices.SortFunc(tasks, func(a, b []int) int {
return (a[1] - a[0]) - (b[1] - b[0]) // 按照 minimum - actual 从小到大排序
})

for _, t := range tasks {
actual, minimum := t[0], t[1]
// 完成 t 之后的能量为 e,那么完成 t 之前的能量为 e+actual,同时该能量必须至少为 minimum
e = max(e+actual, minimum)
}
return
}

###js

var minimumEffort = function(tasks) {
    tasks.sort((a, b) => (a[1] - a[0]) - (b[1] - b[0])); // 按照 minimum - actual 从小到大排序

    let e = 0;
    for (const [actual, minimum] of tasks) {
        // 完成该任务之后的能量为 e,那么完成该任务之前的能量为 e+actual,同时该能量必须至少为 minimum
        e = Math.max(e + actual, minimum);
    }
    return e;
};

###rust

impl Solution {
    pub fn minimum_effort(mut tasks: Vec<Vec<i32>>) -> i32 {
        tasks.sort_unstable_by_key(|t| t[1] - t[0]); // 按照 minimum - actual 从小到大排序

        let mut e = 0;
        for t in tasks {
            let actual = t[0];
            let minimum = t[1];
            // 完成 t 之后的能量为 e,那么完成 t 之前的能量为 e+actual,同时该能量必须至少为 minimum
            e = minimum.max(e + actual);
        }
        e
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{tasks}$ 的长度。瓶颈在排序上。
  • 空间复杂度:$\mathcal{O}(1)$。不计入排序的栈开销。

专题训练

见下面贪心题单的「§1.7 交换论证法」。

分类题单

如何科学刷题?

  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站@灵茶山艾府

【儿须成名酒须醉】Python3+排序+贪心+数学+模拟

【儿须成名酒须醉】Python3+排序+贪心+数学证明+模拟


提交结果

  • 执行用时: 164 ms , 在所有 Python3 提交中击败了 94.62% 的用户
  • 内存消耗: 42.6 MB , 在所有 Python3 提交中击败了 58.06% 的用户
  • 通过测试用例: 42 / 42

解题思路

  1. 考虑两个任务的能量组合任务1为$[a_1,m_1]$与任务2为$[a_2,m_2]$
  2. 则先做任务1再做任务2的最小初始能量为$s_{12}=max(m_1,m_2+a_1)$,反之则是$s_{21}=max(m_2,m_1+a_2)$
  3. 不失一般地设$m_1-a_1>=m_2-a_2$,则有$m_1+a_2>=m_2+a_1$
  • 当$m_1<=m_2$时,显然有$s_{12}=m_2+a_1$,而$s_{21}=max(m_2,m_1+a_2)$,由上可得$m_2+a_1<=m_1+a_2$,因此有$s_{12}<=s_{21}$
  • 当$m_1>m_2$时,显然有$s_{12}=max(m_1,m_2+a_1)$,而$s_{21}=m_1+a_2$,由上可得$m_2+a_1<=m_1+a_2$,因此有$s_{12}<=s_{21}$
  1. 由此使用归纳法可知,贪心地将任务$[a,m]$当中$m-a$较大即$a-m$较小的排在前面进行即可

性能优化

复杂度分析

  • 设任务个数为$n$,则有
    • 时间复杂度:$O(nlogn)$
    • 空间复杂度:$O(1)$

代码

###python3

class Solution:
    def minimumEffort(self, tasks: List[List[int]]) -> int:
        tasks.sort(key=lambda x: x[0] - x[1])
        ans = 0
        cur = 0
        for a, m in tasks:
            if cur < m:
                ans += m - cur
                cur = m
            cur -= a
        return ans


逆向思维

从结果状态反推:$初始能量=总消耗能量+剩余能量$ 在剩余能量为$0$时的初始能量最优

  • 执行用时: 180 ms , 在所有 Python3 提交中击败了 80.65% 的用户
  • 内存消耗: 42.5 MB , 在所有 Python3 提交中击败了 81.72% 的用户
  • 通过测试用例: 42 / 42

代码

###python3

class Solution:
    def minimumEffort(self, tasks: List[List[int]]) -> int:
        tasks.sort(key=lambda x: x[1] - x[0])
        ans = 0
        for a, m in tasks:
            ans = ans + a if ans + a > m else m
        return ans

[纯例子讲解] 帮助理解 (老实人,装逼人举例)

纯例子讲解帮助不太明白的人感受到其中的道理;

例1:【5,5】【5,7】

定义:minimum-actual越小的人称为越老实的人;反之,称为装逼人。
讲解: 显然【5,5】是个老实人,【5,7】是个装逼人。老实人很实在,最后剩下5给他,他就会欣然接受,不需要额外的需求,但是装逼人不一样,他明明只要5,确装逼说自己要7,这个时候怎么办呢,拿什么东西来装逼呢,只能借一下老实人的来装一下逼

我们明白,这个答案显然是10,装逼人【5,7】可以拿老实人的5给自己来装逼,所以在不改变结果的情况下他可以乱叫6,7,8,9,10,这些情况下答案都不会改变,一旦装逼人叫了【5,11】,就会发现老实人借给他的也不够他装逼了,无可奈何,我们只能给他分配额外资源让他装逼。

一旦相通这个之后,我们解题思路变成了:倒着安排,先把大量“老实人”放在前面把需求数字垫起来,这样后面的装逼人能装更大的逼,例2有两个老实人【5,5】【5,5】,装逼人就能叫道【5,15】,只要老实人还能借,他就还能装

下面的例子是举例让大家理解装逼程度一致,起始先后顺序是一样的
例3:【1,3】【3,5】 他们的老实程度一致 结果: 3(给第一个装逼) + 3(因为3+3>5) = 6 = 5(给第二个先装逼) + 1(5+ 1 > 3,够装逼了)

例4: 【1,4】【3,5】
大家自己思考一下这个就明白结果了,结果放在评论区,希望大家都能理解,喜欢的或者觉得有趣的不妨点个赞!!!!谢谢你们!

class Solution {
public:
    int minimumEffort(vector<vector<int>>& tasks) {//把握额外报的这部分如何让其无损
        sort(tasks.begin(), tasks.end(), [&](vector<int> &a, vector<int> &b){
            return a[1] - a[0] < b[1] - b[0];
        });

        int result = 0;

        for (const auto &task: tasks){
            result = max(result + task[0], task[1]);
        }

        return result;
    }
};

dd Command in Linux: Copy Disks, Partitions, and Files

Most of the time when you copy a file on Linux, you reach for cp. It walks the filesystem, respects permissions, and works on individual files. But when you need to copy an entire disk, write an ISO image to a USB stick, or create a byte-for-byte backup of a partition, the filesystem view is not enough. For that, Linux ships with dd, a tool that copies raw data block by block between any two files or devices.

This guide explains how to use dd safely to write ISO images, clone disks, back up partitions, create test files, and benchmark storage performance.

dd Syntax

The general form of the dd command is:

txt
dd if=INPUT of=OUTPUT [OPTIONS]

The two key operands are if (input file) and of (output file). Either one can be a regular file, a block device such as /dev/sda, or even /dev/zero and /dev/urandom. Options control the block size, how many blocks to copy, and what conversions to apply.

Warning
dd writes exactly what you tell it to write, to exactly the target you point it at. Pointing of= at the wrong disk will overwrite that disk without warning or confirmation. Always double-check the target device with lsblk or sudo fdisk -l before running a dd command.

Writing an ISO Image to a USB Drive

The most common use for dd is flashing a Linux ISO to a USB drive so you can boot and install from it. First, identify the USB device with lsblk or sudo fdisk -l:

Terminal
lsblk
output
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 0 465.8G 0 disk
├─sda1 8:1 0 512M 0 part /boot/efi
└─sda2 8:2 0 465.3G 0 part /
sdb 8:16 1 14.5G 0 disk
└─sdb1 8:17 1 14.5G 0 part

Here sda is the system disk and sdb is the USB drive. Make sure the USB is unmounted before writing to it:

Terminal
sudo umount /dev/sdb1

Then write the ISO image:

Terminal
sudo dd if=ubuntu-24.04-desktop-amd64.iso of=/dev/sdb bs=4M status=progress oflag=sync

Notice that the output target is the whole disk (/dev/sdb), not a partition (/dev/sdb1). Writing an ISO to a partition would leave the USB unbootable. The options do the following:

  • bs=4M - Read and write 4 MB at a time, which is much faster than the 512-byte default.
  • status=progress - Print a live progress indicator so you can see how much has been written.
  • oflag=sync - Flush every write to the device, so the final byte count reflects data actually on disk.

When dd finishes, run sync once more to make sure all buffered writes hit the USB stick before you remove it:

Terminal
sync

For alternative methods and distro-specific tips, see the guide on how to create a bootable Linux USB drive .

Cloning a Whole Disk

To make an exact block-level copy of one disk onto another, point if= and of= at two different block devices:

Terminal
sudo dd if=/dev/sda of=/dev/sdb bs=64K conv=noerror,sync status=progress

The destination disk must be at least as large as the source. Anything already on it is overwritten. The conversion flags handle read errors gracefully:

  • conv=noerror - Keep going when a read error occurs instead of aborting.
  • conv=sync - Pad short reads with zeros so the output stays aligned with the source.

Clone offline whenever possible. Cloning a disk that is actively being written to produces an inconsistent copy, especially for databases and journaling filesystems.

Backing Up a Partition to an Image File

dd can also write a partition or whole disk to a regular file. That file becomes a bit-for-bit image you can restore later:

Terminal
sudo dd if=/dev/sda1 of=/backup/sda1.img bs=4M status=progress

To save space, pipe the output through gzip:

Terminal
sudo dd if=/dev/sda1 bs=4M status=progress | gzip -c > /backup/sda1.img.gz

The compressed image is much smaller for filesystems that contain text or empty space, though the compression step adds CPU time.

To restore the image to a partition:

Terminal
gunzip -c /backup/sda1.img.gz | sudo dd of=/dev/sda1 bs=4M status=progress

The target partition must be unmounted during restore, and it must be at least as large as the original.

Backing Up and Restoring the MBR

The Master Boot Record sits in the first 512 bytes of a disk that uses a traditional BIOS partition table. You can back it up with dd:

Terminal
sudo dd if=/dev/sda of=/backup/mbr.img bs=512 count=1

To restore the MBR, swap the input and output:

Terminal
sudo dd if=/backup/mbr.img of=/dev/sda bs=512 count=1

On modern UEFI systems the partition table uses GPT instead of MBR, so this trick only applies to older BIOS installations. For GPT disks, use sgdisk --backup and sgdisk --load-backup from the gdisk package.

Creating an Empty File of a Specific Size

dd is often used to create test files of a known size. The count option sets how many blocks to write, and bs sets the block size:

Terminal
dd if=/dev/zero of=testfile bs=1M count=100

This creates a 100 MB file filled with zeros, useful for testing backups, simulating quota limits, or preparing a swap file. To create a file filled with random data instead, read from /dev/urandom:

Terminal
dd if=/dev/urandom of=random.bin bs=1M count=10

Random data is slower to generate because the kernel has to produce the bytes, while /dev/zero is effectively free.

Creating a Swap File

One practical use of the empty-file pattern is creating a swap file :

Terminal
sudo dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

The chmod 600 step keeps the swap file readable only by root, which is required by mkswap on most distros. After swapon, the 2 GB swap file is active immediately. Add an entry to /etc/fstab to make it permanent.

Wiping a Disk

To destroy data on a disk before disposing of it or repurposing it, overwrite the entire device with zeros:

Terminal
sudo dd if=/dev/zero of=/dev/sdb bs=4M status=progress

For a stronger wipe that makes recovery harder on traditional spinning disks, use random data:

Terminal
sudo dd if=/dev/urandom of=/dev/sdb bs=4M status=progress

On SSDs, a dd wipe is not the right tool. SSDs remap blocks internally, so a full write does not guarantee every cell was overwritten. Use blkdiscard or the drive vendor’s secure-erase utility instead.

Benchmarking Disk Write Speed

dd is a quick way to measure sustained write throughput. Write a 1 GB file of zeros and let dd report the rate:

Terminal
dd if=/dev/zero of=tempfile bs=1M count=1024 oflag=direct
output
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 3.1 s, 346 MB/s

The oflag=direct flag bypasses the page cache, so the number reflects actual disk speed rather than memory throughput. For a read benchmark, drop the caches first and then read the file back:

Terminal
sudo sh -c "sync && echo 3 > /proc/sys/vm/drop_caches"
dd if=tempfile of=/dev/null bs=1M

Remove the test file when you are done:

Terminal
rm tempfile

For more detailed I/O profiling, tools like fio and ioping produce more accurate, repeatable results.

Showing Progress on a Running dd

If you started dd without status=progress, you can still ask it to print a status line by sending the USR1 signal to its process. From another terminal, run:

Terminal
sudo kill -USR1 $(pidof dd)

dd will respond by printing the number of bytes copied so far and its current rate, then keep running.

Common Options

A quick rundown of the options covered in this guide, plus a few more worth knowing:

  • if=FILE - Input file or device.
  • of=FILE - Output file or device.
  • bs=N - Block size for both reads and writes (e.g., 4M for 4 MB).
  • ibs=N / obs=N - Separate input and output block sizes.
  • count=N - Number of input blocks to copy.
  • skip=N - Skip N blocks at the start of the input.
  • seek=N - Skip N blocks at the start of the output.
  • status=progress - Print progress while copying.
  • conv=noerror - Continue past read errors.
  • conv=sync - Pad short reads with zeros to keep block alignment.
  • oflag=direct - Bypass the kernel page cache on write.
  • oflag=sync - Flush each write to the device before continuing.

Quick Reference

Command Description
sudo dd if=image.iso of=/dev/sdX bs=4M status=progress oflag=sync Write an ISO image to a USB drive
sudo dd if=/dev/sda of=/dev/sdb bs=64K conv=noerror,sync status=progress Clone one disk to another
sudo dd if=/dev/sda1 of=/backup/sda1.img bs=4M status=progress Back up a partition to an image file
sudo dd if=/dev/sda bs=4M status=progress | gzip -c > disk.img.gz Compressed disk image backup
sudo dd if=/dev/sda of=/backup/mbr.img bs=512 count=1 Back up the MBR
dd if=/dev/zero of=testfile bs=1M count=100 Create a 100 MB file of zeros
sudo dd if=/dev/zero of=/dev/sdX bs=4M status=progress Wipe a disk with zeros
dd if=/dev/zero of=tempfile bs=1M count=1024 oflag=direct Benchmark disk write speed
sudo kill -USR1 $(pidof dd) Ask a running dd for its progress

Troubleshooting

dd: failed to open ‘/dev/sdX’: Permission denied
Writing to a block device requires root privileges. Prefix the command with sudo. If you are already using sudo, double-check that the device path is correct and that the device is not exclusively held by another process.

dd: error writing ‘/dev/sdX’: No space left on device
The destination is smaller than the source. When cloning, the target disk or partition must be at least as large as the source. When writing an ISO, the USB drive must be larger than the ISO file.

The write is much slower than expected
The default block size of 512 bytes forces millions of tiny syscalls. Set bs=4M or bs=64K to speed things up. Also check whether other processes are doing heavy I/O on the same device.

The USB does not boot after writing the ISO
Verify that you wrote the image to the disk (/dev/sdb), not a partition (/dev/sdb1). Also confirm the ISO download with its SHA256 checksum; a truncated or corrupted download produces an unbootable stick.

dd: invalid number: ‘4M’
Older or minimal systems may ship a dd that does not accept the M, G, or K suffixes. On those systems, spell out the block size in bytes (bs=4194304 for 4 MB), or install GNU coreutils.

FAQ

What does dd stand for?
The name comes from the IBM Job Control Language statement “Data Definition.” Because of how often the command is used to overwrite disks by mistake, many Linux users jokingly call it “disk destroyer.” Either way, the behavior is the same: it copies bytes from input to output without touching the filesystem layer.

Is dd faster than cp for copying large files?
For regular files on a normal filesystem, cp is usually just as fast and safer to use. dd only wins when you are copying raw devices, working with exact byte offsets, or deliberately limiting how much data is copied with count.

Can I use dd on an SSD without damaging it?
Reading and writing with dd is fine. The concern with SSDs is full-disk wipes: a single pass of zeros is enough to erase visible data, but repeated full-drive writes wear out the flash cells. For secure erase, use blkdiscard or the drive’s built-in secure-erase command.

How do I see how much data dd has copied?
Add status=progress to the command, or send the running process the USR1 signal with sudo kill -USR1 $(pidof dd). Both methods print the byte count and current throughput without interrupting the copy.

Why use bs=4M instead of the default?
The default block size of 512 bytes means dd issues a separate read and write for every 512 bytes of data. Larger block sizes such as 4 MB dramatically reduce syscall overhead and let the kernel fill the disk pipeline efficiently, often cutting copy times by a factor of ten.

Conclusion

dd is one of the most direct tools in the Linux toolbox. It copies bytes, no more and no less, between any two files or devices. That power makes it indispensable for flashing installers, cloning disks, and building image backups, and the same power makes it unforgiving if you point it at the wrong target.

For related disk tools, see the guides on fdisk for partitioning and df for checking free space.

每日一题-分割数组中数字的数位🟢

给你一个正整数数组 nums ,请你返回一个数组 answer ,你需要将 nums 中每个整数进行数位分割后,按照 nums 中出现的 相同顺序 放入答案数组中。

对一个整数进行数位分割,指的是将整数各个数位按原本出现的顺序排列成数组。

  • 比方说,整数 10921 ,分割它的各个数位得到 [1,0,9,2,1] 。

 

示例 1:

输入:nums = [13,25,83,77]
输出:[1,3,2,5,8,3,7,7]
解释:
- 分割 13 得到 [1,3] 。
- 分割 25 得到 [2,5] 。
- 分割 83 得到 [8,3] 。
- 分割 77 得到 [7,7] 。
answer = [1,3,2,5,8,3,7,7] 。answer 中的数字分割结果按照原数字在数组中的相同顺序排列。

示例 2:

输入:nums = [7,1,3,9]
输出:[7,1,3,9]
解释:nums 中每个整数的分割是它自己。
answer = [7,1,3,9] 。

 

提示:

  • 1 <= nums.length <= 1000
  • 1 <= nums[i] <= 105

2553. 分割数组中数字的数位

解法

思路和算法

这道题要求将给定的正整数数组 $\textit{nums}$ 中的所有正整数按数位分割,保持数位顺序存入答案数组。需要首先遍历数组 $\textit{nums}$ 得到数位总数,然后将每个正整数按数位分割并存入答案数组。

首先遍历数组 $\textit{nums}$ 得到所有正整数的数位总数 $\textit{totalLength}$,然后创建长度为 $\textit{totalLength}$ 的答案数组 $\textit{answer}$,再次遍历数组 $\textit{nums}$,遍历过程中维护答案数组的当前下标 $\textit{index}$,对于每个正整数,执行如下操作。

  1. 用 $\textit{start}$ 表示当前正整数的数位填入答案数组的起始下标,$\textit{start} = \textit{index}$。

  2. 每次将 $\textit{num}$ 的最低位填入 $\textit{answer}[\textit{index}]$,然后将 $\textit{index}$ 的值增加 $1$,重复该操作直到 $\textit{num}$ 的所有位都填入答案数组。

  3. 当前正整数 $\textit{num}$ 填入答案数组的下标范围是 $[\textit{start}, \textit{index} - 1]$,为按照数位从低到高的顺序填入。为了和数组 $\textit{nums}$ 中的数位顺序保持一致,需要将答案数组的下标范围 $[\textit{start}, \textit{index} - 1]$ 的子数组翻转。

遍历结束之后,即可得到答案数组。

代码

###Java

class Solution {
    public int[] separateDigits(int[] nums) {
        int totalLength = 0;
        for (int num : nums) {
            totalLength += getLength(num);
        }
        int[] answer = new int[totalLength];
        int index = 0;
        for (int num : nums) {
            int start = index;
            int temp = num;
            while (temp != 0) {
                answer[index] = temp % 10;
                index++;
                temp /= 10;
            }
            reverse(answer, start, index - 1);
        }
        return answer;
    }

    public int getLength(int num) {
        int length = 0;
        while (num != 0) {
            length++;
            num /= 10;
        }
        return length;
    }

    public void reverse(int[] answer, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            int temp = answer[i];
            answer[i] = answer[j];
            answer[j] = temp;
        }
    }
}

###C#

public class Solution {
    public int[] SeparateDigits(int[] nums) {
        int totalLength = 0;
        foreach (int num in nums) {
            totalLength += GetLength(num);
        }
        int[] answer = new int[totalLength];
        int index = 0;
        foreach (int num in nums) {
            int start = index;
            int temp = num;
            while (temp != 0) {
                answer[index] = temp % 10;
                index++;
                temp /= 10;
            }
            Reverse(answer, start, index - 1);
        }
        return answer;
    }

    public int GetLength(int num) {
        int length = 0;
        while (num != 0) {
            length++;
            num /= 10;
        }
        return length;
    }

    public void Reverse(int[] answer, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            int temp = answer[i];
            answer[i] = answer[j];
            answer[j] = temp;
        }
    }
}

###C++

class Solution {
public:
    vector<int> separateDigits(vector<int>& nums) {
        int totalLength = 0;
        for (int num : nums) {
            totalLength += getLength(num);
        }
        vector<int> answer(totalLength);
        int index = 0;
        for (int num : nums) {
            int start = index;
            int temp = num;
            while (temp != 0) {
                answer[index] = temp % 10;
                index++;
                temp /= 10;
            }
            reverse(answer, start, index - 1);
        }
        return answer;
    }

    int getLength(int num) {
        int length = 0;
        while (num != 0) {
            length++;
            num /= 10;
        }
        return length;
    }

    void reverse(vector<int>& answer, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            swap(answer[i], answer[j]);
        }
    }
};

###Python

class Solution:
    def separateDigits(self, nums: List[int]) -> List[int]:
        answer = []
        index = 0
        for num in nums:
            start = index
            temp = num
            while temp != 0:
                answer.append(temp % 10)
                index += 1
                temp //= 10
            self.reverse(answer, start, index - 1)
        return answer

    def getLength(self, num: int) -> int:
        length = 0
        while num != 0:
            length += 1
            num //= 10
        return length

    def reverse(self, answer: List[int], start: int, end: int) -> None:
        i, j = start, end
        while i < j:
            answer[i], answer[j] = answer[j], answer[i]
            i += 1
            j -= 1

###C

int getLength(int num) {
    int length = 0;
    while (num != 0) {
        length++;
        num /= 10;
    }
    return length;
}

void reverse(int* answer, int start, int end) {
    for (int i = start, j = end; i < j; i++, j--) {
        int temp = answer[i];
        answer[i] = answer[j];
        answer[j] = temp;
    }
}

int* separateDigits(int* nums, int numsSize, int* returnSize) {
    int totalLength = 0;
    for (int i = 0; i < numsSize; i++) {
        totalLength += getLength(nums[i]);
    }
    int* answer = (int*) malloc(sizeof(int) * totalLength);
    *returnSize = totalLength;
    int index = 0;
    for (int i = 0; i < numsSize; i++) {
        int start = index;
        int temp = nums[i];
        while (temp != 0) {
            answer[index] = temp % 10;
            index++;
            temp /= 10;
        }
        reverse(answer, start, index - 1);
    }
    return answer;
}

###Go

func separateDigits(nums []int) []int {
    totalLength := 0
    for _, num := range nums {
        totalLength += getLength(num)
    }
    answer := make([]int, totalLength)
    index := 0
    for _, num := range nums {
        start := index
        temp := num
        for temp != 0 {
            answer[index] = temp % 10
            index++
            temp /= 10
        }
        reverse(answer, start, index - 1)
    }
    return answer
}

func getLength(num int) int {
    length := 0
    for num != 0 {
        length++
        num /= 10
    }
    return length
}

func reverse(answer []int, start int, end int) {
    for i, j := start, end; i < j; i, j = i + 1, j - 1 {
        answer[i], answer[j] = answer[j], answer[i]
    }
}

###JavaScript

var separateDigits = function(nums) {
    let totalLength = 0;
    for (let num of nums) {
        totalLength += getLength(num);
    }
    let answer = new Array(totalLength);
    let index = 0;
    for (let num of nums) {
        let start = index;
        let temp = num;
        while (temp !== 0) {
            answer[index] = temp % 10;
            index++;
            temp = Math.floor(temp / 10);
        }
        reverse(answer, start, index - 1);
    }
    return answer;
};

var getLength = function(num) {
    let length = 0;
    while (num !== 0) {
        length++;
        num = Math.floor(num / 10);
    }
    return length;
};

var reverse = function(answer, start, end) {
    for (let i = start, j = end; i < j; i++, j--) {
        let temp = answer[i];
        answer[i] = answer[j];
        answer[j] = temp;
    }
};

###TypeScript

function separateDigits(nums: number[]): number[] {
    let totalLength = 0;
    for (let num of nums) {
        totalLength += getLength(num);
    }
    let answer = new Array(totalLength);
    let index = 0;
    for (let num of nums) {
        let start = index;
        let temp = num;
        while (temp !== 0) {
            answer[index] = temp % 10;
            index++;
            temp = Math.floor(temp / 10);
        }
        reverse(answer, start, index - 1);
    }
    return answer;
};

function getLength(num: number): number {
    let length = 0;
    while (num !== 0) {
        length++;
        num = Math.floor(num / 10);
    }
    return length;
};

function reverse(answer: number[], start: number, end: number): void {
    for (let i = start, j = end; i < j; i++, j--) {
        let temp = answer[i];
        answer[i] = answer[j];
        answer[j] = temp;
    }
};

复杂度分析

  • 时间复杂度:$O(n \log_{10} m)$,其中 $n$ 是数组 $\textit{nums}$ 的长度,$m$ 是数组 $\textit{nums}$ 的最大元素。计算答案数组的长度与将数位填入答案数组的时间是 $O(n \log_{10} m)$。

  • 空间复杂度:$O(1)$。注意返回值不计入空间复杂度。

倒序插入

这个刚开始写的时候担心数组的长度和时间复杂度的原因,担心通过不了,结果没想过通过了。
我是用一个result数组来装将返回的分割数位值,考虑他们的长度,每一个元素的大小在10的五次方以内,分割之后100000共有六位,也就是最大是6,加上数组的长度最大是1000,于是我就把数组的大小取为6*10000=60000.
虽然我也知道浪费空间,但是我目前没有更好的方法,希望有大佬能够指点。

确定了返回数组了,然后就是然后把原始的整数拆分插入到结果数组中,后面我想到只要逐个不断求余就可以得到每一位数,后面我就在第一层遍历中添加一个循环得到整数的各个数位,但是提交的结果不符合要求,题目要求将数位按原本出现的顺序排列成数组,于是我就用一个数组作为辅助空间,将它反向添加,于是就得到了和题目意思相同的结果,辅助空间的大小就是一个整数拥有的各个位数的数量,由前面可以知道最大是6。

之后返回即可。

int* separateDigits(int* nums, int numsSize, int* returnSize){
    int *result=(int*)malloc(sizeof(int)*60000);
    int count=0;
    //遍历整个数组
    for(int i=0;i<numsSize;i++){
        int tmp=nums[i];
        int help[6];
        int tmpCount=0;
        //得到整数的各个数位,将他们储存在一个辅助数组中
        while(tmp){
            help[tmpCount++]=tmp%10;
            tmp=tmp/10;
        }
        //把数组中的元素倒序添加到结果数组中
        for(int j=tmpCount-1;j>=0;j--){
            result[count++]=help[j];
        }
    }
    *returnSize=count;
    return result;
}

两种方法:用字符串 / 不用字符串(Python/Java/C++/Go)

写法一:用字符串

把 $\textit{nums}[i]$ 转成字符串,即可从高到低遍历数位。

class Solution:
    def separateDigits(self, nums: List[int]) -> List[int]:
        return [int(ch) for x in nums for ch in str(x)]
class Solution {
    public int[] separateDigits(int[] nums) {
        List<Integer> digits = new ArrayList<>();
        for (int x : nums) {
            for (char ch : String.valueOf(x).toCharArray()) {
                digits.add(ch - '0');
            }
        }

        int m = digits.size();
        int[] ans = new int[m];
        for (int i = 0; i < m; i++) {
            ans[i] = digits.get(i);
        }
        return ans;
    }
}
class Solution {
public:
    vector<int> separateDigits(vector<int>& nums) {
        vector<int> ans;
        for (int x : nums) {
            for (char ch : to_string(x)) {
                ans.push_back(ch - '0');
            }
        }
        return ans;
    }
};
func separateDigits(nums []int) (ans []int) {
for _, x := range nums {
for _, ch := range strconv.Itoa(x) {
ans = append(ans, int(ch-'0'))
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log U)$,其中 $n$ 是 $\textit{nums}$ 的长度,$U=\max(\textit{nums})$。
  • 空间复杂度:$\mathcal{O}(\log U)$。返回值不计入。

方法二:不用字符串

不断地把 $n$ 除以 $10$(下取整)直到 $0$,例如 $123\to 12\to 1\to 0$。在这个过程中的 $n\bmod 10$,即为每个数位。

这样做是从低到高遍历数位,和题目要求的顺序相反。

我们可以从右到左遍历 $\textit{nums}$,从低到高遍历 $\textit{nums}[i]$ 的数位。最后把遍历过的数位反转,即为答案。

class Solution:
    def separateDigits(self, nums: list[int]) -> list[int]:
        ans = []
        for x in reversed(nums):
            while x > 0:
                ans.append(x % 10)
                x //= 10
        ans.reverse()
        return ans
class Solution {
    public int[] separateDigits(int[] nums) {
        List<Integer> digits = new ArrayList<>();
        for (int i = nums.length - 1; i >= 0; i--) {
            for (int x = nums[i]; x > 0; x /= 10) {
                digits.add(x % 10);
            }
        }

        int m = digits.size();
        int[] ans = new int[m];
        for (int i = 0; i < m; i++) {
            ans[i] = digits.get(m - 1 - i);
        }
        return ans;
    }
}
class Solution {
public:
    vector<int> separateDigits(vector<int>& nums) {
        vector<int> ans;
        for (int x : nums | views::reverse) {
            for (; x > 0; x /= 10) {
                ans.push_back(x % 10);
            }
        }
        ranges::reverse(ans);
        return ans;
    }
};
func separateDigits(nums []int) (ans []int) {
for _, x := range slices.Backward(nums) {
for ; x > 0; x /= 10 {
ans = append(ans, x%10)
}
}
slices.Reverse(ans)
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log U)$,其中 $n$ 是 $\textit{nums}$ 的长度,$U=\max(\textit{nums})$。
  • 空间复杂度:$\mathcal{O}(1)$。返回值不计入。

分类题单

如何科学刷题?

  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站@灵茶山艾府

每日一题-达到末尾下标所需的最大跳跃次数🟡

给你一个下标从 0 开始、由 n 个整数组成的数组 nums 和一个整数 target

你的初始位置在下标 0 。在一步操作中,你可以从下标 i 跳跃到任意满足下述条件的下标 j

  • 0 <= i < j < n
  • -target <= nums[j] - nums[i] <= target

返回到达下标 n - 1 处所需的 最大跳跃次数

如果无法到达下标 n - 1 ,返回 -1

 

示例 1:

输入:nums = [1,3,6,4,1,2], target = 2
输出:3
解释:要想以最大跳跃次数从下标 0 到下标 n - 1 ,可以按下述跳跃序列执行操作:
- 从下标 0 跳跃到下标 1 。 
- 从下标 1 跳跃到下标 3 。 
- 从下标 3 跳跃到下标 5 。 
可以证明,从 0 到 n - 1 的所有方案中,不存在比 3 步更长的跳跃序列。因此,答案是 3 。 

示例 2:

输入:nums = [1,3,6,4,1,2], target = 3
输出:5
解释:要想以最大跳跃次数从下标 0 到下标 n - 1 ,可以按下述跳跃序列执行操作:
- 从下标 0 跳跃到下标 1 。 
- 从下标 1 跳跃到下标 2 。 
- 从下标 2 跳跃到下标 3 。 
- 从下标 3 跳跃到下标 4 。 
- 从下标 4 跳跃到下标 5 。 
可以证明,从 0 到 n - 1 的所有方案中,不存在比 5 步更长的跳跃序列。因此,答案是 5 。 

示例 3:

输入:nums = [1,3,6,4,1,2], target = 0
输出:-1
解释:可以证明不存在从 0 到 n - 1 的跳跃序列。因此,答案是 -1 。 

 

提示:

  • 2 <= nums.length == n <= 1000
  • -109 <= nums[i] <= 109
  • 0 <= target <= 2 * 109

C++, 动态规划

思路

  1. f[i]表示从0到下标i的最大跳跃次数;
  2. f[0]=0, f[i]=f[j]+1, 可以从j跳过来;
class Solution {
public:
    int maximumJumps(vector<int>& nums, int target) {
        int n = nums.size();
        vector<int> f(n, -1);
        f[0] = 0;
        /* 开始递推 */
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (f[j] != -1 && abs(nums[i] - nums[j]) <= target) {
                    f[i] = max(f[i], f[j] + 1);
                }
            }
        }
        return f[n - 1];
    }
};
class Solution:
    def maximumJumps(self, nums: List[int], target: int) -> int:
        n = len(nums)
        f = [-1] * n
        f[0] = 0
        for j in range(1, n):
            for i in range(j):
                if f[i] != -1 and abs(nums[i] - nums[j]) <= target:
                    f[j] = max(f[j], f[i] + 1)
        return f[-1]

【什码情况】Java记忆化搜索

image.png{:width=400}

解题思路

此处撰写解题思路

代码

###java

class Solution {
    // jump[i] 表示从下标 0 开始跳到 nums[i] 所需的最大跳跃次数
    int[] jump;

    public int maximumJumps(int[] nums, int target) {
        this.jump = new int[nums.length];
        Arrays.fill(jump, Integer.MIN_VALUE);
        jump[0] = 0;
        int maximumJumps = dfs(nums, nums.length - 1, target);
        return maximumJumps >= 0 ? maximumJumps : -1;
    }

    // 返回从下标 i 跳到下标 0 所需最大的跳跃次数
    private int dfs(int[] nums, int i, int target) {
        if (jump[i] != Integer.MIN_VALUE) {
            return jump[i];
        }

        int max = Integer.MIN_VALUE;
        for (int j = i - 1; j >= 0; j--) {
            int val = nums[j] - nums[i];
            if (-target <= val && val <= target) {
                int jump = dfs(nums, j, target) + 1;
                max = Math.max(max, jump);
            }
        }

        return jump[i] = max;
    }

}

最后

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

两种方法:普通 DP / 值域线段树优化 DP(Python/Java/C++/Go)

一、寻找子问题

想一想,最后一步发生了什么?

最后一步,我们从某个满足条件的下标 $i$ 跳到了下标 $n-1$。

枚举满足条件的 $i$,问题变成:

  • 从下标 $0$ 到达下标 $i$ 所需的最大跳跃次数。

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

注:从右往左思考,主要是方便把递归翻译成从左往右的递推。从左往右思考也是可以的。

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

根据上面的讨论,定义 $\textit{dfs}(j)$ 表示从下标 $0$ 到达下标 $j$ 所需的最大跳跃次数。

枚举满足 $0\le i<j$ 且 $|\textit{nums}[i]-\textit{nums}[j]|\le \textit{target}$ 的下标 $i$,问题变成从下标 $0$ 到达下标 $i$ 所需的最大跳跃次数,再加上从 $i$ 跳到 $j$ 的一次。

取最大值,得

$$
\textit{dfs}(j) = \max_{i} \textit{dfs}(i) + 1
$$

其中 $0\le i<j$ 且 $|\textit{nums}[i]-\textit{nums}[j]|\le \textit{target}$。

递归边界

  • $\textit{dfs}(0)=0$。从 $0$ 到 $0$ 不用跳。
  • 如果没有满足条件的 $i$,那么 $\textit{dfs}(j) = -\infty$。

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

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

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

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

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

关于记忆化搜索的原理,请看视频讲解 动态规划入门:从记忆化搜索到递推【基础算法精讲 17】,其中包含把记忆化搜索 1:1 翻译成递推的技巧。

class Solution:
    def maximumJumps(self, nums: List[int], target: int) -> int:
        @cache  # 缓存装饰器,避免重复计算 dfs(一行代码实现记忆化)
        def dfs(j: int) -> int:
            if j == 0:  # 起点
                return 0
            res = -inf
            for i in range(j):
                if abs(nums[i] - nums[j]) <= target:  # 可以从 i 跳到 j
                    res = max(res, dfs(i) + 1)
            return res

        ans = dfs(len(nums) - 1)  # 终点
        return -1 if ans < 0 else ans
class Solution {
    public int maximumJumps(int[] nums, int target) {
        int n = nums.length;
        int[] memo = new int[n];
        int ans = dfs(n - 1, nums, target, memo);
        return ans < 0 ? -1 : ans;
    }

    private int dfs(int j, int[] nums, int target, int[] memo) {
        if (j == 0) { // 起点
            return 0;
        }

        if (memo[j] != 0) { // 之前计算过
            return memo[j];
        }

        int res = Integer.MIN_VALUE;
        for (int i = 0; i < j; i++) {
            if (Math.abs(nums[i] - nums[j]) <= target) { // 可以从 i 跳到 j
                res = Math.max(res, dfs(i, nums, target, memo) + 1);
            }
        }
        memo[j] = res; // 记忆化
        return res;
    }
}
class Solution {
public:
    int maximumJumps(vector<int>& nums, int target) {
        int n = nums.size();
        vector<int> memo(n);

        auto dfs = [&](this auto&& dfs, int j) -> int {
            if (j == 0) { // 起点
                return 0;
            }

            int& res = memo[j]; // 注意这里是引用
            if (res) { // 之前计算过
                return res;
            }

            res = INT_MIN;
            for (int i = 0; i < j; i++) {
                if (abs(nums[i] - nums[j]) <= target) { // 可以从 i 跳到 j
                    res = max(res, dfs(i) + 1);
                }
            }
            return res;
        };

        int ans = dfs(n - 1); // 终点
        return ans < 0 ? -1 : ans;
    }
};
func maximumJumps(nums []int, target int) int {
n := len(nums)
memo := make([]int, n)

var dfs func(int) int
dfs = func(j int) int {
if j == 0 { // 起点
return 0
}

p := &memo[j]
if *p != 0 { // 之前计算过
return *p
}

res := math.MinInt
for i, x := range nums[:j] {
if abs(x-nums[j]) <= target { // 可以从 i 跳到 j
res = max(res, dfs(i)+1)
}
}
*p = res // 记忆化
return res
}

ans := dfs(n - 1) // 终点
if ans < 0 {
return -1
}
return ans
}

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

复杂度分析

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

四、1:1 翻译成递推

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

具体来说,$f[j]$ 的定义和 $\textit{dfs}(j)$ 的定义是完全一样的,都表示从下标 $0$ 到达下标 $j$ 所需的最大跳跃次数。

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

$$
f[j] = \max_{i} f[i] + 1
$$

其中 $0\le i<j$ 且 $|\textit{nums}[i]-\textit{nums}[j]|\le \textit{target}$。

如果没有满足条件的 $i$,那么 $f[j] = -\infty$。

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

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

class Solution:
    def maximumJumps(self, nums: List[int], target: int) -> int:
        n = len(nums)
        f = [-inf] * n
        f[0] = 0
        for j in range(1, n):
            for i in range(j):
                if abs(nums[i] - nums[j]) <= target:  # 可以从 i 跳到 j
                    f[j] = max(f[j], f[i] + 1)
        return -1 if f[-1] < 0 else f[-1]
class Solution {
    public int maximumJumps(int[] nums, int target) {
        int n = nums.length;
        int[] f = new int[n];
        for (int j = 1; j < n; j++) {
            f[j] = Integer.MIN_VALUE;
            for (int i = 0; i < j; i++) {
                if (Math.abs(nums[i] - nums[j]) <= target) { // 可以从 i 跳到 j
                    f[j] = Math.max(f[j], f[i] + 1);
                }
            }
        }
        return f[n - 1] < 0 ? -1 : f[n - 1];
    }
}
class Solution {
public:
    int maximumJumps(vector<int>& nums, int target) {
        int n = nums.size();
        vector<int> f(n, INT_MIN);
        f[0] = 0;
        for (int j = 1; j < n; j++) {
            for (int i = 0; i < j; i++) {
                if (abs(nums[i] - nums[j]) <= target) { // 可以从 i 跳到 j
                    f[j] = max(f[j], f[i] + 1);
                }
            }
        }
        return f[n - 1] < 0 ? -1 : f[n - 1];
    }
};
func maximumJumps(nums []int, target int) int {
n := len(nums)
f := make([]int, n)

for j := 1; j < n; j++ {
f[j] = math.MinInt
for i, x := range nums[:j] {
if abs(x-nums[j]) <= target { // 可以从 i 跳到 j
f[j] = max(f[j], f[i]+1)
}
}
}

if f[n-1] < 0 {
return -1
}
return f[n-1]
}

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

复杂度分析

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

五、值域线段树优化 DP

如果 $n=10^5$,上面的做法就超时了。

遍历到 $\textit{nums}[j]$ 时,我们需要知道满足 $\textit{nums}[j]-\textit{target} \le \textit{nums}[i] \le \textit{nums}[j]+\textit{target}$ 的最大的 $f[i]$。

这可以用一棵值域线段树维护。线段树的区间是值域区间,例如区间 $[20,23]$ 指的是 $\textit{nums}$ 中的元素 $20,21,22,23$。线段树的每个节点保存的是值域区间对应的最大的 $f[i]$。例如 $\textit{nums}[4]=20$ 且 $f[4] = 3$,那么线段树维护的位置 $20$ 更新为 $3$。

如此一来,满足 $\textit{nums}[j]-\textit{target} \le \textit{nums}[i] \le \textit{nums}[j]+\textit{target}$ 的最大的 $f[i]$,可以通过线段树的区间最值查询得到。

# 完整的线段树模板见 https://leetcode.cn/circle/discuss/mOr1u6/
class SegmentTree:
    def __init__(self, n: int) -> None:
        self.t = [-inf] * (2 << (n - 1).bit_length())

    def update(self, node: int, l: int, r: int, i: int, val: int) -> None:
        if l == r:  # 叶子
            self.t[node] = val
            return
        m = (l + r) // 2
        if i <= m:  # i 在左子树
            self.update(node * 2, l, m, i, val)
        else:  # i 在右子树
            self.update(node * 2 + 1, m + 1, r, i, val)
        self.t[node] = max(self.t[node * 2], self.t[node * 2 + 1])

    def query(self, node: int, l: int, r: int, ql: int, qr: int) -> int:
        if ql <= l and r <= qr:  # 当前子树完全在 [ql, qr] 内
            return self.t[node]
        m = (l + r) // 2
        if qr <= m:  # [ql, qr] 在左子树
            return self.query(node * 2, l, m, ql, qr)
        if ql > m:  # [ql, qr] 在右子树
            return self.query(node * 2 + 1, m + 1, r, ql, qr)
        return max(self.query(node * 2, l, m, ql, qr), self.query(node * 2 + 1, m + 1, r, ql, qr))


class Solution:
    def maximumJumps(self, nums: List[int], target: int) -> int:
        # 去重排序,便于离散化
        sorted_nums = sorted(set(nums))

        n = len(nums)
        m = len(sorted_nums)
        t = SegmentTree(m)  # 值域线段树

        # nums[0] 对应的 f[0] = 0
        t.update(1, 0, m - 1, bisect_left(sorted_nums, nums[0]), 0)

        for j in range(1, n):
            l = bisect_left(sorted_nums, nums[j] - target)       # >= nums[j]-target 的第一个数
            r = bisect_right(sorted_nums, nums[j] + target) - 1  # <= nums[j]+target 的最后一个数
            # t.query 返回满足 nums[j]-target <= nums[i] <= nums[j]+target 的最大的 f[i]
            fj = t.query(1, 0, m - 1, l, r) + 1
            t.update(1, 0, m - 1, bisect_left(sorted_nums, nums[j]), fj)

        return -1 if fj < 0 else fj
class Solution {
    // 完整的线段树模板见 https://leetcode.cn/circle/discuss/mOr1u6/
    private int[] tree;

    private void update(int node, int l, int r, int i, int val) {
        if (l == r) { // 叶子
            tree[node] = val;
            return;
        }
        int m = (l + r) / 2;
        if (i <= m) { // i 在左子树
            update(node * 2, l, m, i, val);
        } else { // i 在右子树
            update(node * 2 + 1, m + 1, r, i, val);
        }
        tree[node] = Math.max(tree[node * 2], tree[node * 2 + 1]);
    }

    private int query(int node, int l, int r, int ql, int qr) {
        if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
            return tree[node];
        }
        int m = (l + r) / 2;
        if (qr <= m) { // [ql, qr] 在左子树
            return query(node * 2, l, m, ql, qr);
        }
        if (ql > m) { // [ql, qr] 在右子树
            return query(node * 2 + 1, m + 1, r, ql, qr);
        }
        return Math.max(query(node * 2, l, m, ql, qr), query(node * 2 + 1, m + 1, r, ql, qr));
    }

    public int maximumJumps(int[] nums, int target) {
        int n = nums.length;
        int[] sorted = nums.clone(); // 用于离散化
        Arrays.sort(sorted);

        tree = new int[2 << (32 - Integer.numberOfLeadingZeros(n - 1))];
        Arrays.fill(tree, Integer.MIN_VALUE);

        // nums[0] 对应的 f[0] = 0
        update(1, 0, n - 1, lowerBound(sorted, nums[0]), 0);

        for (int j = 1; ; j++) {
            int l = lowerBound(sorted, (long) nums[j] - target);         // >= nums[j]-target 的第一个数
            int r = lowerBound(sorted, (long) nums[j] + target + 1) - 1; // <= nums[j]+target 的最后一个数
            // query 返回满足 nums[j]-target <= nums[i] <= nums[j]+target 的最大的 f[i]
            int fj = query(1, 0, n - 1, l, r) + 1;
            if (j == n - 1) {
                return fj < 0 ? -1 : fj;
            }
            update(1, 0, n - 1, lowerBound(sorted, nums[j]), fj);
        }
    }

    // 见 https://www.bilibili.com/video/BV1AP41137w7/
    private int lowerBound(int[] nums, long target) {
        int left = -1;
        int right = nums.length;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            if (nums[mid] >= target) {
                right = mid;
            } else {
                left = mid;
            }
        }
        return right;
    }
}
// 完整的线段树模板见 https://leetcode.cn/circle/discuss/mOr1u6/
class SegmentTree {
    vector<int> tree;

public:
    SegmentTree(int n) : tree(2 << bit_width(n - 1u), INT_MIN) {}

    void update(int node, int l, int r, int i, int val) {
        if (l == r) { // 叶子
            tree[node] = val;
            return;
        }
        int m = (l + r) / 2;
        if (i <= m) { // i 在左子树
            update(node * 2, l, m, i, val);
        } else { // i 在右子树
            update(node * 2 + 1, m + 1, r, i, val);
        }
        tree[node] = max(tree[node * 2], tree[node * 2 + 1]);
    }

    int query(int node, int l, int r, int ql, int qr) const {
        if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
            return tree[node];
        }
        int m = (l + r) / 2;
        if (qr <= m) { // [ql, qr] 在左子树
            return query(node * 2, l, m, ql, qr);
        }
        if (ql > m) { // [ql, qr] 在右子树
            return query(node * 2 + 1, m + 1, r, ql, qr);
        }
        return max(query(node * 2, l, m, ql, qr), query(node * 2 + 1, m + 1, r, ql, qr));
    }
};

class Solution {
public:
    int maximumJumps(vector<int>& nums, int target) {
        // 排序去重,便于离散化
        auto sorted = nums;
        ranges::sort(sorted);
        sorted.erase(ranges::unique(sorted).begin(), sorted.end());

        int n = nums.size();
        int m = sorted.size();

        SegmentTree t(m); // 值域线段树

        // nums[0] 对应的 f[0] = 0
        int pos = ranges::lower_bound(sorted, nums[0]) - sorted.begin();
        t.update(1, 0, m - 1, pos, 0);

        long long tar = target;
        for (int j = 1; ; j++) {
            int l = ranges::lower_bound(sorted, nums[j] - tar) - sorted.begin();     // >= nums[j]-target 的第一个数
            int r = ranges::upper_bound(sorted, nums[j] + tar) - sorted.begin() - 1; // <= nums[j]+target 的最后一个数
            // t.query 返回满足 nums[j]-target <= nums[i] <= nums[j]+target 的最大的 f[i]
            int fj = t.query(1, 0, m - 1, l, r) + 1;
            if (j == n - 1) {
                return fj < 0 ? -1 : fj;
            }
            pos = ranges::lower_bound(sorted, nums[j]) - sorted.begin();
            t.update(1, 0, m - 1, pos, fj);
        }
    }
};
// 完整的线段树模板见 https://leetcode.cn/circle/discuss/mOr1u6/
type seg []int

func (t seg) update(node, l, r, i, val int) {
if l == r { // 叶子
t[node] = val
return
}
m := (l + r) / 2
if i <= m { // i 在左子树
t.update(node*2, l, m, i, val)
} else { // i 在右子树
t.update(node*2+1, m+1, r, i, val)
}
t[node] = max(t[node*2], t[node*2+1])
}

func (t seg) query(node, l, r, ql, qr int) int {
if ql <= l && r <= qr { // 当前子树完全在 [ql, qr] 内
return t[node]
}
m := (l + r) / 2
if qr <= m { // [ql, qr] 在左子树
return t.query(node*2, l, m, ql, qr)
}
if ql > m { // [ql, qr] 在右子树
return t.query(node*2+1, m+1, r, ql, qr)
}
return max(t.query(node*2, l, m, ql, qr), t.query(node*2+1, m+1, r, ql, qr))
}

func maximumJumps(nums []int, target int) int {
// 排序去重,便于离散化
sorted := slices.Clone(nums)
slices.Sort(sorted)
sorted = slices.Compact(sorted)

n := len(nums)
m := len(sorted)

t := make(seg, 2<<bits.Len(uint(m-1))) // 值域线段树
for i := range t {
t[i] = math.MinInt
}

// nums[0] 对应的 f[0] = 0
t.update(1, 0, m-1, sort.SearchInts(sorted, nums[0]), 0)

for j := 1; ; j++ {
l := sort.SearchInts(sorted, nums[j]-target)       // >= nums[j]-target 的第一个数
r := sort.SearchInts(sorted, nums[j]+target+1) - 1 // <= nums[j]+target 的最后一个数
// t.query 返回满足 nums[j]-target <= nums[i] <= nums[j]+target 的最大的 f[i]
fj := t.query(1, 0, m-1, l, r) + 1
if j == n-1 {
if fj < 0 {
return -1
}
return fj
}
t.update(1, 0, m-1, sort.SearchInts(sorted, nums[j]), fj)
}
}

复杂度分析

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

相似题目

2407. 最长递增子序列 II

更多相似题目,见下面动态规划题单的「§11.4 树状数组/线段树优化 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站@灵茶山艾府

把一份前端 checklist 变成 AI 的 Skill:让 CR 不再靠记忆

引子:一个吃灰三年的项目被重新盘活

写这篇博客的由头有点特别。

我有一个叫 front-end-checklist 的老项目(网页:wsafight.github.io/front-end-c…),2023 年初在公司做 Code Review 的时候顺手整理出来的。那会儿评审新同学的代码,总是在重复同样的话:"这里没做 XSS 转义"、"useEffect 里有竞态"、"label 没和 input 关联"。后来干脆把这些反复出现的问题写成清单,用 Jekyll 挂在 GitHub Pages 上。

然后它就一直在那儿积灰。2024 只有一次提交,2025 只有一次,到了 2026 年 5 月也还没动过。

2026-05-08 晚上,我本来只想顺手改一下清单里过时的条目,结果一头扎进 Claude Code,两小时做了 22 次提交,把这个静态页面彻底改造了一遍。

真正让我想写这篇文章的,不是"AI 让我写代码变快"。快是肯定快,但这次改造最后落到了一件我之前没想过的事情上——一份静态文档,换了个交付形式,才真正开始被用起来。

清单本身:三年沉淀下来的 160 条检查项

先交代清楚这个清单是什么。

它不是 ESLint 能查的那种风格规则,而是 Code Review 时需要结合业务和上下文判断的问题。目前一共 25 个分组、160 条检查项:

命名规范      数据与类型    函数设计      状态管理      控制流
异步处理      数据请求      UI 与渲染     样式与响应式  路由与权限
性能          安全与健壮性  表单与交互    错误处理      测试
无障碍访问    用户体验      代码质量      工程化        国际化
日志与监控    依赖管理      浏览器兼容    文档与协作    PR 自检

随便挑几条看看:

  • 区分"缺失"、"为空"、"为 0"、"为空数组"、"为空字符串"的业务含义
  • 处理并发请求的竞态问题,避免旧响应覆盖新状态
  • 同一表单/输入控件不要在受控与非受控之间切换
  • useEffect / watch 的依赖项必须完整,避免闭包捕获过期值
  • 定时器和事件监听记得清除,否则可能引发内存泄漏
  • 表单错误应定位到具体字段,而不是只给出笼统提示

这些不是靠格式化、类型推导或者简单 AST 规则就能稳定抓出来的问题。它们往往来自真实线上事故、返工、误解和交接成本。以前这些经验只能靠人去记,记不住就会在别的项目里重新踩一次。

痛点:清单挂在网上,但不会进入工作流

清单做完这三年,我一直有个遗憾:没人真的会去翻

我自己都不翻。做一个 PR 的时候,我知道清单里有条"处理并发请求的竞态",但我会不会每次都老老实实打开页面对一遍?不会。同事更不会。新人入职时我会把链接丢给他们,他们收藏,然后就再也不打开了。

这是很多静态文档共同的问题:信息在那里,但你必须主动去触达它。而在 CR 场景里,主动触达的前提是你已经意识到自己可能漏了什么。可真正漏掉的时候,人往往意识不到。

我试过一些办法:

  • 写成 Markdown 放 README:没人会在写代码时切到 README 里逐条对照。
  • 做成可搜索的网页:搜索的前提是你已经知道关键词,可 XSS、竞态、状态错位这类问题,经常就是因为你没意识到该搜什么。
  • 加上勾选和进度追踪:这次改造时我反而把它们删了,因为它把"查阅文档"变成了"填表",使用意愿更低。

根本问题不是文档形式不够花哨,而是查阅式文档很难主动进入人的工作流。

转折:把清单变成 AI 的上下文

Claude Code 的 Skill 机制改变了这件事。

简单说,Skill 是一段给 AI 读的说明书,加上它执行任务时需要参考的资料。当你在对话里说到特定触发词时,AI 会加载这个 Skill,并按说明书走流程。

我的 frontend-checklist Skill 里写的是这样的逻辑:

  1. 只在用户明确请求时触发:比如 /frontend-checklist、"按前端清单 review"、"按 checklist 检查这段代码"。用户没提,它不会自作主张去扫代码。
  2. 确认检查范围:如果用户指定了文件,就只看指定文件;如果说 "review PR",就用 git diff 定位变更;如果都没有,就先确认范围,不盲目扫整个仓库。
  3. 按语言选择清单:中文提问读中文版清单,英文提问读英文版清单。
  4. 只报命中的问题:通过和不适用的条目一律不输出。
  5. 按严重度排序:安全、数据丢失、竞态、内存泄漏这类硬问题优先,命名风格靠后。
  6. 每个问题都标出文件路径和行号:让人能直接跳到具体位置。

关键的翻转在这里:

以前是"你去翻清单"。现在是"清单来找你"。

写 PR 的时候,你不需要记得清单里每一条是什么,直接在 IDE 里说一句"按前端清单 review 我这个 PR",AI 会把 160 条和当前 diff 放在一起看,只告诉你命中的那些,并给出文件路径和行号,必要时附 1-3 行示例代码。心智负担从"记住 160 条"降到"知道有这么一个入口"。

一个真实跑出来的 review

空讲没意思,看一段 showcase 里的实际输出。

下面这段代码是我写的一个有意设计的"坏例子"(showcase/cases/01-xss/bad.tsx),一个评论列表组件:

export function CommentList(props: any) {
  const [list, setList] = useState([] as any);
  const [keyword, setKeyword] = useState('');

  useEffect(() => {
    fetch('/api/comments?topic=' + props.topic)
      .then((r) => r.json())
      .then((d) => {
        setList(d.data);
      });
  }, [props.topic]);

  const highlight = (text, kw) => {
    if (!kw) return text;
    return text.replace(new RegExp(kw, 'g'), '<mark>' + kw + '</mark>');
  };

  return (
    <div>
      <input type="text" onChange={onSearch} />
      <div id="tip" dangerouslySetInnerHTML={{ __html: props.tip }} />
      {list.map((c: Comment, i: number) => (
        <div key={i} className="comment">
          <img src={c.avatar} />
          <a href={'javascript:void(0)'} onClick={() => eval(c.author.onClick)}>
            {c.author.name}
          </a>
          <div dangerouslySetInnerHTML={{ __html: highlight(c.body, keyword) }} />
        </div>
      ))}
    </div>
  );
}

乍看能跑,TypeScript 不一定报错,ESLint 也不一定能拦住关键问题。但 Skill 跑完吐出来 11 条命中,挑几条看(行号对应 showcase/cases/01-xss/bad.tsx 原文件,不是上面代码块里的相对行号):

安全与健壮性(最严重的一批)

  • line 35dangerouslySetInnerHTML={{ __html: props.tip }} 直接吃外部 tip,存在 XSS 风险。建议默认文本渲染,真要富文本时先用 DOMPurify 消毒。
  • line 44highlight 用字符串拼接 HTML 再 dangerouslySetInnerHTMLbodykeyword 都没转义。建议改成 String.split 分段渲染,把命中段包进 <mark>,不要注入 HTML。
  • line 39eval(c.author.onClick) 把接口返回的字符串当代码执行,等于把任意脚本执行权交给接口数据。建议彻底删除,交互改成前端静态映射。

数据请求

  • line 14-20props.topic 变化时发起新请求但没取消旧请求,旧响应可能覆盖新数据。建议用 AbortControllerignore 标志在 cleanup 里关掉。

UI 与渲染

  • line 37key={i} 用数组下标,列表增删重排时可能导致状态错位。建议用稳定的业务 id,比如 c.id

无障碍

  • line 34, 38:搜索 input 没有 aria-label 或关联 label<img> 没有 alt

泛泛地让 AI 做 review,它很容易给出"结构清晰、建议补充错误处理"这类通用意见。扔给 ESLint,也大概率只会在 any、未定义变量、hook 依赖这类规则上发声。Skill 的价值在于把评审标准显式化:AI 不是凭感觉聊几句,而是按一把真实的尺子在量代码。

我还拿其他 showcase 跑了一遍:用户资料页命中 12 条(竞态、未清理副作用、无空值守卫),注册表单命中 17 条(a11y、字段级错误、密码明文 input、防重复提交)。这些都是一眼看过去"差不多能用",但上线后很容易变成坑的代码。

怎么用

Skill 装起来一条命令的事:

curl -fsSL https://github.com/wsafight/front-end-checklist/releases/latest/download/install.sh \
  | sh -s -- claude

装完在对话里说 /frontend-checklist 或"按前端清单 review 我这个 PR",就会触发它。也支持 Kiro / Cursor / Codex,把命令结尾换一下就行。

清单是中英双语的,AI 会根据你提问的语言自动选对应版本。


两小时改造一个老项目听起来像标题党,但实际发生的事情比这更有意思:一份躺了三年没人翻的清单,换了个交付形式,突然就活过来了。内容还是那 160 条,变的只是"它怎么到达读者"。

如果你手里也有这种"明明有价值但没人用"的老文档,值得花个晚上,把它接进 AI 看看。

— 2026-05-09 夜

你还在手动敲命令部署?GitHub Actions 让你 push 即上线,摸鱼时间翻倍

你改完代码,打开终端,输入 npm run build,然后 FTP 上传,或者登录服务器 git pull。这一套操作每天重复 N 次,不累吗?今天我们来把“部署”这件事自动化——用 GitHub Actions,只要你 git push,代码自动测试、自动打包、自动发到服务器。以后你只管写代码,上线交给机器人。

前言

我见过太多团队还停留在“手工部署”时代:上线先发个群消息“我要部署了,大家别动”,然后手动打包、上传、解压、重启。万一忘了执行某个步骤,线上就挂了。

GitHub Actions 就是你的免费 DevOps 机器人。它能监听 GitHub 上的事件(push、pull request、issue),然后执行你写好的自动化脚本。我们只需要写一个 YAML 文件,放在 .github/workflows 目录下,剩下的全部自动。

今天我们就来写一个完整的工作流:当推送到 main 分支时,自动运行测试、构建、并部署到服务器(或 Vercel / 阿里云 OSS)。全程保姆级,复制粘贴就能用。

一、准备工作:你需要什么?

  • GitHub 仓库(私有或公开都可以)。
  • 一台服务器(或云存储,如阿里云 OSS、Vercel)。
  • 如果部署到自己的服务器,需要服务器的 SSH 密钥(免密登录)。

如果你没有服务器,可以用 Vercel(个人项目免费,连 GitHub 自动部署也是免费的,甚至不需要写 Actions——但为了教学,我们还是会演示自定义部署到服务器的流程)。

二、基础工作流:跑测试 + 打包

在项目根目录创建 .github/workflows/deploy.yml

name: CI/CD Pipeline

on:
  push:
    branches: [ main ]   # 当推送到 main 分支时触发
  pull_request:
    branches: [ main ]   # PR 时也跑测试,但不部署

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: 检出代码
        uses: actions/checkout@v4

      - name: 安装 Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 18

      - name: 安装依赖
        run: npm ci

      - name: 运行测试
        run: npm test

  build:
    runs-on: ubuntu-latest
    needs: test   # 测试通过后才构建
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
      - run: npm ci
      - run: npm run build
      - name: 上传构建产物(给后续部署用)
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

提交这个文件后,每次 git push main,GitHub 就会自动跑测试和构建。你可以在仓库的 Actions 标签页看到实时日志。

三、部署到自己的服务器(通过 SSH)

deploy.yml 中增加一个 job,依赖 build,然后通过 SSH 把构建产物上传到服务器。

首先在 GitHub 仓库 Settings → Secrets and variables → Actions 中添加几个密钥:

  • SERVER_HOST:你的服务器 IP
  • SERVER_USERNAME:登录用户名(如 root、ubuntu)
  • SSH_PRIVATE_KEY:服务器的私钥内容(复制 ~/.ssh/id_rsa 整个内容)

然后在 deploy.yml 中添加:

  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.event_name == 'push'   # 只有 push 时部署,PR 不部署
    steps:
      - name: 下载构建产物
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist

      - name: 通过 SSH 部署
        uses: easingthemes/ssh-deploy@main
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          ARGS: "-rlgoDzvc -i --delete"
          SOURCE: "dist/"
          REMOTE_HOST: ${{ secrets.SERVER_HOST }}
          REMOTE_USER: ${{ secrets.SERVER_USERNAME }}
          TARGET: "/var/www/myapp/"   # 服务器上的目标目录

这样,每次 git push main,代码会自动出现在 /var/www/myapp 中。如果服务器上跑着 Nginx,刷新页面就是新版。

如果想要重启 PM2 进程,可以在部署步骤后加一个 exec 命令:

      - name: 重启 PM2 服务(如果后端是 Node)
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/myapp
            pm2 restart myapp

四、部署到 Vercel(更简单)

如果你的项目是前端静态站点,Vercel 本身就是和 GitHub 集成的。但你也可以手动写 Actions 来调用 Vercel CLI。不过更推荐直接在 Vercel 网站导入 GitHub 仓库,它会自动监听 main 分支并部署,连 YAML 都不用写。

如果你坚持要用 Actions 调用 Vercel:

      - name: 部署到 Vercel
        uses: amondnet/vercel-action@v20
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID}}
          vercel-project-id: ${{ secrets.PROJECT_ID}}
          vercel-args: '--prod'

五、部署到阿里云 OSS(静态网站托管)

阿里云 OSS 支持静态网站。我们可以用 aliyun-cli 同步文件:

      - name: 安装阿里云 CLI
        run: npm install -g @alicloud/oss

      - name: 同步到 OSS
        run: |
          oss cp dist/ oss://my-bucket/ -r --force --access-key-id ${{ secrets.OSS_KEY_ID }} --access-key-secret ${{ secrets.OSS_KEY_SECRET }} --endpoint oss-cn-hangzhou.aliyuncs.com

六、进阶:分环境部署(dev/staging/prod)

你可以通过分支名来区分环境:

  • main 分支 → 生产环境
  • develop 分支 → 测试环境

on.push.branches 里可以写多个分支,然后在 job 里根据 github.ref_name 做判断,选择不同的服务器目录或环境变量。

七、常见坑点

  • 密钥泄露:永远不要把 SSH 私钥、密码明文写在代码里,要用 GitHub Secrets。
  • 构建产物太大upload-artifact 可能较慢,对于几 MB 的项目还好,几百 MB 建议直接推送到服务器或云存储。
  • 权限问题:确保服务器上目标目录有写入权限。
  • 缓存依赖:可以加 actions/cache 来缓存 node_modules,每次 build 快很多。

八、总结:让机器人替你干活

  • 写好 .github/workflows/deploy.yml,push 即触发。
  • 用 Secrets 存放敏感信息。
  • 可以串联测试、构建、部署,还能加个钉钉/飞书通知。

从此你只需要 git add . && git commit -m "fix: xxx" && git push,然后去倒杯水。回来群里就会收到:“生产环境部署成功,版本 v1.2.3”。不用记命令、不用等上传、不怕忘步骤。这才是现代前端该有的体验。

如果你觉得今天的“自动化”够解放双手,点个赞让更多人看到。评论区聊聊:你经历过哪些手工部署的惨案?

Pinia 状态管理

引言

在 Vue 3 项目中,状态管理是不可或缺的一部分。随着 Vuex 逐渐被 Pinia 取代,Pinia 凭借其更简洁的 API、更好的 TypeScript 支持和更轻量的体积,成为了 Vue 生态中的首选状态管理方案。本文将深入讲解 Pinia 的核心概念和实战技巧。

什么是 Pinia?

Pinia 是 Vue 官方推荐的状态管理库,可以看作是 Vuex 的继任者。它具有以下特点:

  • 轻量级:仅 1KB 大小(压缩后)
  • TypeScript 友好:完整的类型推断
  • 简洁 API:基于 Composition API 的设计
  • 模块化:天然支持代码分割
  • DevTools 支持:集成 Vue DevTools

核心概念

Store

Store 是 Pinia 的核心,用于存储和管理应用状态。每个 Store 都是独立的,可以单独使用。

State

State 是响应式的数据源,类似于 Vue 组件中的 data

Getters

Getters 用于计算派生状态,类似于 Vue 的 computed

Actions

Actions 用于处理业务逻辑,可以包含异步操作,类似于 Vue 组件中的 methods

快速上手

安装

npm install pinia

创建 Store

使用 defineStore 创建 Store:

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  // State
  state: () => ({
    count: 0,
    name: 'Pinia'
  }),
  
  // Getters
  getters: {
    doubleCount: (state) => state.count * 2,
    fullName: (state) => `Hello, ${state.name}!`
  },
  
  // Actions
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    setCount(value) {
      this.count = value
    },
    async fetchCount() {
      const response = await fetch('/api/count')
      const data = await response.json()
      this.count = data.count
    }
  }
})

在组件中使用

<template>
  <div>
    <h1>{{ counterStore.fullName }}</h1>
    <p>Count: {{ counterStore.count }}</p>
    <p>Double: {{ counterStore.doubleCount }}</p>
    <button @click="counterStore.increment">+</button>
    <button @click="counterStore.decrement">-</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()
</script>

高级用法

Store 持久化

使用 pinia-plugin-persistedstate 实现状态持久化:

npm install pinia-plugin-persistedstate
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

app.use(pinia)
// stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: null
  }),
  actions: {
    login(token, userInfo) {
      this.token = token
      this.userInfo = userInfo
    }
  },
  persist: true // 自动持久化到 localStorage
})

多个 Store 协作

// stores/cart.js
import { defineStore } from 'pinia'
import { useProductStore } from './product'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  actions: {
    addToProduct(productId, quantity) {
      const productStore = useProductStore()
      const product = productStore.getProductById(productId)
      
      const existingItem = this.items.find(item => item.id === productId)
      if (existingItem) {
        existingItem.quantity += quantity
      } else {
        this.items.push({
          id: productId,
          name: product.name,
          price: product.price,
          quantity
        })
      }
    },
    
    get totalPrice() {
      return this.items.reduce((sum, item) => {
        return sum + item.price * item.quantity
      }, 0)
    }
  }
})

组合 Store

// stores/composed.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useCartStore } from './cart'

export const useCheckoutStore = defineStore('checkout', () => {
  const userStore = useUserStore()
  const cartStore = useCartStore()
  
  const canCheckout = computed(() => {
    return userStore.isLoggedIn && cartStore.items.length > 0
  })
  
  const checkout = async () => {
    if (!canCheckout.value) {
      throw new Error('Cannot checkout')
    }
    
    // 处理结账逻辑
    const result = await fetch('/api/checkout', {
      method: 'POST',
      body: JSON.stringify({
        userId: userStore.userInfo.id,
        items: cartStore.items
      })
    })
    
    return result.json()
  }
  
  return { canCheckout, checkout }
})

最佳实践

1. 合理的 Store 划分

按业务模块划分 Store,而不是按功能类型:

stores/
├── user.js      # 用户相关
├── product.js   # 商品相关
├── cart.js      # 购物车
└── order.js     # 订单

2. 使用 TypeScript

interface User {
  id: number
  name: string
  email: string
}

export const useUserStore = defineStore('user', {
  state: (): { user: User | null } => ({
    user: null
  }),
  actions: {
    setUser(user: User) {
      this.user = user
    }
  }
})

3. 避免直接修改 State

始终通过 Actions 修改状态:

// ❌ 不推荐
store.count = 100

// ✅ 推荐
store.setCount(100)

4. 使用 Getters 进行派生计算

// ✅ 推荐
getters: {
  activeUsers: (state) => state.users.filter(user => user.isActive)
}

// ❌ 不推荐
computed: {
  activeUsers() {
    return this.users.filter(user => user.isActive)
  }
}

总结

Pinia 以其简洁的 API、优秀的 TypeScript 支持和轻量的体积,成为了 Vue 3 项目状态管理的首选。通过合理的 Store 划分、类型安全和最佳实践,可以构建出可维护、可扩展的状态管理方案。

关键要点:

  • 使用 defineStore 创建 Store
  • State、Getters、Actions 三大核心
  • 支持持久化和多 Store 协作
  • 推荐按业务模块划分 Store
  • 充分利用 TypeScript 类型推断

Pinia 让 Vue 应用的状态管理变得更加简单和优雅!

Claude Code + Amazon Bedrock 使用指南

Claude Code + Amazon Bedrock 使用指南

一、Claude Code 是什么?

Claude Code 是 Anthropic 官方推出的 AI 编程 CLI 工具,可以:

  • 直接在终端中与 AI 对话,完成代码编写、调试、重构
  • 自动读取项目代码上下文,理解整个代码库
  • 执行 shell 命令、编辑文件、运行测试
  • 支持 VS Code / Cursor 等 IDE 集成

通过 Amazon Bedrock 接入,我们无需使用个人 Anthropic API Key,统一走公司 AWS 账号,费用由公司统一结算。

二、前置条件

在开始之前,请确认以下环境已准备就绪:

条件 要求 检查方式
Node.js >= 18.0 node --version
AWS CLI v2 aws --version
AWS 凭证 已配置 aws sts get-caller-identity
操作系统 macOS / Linux / Windows (WSL) -

2.1 安装 Node.js

如果尚未安装 Node.js,推荐使用 nvm

# macOS / Linux
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.zshrc  # 或 source ~/.bashrc
nvm install 22
nvm use 22

2.2 安装并配置 AWS CLI

# macOS
brew install awscli

# 验证安装
aws --version

2.3 配置 AWS 凭证

请联系 DevOps 团队获取你的 AWS Access Key,然后执行:

aws configure

按提示输入:

AWS Access Key ID: <你的 Access Key ID>
AWS Secret Access Key: <你的 Secret Access Key>
Default region name: us-east-1
Default output format: json

验证凭证是否生效:

aws sts get-caller-identity

如果返回了你的 Account ID 和 ARN,说明配置成功。

三、安装 Claude Code

# 全局安装
npm install -g @anthropic-ai/claude-code

# 验证安装
claude --version

如遇权限问题(macOS),可尝试:

sudo npm install -g @anthropic-ai/claude-code --unsafe-perm

四、配置 Bedrock 连接

4.1 创建配置文件

编辑(或创建)Claude Code 的配置文件 ~/.claude/settings.json

{
  "env": {
    "CLAUDE_CODE_USE_BEDROCK": "1",
    "ANTHROPIC_MODEL": "us.anthropic.claude-sonnet-4-6-v1",
    "AWS_REGION": "us-east-1",
    "AWS_PROFILE": "default",
    "DISABLE_AUTOUPDATE": "1"
  },
  "permissions": {
    "allow": [],
    "deny": []
  }
}

配置文件路径:

操作系统 路径
macOS / Linux ~/.claude/settings.json
Windows %USERPROFILE%.claude\settings.json

注意:如果文件已存在其他配置,请将 env 字段合并进去,不要覆盖已有内容。

4.2 跳过登录引导流程

Claude Code 首次启动会进入 Anthropic 的登录引导(Onboarding)流程。使用 Bedrock 时需要跳过此步骤。

创建(或编辑)文件 ~/.claude.json(注意:是用户主目录下的 .claude.json,不是 .claude/ 目录里的):

{
  "hasCompletedOnboarding": true
}

说明:设置 hasCompletedOnboardingtrue 后,Claude Code 启动时会跳过默认的 Anthropic OAuth 登录流程,直接使用 settings.json 中配置的 Bedrock 连接。

4.3 验证配置

修改完成后重新打开终端,然后执行:

claude --version

如果没有弹出登录提示,说明配置成功。

五、可用模型

公司 Bedrock 账号已开通以下 Claude 模型:

模型 Bedrock Model ID 适用场景 相对成本
Claude Sonnet 4.6 us.anthropic.claude-sonnet-4-6-v1 日常编码、代码审查、调试(推荐默认)
Claude Haiku 4.5 us.anthropic.claude-haiku-4-5-v1 快速问答、简单任务、节省成本 ★(最低)
Claude Opus 4.6 us.anthropic.claude-opus-4-6-v1 复杂架构设计、深度推理 ★★★(最高)

切换模型

# 方式一:启动时指定
claude --model us.anthropic.claude-opus-4-6-v1

# 方式二:对话中切换
# 输入 /model 命令选择模型

成本提醒:Opus 模型费用约为 Sonnet 的 5 倍,请根据任务复杂度合理选择。日常开发建议使用 Sonnet,简单查询使用 Haiku。

六、快速上手

6.1 启动 Claude Code

# 在项目根目录启动
cd your-project
claude

首次启动会显示欢迎信息,确认模型连接成功。

6.2 基本用法

# 让 AI 解释代码
> 解释一下 src/main/java/com/example/UserService.java 的主要逻辑

# 让 AI 写代码
> 帮我写一个用户注册的 REST API,使用 Spring Boot

# 让 AI 修 Bug
> 这个 NullPointerException 是什么原因?帮我修复

# 让 AI 重构
> 把这个方法拆分成更小的函数,遵循单一职责原则

# 运行命令
> 运行项目的单元测试并分析失败原因

6.3 IDE 集成

Claude Code 同时支持在 IDE 中使用:

VS Code:

  1. 安装扩展:搜索 "Claude Code" 并安装
  2. 打开命令面板(Cmd+Shift+P),输入 "Claude"
  3. 快捷键 Cmd+Esc 打开 Claude 面板

JetBrains(IntelliJ IDEA 等):

  1. Settings -> Plugins -> 搜索 "Claude Code" 并安装
  2. 重启 IDE
  3. 右侧工具栏会出现 Claude 图标

IDE 集成会自动继承终端中配置的环境变量(Bedrock 配置),无需额外设置。

七、常用技巧

7.1 让 AI 理解你的项目

在项目根目录创建 CLAUDE.md 文件,写入项目背景信息:

# 项目说明

- 这是一个 Spring Boot 微服务项目
- 使用 MyBatis 作为 ORM
- 数据库:MySQL 8.0
- 构建工具:Maven
- Java 版本:17

## 代码规范

- 遵循阿里巴巴 Java 开发手册
- Controller 层不写业务逻辑
- Service 层通过接口定义

Claude Code 每次启动时会自动读取该文件,确保 AI 理解项目上下文。

7.2 实用快捷键

快捷键 功能
Esc(连按两次) 退出 Claude Code
Cmd+C 中断当前 AI 响应
/help 查看所有可用命令
/clear 清空对话历史
/compact 压缩上下文(对话太长时使用)
Tab 自动补全文件路径

7.3 权限模式

Claude Code 在执行文件操作和 shell 命令时会请你确认:

  • 输入 y:允许本次操作
  • 输入 n:拒绝本次操作
  • 输入 !:本次会话中始终允许该类型操作

八、常见问题

Q1:报错 "Could not connect to Bedrock"

排查步骤:

# 1. 检查 AWS 凭证是否有效
aws sts get-caller-identity

# 2. 检查区域配置
echo $AWS_REGION

# 3. 检查 Bedrock 访问权限
aws bedrock list-foundation-models --region us-east-1 --query "modelSummaries[?contains(modelId, 'claude')]"

如果第 3 步报权限不足,请联系 DevOps 团队申请 Bedrock 模型访问权限。

Q2:报错 "Model not found" 或 "Access denied"

可能原因:

  • Bedrock 未开通对应模型 -> 联系 DevOps 开通
  • Region 不匹配 -> 确认 AWS_REGION 设置为 us-east-1
  • AWS 凭证过期 -> 重新执行 aws sso login

Q3:响应速度很慢

  • 切换到 Haiku 模型(更快、更便宜)
  • 检查网络连接,确保能访问 AWS
  • 使用 /compact 压缩过长的对话上下文

Q4:JSON 配置不生效

  • 确保 JSON 格式合法(可以用 jsonlint.com 校验)
  • 修改配置后必须重新打开终端
  • 检查配置文件路径是否正确

Q5:macOS 安装权限错误

# 方案一:使用 sudo
sudo npm install -g @anthropic-ai/claude-code --unsafe-perm

# 方案二:修改 npm 全局目录(推荐)
mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.zshrc
source ~/.zshrc
npm install -g @anthropic-ai/claude-code

九、安全须知

  1. 不要在 Claude Code 对话中粘贴密码、Token 等敏感信息,AI 会将其作为上下文处理
  2. 不要让 AI 直接操作生产环境,所有生产操作请走正规发布流程
  3. 代码审查仍然必要,AI 生成的代码需要经过 Code Review 后才能合并
  4. 注意成本控制,避免无意义的长对话消耗 Token

十、获取帮助

场景 联系方式
AWS 凭证 / 权限问题 DevOps 团队

前端周刊:axios 疑遭朝鲜黑客“钓鱼“;CSS 新函数上线;npm 上线深色主题;Oxlint 兼容表;ESLint 支持 Temporal......

🌐 今日要闻

打破信息壁垒,走近全球前端。Hello World 大家好,我是林语冰。

欢迎阅读《Web 周刊》,上周 Web 开发圈的主要情报包括:

  • 🐞 Axios 主席疑遭朝鲜黑客“钓鱼“,自爆了社会工程的诈骗过程
  • 🌗 npm 可信发布支持 CircleCI,npm 官网新增深色主题
  • ✅ ESLint 10.2 支持 JS 最新的 Temporal API
  • ✅ Axios 1.15 支持 Deno / Bun,源码重构了 url.parse()

PS:本文附带甜妹解说的动画视频,粉丝请搜索哔哩哔哩@Web情报局

🎉 每周热搜

🔗 Axios 主席疑遭朝鲜黑客“钓鱼“

Axios 是 GitHub 第一请求库,周下载量过亿。

不幸的是,愚人节前夕,Axios 突然发布了 2 个中毒版本,它们只坚挺了 3 小时就被封杀了,但至少波及几十万用户,这是近一年内最大规模的 npm 供应链攻击。

随后 Axios 团队主席爆料了攻击事件的完整事后分析,首先是它遭到了社会工程“钓鱼“,通过伪造的在线会议安装了有毒软件,导致 npm 账户被盗用。

axios-post.png

Axios 源码本身没有 bug,黑客只添加了一个幽灵依赖 plain-crypto-js,其中包含了一个 postinstall 脚本,随后使用传统 npm token 发布中毒版本。

用户使用 npm install axios 之后,postinstall 脚本会自动执行,请求其他恶意软件,盗用系统资料。

post-install.png

之后,这个脚本会自尽,删除 postinstall 脚本,替换为正常 package.json,用户对这种“完美犯罪“浑然不知。

谷歌和微软深入调查了本次赛博攻击,部分证据表明攻击来自朝鲜的黑客组织,但可能很难像川普打伊朗那样直接证明。

korea-npm.png

总之,供应链攻击是一种系统性原罪。我们应该遵循 npm 发包的最佳实践,防止黑客轻易绕过了现代化的可信发布流程。

🛜 官方情报

🔗 Node bug 悬赏项目破产

2016 年,Node 加盟了 HackerOne 的 IBB(互联网 bug 悬赏)项目,通过众筹为 fix bug 的志愿者提供奖金。

Node 团队会继续接收 bug,但由于资金链中断,该悬赏项目现已暂停。

node-bug.png

特别鸣谢一直以来为 Node 安全贡献的开发者和赞助商!Node 是 Web 开发的重要基建,如果你愿意提供赞助,请随时联系 OpenJS 基金会。

🔗 npm 可信发布支持 CircleCI

GitHub 官宣,npm 可信发布支持 CircleCI 作为 OIDC(OpenID 连接)供应商。

circle-ci.png

CircleCI 现在和 GitHub Actions 与 GitLab CI / CD 一样,维护者能从部署流程鉴权发包,无需长期 token。

此外,npm 官网上线深色主题了。

🔗 Oxlint / Oxfmt 兼容表

Oxc 官网新增了 Oxlint / Oxfmt 兼容表,可以直观地查看它们支持哪些 JS 框架和文件类型,从夯到拉分为四大梯队:

  1. 完整支持,比如 Oxlint + Oxfmt 完整支持 React 的代码质检和格式化
  2. 部分支持,比如 Oxlint 暂不支持 Vue 模板的代码质检
  3. 不支持,比如 Svelte 没有提供 Prettier 插件,Oxfmt 也不支持
  4. 越界功能,比如 Oxlint 不支持 CSS 代码质检

oxc.png

🚦 版本更新

🔗 ESLint 10.2

ESLint 是 GitHub 第一 JS Linter(代码质检工具),最近更新了 10.2 次版本。

首先,ESLint 新增了 meta.languages 属性,作者可以指定规则适用的语言,比如 JS 专属规则或 Markdown 专属规则等。

eslint.png

此外,ESLint 还支持 JS 最新的 Temporal 全局变量,no-undef 规则能识别 Temporal 而不会报警,no-obj-calls 规则会在直接调用 Temporal 时报警。

temporal.png

🔗 Axios 1.15

Axios 发布了 1.15 次版本,现在能支持 Bun / Deno。

Axios 还修复了代理处理和头部注入等安全漏洞,CI(持续集成)采用 OIDC 来守卫 npm 发布。

源码使用了原生 URL API,url.parse() 重构为 new URL()

url.png

💡 前端信息差

🔗 CSS 新函数 contrast-color()

最近 npm 上线了深色主题,但还有一些无障碍 bug,比如切换深色主题后文字对比度不够,看不清楚。

npm-a11y.png

Google 专家 Una 之前就提出了一个新的 CSS 函数 contrast-color(),它可以接受任意颜色,然后根据输入的颜色去计算对比度,最终返回 blackwhite

contrast.png

contrast-color() 可以自动生成对比度更好的文字,完美解决 npm 的无障碍文字 bug。

CSS contrast-color() 函数目前已经达到 Baseline Newly Available(全新可用基线),所有新版的主流浏览器都支持这个功能喔。

baseline.png

🛠️ 工具推荐

为了对抗供应链攻击,本期我们主要分享一些 npm 防御式指南

🔗 npm 可信发布

首先是 npm 官方文档出品的 trusted publishing(可信发布),它可以让 npm 和 GitHub CI(持续集成)完美搭配。

npm-trust.png

🔗 npmx 网站

再来是比 npm 更现代化的网站 npmx,npmx 会显示盾牌来说明这个 axios 版本是通过可信发布持续部署的。

你会发现 axios 的某些版本没有盾牌,说明 axios 没有严格使用可信发布,黑客就会从这里寻找机会。

npmx-oidc.png

npmx 不仅会警告你这个版本可信度降低,还会爆料模块的性能问题或安全漏洞,我知道你很急,但是你先别升级。

npmx-axios.png

🔗 pnpm 供应链攻击

还有就是比 npm 更现代化的包管理器 pnpm,pnpm v10 的官网提供了缓解供应链攻击的完整指南。

pnpm-trust.png

这些配置在最新版的 pnpm 11 中会默认启用,所以提前了解也方便你之后升级到 pnpm 11。

🔗 npm 安全最佳实践

最后,Node 安全专家 Liran 也在 GitHub 上分享了一份《npm 安全最佳实践》,建议收藏。

npm-best.png

🙏 特别鸣谢

以上就是本期《Web 周刊》的全部内容了,希望对你有所帮助。

👍 感谢大家按赞跟转发分享本文,你的手动支持是我坚持创作的不竭动力喔。

😘 已经关注我的粉丝们,我们下期再见啦,掰掰~~

cat-thank.gif

❌