普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月24日LeetCode 每日一题题解

[Python3/Java/C++/Go/TypeScript] 一题一解:贪心+排序(清晰题解)

作者 lcbin
2025年12月24日 07:20

方法一:贪心 + 排序

为了使得需要的箱子数量尽可能少,我们应该优先使用容量大的箱子。因此,我们可以对箱子按照容量从大到小排序,然后依次使用箱子,直到所有的苹果都被分装完,返回此时使用的箱子数量。

###python

class Solution:
    def minimumBoxes(self, apple: List[int], capacity: List[int]) -> int:
        capacity.sort(reverse=True)
        s = sum(apple)
        for i, c in enumerate(capacity, 1):
            s -= c
            if s <= 0:
                return i

###java

class Solution {
    public int minimumBoxes(int[] apple, int[] capacity) {
        Arrays.sort(capacity);
        int s = 0;
        for (int x : apple) {
            s += x;
        }
        for (int i = 1, n = capacity.length;; ++i) {
            s -= capacity[n - i];
            if (s <= 0) {
                return i;
            }
        }
    }
}

###cpp

class Solution {
public:
    int minimumBoxes(vector<int>& apple, vector<int>& capacity) {
        sort(capacity.rbegin(), capacity.rend());
        int s = accumulate(apple.begin(), apple.end(), 0);
        for (int i = 1;; ++i) {
            s -= capacity[i - 1];
            if (s <= 0) {
                return i;
            }
        }
    }
};

###go

func minimumBoxes(apple []int, capacity []int) int {
sort.Ints(capacity)
s := 0
for _, x := range apple {
s += x
}
for i := 1; ; i++ {
s -= capacity[len(capacity)-i]
if s <= 0 {
return i
}
}
}

###ts

function minimumBoxes(apple: number[], capacity: number[]): number {
    capacity.sort((a, b) => b - a);
    let s = apple.reduce((acc, cur) => acc + cur, 0);
    for (let i = 1; ; ++i) {
        s -= capacity[i - 1];
        if (s <= 0) {
            return i;
        }
    }
}

###rust

impl Solution {
    pub fn minimum_boxes(apple: Vec<i32>, mut capacity: Vec<i32>) -> i32 {
        capacity.sort();

        let mut s: i32 = apple.iter().sum();

        let n = capacity.len();
        let mut i = 1;
        loop {
            s -= capacity[n - i];
            if s <= 0 {
                return i as i32;
            }
            i += 1;
        }
    }
}

时间复杂度 $O(m \times \log m + n)$,空间复杂度 $O(\log m)$。其中 $m$ 和 $n$ 分别是数组 $\textit{capacity}$ 和 $\textit{apple}$ 的长度。


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

每日一题-重新分装苹果🟢

2025年12月24日 00:00

给你一个长度为 n 的数组 apple 和另一个长度为 m 的数组 capacity

一共有 n 个包裹,其中第 i 个包裹中装着 apple[i] 个苹果。同时,还有 m 个箱子,第 i 个箱子的容量为 capacity[i] 个苹果。

请你选择一些箱子来将这 n 个包裹中的苹果重新分装到箱子中,返回你需要选择的箱子的 最小 数量。

注意,同一个包裹中的苹果可以分装到不同的箱子中。

 

示例 1:

输入:apple = [1,3,2], capacity = [4,3,1,5,2]
输出:2
解释:使用容量为 4 和 5 的箱子。
总容量大于或等于苹果的总数,所以可以完成重新分装。

示例 2:

输入:apple = [5,5,5], capacity = [2,4,2,7]
输出:4
解释:需要使用所有箱子。

 

提示:

  • 1 <= n == apple.length <= 50
  • 1 <= m == capacity.length <= 50
  • 1 <= apple[i], capacity[i] <= 50
  • 输入数据保证可以将包裹中的苹果重新分装到箱子中。

排序贪心(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2024年3月10日 16:46

既然同一个包裹中的苹果可以分装到不同的箱子中,那就先把所有苹果堆在一起,然后一个个地装箱。

为了少用箱子,要先装大箱子,再装小箱子。

注:题目保证可以将所有苹果重新分装到箱子中。

###py

class Solution:
    def minimumBoxes(self, apple: List[int], capacity: List[int]) -> int:
        s = sum(apple)  # 把所有苹果堆在一起
        capacity.sort(reverse=True)  # 先装大箱子,再装小箱子
        for i, x in enumerate(capacity):
            s -= x
            if s <= 0:  # 所有苹果都装入了箱子
                return i + 1  # 从 0 到 i 有 i+1 个箱子

###java

class Solution {
    public int minimumBoxes(int[] apple, int[] capacity) {
        int s = 0;
        for (int x : apple) {
            s += x; // 把所有苹果堆在一起
        }

        Arrays.sort(capacity);

        int m = capacity.length;
        int i = m - 1; // 先装大箱子,再装小箱子
        while (s > 0) { // 还有剩余苹果
            s -= capacity[i--]; // 使用一个箱子
        }
        return m - 1 - i;
    }
}

###cpp

class Solution {
public:
    int minimumBoxes(vector<int>& apple, vector<int>& capacity) {
        int s = reduce(apple.begin(), apple.end()); // 把所有苹果堆在一起
        ranges::sort(capacity, greater()); // 先装大箱子,再装小箱子
        int i = 0;
        while (s > 0) { // 还有剩余苹果
            s -= capacity[i++]; // 使用一个箱子
        }
        return i;
    }
};

###c

int cmp(const void* a, const void* b) {
    return *(int*)b - *(int*)a;
}

int minimumBoxes(int* apple, int appleSize, int* capacity, int capacitySize) {
    int s = 0;
    for (int i = 0; i < appleSize; i++) {
        s += apple[i]; // 把所有苹果堆在一起
    }

    qsort(capacity, capacitySize, sizeof(int), cmp); // 先装大箱子,再装小箱子

    int i = 0;
    while (s > 0) { // 还有剩余苹果
        s -= capacity[i++]; // 使用一个箱子
    }
    return i;
}

###go

func minimumBoxes(apple, capacity []int) int {
s := 0
for _, x := range apple {
s += x // 把所有苹果堆在一起
}
slices.SortFunc(capacity, func(a, b int) int { return b - a }) // 先装大箱子,再装小箱子
for i, c := range capacity {
s -= c
if s <= 0 { // 所有苹果都装入了箱子
return i + 1 // 从 0 到 i 有 i+1 个箱子
}
}
return -1
}

###js

var minimumBoxes = function(apple, capacity) {
    let s = _.sum(apple); // 把所有苹果堆在一起
    capacity.sort((a, b) => b - a); // 先装大箱子,再装小箱子
    let i = 0;
    while (s > 0) { // 还有剩余苹果
        s -= capacity[i++]; // 使用一个箱子
    }
    return i;
};

###rust

impl Solution {
    pub fn minimum_boxes(apple: Vec<i32>, mut capacity: Vec<i32>) -> i32 {
        let mut s = apple.iter().sum::<i32>(); // 把所有苹果堆在一起
        capacity.sort_unstable_by_key(|a| -a); // 先装大箱子,再装小箱子
        for (i, x) in capacity.iter().enumerate() {
            s -= x;
            if s <= 0 { // 所有苹果都装入了箱子
                return (i + 1) as _; // 从 0 到 i 有 i+1 个箱子
            }
        }
        unreachable!()
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n+m\log m)$,其中 $n$ 为 $\textit{apple}$ 的长度,$m$ 为 $\textit{capacity}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。忽略排序的栈开销。

专题训练

见贪心题单的「§1.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站@灵茶山艾府

贪心+优先队列

作者 ylrk
2024年3月10日 13:25

Problem: 100233. 重新分装苹果

[TOC]

思路

这个问题的核心思路是贪心算法。我们需要找到最少需要的箱子数量,因此我们应该优先使用容量最大的箱子,因为这样可以装更多的苹果,从而减少需要的箱子数量。为了快速找到最大的箱子,我们可以使用一个优先队列来存储所有的箱子,优先队列的顶部始终是最大的元素。

解题方法

首先,我们计算出所有苹果的总数。然后,我们将所有的箱子的容量放入一个优先队列中。然后我们进入一个循环,只要还有苹果没有装箱,我们就从优先队列中取出最大的箱子,将这个箱子的容量从苹果的总数中减去,并将箱子的数量加1。当所有的苹果都装箱,或者没有更多的箱子时,我们停止循环,返回箱子的数量。

Code

###C++

class Solution {
public:
    int minimumBoxes(vector<int>& apple, vector<int>& capacity) {
        int total_apples = std::accumulate(apple.begin(), apple.end(), 0);
        std::priority_queue<int> pq(capacity.begin(), capacity.end());
        int count = 0;
        while(total_apples > 0 && !pq.empty()) {
            total_apples -= pq.top();
            pq.pop();
            ++count;
        }
        return count;
    }
};
昨天以前LeetCode 每日一题题解

每日一题-两个最好的不重叠活动🟡

2025年12月23日 00:00

给你一个下标从 0 开始的二维整数数组 events ,其中 events[i] = [startTimei, endTimei, valuei] 。第 i 个活动开始于 startTimei ,结束于 endTimei ,如果你参加这个活动,那么你可以得到价值 valuei 。你 最多 可以参加 两个时间不重叠 活动,使得它们的价值之和 最大 。

请你返回价值之和的 最大值 。

注意,活动的开始时间和结束时间是 包括 在活动时间内的,也就是说,你不能参加两个活动且它们之一的开始时间等于另一个活动的结束时间。更具体的,如果你参加一个活动,且结束时间为 t ,那么下一个活动必须在 t + 1 或之后的时间开始。

 

示例 1:

输入:events = [[1,3,2],[4,5,2],[2,4,3]]
输出:4
解释:选择绿色的活动 0 和 1 ,价值之和为 2 + 2 = 4 。

示例 2:

Example 1 Diagram

输入:events = [[1,3,2],[4,5,2],[1,5,5]]
输出:5
解释:选择活动 2 ,价值和为 5 。

示例 3:

输入:events = [[1,5,3],[1,5,1],[6,6,5]]
输出:8
解释:选择活动 0 和 2 ,价值之和为 3 + 5 = 8 。

 

提示:

  • 2 <= events.length <= 105
  • events[i].length == 3
  • 1 <= startTimei <= endTimei <= 109
  • 1 <= valuei <= 106

两个最好的不重叠活动

2021年11月1日 00:19

方法一:时间戳排序

思路与算法

我们可以将所有活动的左右边界放在一起进行自定义排序。具体地,我们用 $(\textit{ts}, \textit{op}, \textit{val})$ 表示一个「事件」:

  • $\textit{op}$ 表示该事件的类型。如果 $\textit{op} = 0$,说明该事件表示一个活动的开始;如果 $\textit{op} = 1$,说明该事件表示一个活动的结束。

  • $\textit{ts}$ 表示该事件发生的时间,即活动的开始时间或结束时间。

  • $\textit{val}$ 表示该事件的价值,即对应活动的 $\textit{value}$ 值。

我们将所有的时间按照 $\textit{ts}$ 为第一关键字升序排序,这样我们就能按照时间顺序依次处理每一个事件。当 $\textit{ts}$ 相等时,我们按照 $\textit{op}$ 为第二关键字升序排序,这是因为题目中要求了「第一个活动的结束时间不能等于第二个活动的起始时间」,因此当时间相同时,我们先处理开始的事件,再处理结束的事件。

当排序完成后,我们就可以通过对所有的事件进行一次遍历,从而算出最多两个时间不重叠的活动的最大价值:

  • 当我们遍历到一个结束事件时,我们用 $\textit{val}$ 来更新 $\textit{bestFirst}$,其中 $\textit{bestFirst}$ 表示当前已经结束的所有活动的最大价值。这样做的意义在于,所有已经结束的事件都可以当作第一个活动

  • 当我们遍历到一个开始事件时,我们将该活动当作第二个活动,由于第一个活动的最大价值为 $\textit{bestFirst}$,因此我们用 $\textit{val} + \textit{bestFirst}$ 更新答案即可。

代码

###C++

struct Event {
    // 时间戳
    int ts;
    // op = 0 表示左边界,op = 1 表示右边界
    int op;
    int val;
    Event(int _ts, int _op, int _val): ts(_ts), op(_op), val(_val) {}
    bool operator< (const Event& that) const {
        return tie(ts, op) < tie(that.ts, that.op);
    }
};

class Solution {
public:
    int maxTwoEvents(vector<vector<int>>& events) {
        vector<Event> evs;
        for (const auto& event: events) {
            evs.emplace_back(event[0], 0, event[2]);
            evs.emplace_back(event[1], 1, event[2]);
        }
        sort(evs.begin(), evs.end());
        
        int ans = 0, bestFirst = 0;
        for (const auto& [ts, op, val]: evs) {
            if (op == 0) {
                ans = max(ans, val + bestFirst);
            }
            else {
                bestFirst = max(bestFirst, val);
            }
        }
        return ans;
    }
};

###Python

class Event:
    def __init__(self, ts: int, op: int, val: int):
        self.ts = ts
        self.op = op
        self.val = val
    
    def __lt__(self, other: "Event") -> bool:
        return (self.ts, self.op) < (other.ts, other.op)


class Solution:
    def maxTwoEvents(self, events: List[List[int]]) -> int:
        evs = list()
        for event in events:
            evs.append(Event(event[0], 0, event[2]))
            evs.append(Event(event[1], 1, event[2]))
        evs.sort()

        ans = bestFirst = 0
        for ev in evs:
            if ev.op == 0:
                ans = max(ans, ev.val + bestFirst)
            else:
                bestFirst = max(bestFirst, ev.val)
        
        return ans

###Java

class Solution {
    public int maxTwoEvents(int[][] events) {
        List<Event> evs = new ArrayList<>();
        for (int[] event : events) {
            evs.add(new Event(event[0], 0, event[2]));
            evs.add(new Event(event[1], 1, event[2]));
        }
        Collections.sort(evs);
        int ans = 0, bestFirst = 0;
        for (Event event : evs) {
            if (event.op == 0) {
                ans = Math.max(ans, event.val + bestFirst);
            } else {
                bestFirst = Math.max(bestFirst, event.val);
            }
        }
        return ans;
    }
    
    class Event implements Comparable<Event> {
        int ts;
        int op;
        int val;
        
        Event(int ts, int op, int val) {
            this.ts = ts;
            this.op = op;
            this.val = val;
        }
        
        @Override
        public int compareTo(Event other) {
            if (this.ts != other.ts) {
                return Integer.compare(this.ts, other.ts);
            }
            return Integer.compare(this.op, other.op);
        }
    }
}

###C#

public class Solution {
    public int MaxTwoEvents(int[][] events) {
        List<Event> evs = new List<Event>();
        foreach (var eventArr in events) {
            evs.Add(new Event(eventArr[0], 0, eventArr[2]));
            evs.Add(new Event(eventArr[1], 1, eventArr[2]));
        }
        evs.Sort();
        
        int ans = 0, bestFirst = 0;
        foreach (var ev in evs) {
            if (ev.Op == 0) {
                ans = Math.Max(ans, ev.Val + bestFirst);
            } else {
                bestFirst = Math.Max(bestFirst, ev.Val);
            }
        }
        return ans;
    }
    
    class Event : IComparable<Event> {
        public int Ts { get; set; }
        public int Op { get; set; }
        public int Val { get; set; }
        
        public Event(int ts, int op, int val) {
            Ts = ts;
            Op = op;
            Val = val;
        }
        
        public int CompareTo(Event other) {
            if (Ts != other.Ts) {
                return Ts.CompareTo(other.Ts);
            }
            return Op.CompareTo(other.Op);
        }
    }
}

###Go

func maxTwoEvents(events [][]int) int {
    type Event struct {
        ts  int
        op  int
        val int
    }
    
    evs := make([]Event, 0)
    for _, event := range events {
        evs = append(evs, Event{event[0], 0, event[2]})
        evs = append(evs, Event{event[1], 1, event[2]})
    }
    
    sort.Slice(evs, func(i, j int) bool {
        if evs[i].ts != evs[j].ts {
            return evs[i].ts < evs[j].ts
        }
        return evs[i].op < evs[j].op
    })
    
    ans, bestFirst := 0, 0
    for _, ev := range evs {
        if ev.op == 0 {
            if ev.val + bestFirst > ans {
                ans = ev.val + bestFirst
            }
        } else {
            if ev.val > bestFirst {
                bestFirst = ev.val
            }
        }
    }
    return ans
}

###C

typedef struct {
    int ts;
    int op;
    int val;
} Event;

int compareEvents(const void* a, const void* b) {
    Event* e1 = (Event*)a;
    Event* e2 = (Event*)b;
    if (e1->ts != e2->ts) {
        return e1->ts - e2->ts;
    }
    return e1->op - e2->op;
}

int maxTwoEvents(int** events, int eventsSize, int* eventsColSize) {
    Event* evs = (Event*)malloc(2 * eventsSize * sizeof(Event));
    int idx = 0;
    for (int i = 0; i < eventsSize; i++) {
        evs[idx++] = (Event){events[i][0], 0, events[i][2]};
        evs[idx++] = (Event){events[i][1], 1, events[i][2]};
    }
    qsort(evs, 2 * eventsSize, sizeof(Event), compareEvents);

    int ans = 0, bestFirst = 0;
    for (int i = 0; i < 2 * eventsSize; i++) {
        if (evs[i].op == 0) {
            if (evs[i].val + bestFirst > ans) {
                ans = evs[i].val + bestFirst;
            }
        } else {
            if (evs[i].val > bestFirst) {
                bestFirst = evs[i].val;
            }
        }
    }
    
    free(evs);
    return ans;
}

###JavaScript

var maxTwoEvents = function(events) {
    const evs = [];
    for (const event of events) {
        evs.push({ts: event[0], op: 0, val: event[2]});
        evs.push({ts: event[1], op: 1, val: event[2]});
    }
    
    evs.sort((a, b) => {
        if (a.ts !== b.ts) {
            return a.ts - b.ts;
        }
        return a.op - b.op;
    });
    
    let ans = 0, bestFirst = 0;
    for (const ev of evs) {
        if (ev.op === 0) {
            ans = Math.max(ans, ev.val + bestFirst);
        } else {
            bestFirst = Math.max(bestFirst, ev.val);
        }
    }
    return ans;
};

###TypeScript

function maxTwoEvents(events: number[][]): number {
    interface Event {
        ts: number;
        op: number;
        val: number;
    }
    
    const evs: Event[] = [];
    for (const event of events) {
        evs.push({ts: event[0], op: 0, val: event[2]});
        evs.push({ts: event[1], op: 1, val: event[2]});
    }
    
    evs.sort((a, b) => {
        if (a.ts !== b.ts) {
            return a.ts - b.ts;
        }
        return a.op - b.op;
    });
    
    let ans = 0, bestFirst = 0;
    for (const ev of evs) {
        if (ev.op === 0) {
            ans = Math.max(ans, ev.val + bestFirst);
        } else {
            bestFirst = Math.max(bestFirst, ev.val);
        }
    }
    return ans;
}

###Rust

#[derive(Debug)]
struct Event {
    ts: i32,
    op: i32,
    val: i32,
}

impl Solution {
    pub fn max_two_events(events: Vec<Vec<i32>>) -> i32 {
        let mut evs: Vec<Event> = Vec::new();
        for event in events {
            evs.push(Event { ts: event[0], op: 0, val: event[2] });
            evs.push(Event { ts: event[1], op: 1, val: event[2] });
        }
        
        evs.sort_by(|a, b| {
            if a.ts != b.ts {
                a.ts.cmp(&b.ts)
            } else {
                a.op.cmp(&b.op)
            }
        });
        
        let mut ans = 0;
        let mut best_first = 0;
        for ev in evs {
            if ev.op == 0 {
                ans = ans.max(ev.val + best_first);
            } else {
                best_first = best_first.max(ev.val);
            }
        }
        
        ans
    }
}

复杂度分析

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

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

排序 + 单调栈二分(Python/Java/C++/Go)

作者 endlesscheng
2021年10月31日 07:38

设参加的第二个活动的开始时间为 $\textit{startTime}$,那么第一个活动选哪个最好?

在结束时间小于 $\textit{startTime}$ 的活动中,选择价值最大的活动。

为了方便查找,先把 $\textit{events}$ 按照结束时间从小到大排序。

排序后,对比如下两个活动:

  • 活动一:结束于 $3$ 时刻,价值 $999$。
  • 活动二:结束于 $6$ 时刻,价值 $9$。

活动二的结束时间又晚,价值又小,全方面不如活动一,是垃圾数据,直接忽略。

换句话说,在遍历 $\textit{events}$ 的过程中(注意 $\textit{events}$ 已按照结束时间排序),只在遇到更大价值的活动时,才记录该活动。把这些活动记录到一个栈(列表)中,那么从栈底到栈顶,结束时间是递增的,价值也是递增的,非常适合二分查找。关于二分查找的原理,请看 二分查找 红蓝染色法【基础算法精讲 04】

枚举第二个活动,在单调栈中二分查找结束时间严格小于 $\textit{startTime}$ 的最后一个活动,即为价值最大的第一个活动。如果没找到,那么只能选一个活动。

为了简化判断逻辑,可以在栈底加一个结束时间为 $0$,价值也为 $0$ 的哨兵。

写法一

class Solution:
    def maxTwoEvents(self, events: List[List[int]]) -> int:
        # 按照结束时间排序
        events.sort(key=lambda e: e[1])  

        # 从栈底到栈顶,结束时间递增,价值递增
        st = [(0, 0)]  # 栈底哨兵 
        ans = 0
        for start_time, end_time, value in events:
            # 二分查找最后一个结束时间 < start_time 的活动
            i = bisect_left(st, (start_time,)) - 1
            ans = max(ans, st[i][1] + value)
            # 遇到比栈顶更大的价值,入栈
            if value > st[-1][1]:
                st.append((end_time, value))
        return ans
class Solution {
    public int maxTwoEvents(int[][] events) {
        // 按照结束时间排序
        Arrays.sort(events, (a, b) -> a[1] - b[1]);

        // 从栈底到栈顶,结束时间递增,价值递增
        ArrayList<int[]> st = new ArrayList<>(); // (结束时间, 价值)
        st.add(new int[]{0, 0}); // 栈底哨兵

        int ans = 0;
        for (int[] e : events) {
            int i = search(st, e[0]);
            int value = e[2];
            ans = Math.max(ans, st.get(i)[1] + value);
            // 遇到比栈顶更大的价值,入栈
            if (value > st.getLast()[1]) {
                st.add(new int[]{e[1], value});
            }
        }
        return ans;
    }

    // 返回最后一个满足 st[i][0] < target 的 i
    private int search(List<int[]> st, int target) {
        int left = -1, right = st.size();
        while (left + 1 < right) { // 开区间二分
            int mid = left + (right - left) / 2;
            if (st.get(mid)[0] < target) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }
}
class Solution {
public:
    int maxTwoEvents(vector<vector<int>>& events) {
        // 按照结束时间排序
        ranges::sort(events, {}, [](auto& e) { return e[1]; });

        // 从栈底到栈顶,结束时间递增,价值递增
        vector<pair<int, int>> st = {{0, 0}}; // 栈底哨兵
        int ans = 0;
        for (auto& e : events) {
            int start_time = e[0], value = e[2];
            // 二分查找最后一个结束时间 < start_time 的活动
            auto it = --ranges::lower_bound(st, start_time, {}, &pair<int, int>::first);
            ans = max(ans, it->second + value);
            // 遇到比栈顶更大的价值,入栈
            if (value > st.back().second) {
                st.emplace_back(e[1], value);
            }
        }
        return ans;
    }
};
func maxTwoEvents(events [][]int) (ans int) {
// 按照结束时间排序
slices.SortFunc(events, func(a, b []int) int { return a[1] - b[1] })

// 从栈底到栈顶,结束时间递增,价值递增
type pair struct{ endTime, value int }
st := []pair{{}} // 栈底哨兵
for _, e := range events {
startTime, value := e[0], e[2]
// 二分查找最后一个结束时间 < startTime 的活动
i := sort.Search(len(st), func(i int) bool { return st[i].endTime >= startTime }) - 1
ans = max(ans, st[i].value+value)
// 遇到比栈顶更大的价值,入栈
if value > st[len(st)-1].value {
st = append(st, pair{e[1], value})
}
}
return
}

复杂度分析

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

写法二:原地

也可以把 $\textit{events}$ 当作栈。

缺点是没法用哨兵。

class Solution:
    def maxTwoEvents(self, events: List[List[int]]) -> int:
        events.sort(key=lambda e: e[1])

        ans = size = 0  # 把 events 当作栈
        for start_time, end_time, value in events:
            i = bisect_left(events, (start_time,), hi=size) - 1
            if i >= 0:
                ans = max(ans, value + events[i][1])
            else:
                ans = max(ans, value)

            if size == 0 or value > events[size - 1][1]:
                events[size] = (end_time, value)
                size += 1
        return ans
class Solution {
    public int maxTwoEvents(int[][] events) {
        Arrays.sort(events, (a, b) -> a[1] - b[1]);

        int ans = 0;
        int size = 0; // 把 events 当作栈
        for (int[] e : events) {
            int i = search(events, size, e[0]);
            int value = e[2];
            if (i >= 0) {
                ans = Math.max(ans, value + events[i][2]);
            } else {
                ans = Math.max(ans, value);
            }

            if (size == 0 || value > events[size - 1][2]) {
                events[size++] = e;
            }
        }
        return ans;
    }

    // 返回最后一个满足 st[i][1] < target 的 i
    private int search(int[][] st, int right, int target) {
        int left = -1;
        while (left + 1 < right) { // 开区间二分
            int mid = left + (right - left) / 2;
            if (st[mid][1] < target) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }
}
class Solution {
public:
    int maxTwoEvents(vector<vector<int>>& events) {
        ranges::sort(events, {}, [](auto& e) { return e[1]; });

        int ans = 0, size = 0; // 把 events 当作栈
        for (auto& e : events) {
            int start_time = e[0], value = e[2];
            auto it = ranges::lower_bound(events.begin(), events.begin() + size, start_time, {}, [](auto& e) { return e[1]; });
            if (it != events.begin()) {
                ans = max(ans, value + (*--it)[2]);
            } else {
                ans = max(ans, value);
            }

            if (size == 0 || value > events[size - 1][2]) {
                events[size++] = e;
            }
        }
        return ans;
    }
};
func maxTwoEvents(events [][]int) (ans int) {
slices.SortFunc(events, func(a, b []int) int { return a[1] - b[1] })

st := events[:0] // 把 events 当作栈
for _, e := range events {
startTime, value := e[0], e[2]
i := sort.Search(len(st), func(i int) bool { return st[i][1] >= startTime }) - 1
if i >= 0 {
ans = max(ans, value+events[i][2])
} else {
ans = max(ans, value)
}
if len(st) == 0 || value > st[len(st)-1][2] {
st = append(st, e)
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{events}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。忽略排序的栈开销。

专题训练

  1. 单调栈题单。
  2. 动态规划题单的「§7.2 不相交区间」。

分类题单

如何科学刷题?

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

每日一题-删列造序 III🔴

2025年12月22日 00:00

给定由 n 个小写字母字符串组成的数组 strs ,其中每个字符串长度相等。

选取一个删除索引序列,对于 strs 中的每个字符串,删除对应每个索引处的字符。

比如,有 strs = ["abcdef","uvwxyz"] ,删除索引序列 {0, 2, 3} ,删除后为 ["bef", "vyz"] 。

假设,我们选择了一组删除索引 answer ,那么在执行删除操作之后,最终得到的数组的行中的 每个元素 都是按字典序排列的(即 (strs[0][0] <= strs[0][1] <= ... <= strs[0][strs[0].length - 1]) 和 (strs[1][0] <= strs[1][1] <= ... <= strs[1][strs[1].length - 1]) ,依此类推)。

请返回 answer.length 的最小可能值 。

 

示例 1:

输入:strs = ["babca","bbazb"]
输出:3
解释:
删除 0、1 和 4 这三列后,最终得到的数组是 strs = ["bc", "az"]。
这两行是分别按字典序排列的(即,strs[0][0] <= strs[0][1] 且 strs[1][0] <= strs[1][1])。
注意,strs[0] > strs[1] —— 数组 strs 不一定是按字典序排列的。

示例 2:

输入:strs = ["edcba"]
输出:4
解释:如果删除的列少于 4 列,则剩下的行都不会按字典序排列。

示例 3:

输入:strs = ["ghi","def","abc"]
输出:0
解释:所有行都已按字典序排列。

 

提示:

  • n == strs.length
  • 1 <= n <= 100
  • 1 <= strs[i].length <= 100
  • strs[i] 由小写英文字母组成

最长递增子序列(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2025年12月11日 13:00

考虑最多保留多少列。

如果 $n=1$,那么本题是允许相邻元素相等的 300. 最长递增子序列。视频讲解:最长递增子序列【基础算法精讲 20】

如果 $n>1$ 呢?能不能用同样的套路(枚举选哪个)解决?学习一下 300 题,动手试一试吧!

定义 $f[i]$ 表示每个子序列都以 $i$ 列结尾时,最多保留的列数。

枚举子序列的倒数第二列是 $j$。如果对于每一行,$j$ 列的字母都不超过 $i$ 列的字母,那么和 300 题一样,用 $f[j] + 1$ 更新 $f[i]$ 的最大值。(代码先用 $f[j]$ 更新 $f[i]$ 的最大值,循环结束后再加一。)

注意 $i$ 列也可以单独形成一个长为 $1$ 的子序列。

答案为 $m - \max(f)$。其中 $m$ 是 $\textit{strs}[i]$ 的长度。

###py

class Solution:
    def minDeletionSize(self, strs: List[str]) -> int:
        m = len(strs[0])
        f = [0] * m
        for i in range(m):
            for j in range(i):
                # 如果 f[j] <= f[i],就不用跑 O(n) 的 all 了
                if f[j] > f[i] and all(s[j] <= s[i] for s in strs):
                    f[i] = f[j]
            f[i] += 1
        return m - max(f)

###java

class Solution {
    public int minDeletionSize(String[] strs) {
        int m = strs[0].length();
        int[] f = new int[m];
        int maxF = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < i; j++) {
                // 如果 f[j] <= f[i],就不用跑 O(n) 的 lessEq 了
                if (f[j] > f[i] && lessEq(strs, j, i)) {
                    f[i] = f[j];
                }
            }
            f[i]++;
            maxF = Math.max(maxF, f[i]);
        }
        return m - maxF;
    }

    // 对于每一行,j 列的字母都 <= i 列的字母?
    private boolean lessEq(String[] strs, int j, int i) {
        for (String s : strs) {
            if (s.charAt(j) > s.charAt(i)) {
                return false;
            }
        }
        return true;
    }
}

###cpp

class Solution {
public:
    int minDeletionSize(vector<string>& strs) { 
        // 对于每一行,j 列的字母都 <= i 列的字母?
        auto less_eq = [&](int j, int i) -> bool {
            for (auto& s : strs) {
                if (s[j] > s[i]) {
                    return false;
                }
            }
            return true;
        };

        int m = strs[0].size();
        vector<int> f(m);
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < i; j++) {
                // 如果 f[j] <= f[i],就不用跑 O(n) 的 less_eq 了
                if (f[j] > f[i] && less_eq(j, i)) {
                    f[i] = f[j];
                }
            }
            f[i]++;
        }
        return m - ranges::max(f);
    }
};

###c

#define MAX(a, b) ((b) > (a) ? (b) : (a))

int minDeletionSize(char** strs, int strsSize) { 
    // 对于每一行,j 列的字母都 <= i 列的字母?
    bool less_eq(int j, int i) {
        for (int k = 0; k < strsSize; k++) {
            if (strs[k][j] > strs[k][i]) {
                return false;
            }
        }
        return true;
    }

    int m = strlen(strs[0]);
    int* f = calloc(m, sizeof(int));
    int max_f = 0;

    for (int i = 0; i < m; i++) {
        for (int j = 0; j < i; j++) {
            // 如果 f[j] <= f[i],就不用跑 O(n) 的 less_eq 了
            if (f[j] > f[i] && less_eq(j, i)) {
                f[i] = f[j];
            }
        }
        f[i]++;
        max_f = MAX(max_f, f[i]);
    }

    free(f);
    return m - max_f;
}

###go

func minDeletionSize(strs []string) int {
// 对于每一行,j 列的字母都 <= i 列的字母?
lessEq := func(j, i int) bool {
for _, s := range strs {
if s[j] > s[i] {
return false
}
}
return true
}

m := len(strs[0])
f := make([]int, m)
for i := range m {
for j := range i {
// 如果 f[j] <= f[i],就不用跑 O(n) 的 lessEq 了
if f[j] > f[i] && lessEq(j, i) {
f[i] = f[j]
}
}
f[i]++
}
return m - slices.Max(f)
}

###js

var minDeletionSize = function(strs) {
    // 对于每一行,j 列的字母都 <= i 列的字母?
    function lessEq(j, i) {
        for (const s of strs) {
            if (s[j] > s[i]) {
                return false;
            }
        }
        return true;
    }

    const m = strs[0].length;
    const f = Array(m).fill(0);
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < i; j++) {
            // 如果 f[j] <= f[i],就不用跑 O(n) 的 lessEq 了
            if (f[j] > f[i] && lessEq(j, i)) {
                f[i] = f[j];
            }
        }
        f[i]++;
    }
    return m - Math.max(...f);
};

###rust

impl Solution {
    pub fn min_deletion_size(strs: Vec<String>) -> i32 {
        let m = strs[0].len();
        let mut f = vec![0; m];
        for i in 0..m {
            for j in 0..i {
                // 如果 f[j] <= f[i],就不用跑 O(n) 的 all 了
                if f[j] > f[i] && strs.iter().all(|s| s.as_bytes()[j] <= s.as_bytes()[i]) {
                    f[i] = f[j];
                }
            }
            f[i] += 1;
        }
        m as i32 - *f.iter().max().unwrap()
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(nm^2)$,其中 $n$ 是 $\textit{strs}$ 的长度,$m$ 是 $\textit{strs}[i]$ 的长度。
  • 空间复杂度:$\mathcal{O}(m)$。

专题训练

见下面动态规划题单的「§4.2 最长递增子序列(LIS)」。

分类题单

如何科学刷题?

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

960. 删列造序 III

作者 stormsunshine
2023年7月16日 13:32

解法

思路和算法

这道题要求在数组 $\textit{strs}$ 中删除尽可能少的下标处的字符,满足每个字符串剩余字符都按字典序排序。为了使删除的下标个数最少,应使保留的下标个数最多,因此问题等价于计算数组 $\textit{strs}$ 中的所有字符串的最长公共递增子序列的长度,这里的公共的含义是下标相同。

用 $n$ 表示数组 $\textit{strs}$ 的长度,用 $m$ 表示数组 $\textit{strs}$ 中每个字符串的长度。对于 $0 \le i < m$,需要分别计算以每个下标 $i$ 结尾的最长公共递增子序列的长度,得到所有字符串的最长公共递增子序列的长度。如果存在下标 $j$ 满足 $0 \le j < i$ 且下标 $j$ 和下标 $i$ 都保留,则应满足对于所有 $0 \le k < n$ 都有 $\textit{strs}[k][j] \le \textit{strs}[k][i]$,在每个字符串的下标 $j$ 的字符之后添加下标 $i$ 的字符即可得到新的公共递增子序列。因此可以使用动态规划计算以每个下标结尾的最长公共递增子序列的长度。

为方便表述,对于给定下标 $i$,如果下标 $j$ 满足 $0 \le j < i$ 且对于所有 $0 \le k < n$ 都有 $\textit{strs}[k][j] \le \textit{strs}[k][i]$,则称下标 $j$ 是下标 $i$ 的「前驱下标」。

创建长度为 $m$ 的数组 $\textit{dp}$,其中 $\textit{dp}[i]$ 为以下标 $i$ 结尾的最长公共递增子序列长度。由于以任意一个下标结尾的最长公共递增子序列长度都大于等于 $1$,因此将 $\textit{dp}$ 中的所有值初始化为 $1$。

当 $i = 0$ 时,每个字符串的以下标 $i$ 结尾的子序列都只有一个,长度为 $1$,因此动态规划的边界情况是 $\textit{dp}[0] = 1$。

当 $i > 0$ 时,对于下标 $i$ 的任意前驱下标 $j$,$\textit{dp}[i] \ge \textit{dp}[j] + 1$,为了使 $\textit{dp}[i]$ 最大化,应寻找符合要求的最大的 $\textit{dp}[j]$,此时 $\textit{dp}[i] = \max{\textit{dp}[j]} + 1$。因此动态规划的状态转移方程是:对于下标 $i$ 的所有前驱下标 $j$,$\textit{dp}[i] = \max {\textit{dp}[j]} + 1$。

由于每一项依赖于之前的项,因此应从小到大遍历每个 $i$ 并计算 $\textit{dp}[i]$。计算得到 $\textit{dp}$ 中的所有状态值之后,其中的最大值即为最长公共递增子序列的长度。

用 $\textit{maxRemain}$ 表示 $\textit{dp}$ 中的最大值,则最多保留的下标个数是 $\textit{maxRemain}$,最少删除的下标个数是 $m - \textit{maxRemain}$。

代码

class Solution {
    public int minDeletionSize(String[] strs) {
        int n = strs.length, m = strs[0].length();
        int[] dp = new int[m];
        Arrays.fill(dp, 1);
        for (int i = 1; i < m; i++) {
            for (int j = 0; j < i; j++) {
                boolean sorted = true;
                for (int k = 0; k < n && sorted; k++) {
                    if (strs[k].charAt(j) > strs[k].charAt(i)) {
                        sorted = false;
                    }
                }
                if (sorted) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }
        int maxRemain = 0;
        for (int remain : dp) {
            maxRemain = Math.max(maxRemain, remain);
        }
        return m - maxRemain;
    }
}
public class Solution {
    public int MinDeletionSize(string[] strs) {
        int n = strs.Length, m = strs[0].Length;
        int[] dp = new int[m];
        Array.Fill(dp, 1);
        for (int i = 1; i < m; i++) {
            for (int j = 0; j < i; j++) {
                bool sorted = true;
                for (int k = 0; k < n && sorted; k++) {
                    if (strs[k][j] > strs[k][i]) {
                        sorted = false;
                    }
                }
                if (sorted) {
                    dp[i] = Math.Max(dp[i], dp[j] + 1);
                }
            }
        }
        int maxRemain = 0;
        foreach (int remain in dp) {
            maxRemain = Math.Max(maxRemain, remain);
        }
        return m - maxRemain;
    }
}
class Solution {
public:
    int minDeletionSize(vector<string>& strs) {
        int n = strs.size(), m = strs[0].size();
        vector<int> dp(m, 1);
        for (int i = 1; i < m; i++) {
            for (int j = 0; j < i; j++) {
                bool sorted = true;
                for (int k = 0; k < n && sorted; k++) {
                    if (strs[k][j] > strs[k][i]) {
                        sorted = false;
                    }
                }
                if (sorted) {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
        }
        int maxRemain = 0;
        for (int remain : dp) {
            maxRemain = max(maxRemain, remain);
        }
        return m - maxRemain;
    }
};
class Solution:
    def minDeletionSize(self, strs: List[str]) -> int:
        n, m = len(strs), len(strs[0])
        dp = [1 for _ in range(m)]
        for i in range(1, m):
            for j in range(0, i):
                sorted = True
                for k in range(0, n):
                    if strs[k][j] > strs[k][i]:
                        sorted = False
                        break
                if sorted:
                    dp[i] = max(dp[i], dp[j] + 1)
        maxRemain = 0
        for remain in dp:
            maxRemain = max(maxRemain, remain)
        return m - maxRemain

复杂度分析

  • 时间复杂度:$O(nm^2)$,其中 $n$ 是数组 $\textit{strs}$ 的长度,$m$ 是数组 $\textit{strs}$ 中每个字符串的长度。状态数是 $O(m)$,计算每个状态时需要遍历当前下标之前的 $O(m)$ 个下标,对于之前的每个下标需要 $O(n)$ 的时间判断之前的下标是否为当前下标的前驱下标,因此时间复杂度是 $O(nm^2)$。

  • 空间复杂度:$O(m)$,其中 $m$ 是数组 $\textit{strs}$ 中每个字符串的长度。需要创建长度为 $m$ 的数组 $\textit{dp}$。

删列造序 III

作者 LeetCode
2019年8月7日 10:19

方法 1:动态规划

想法和算法

这是一个复杂的问题,很难抽象出解题思路。

首先,找出需要保留的列数,而不是需要删除的列数。最后,可以相减得到答案。

假设我们一定保存第一列 C,那么保存的下一列 D 就必须保证每行都是字典有序的,也就是 C[i] <= D[i]。那么我们就可以删除 CD 之间的所有列。

我们可以用动态规划来解决这个问题,让 dp[k] 表示在输入为 [row[k:] for row in A] 时保存的列数,那么 dp[k] 的递推式显而易见。

###Java

class Solution {
    public int minDeletionSize(String[] A) {
        int W = A[0].length();
        int[] dp = new int[W];
        Arrays.fill(dp, 1);
        for (int i = W-2; i >= 0; --i)
            search: for (int j = i+1; j < W; ++j) {
                for (String row: A)
                    if (row.charAt(i) > row.charAt(j))
                        continue search;

                dp[i] = Math.max(dp[i], 1 + dp[j]);
            }

        int kept = 0;
        for (int x: dp)
            kept = Math.max(kept, x);
        return W - kept;
    }
}

###Python

class Solution(object):
    def minDeletionSize(self, A):
        W = len(A[0])
        dp = [1] * W
        for i in xrange(W-2, -1, -1):
            for j in xrange(i+1, W):
                if all(row[i] <= row[j] for row in A):
                    dp[i] = max(dp[i], 1 + dp[j])

        return W - max(dp)

复杂度分析

  • 时间复杂度:$O(N * W^2)$,其中 $N$ 是 A 的长度,$W$ 是 A 中每个单词的长度。
  • 空间复杂度:$O(W)$。

每日一题-删列造序 II🟡

2025年12月21日 00:00

给定由 n 个字符串组成的数组 strs,其中每个字符串长度相等。

选取一个删除索引序列,对于 strs 中的每个字符串,删除对应每个索引处的字符。

比如,有 strs = ["abcdef", "uvwxyz"],删除索引序列 {0, 2, 3},删除后 strs["bef", "vyz"]

假设,我们选择了一组删除索引 answer,那么在执行删除操作之后,最终得到的数组的元素是按 字典序strs[0] <= strs[1] <= strs[2] ... <= strs[n - 1])排列的,然后请你返回 answer.length 的最小可能值。

 

    示例 1:

    输入:strs = ["ca","bb","ac"]
    输出:1
    解释: 
    删除第一列后,strs = ["a", "b", "c"]。
    现在 strs 中元素是按字典排列的 (即,strs[0] <= strs[1] <= strs[2])。
    我们至少需要进行 1 次删除,因为最初 strs 不是按字典序排列的,所以答案是 1。
    

    示例 2:

    输入:strs = ["xc","yb","za"]
    输出:0
    解释:
    strs 的列已经是按字典序排列了,所以我们不需要删除任何东西。
    注意 strs 的行不需要按字典序排列。
    也就是说,strs[0][0] <= strs[0][1] <= ... 不一定成立。
    

    示例 3:

    输入:strs = ["zyx","wvu","tsr"]
    输出:3
    解释:
    我们必须删掉每一列。
    

     

    提示:

    • n == strs.length
    • 1 <= n <= 100
    • 1 <= strs[i].length <= 100
    • strs[i] 由小写英文字母组成

    从左到右贪心 + 优化(Python/Java/C++/Go)

    作者 endlesscheng
    2025年12月11日 11:29

    例如 $\textit{strs}=[\texttt{ac},\texttt{ad},\texttt{ba},\texttt{bb}]$,竖着看就是

    $$
    \begin{aligned}
    & \texttt{ac} \
    & \texttt{ad} \
    & \texttt{ba} \
    & \texttt{bb} \
    \end{aligned}
    $$

    第一列是升序,可以不删。

    • 如果删第一列,那么需要完整地比较第二列的四个字母是不是升序。
    • 如果不删第一列,那么对于第二列,由于 $\texttt{d}$ 和 $\texttt{a}$ 前面的字母不同,只看第一列的字母就能确定 $\texttt{ad} < \texttt{ba}$,所以我们不需要比较 $\texttt{d}$ 和 $\texttt{a}$ 的大小。此时第二列分成了两组 $[\texttt{c},\texttt{d}]$ 和 $[\texttt{a},\texttt{b}]$,只需判断组内字母是不是升序,而不是完整地比较第二列的四个字母。

    由此可见,当列已经是升序时,不删更好,后面需要比较的字母更少,更容易满足要求,最终删除的列更少。

    如果列不是升序,那么一定要删(否则最终得到的数组不是字典序排列)。

    优化前

    ###py

    class Solution:
        def minDeletionSize(self, strs: List[str]) -> int:
            n, m = len(strs), len(strs[0])
            a = [''] * n  # 最终得到的字符串数组
            ans = 0
            for j in range(m):
                for i in range(n - 1):
                    if a[i] + strs[i][j] > a[i + 1] + strs[i + 1][j]:
                        # j 列不是升序,必须删
                        ans += 1
                        break
                else:
                    # j 列是升序,不删更好
                    for i, s in enumerate(strs):
                        a[i] += s[j]
            return ans
    

    ###java

    class Solution {
        public int minDeletionSize(String[] strs) {
            int n = strs.length;
            int m = strs[0].length();
            String[] a = new String[n]; // 最终得到的字符串数组
            Arrays.fill(a, "");
    
            int ans = 0;
            next:
            for (int j = 0; j < m; j++) {
                for (int i = 0; i < n - 1; i++) {
                    if ((a[i] + strs[i].charAt(j)).compareTo(a[i + 1] + strs[i + 1].charAt(j)) > 0) {
                        // j 列不是升序,必须删
                        ans++;
                        continue next;
                    }
                }
                // j 列是升序,不删更好
                for (int i = 0; i < n; i++) {
                    a[i] += strs[i].charAt(j);
                }
            }
            return ans;
        }
    }
    

    ###cpp

    class Solution {
    public:
        int minDeletionSize(vector<string>& strs) {
            int n = strs.size(), m = strs[0].size();
            vector<string> a(n); // 最终得到的字符串数组
            int ans = 0;
            for (int j = 0; j < m; j++) {
                bool del = false;
                for (int i = 0; i < n - 1; i++) {
                    if (a[i] + strs[i][j] > a[i + 1] + strs[i + 1][j]) {
                        // j 列不是升序,必须删
                        ans++;
                        del = true;
                        break;
                    }
                }
                if (!del) {
                    // j 列是升序,不删更好
                    for (int i = 0; i < n; i++) {
                        a[i] += strs[i][j];
                    }
                }
            }
            return ans;
        }
    };
    

    ###go

    func minDeletionSize(strs []string) (ans int) {
    n, m := len(strs), len(strs[0])
    a := make([]string, n) // 最终得到的字符串数组
    next:
    for j := range m {
    for i := range n - 1 {
    if a[i]+string(strs[i][j]) > a[i+1]+string(strs[i+1][j]) {
    // j 列不是升序,必须删
    ans++
    continue next
    }
    }
    // j 列是升序,不删更好
    for i, s := range strs {
    a[i] += string(s[j])
    }
    }
    return
    }
    

    复杂度分析

    • 时间复杂度:$\mathcal{O}(nm^2)$,其中 $n$ 是 $\textit{strs}$ 的长度,$m$ 是 $\textit{strs}[i]$ 的长度。比较 $\mathcal{O}(nm)$ 次大小,每次 $\mathcal{O}(m)$。
    • 空间复杂度:$\mathcal{O}(nm)$。

    优化

    回顾前文的例子:

    $$
    \begin{aligned}
    & \texttt{ac} \
    & \texttt{ad} \
    & \texttt{ba} \
    & \texttt{bb} \
    \end{aligned}
    $$

    第一列升序,不删。由于 $\textit{strs}[1][0] < \textit{strs}[2][0]$,后续 $a[1] < a[2]$ 必定成立,所以不需要比较这两个字符串。对于其余相邻字符串来说,由于第一列的字母都一样,所以只需比较第二列的字母,无需比较整个字符串

    怎么维护需要比较的下标(行号)呢?可以用哈希集合,或者布尔数组,或者创建一个下标列表,删除列表中的无需比较的下标。最后一种方法最高效,我们可以用 27. 移除元素 的方法,原地删除无需比较的下标,见 我的题解

    ###py

    class Solution:
        def minDeletionSize(self, strs: List[str]) -> int:
            n, m = len(strs), len(strs[0])
            check_list = list(range(n - 1))
    
            ans = 0
            for j in range(m):
                for i in check_list:
                    if strs[i][j] > strs[i + 1][j]:
                        # j 列不是升序,必须删
                        ans += 1
                        break
                else:
                    # j 列是升序,不删更好
                    new_size = 0
                    for i in check_list:
                        if strs[i][j] == strs[i + 1][j]:
                            # 相邻字母相等,下一列 i 和 i+1 需要继续比大小
                            check_list[new_size] = i  # 原地覆盖
                            new_size += 1
                    del check_list[new_size:]
            return ans
    

    ###java

    class Solution {
        public int minDeletionSize(String[] strs) {
            int n = strs.length;
            int m = strs[0].length();
            int size = n - 1;
            int[] checkList = new int[size];
            for (int i = 0; i < size; i++) {
                checkList[i] = i;
            }
    
            int ans = 0;
            next:
            for (int j = 0; j < m; j++) {
                for (int t = 0; t < size; t++) {
                    int i = checkList[t];
                    if (strs[i].charAt(j) > strs[i + 1].charAt(j)) {
                        // j 列不是升序,必须删
                        ans++;
                        continue next;
                    }
                }
                // j 列是升序,不删更好
                int newSize = 0;
                for (int t = 0; t < size; t++) {
                    int i = checkList[t];
                    if (strs[i].charAt(j) == strs[i + 1].charAt(j)) {
                        // 相邻字母相等,下一列 i 和 i+1 需要继续比大小
                        checkList[newSize++] = i; // 原地覆盖
                    }
                }
                size = newSize;
            }
            return ans;
        }
    }
    

    ###cpp

    class Solution {
    public:
        int minDeletionSize(vector<string>& strs) {
            int n = strs.size(), m = strs[0].size();
            vector<int> check_list(n - 1);
            ranges::iota(check_list, 0);
    
            int ans = 0;
            for (int j = 0; j < m; j++) {
                bool del = false;
                for (int i : check_list) {
                    if (strs[i][j] > strs[i + 1][j]) {
                        // j 列不是升序,必须删
                        ans++;
                        del = true;
                        break;
                    }
                }
                if (del) {
                    continue;
                }
                // j 列是升序,不删更好
                int new_size = 0;
                for (int i : check_list) {
                    if (strs[i][j] == strs[i + 1][j]) {
                        // 相邻字母相等,下一列 i 和 i+1 需要继续比大小
                        check_list[new_size++] = i; // 原地覆盖
                    }
                }
                check_list.resize(new_size);
            }
            return ans;
        }
    };
    

    ###go

    func minDeletionSize(strs []string) (ans int) {
    n, m := len(strs), len(strs[0])
    checkList := make([]int, n-1)
    for i := range checkList {
    checkList[i] = i
    }
    
    next:
    for j := range m {
    for _, i := range checkList {
    if strs[i][j] > strs[i+1][j] {
    // j 列不是升序,必须删
    ans++
    continue next
    }
    }
    // j 列是升序,不删更好
    newCheckList := checkList[:0] // 原地
    for _, i := range checkList {
    if strs[i][j] == strs[i+1][j] {
    // 相邻字母相等,下一列 i 和 i+1 需要继续比大小
    newCheckList = append(newCheckList, i)
    }
    }
    checkList = newCheckList
    }
    return
    }
    

    复杂度分析

    • 时间复杂度:$\mathcal{O}(nm)$,其中 $n$ 是 $\textit{strs}$ 的长度,$m$ 是 $\textit{strs}[i]$ 的长度。
    • 空间复杂度:$\mathcal{O}(n)$。

    专题训练

    见下面贪心题单的「§1.4 从最左/最右开始贪心」。

    分类题单

    如何科学刷题?

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

    删列造序

    作者 yue-176
    2020年8月21日 09:00

    贪心的想法,我们从前往后的比较每一个字符,如果发现A[i][j]<A[i-1][j],那么就意味着这一列是必须删除的,但是这样做有一个问题:就是如果之前我们已经确定了这两列的大小关系,那么这个时候即使小于,也不用删除,比如acd,adc,对于这两个字符串来说,当我们比较前面两个字符时就已经确定大小关系了,那么当比较第三个字符时,即使d>c,也是不需要删除的。
    所以针对多个字符串,我们开一个数组vis[n],vis[i]表示第i行和第i-1行是否已经确定大小关系了。
    思路:
    从第0列开始比较,如果发现这一列中有不确定大小关系并且是小于前面行的,就意味着是必须删除的,这个时候就不需要更新vis,否则我们就跟新vis;

    ###java

    class Solution {
        public int minDeletionSize(String[] A) {
            int n = A.length;
            int m = A[0].length();
    
            int[] vis = new int[n];
            int ans = 0;
            for (int i = 0; i < m; i++) {
                boolean isDelete=false;
                for (int j = 1; j < n; j++) {
                    if(vis[j]==1)continue;
                    if(A[j].charAt(i)<A[j-1].charAt(i)){
                        isDelete = true;
                        break;
                    }
                }
                if(isDelete)ans++;
                else{
                    for (int j = 1; j < n; j++) {
                        if(A[j].charAt(i)>A[j-1].charAt(i)){
                            vis[j]=1;
                        }
                    }
                }
            }
            return ans;
        }
    }
    

    删列造序 II

    作者 LeetCode
    2019年8月1日 11:42

    方法 1:贪心

    想法

    针对该问题,我们考虑保留哪些列去获得最终的有序结果,而不是删除哪些列。

    如果第一列不是字典序排列的,我们就必须删除它。

    否则,我们需要讨论是否保留第一列。会出现以下两种情况:

    • 如果我们不保留第一列,则最后答案的行需要保证有序;

    • 如果我们保留了第一列,那么最终答案的行(除去第一列)只需要在第一个字母相同的情况下需要保证有序。

      这个描述很难理解,看下面的例子:

      假设我们有 A = ["axx", "ayy", "baa", "bbb", "bcc"],当我们保留第一列之后,最终行变成 R = ["xx", "yy", "aa", "bb", "cc"],对于这些行,并不要求所有有序(R[0] <= R[1] <= R[2] <= R[3] <= R[4]),只需要达到一个较弱的要求:对于第一个字母相同的行保证有序(R[0] <= R[1]R[2] <= R[3] <= R[4])。

    现在,我们只将结论应用到第一列,但实际上这个结论对每列都合适。如果我们不能取用第一列,就删除它。否则,我们就取用第一列,因为无论如何都可以使要求更简单。

    算法

    首先没有任意列保留,对于每一列:如果保留后结果保持有序,就保留这一列;否则删除它。

    ###Java

    class Solution {
        public int minDeletionSize(String[] A) {
            int N = A.length;
            int W = A[0].length();
            int ans = 0;
    
            // cur : all rows we have written
            // For example, with A = ["abc","def","ghi"] we might have
            // cur = ["ab", "de", "gh"].
            String[] cur = new String[N];
            for (int j = 0; j < W; ++j) {
                // cur2 : What we potentially can write, including the
                //        newest column col = [A[i][j] for i]
                // Eg. if cur = ["ab","de","gh"] and col = ("c","f","i"),
                // then cur2 = ["abc","def","ghi"].
                String[] cur2 = Arrays.copyOf(cur, N);
                for (int i = 0; i < N; ++i)
                    cur2[i] += A[i].charAt(j);
    
                if (isSorted(cur2))
                    cur = cur2;
                else
                    ans++;
            }
    
            return ans;
        }
    
        public boolean isSorted(String[] A) {
            for (int i = 0; i < A.length - 1; ++i)
                if (A[i].compareTo(A[i+1]) > 0)
                    return false;
    
            return true;
        }
    }
    

    ###Python

    class Solution(object):
        def minDeletionSize(self, A):
            def is_sorted(A):
                return all(A[i] <= A[i+1] for i in xrange(len(A) - 1))
    
            ans = 0
            # cur : all rows we have written
            # For example, with A = ["abc","def","ghi"] we might have
            # cur = ["ab", "de", "gh"].
            cur = [""] * len(A)  
    
            for col in zip(*A):
                # cur2 : What we potentially can write, including the
                #        newest column 'col'.
                # Eg. if cur = ["ab","de","gh"] and col = ("c","f","i"),
                # then cur2 = ["abc","def","ghi"].
                cur2 = cur[:]
                for i, letter in enumerate(col):
                    cur2[i] = cur2[i] + letter
    
                if is_sorted(cur2):
                    cur = cur2
                else:
                    ans += 1
    
            return ans
    

    复杂度分析

    • 时间复杂度:$O(NW^2)$,其中 $N$ 是 A 的长度,$W$ 是 A[i] 的长度。
    • 空间复杂度:$O(NW)$。

    方法 2:优化贪心

    解释

    方法 1 可以用更少的空间和时间。

    核心思路是记录每一列的”割“信息。在第一个例子中,A = ["axx","ayy","baa","bbb","bcc"]R 也是相同的定义),第一列将条件 R[0] <= R[1] <= R[2] <= R[3] <= R[4] 切成了 R[0] <= R[1]R[2] <= R[3] <= R[4]。也就是说,"a" == column[1] != column[2] == "b" ”切割“了 R 中的一个条件。

    从更高层面上说,我们的算法只需要考虑新加进的列是否保证有序。通过维护”割“的信息,只需要比较新列的字符。

    ###Java

    class Solution {
        public int minDeletionSize(String[] A) {
            int N = A.length;
            int W = A[0].length();
            // cuts[j] is true : we don't need to check any new A[i][j] <= A[i][j+1]
            boolean[] cuts = new boolean[N-1];
    
            int ans = 0;
            search: for (int j = 0; j < W; ++j) {
                // Evaluate whether we can keep this column
                for (int i = 0; i < N-1; ++i)
                    if (!cuts[i] && A[i].charAt(j) > A[i+1].charAt(j)) {
                        // Can't keep the column - delete and continue
                        ans++;
                        continue search;
                    }
    
                // Update 'cuts' information
                for (int i = 0; i < N-1; ++i)
                    if (A[i].charAt(j) < A[i+1].charAt(j))
                        cuts[i] = true;
            }
    
            return ans;
        }
    }
    
    

    ###Python

    class Solution(object):
        def minDeletionSize(self, A):
            # cuts[i] is True : we don't need to check col[i] <= col[i+1]
            cuts = [False] * (len(A) - 1)
    
            ans = 0
            for col in zip(*A):
                if all(cuts[i] or col[i] <= col[i+1] for i in xrange(len(col) - 1)):
                    for i in xrange(len(col) - 1):
                        if col[i] < col[i+1]:
                            cuts[i] = True
                else:
                    ans += 1
            return ans
    

    复杂度分析

    • 时间复杂度:$O(NW)$,其中 $N$ 是 A 的长度,$W$ 是 A[i] 的长度。
    • 空间复杂度:额外空间开销 $O(N)$(在 Python 中,zip(*A) 需要 $O(NW)$ 的空间)。

    每日一题-删列造序🟢

    2025年12月20日 00:00

    给你由 n 个小写字母字符串组成的数组 strs,其中每个字符串长度相等。

    这些字符串可以每个一行,排成一个网格。例如,strs = ["abc", "bce", "cae"] 可以排列为:

    abc
    bce
    cae

    你需要找出并删除 不是按字典序非严格递增排列的 列。在上面的例子(下标从 0 开始)中,列 0('a', 'b', 'c')和列 2('c', 'e', 'e')都是按字典序非严格递增排列的,而列 1('b', 'c', 'a')不是,所以要删除列 1 。

    返回你需要删除的列数。

     

    示例 1:

    输入:strs = ["cba","daf","ghi"]
    输出:1
    解释:网格示意如下:
      cba
      daf
      ghi
    列 0 和列 2 按升序排列,但列 1 不是,所以只需要删除列 1 。
    

    示例 2:

    输入:strs = ["a","b"]
    输出:0
    解释:网格示意如下:
      a
      b
    只有列 0 这一列,且已经按升序排列,所以不用删除任何列。
    

    示例 3:

    输入:strs = ["zyx","wvu","tsr"]
    输出:3
    解释:网格示意如下:
      zyx
      wvu
      tsr
    所有 3 列都是非升序排列的,所以都要删除。
    

     

    提示:

    • n == strs.length
    • 1 <= n <= 100
    • 1 <= strs[i].length <= 1000
    • strs[i] 由小写英文字母组成

    简单题,简单做(Python/Java/C++/Go)

    作者 endlesscheng
    2025年12月9日 13:41

    题意:字符网格中,有多少列不是递增的?

    对于每一列,从上到下遍历,如果发现上面的字母比下面的大,那么这一列不是递增的,答案加一。

    class Solution:
        def minDeletionSize(self, strs: List[str]) -> int:
            ans = 0
            for col in zip(*strs):
                if any(x > y for x, y in pairwise(col)):
                    ans += 1
            return ans
    
    class Solution:
        def minDeletionSize(self, strs: List[str]) -> int:
            ans = 0
            for col in zip(*strs):
                if list(col) != sorted(col):
                    ans += 1
            return ans
    
    class Solution {
        public int minDeletionSize(String[] strs) {
            int n = strs.length;
            int m = strs[0].length();
            int ans = 0;
            for (int j = 0; j < m; j++) {
                for (int i = 1; i < n; i++) { // 遍历 j 列的字母
                    if (strs[i - 1].charAt(j) > strs[i].charAt(j)) {
                        ans++;
                        break;
                    }
                }
            }
            return ans;
        }
    }
    
    class Solution {
    public:
        int minDeletionSize(vector<string>& strs) {
            int n = strs.size(), m = strs[0].size();
            int ans = 0;
            for (int j = 0; j < m; j++) {
                for (int i = 1; i < n; i++) { // 遍历 j 列的字母
                    if (strs[i - 1][j] > strs[i][j]) {
                        ans++;
                        break;
                    }
                }
            }
            return ans;
        }
    };
    
    func minDeletionSize(strs []string) (ans int) {
    n, m := len(strs), len(strs[0])
    for j := range m {
    for i := range n - 1 { // 遍历 j 列的字母
    if strs[i][j] > strs[i+1][j] {
    ans++
    break
    }
    }
    }
    return
    }
    

    复杂度分析

    • 时间复杂度:$\mathcal{O}(nm)$,其中 $n$ 是 $\textit{strs}$ 的长度,$m$ 是 $\textit{strs}[i]$ 的长度。
    • 空间复杂度:$\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站@灵茶山艾府

    【负雪明烛】算法图解:详解题意,一图胜千言

    作者 fuxuemingzhu
    2022年5月12日 09:32

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

    题目大意

    题意是:对于输入的字符串数组 strs,让你找出有几列不是升序的。(题目保证了strs中每个字符串的长度相等)

    「升序」是指每个元素都大于等于前面的元素。「等于」也算是升序。

    注意,题目中所说的「删除」,是指某列不满足升序条件,而不是真的要把它从strs中直接删除。

    如下图所示:

    • 输入字符串 strs = ["abc", "bce", "cae"]
    • 按行排列成二维数组;
    • 逐一判断每列是否是升序的,如果不是升序,就是题目中所说的要被“删除”;
    • 其中第 1 列不是升序的,因此被删除;
    • 下图中的strs共需要删除 1 列。

    944. 删列造序.001.png

    解题方法

    一图胜千言,上图已经把解法阐明了。

    我们需要对每列进行遍历,判断每一列是不是都升序的:

    • 升序的列是符合条件的。
    • 非升序的列是不符合条件的,也就是题目中所谓要「删除」的。

    对于本题,肯定是两重 $for$ 循环:

    • 外层的 $for$ 循环是对进行循环:遍历所有的列。
    • 内层的 $for$ 循环是对进行循环:判断该列中的是否是升序的。
    • 当内层的 $for$ 循环找到「降序」的,那么把结果 $+1$,并把内层 $for$ 循环 $break$ 掉。

    代码如下:

    ###Python

    class Solution(object):
        def minDeletionSize(self, strs):
            rows = len(strs)
            cols = len(strs[0])
            res = 0
            for col in range(cols):
                for row in range(1, rows):
                    if strs[row][col] < strs[row - 1][col]:
                        res += 1
                        break
            return res
    

    ###C++

    class Solution {
        public:
        int minDeletionSize(vector<string>& strs) {
            const int rows = strs.size();
            const int cols = strs[0].size();
            int res = 0;
            for (int col = 0; col < cols; ++col) {
                for (int row = 1; row < rows; ++row) {
                    if (strs[row][col] < strs[row - 1][col]) {
                        res ++;
                        break;
                    }
                }
            }
            return res;
        }
    };
    

    ###Java

    class Solution {
        public int minDeletionSize(String[] strs) {
            int rows = strs.length;
            int cols = strs[0].length();
            int res = 0;
            for (int col = 0; col < cols; ++col) {
                for (int row = 1; row < rows; ++row) {
                    if (strs[row].charAt(col) < strs[row - 1].charAt(col)) {
                        res ++;
                        break;
                    }
                }
            }
            return res;
        }
    }
    

    复杂度

    • 时间复杂度:$O(M * N)$,M 和 N 分别是行列数;
    • 空间复杂度:$O(1)$

    总结

    1. 力扣题目有点像阅读理解题吗,如果看不懂题目,不妨在题解区找找「负雪明烛」的题解,每篇都会把题意讲清楚。

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

    ❌
    ❌