Q1、链表的中间结点

1、题目描述

给你单链表的头结点 head ,请你找出并返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例 1:

img
输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间结点,值为 3 。

示例 2:

img
输入:head = [1,2,3,4,5,6]
输出:[4,5,6]
解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。

提示:

  • 链表的结点数范围是 [1, 100]
  • 1 <= Node.val <= 100
2、解题思路
  1. 数组存储法:将所有节点存储到数组中,然后直接访问中间节点。
  2. 两次遍历法:第一次遍历计算链表长度,第二次遍历到中间节点。
  3. 快慢指针法:使用两个指针,慢指针每次移动一步,快指针每次移动两步,当快指针到达末尾时,慢指针指向中间节点。
3、代码实现
C++
// 方法1: 数组存储法
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        vector<ListNode*> nodes = {head};
        while (nodes.back()->next != nullptr) {
            nodes.push_back(nodes.back()->next);
        }
        return nodes[nodes.size() / 2];
    }
};
// 方法2: 两次遍历法
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        int n = 0;
        ListNode* cur = head;
        while (cur != nullptr) {
            ++n;
            cur = cur->next;
        }
        int k = 0;
        cur = head;
        while (k < n / 2) {
            ++k;
            cur = cur->next;
        }
        return cur;
    }
};
// 方法3: 快慢指针法
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast != nullptr && fast->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
};
Java
// 方法1: 数组存储法
class Solution {
    public ListNode middleNode(ListNode head) {
        ListNode[] nodes = new ListNode[100];
        int index = 0;
        while (head != null) {
            nodes[index++] = head;
            head = head.next;
        }
        return nodes[index / 2];
    }
}
// 方法2: 两次遍历法
class Solution {
    public ListNode middleNode(ListNode head) {
        int n = 0;
        ListNode cur = head;
        while (cur != null) {
            ++n;
            cur = cur.next;
        }
        int k = 0;
        cur = head;
        while (k < n / 2) {
            ++k;
            cur = cur.next;
        }
        return cur;
    }
}
// 方法3: 快慢指针法
class Solution {
    public ListNode middleNode(ListNode head) {
        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }
}
Python
# 方法1: 数组存储法
class Solution:
    def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
        nodes = [head]
        while nodes[-1].next:
            nodes.append(nodes[-1].next)
        return nodes[len(nodes) // 2]
# 方法2: 两次遍历法
class Solution:
    def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
        n = 0
        cur = head
        while cur:
            n += 1
            cur = cur.next
        k = 0
        cur = head
        while k < n // 2:
            k += 1
            cur = cur.next
        return cur
# 方法3: 快慢指针法
class Solution:
    def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        return slow
4、复杂度分析
  • 数组存储法:

    • 时间复杂度:O(n),需要遍历所有节点。
    • 空间复杂度:O(n),需要存储所有节点。
  • 两次遍历法:

    • 时间复杂度:O(n),需要两次遍历。
    • 空间复杂度:O(1),仅使用常数空间。
  • 快慢指针法:

    • 时间复杂度:O(n),只需一次遍历。
    • 空间复杂度:O(1),仅使用常数空间。

Q2、石子游戏

1、题目描述

Alice 和 Bob 用几堆石子在做游戏。一共有偶数堆石子,排成一行;每堆都有 正 整数颗石子,数目为 piles[i] 。

游戏以谁手中的石子最多来决出胜负。石子的 总数 是 奇数 ,所以没有平局。

Alice 和 Bob 轮流进行,Alice 先开始 。 每回合,玩家从行的 开始 或 结束 处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中 石子最多 的玩家 获胜 。

假设 Alice 和 Bob 都发挥出最佳水平,当 Alice 赢得比赛时返回 true ,当 Bob 赢得比赛时返回 false 。

示例 1:

输入:piles = [5,3,4,5]
输出:true
解释:
Alice 先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果 Bob 拿走前 3 颗,那么剩下的是 [4,5],Alice 拿走后 5 颗赢得 10 分。
如果 Bob 拿走后 5 颗,那么剩下的是 [3,4],Alice 拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对 Alice 来说是一个胜利的举动,所以返回 true 。

示例 2:

输入:piles = [3,7,2,3]
输出:true

提示:

  • 2 <= piles.length <= 500
  • piles.length 是 偶数
  • 1 <= piles[i] <= 500
  • sum(piles[i]) 是 奇数
2、解题思路
  1. 动态规划法:

    • 定义 dp[i][j] 表示在石子堆 piles[i...j] 中,当前玩家能比对手多拿的石子数。
    • 状态转移方程:dp[i][j] = max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1])。
    • 最终 dp[0][n-1] > 0 表示 Alice 能赢。
  2. 空间优化动态规划法:

    • 使用一维数组 dp 替代二维数组,优化空间复杂度。
    • 状态转移方程:dp[j] = max(piles[i] - dp[j], piles[j] - dp[j-1])。
  3. 数学法:

    • Alice 作为先手总能选择最优策略,所以直接返回 true。
3、代码实现
C++
// 方法1: 动态规划
class Solution {
public:
    bool stoneGame(vector<int>& piles) {
        int n = piles.size();
        vector<vector<int>> dp(n, vector<int>(n));
        for (int i = 0; i < n; ++i) {
            dp[i][i] = piles[i]; // 初始化, 只剩一堆时当前玩家拿走
        }
        for (int i = n - 2; i >= 0; --i) // 从后往前填充
        {
            for (int j = i + 1; j < n; ++j) {
                dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
            }
        }
        return dp[0][n - 1] > 0; // Alice 能多拿石子则赢
    }
};
// 方法2: 动态规划空间优化
class Solution {
public:
    bool stoneGame(vector<int>& piles) {
        int n = piles.size();
        vector<int> dp(n);
        for (int i = 0; i < n; ++i) {
            dp[i] = piles[i]; // 初始化
        }
        for (int i = n - 2; i >= 0; --i) {
            for (int j = i + 1; j < n; ++j) {
                dp[j] = max(piles[i] - dp[j], piles[j] - dp[j - 1]);
            }
        }
        return dp[n - 1] > 0;
    }
};
// 方法3: 数学法
class Solution {
public:
    bool stoneGame(vector<int>& piles) { 
        return true; 
    }
};
Java
// 方法1: 动态规划
class Solution {
    public boolean stoneGame(int[] piles) {
        int n = piles.length;
        int[][] dp = new int[n][n];
        for (int i = 0; i < n; i++) {
            dp[i][i] = piles[i]; // 初始化
        }
        for (int i = n - 2; i >= 0; i--) {
            for (int j = i + 1; j < n; j++) {
                dp[i][j] = Math.max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
            }
        }
        return dp[0][n - 1] > 0;
    }
}
// 方法2: 动态规划空间优化
class Solution {
    public boolean stoneGame(int[] piles) {
        int n = piles.length;
        int[] dp = new int[n];
        for (int i = 0; i < n; i++) {
            dp[i] = piles[i]; // 初始化
        }
        for (int i = n - 2; i >= 0; i--) {
            for (int j = i + 1; j < n; j++) {
                dp[j] = Math.max(piles[i] - dp[j], piles[j] - dp[j - 1]);
            }
        }
        return dp[n - 1] > 0;
    }
}
// 方法3: 数学法
class Solution {
    public boolean stoneGame(int[] piles) {
        return true;  // Alice 总能赢
    }
}
Python
// 方法1: 动态规划
class Solution:
    def stoneGame(self, piles: List[int]) -> bool:
        n = len(piles)
        dp = [[0] * n for _ in range(n)]
        for i in range(n):
            dp[i][i] = piles[i]  # 初始化
        for i in range(n - 2, -1, -1):
            for j in range(i + 1, n):
                dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1])
        return dp[0][n - 1] > 0
// 方法2: 动态规划空间优化
class Solution:
    def stoneGame(self, piles: List[int]) -> bool:
        n = len(piles)
        dp = piles.copy()  # 初始化
        for i in range(n - 2, -1, -1):
            for j in range(i + 1, n):
                dp[j] = max(piles[i] - dp[j], piles[j] - dp[j - 1])
        return dp[-1] > 0
// 方法3: 数学法
class Solution:
    def stoneGame(self, piles: List[int]) -> bool:
        return True  # Alice 总能赢
4、复杂度分析
  • 动态规划法:
    • 时间复杂度:O(n^2),双重循环遍历石子堆。
    • 空间复杂度:O(n^2),二维数组存储状态。
  • 空间优化动态规划法:
    • 时间复杂度:O(n^2),与普通动态规划相同。
    • 空间复杂度:O(n),使用一维数组优化空间。
  • 数学法:
    • 时间复杂度:O(1),直接返回结果。
    • 空间复杂度:O(1),无需额外空间。

Q3、第 N 个神奇数字

1、题目描述

一个正整数如果能被 a 或 b 整除,那么它是神奇的。

给定三个整数 n , a , b ,返回第 n 个神奇的数字。因为答案可能很大,所以返回答案 对 109 + 7 取模 后的值。

示例 1:

输入:n = 1, a = 2, b = 3
输出:2

示例 2:

输入:n = 4, a = 2, b = 3
输出:6

提示:

  • 1 <= n <= 109
  • 2 <= a, b <= 4 * 104
2、解题思路
  1. 二分查找:

    • 神奇数字的分布满足单调性,可以二分查找第 n 个神奇数字。
    • 对于一个数 mid,计算有多少个数字 ≤ mid 能被 a 或 b 整除:cnt = mid/a + mid/b - mid/lcm(a,b)。
    • 根据 cnt 与 n 的关系调整搜索范围。
  2. 数学周期法:

    • 神奇数字的分布具有周期性,周期为 lcm(a, b)。
    • 每个周期内有 m = c/a + c/b - 1 个神奇数字。
    • 计算完整周期数,剩余部分通过模拟找出。
3、代码实现
C++
// 方法1: 二分查找
class Solution {
public:
    const int MOD = 1e9 + 7;
    
    int nthMagicalNumber(int n, int a, int b) {
        long long l = min(a, b);
        long long r = (long long)n * min(a, b);
        int c = lcm(a, b);
        while (l <= r) {
            long long mid = (l + r) / 2;
            long long cnt = mid / a + mid / b - mid / c;
            if (cnt >= n) {
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return (r + 1) % MOD;
    }
};
// 方法2: 数学周期法
class Solution {
public:
    const int MOD = 1e9 + 7;

    int nthMagicalNumber(int n, int a, int b) {
        int c = lcm(a, b);
        int m = c / a + c / b - 1;
        int r = n % m;
        int res = (long long)c * (n / m) % MOD;
        if (r == 0) {
            return res;
        }
        int addA = a, addB = b;
        for (int i = 0; i < r - 1; ++i) {
            if (addA < addB) {
                addA += a;
            } else {
                addB += b;
            }
        }
        return (res + min(addA, addB) % MOD) % MOD;
    }
};
Java
class Solution {
    private static final int MOD = 1_000_000_007;

    private int lcm(int a, int b) {
        return a * b / gcd(a, b);
    }

    private int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    public int nthMagicalNumber(int n, int a, int b) {
        long l = Math.min(a, b);
        long r = (long) n * Math.min(a, b);
        int c = lcm(a, b);
        while (l <= r) {
            long mid = (l + r) / 2;
            long cnt = mid / a + mid / b - mid / c;
            if (cnt >= n) {
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return (int) ((r + 1) % MOD);
    }
}
class Solution {
    private static final int MOD = 1_000_000_007;

    private int lcm(int a, int b) {
        return a * b / gcd(a, b);
    }

    private int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    public int nthMagicalNumber(int n, int a, int b) {
        int c = lcm(a, b);
        int m = c / a + c / b - 1;
        int r = n % m;
        int res = (int) ((long) c * (n / m) % MOD);
        if (r == 0) {
            return res;
        }
        int addA = a, addB = b;
        for (int i = 0; i < r - 1; ++i) {
            if (addA < addB) {
                addA += a;
            } else {
                addB += b;
            }
        }
        return (res + Math.min(addA, addB) % MOD) % MOD;
    }
}
Python
class Solution:
    def nthMagicalNumber(self, n: int, a: int, b: int) -> int:
        MOD = 10**9 + 7
        l = min(a, b)
        r = n * l
        c = self.lcm(a, b)
        while l <= r:
            mid = (l + r) // 2
            cnt = mid // a + mid // b - mid // c
            if cnt >= n:
                r = mid - 1
            else:
                l = mid + 1
        return (r + 1) % MOD
    
    def lcm(self, a: int, b: int) -> int:
        return a * b // self.gcd(a, b)
    
    def gcd(self, a: int, b: int) -> int:
        return a if b == 0 else self.gcd(b, a % b)
class Solution:
    def nthMagicalNumber(self, n: int, a: int, b: int) -> int:
        MOD = 10**9 + 7
        c = self.lcm(a, b)
        m = c // a + c // b - 1
        r = n % m
        res = c * (n // m) % MOD
        if r == 0:
            return res
        addA, addB = a, b
        for _ in range(r - 1):
            if addA < addB:
                addA += a
            else:
                addB += b
        return (res + min(addA, addB)) % MOD
    
    def lcm(self, a: int, b: int) -> int:
        return a * b // self.gcd(a, b)
    
    def gcd(self, a: int, b: int) -> int:
        return a if b == 0 else self.gcd(b, a % b)
4、复杂度分析
  • 二分查找法:

    • 时间复杂度:O(log(n * min(a, b))),二分查找范围。
    • 空间复杂度:O(1),仅需常数空间。
  • 数学周期法:

    • 时间复杂度:O(1) 计算周期,O(r) 处理剩余部分(r ≤ m)。
    • 空间复杂度:O(1)。

Q4、盈利计划

1、题目描述

集团里有 n 名员工,他们可以完成各种各样的工作创造利润。

第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。如果成员参与了其中一项工作,就不能参与另一项工作。

工作的任何至少产生 minProfit 利润的子集称为 盈利计划 。并且工作的成员总数最多为 n 。

有多少种计划可以选择?因为答案很大,所以 返回结果模 10^9 + 7 的值。

示例 1:

输入:n = 5, minProfit = 3, group = [2,2], profit = [2,3]
输出:2
解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1 ,或仅完成工作 1 。
总的来说,有两种计划。

示例 2:

输入:n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8]
输出:7
解释:至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。
有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。

提示:

  • 1 <= n <= 100
  • 0 <= minProfit <= 100
  • 1 <= group.length <= 100
  • 1 <= group[i] <= 100
  • profit.length == group.length
  • 0 <= profit[i] <= 100
2、解题思路
  1. 动态规划(三维DP):
    • dp[i][j][k] 表示前 i 个工作,使用 j 名员工,至少产生 k 利润的方案数。
    • 初始化 dp[0][0][0] = 1,表示 0 个工作、0 员工、0 利润的方案数为 1。
    • 对于每个工作,可以选择做或不做,更新状态:
      • 不做:dp[i][j][k] = dp[i-1][j][k]
      • 做:dp[i][j][k] += dp[i-1][j-members][max(0, k-earn)]
    • 最后累加所有 dp[len][j][minProfit]。
  2. 动态规划(二维DP,空间优化):
    • 优化空间,使用二维数组 dp[j][k]。
    • 倒序遍历员工数和利润,避免重复计算。
3、代码实现
C++
// 方法1: 动态规划
class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        int len = group.size(), MOD = (int)1e9 + 7;
        // dp[i][j][k]: 前 i 个工作, j 名员工, 至少 k 利润的方案数
        vector<vector<vector<int>>> dp(
            len + 1, vector<vector<int>>(n + 1, vector<int>(minProfit + 1)));
        dp[0][0][0] = 1; // 初始状态
        for (int i = 1; i <= len; ++i) {
            int members = group[i - 1], earn = profit[i - 1];
            for (int j = 0; j <= n; ++j) {
                for (int k = 0; k <= minProfit; ++k) {
                    if (j < members) // 员工不足, 不能做当前工作
                    {
                        dp[i][j][k] = dp[i - 1][j][k];
                    } else // 可以做或不做
                    {
                        dp[i][j][k] = (dp[i - 1][j][k] + dp[i - 1][j - members][max(0, k - earn)]) % MOD;
                    }
                }
            }
        }
        int sum = 0;
        for (int j = 0; j <= n; ++j) // 累加所有员工数能达到的 minProfit 的方案数
        {
            sum = (sum + dp[len][j][minProfit]) % MOD;
        }
        return sum;
    }
};
// 方法2: 动态规划空间优化
class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        int MOD = (int)1e9 + 7;
        // dp[j][k]: j 名员工, 至少 k 利润的方案数
        vector<vector<int>> dp(n + 1, vector<int>(minProfit + 1));
        dp[0][0] = 1; // 初始状态
        for (int i = 0; i < group.size(); ++i) {
            int members = group[i], earn = profit[i];
            for (int j = n; j >= members; --j) {
                for (int k = minProfit; k >= 0; --k) // 倒序遍历利润
                {
                    // 做当前工作: 从 j-members 员工、max(0, k-earn) 利润转移
                    dp[j][k] = (dp[j][k] + dp[j - members][max(0, k - earn)]) % MOD;
                }
            }
        }
        int sum = 0;
        for (int j = 0; j <= n; ++j) // 累加所有员工数的方案
        {
            sum = (sum + dp[j][minProfit]) % MOD;
        }
        return sum;
    }
};
Java
// 方法1: 动态规划
class Solution {
    public int profitableSchemes(int n, int minProfit, int[] group, int[] profit) {
        int len = group.length, MOD = (int) 1e9 + 7;
        int[][][] dp = new int[len + 1][n + 1][minProfit + 1];
        dp[0][0][0] = 1; // 初始状态
        for (int i = 1; i <= len; i++) {
            int members = group[i - 1], earn = profit[i - 1];
            for (int j = 0; j <= n; j++) {
                for (int k = 0; k <= minProfit; k++) {
                    if (j < members) {
                        dp[i][j][k] = dp[i - 1][j][k];
                    } else {
                        dp[i][j][k] = (dp[i - 1][j][k] + dp[i - 1][j - members][Math.max(0, k - earn)]) % MOD;
                    }
                }
            }
        }
        int sum = 0;
        for (int j = 0; j <= n; j++) {
            sum = (sum + dp[len][j][minProfit]) % MOD;
        }
        return sum;
    }
}
// 方法2: 动态规划空间优化
class Solution {
    public int profitableSchemes(int n, int minProfit, int[] group, int[] profit) {
        int MOD = (int)1e9 + 7;
        int[][] dp = new int[n + 1][minProfit + 1];
        dp[0][0] = 1; // 初始状态
        for (int i = 0; i < group.length; i++) {
            int members = group[i], earn = profit[i];
            for (int j = n; j >= members; j--) {
                for (int k = minProfit; k >= 0; k--) {
                    dp[j][k] = (dp[j][k] + dp[j - members][Math.max(0, k - earn)]) % MOD;
                }
            }
        }
        int sum = 0;
        for (int j = 0; j <= n; j++) {
            sum = (sum + dp[j][minProfit]) % MOD;
        }
        return sum;
    }
}
Python
# 方法1: 动态规划
class Solution:
    def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
        MOD = 10**9 + 7
        length = len(group)
        # dp[i][j][k]: 前i个工作,j名员工,至少k利润的方案数
        dp = [[[0] * (minProfit + 1) for _ in range(n + 1)] for __ in range(length + 1)]
        dp[0][0][0] = 1  # 初始状态
        for i in range(1, length + 1):
            members, earn = group[i - 1], profit[i - 1]
            for j in range(n + 1):
                for k in range(minProfit + 1):
                    if j < members:
                        dp[i][j][k] = dp[i - 1][j][k]
                    else:
                        dp[i][j][k] = (dp[i - 1][j][k] + dp[i - 1][j - members][max(0, k - earn)]) % MOD
        return sum(dp[length][j][minProfit] for j in range(n + 1)) % MOD
# 方法2: 动态规划空间优化
class Solution:
    def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
        MOD = 10**9 + 7
        # dp[j][k]: j名员工,至少k利润的方案数
        dp = [[0] * (minProfit + 1) for _ in range(n + 1)]
        dp[0][0] = 1  # 初始状态
        for members, earn in zip(group, profit):
            for j in range(n, members - 1, -1):
                for k in range(minProfit, -1, -1):
                    dp[j][k] = (dp[j][k] + dp[j - members][max(0, k - earn)]) % MOD
        return sum(dp[j][minProfit] for j in range(n + 1)) % MOD
4、复杂度分析
  • 三维DP:
    • 时间复杂度:O(len * n * minProfit),三重循环。
    • 空间复杂度:O(len * n * minProfit),三维数组。
  • 二维DP:
    • 时间复杂度:O(len * n * minProfit),三重循环优化为两重。
    • 空间复杂度:O(n * minProfit),二维数组。



Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐