LeetCode 日题 · 2024 年 1—2 月

Contents
Contents

1.14 822. 翻转卡片游戏

use std::collections::HashSet;
impl Solution {
    pub fn flipgame(fronts: Vec<i32>, backs: Vec<i32>) -> i32 {
        let mut wants = HashSet::new();
        let mut n_wants = HashSet::new();
        for i in 0..fronts.len() {
            wants.insert(fronts[i]);
            wants.insert(backs[i]);
            if fronts[i] == backs[i] {
                n_wants.insert(fronts[i]);
            }
        }
        wants = wants.difference(&n_wants).copied().collect();

        wants.into_iter().min().unwrap_or(0)
    }
}

复健活动…

1.20 1938. 查询最大基因差

此题一见就想到了下面这种暴力解法,如果 queries 长 n,parents 长 m,则时间复杂度 O(n logm)

impl Solution {
    pub fn max_genetic_difference(parents: Vec<i32>, queries: Vec<Vec<i32>>) -> Vec<i32> {
        let mut ans = Vec::<i32>::new();
        for query in queries {
            if let [mut n, v] = query[..] {
                let mut max_v = v ^ n;
                while n != -1 {
                    n = parents[n as usize];
                    max_v = max_v.max(n ^ v);
                }
                ans.push(max_v);
            } else {
                unreachable!()
            }
        }
        ans
    }
}

问题在于测试数据中 m 可能太大,因此 TLE

考虑到 n 不可省略,问题在于如何减小 log m 这个因子。考虑比较的过程在于从 leaf 到 root 计算 log m 次异或并取最大值,转而思考 求若干次异或操作的最大值有无简便方法。

异或为二进制按位比较,相同为 0,不同为 1。而比较大小为二进制按位比较先出现 1 的大。那么一个数 a 若干数异或操作的最大值就可以通过按位判断哪个数最先出现与 a 不同的位。这种按序列不断递进的感觉就让人想到了 Trie 树。

考虑使用 Trie 树,结果如下。因为每个数位数有限,复杂度 O(n+m)

·

const MAX_BIT: usize = 18;

#[derive(Debug)]
struct Trie {
    left:  Option<Box<Trie>>,  // 0
    right: Option<Box<Trie>>, // 1
    cnt: usize
}

impl Trie {

    pub fn new() -> Trie {
        Trie {
            left: None,
            right: None,
            cnt: 0,
        }
    }

    pub fn insert(trie: &mut Trie, n: i32) {
        let mut cur = trie;
        for i in (0..=MAX_BIT).rev()  {
            cur.cnt += 1;
            let bit = n & (1 << i);
            if bit != 0 {
                cur = cur.right.get_or_insert(Box::new(Trie::new()))
            } else {
                cur = cur.left.get_or_insert(Box::new(Trie::new()))
            }
        }
        cur.cnt += 1;
    }

    pub fn remove(trie: &mut Trie, n: i32) {
        let mut cur = trie;
        for i in (0..=MAX_BIT).rev()  {
            cur.cnt -= 1;
            let bit = n & (1 << i);
            if bit != 0 {
                cur = cur.right.get_or_insert(Box::new(Trie::new()))
            } else {
                cur = cur.left.get_or_insert(Box::new(Trie::new()))
            }
        }
        cur.cnt -= 1;
    }


    pub fn max(trie: &Trie, v: i32) -> i32 {
        let mut cur = trie;
        let mut p = 0;
        for i in (0..=MAX_BIT).rev()  {
            let bit = v & (1 << i);
            // try to select different bit:
            if bit != 0 {
                if cur.left.is_some() && cur.left.as_ref().unwrap().cnt != 0 {
                    cur = cur.left.as_ref().unwrap();
                } else {
                    p += 1 << i;
                    cur = cur.right.as_ref().unwrap();
                }

            } else {
                if cur.right.is_some() && cur.right.as_ref().unwrap().cnt != 0{
                    p += 1 << i;
                    cur = cur.right.as_ref().unwrap();
                } else {
                    cur = cur.left.as_ref().unwrap();
                }
            }
        }
        p ^ v
    }

}


impl Solution {

    pub fn max_genetic_difference(parents: Vec<i32>, queries: Vec<Vec<i32>>) -> Vec<i32> {
        // assign queries -> O(n)
        let mut node_queries = vec![Vec::<(usize, i32)>::new(); parents.len()];
        let query_len = queries.len();

        for (i, query) in queries.into_iter().enumerate() {
            if let [n, v] = query[..] {
                node_queries[n as usize].push((i, v))
            } else {
                unreachable!()
            }
        }

        // build graph -> O(m)
        let mut graph = vec![Vec::<i32>::new(); parents.len()];
        let mut root = 0;
        for (n, p) in parents.into_iter().enumerate() {
            if p == -1 {
                root = n;
            } else {
                graph[p as usize].push(n as i32);
            }
        }

        // dfs -> O(m log MAX_BIT),log MAX_BIT 为在 Trie 中插入/删除每个数的时间
        let mut ans = vec![0; query_len];
        let mut stack = vec![(root, false)];
        let mut trie = Trie::new();

        while let Some((n, visited)) = stack.pop() {
            if visited {
                Trie::remove(&mut trie, n as i32);
                continue;
            }
            stack.push((n, true));
            Trie::insert(&mut trie, n as i32);
            for (i, v) in node_queries[n].iter() {
                ans[*i] = Trie::max(&trie, *v);
            }
            for child in graph[n].iter() {
                stack.push((*child as usize, false))
            }
        }


        ans
    }
}

1.21 构造最长非递减子数组

第一眼没有考虑清楚,没有认识到对每一个位置的决策都会对后续决策产生影响,有了下面错误答案。

impl Solution {
    pub fn max_non_decreasing_length(nums1: Vec<i32>, nums2: Vec<i32>) -> i32 {
        let l = nums1.len();

        let mut res = 0;
        let mut maxl = 0;
        let mut cur = 0;
        for i in 0..l {
            let min = nums1[i].min(nums2[i]);
            let max = nums1[i].max(nums2[i]);
            if min >= cur {
                cur = min;
                maxl += 1;
            } else if max >= cur {
                cur = min;
                maxl += 1;
            } else {
                res = res.max(maxl);
                maxl = 1;
                cur = min;
            }
        }
        res.max(maxl)
    }
}

考虑到影响后,意识到每个位置选数组 A 或选数组 B 都要考虑,考虑可以划分子问题,找到依赖关系。又因为是否能选位置 n 上的某个数取决于位置 n-1 上的数,得出如下答案:

    // 如果把「子数组」改成「子序列」呢? -> 转化为求一个数组的最长非递减子序列 -> 存在 O(nlog⁡n) 解法
    // https://leetcode.cn/problems/longest-increasing-subsequence/
    // https://www.bilibili.com/video/BV1XW4y1f7Wv/?spm_id_from=333.999.0.0&vd_source=ebce05e0e6ac0774e7cf8844bf20f437
    pub fn max_non_decreasing_length(nums1: Vec<i32>, nums2: Vec<i32>) -> i32 {

        let (mut last_min, mut last_max) = (0, 0);

        let mut lmin_l = 0;
        let mut lmax_l = 0;

        let mut max_l = 0;


        for i in 0..nums1.len() {
            let min = nums1[i].min(nums2[i]);
            let max = nums1[i].max(nums2[i]);

            let lmin_l_ = if min >= last_max {
                lmax_l + 1
            } else if min >= last_min {
                lmin_l + 1
            } else {
                1
            };

            lmax_l = if max >= last_max {
                lmax_l + 1
            } else if max >= last_min {
                lmin_l + 1
            } else {
                1
            };
            lmin_l = lmin_l_;


            last_min = min;
            last_max = max;
            max_l = max_l.max(lmax_l);
        }

        max_l
    }

1.22 2304. 网格中的最小路径代价

一眼动态规划,但注意:

  1. MAX_COST 最大值
  2. 下标写对
    pub fn min_path_cost(grid: Vec<Vec<i32>>, move_cost: Vec<Vec<i32>>) -> i32 {
        const MAX_COST: i32 = std::i32::MAX;

        let m = grid.len();
        let n = grid[0].len();
        let mut min_path = vec![vec![MAX_COST; n]; m];

        for i in 0..n {
            min_path[0][i] = grid[0][i];
        }

        // O(mn^2)
        for i in 1..m {
            for j in 0..n {
                for k in 0..n {
                    let parent_val = grid[i - 1][j];
                    let total_cost = grid[i][k] + move_cost[parent_val as usize][k];
                    min_path[i][k] = min_path[i][k].min(min_path[i - 1][j] + total_cost);
                }
            }
        }

        *min_path[m - 1].iter().min().unwrap()
    }

1.25 2498. 青蛙过河 II

构造题,这 tm 谁能想到…

证明:间隔跳所得结果为所有间隔的最大值。假设存在非间隔跳最优解,则必拆分最大间隔,最大间隔拆分后反程间隔必大于原最大间隔,矛盾。故间隔跳为最优解。

impl Solution {
    pub fn max_jump(stones: Vec<i32>) -> i32 {
        let mut max_jmp = stones[1] - stones[0];
        for i in 2..stones.len() {
            max_jmp = max_jmp.max(stones[i] - stones[i - 2]);
        }
        max_jmp
    }
}

此外,检查可否在最大长度不超过m的情况下往返,二分查找 m 即可。(这种思路其实更有启发性)


2.17 3007. 价值和小于等于 K 的最大数字

题解:https://leetcode.cn/problems/maximum-number-that-sum-of-the-prices-is-less-than-or-equal-to-k/

视频:https://www.bilibili.com/video/BV1zt4y1R7Tc/?vd_source=226da368954a7c68d6b7e4bbdc91b2cd

如果用最朴素的想法去解题,我们需要知道下面三种操作

  1. 操作1:求一个整数的价值 -> O(1/x)
  2. 操作2:求从 1 到 num 所有整数的价值和 -> O(num/x)
  3. 操作3:求最大的 num,使得 1 … num 的价值和小于 k -> O(k^2/x)

朴素算法的时间复杂度是 O()

又已知

  • 1 <= k <= 10^15
  • 1 <= x <= 8

又已知常见数据量下的时间复杂度:https://www.acwing.com/blog/content/32/

一般ACM或者笔试题的时间限制是1秒或2秒。 在这种情况下,C++代码中的操作次数控制在 10^7∼10^8 为最佳。

因此,上面的操作至少取一个对数;

所以可以想到用二分查找:

    pub fn find_maximum_number(k: i64, x: i32) -> i64 {
        let mut hi = 1 << x * (k.ilog2() as i32 + 1);
        let mut lo = 0;
        while lo <= hi {
            let mid = (hi + lo) / 2;
            let sum = Solution::sum_until(mid, x);
            if sum == k {
                return mid;
            }
            if sum > k {
                lo = mid + 1;
            } else {
                hi = mid - 1;
            }
        }
        unreachable!()
    }

考虑sum_until 有较多重复计算,可使用数位 dp 减少重复。

注意优先级问题:

dp_arr[i] = dp_arr[i - 1] * 2 - 1 + 1 << (*x* as usize * (i - 1))

*h* & !((2 << (i * *x*)) - 1)


数位DP:递归方程和从高到底按位枚举有关

详见:https://algo.itcharge.cn/10.Dynamic-Programming/09.Digit-DP/01.Digit-DP/#_2-2-%E6%95%B0%E5%AD%97-1-%E7%9A%84%E4%B8%AA%E6%95%B0

基本框架:

class Solution:
    def digitDP(self, n: int) -> int:
        # 将 n 转换为字符串 s
        s = str(n)

        @cache
        # pos: 第 pos 个数位
        # state: 之前选过的数字集合。
        # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。
        # isNum: 表示 pos 前面的数位是否填了数字。如果为真,则当前位不可跳过;如果为假,则当前位可跳过。
        def dfs(pos, state, isLimit, isNum):
            if pos == len(s):
                # isNum 为 True,则表示当前方案符合要求
                return int(isNum)

            ans = 0
            if not isNum:
                # 如果 isNumb 为 False,则可以跳过当前数位
                ans = dfs(pos + 1, state, False, False)

            # 如果前一位没有填写数字,则最小可选择数字为 0,否则最少为 1(不能含有前导 0)。
            minX = 0 if isNum else 1
            # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。
            maxX = int(s[pos]) if isLimit else 9

            # 枚举可选择的数字
            for x in range(minX, maxX + 1):
                # x 不在选择的数字集合中,即之前没有选择过 x
                if (state >> x) & 1 == 0:
                    ans += dfs(pos + 1, state | (1 << x), isLimit and x == maxX, True)
            return ans

        return dfs(0, 0, True, False)

此外均 sum_until 和 findMaximumNumber 实际上都有简易的O(n)做法(找规律)