第 95 场周赛:链表的中间结点、石子游戏、第 N 个神奇数字、盈利计划
·
Q1、链表的中间结点
1、题目描述
给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
![]()
输入:head = [1,2,3,4,5] 输出:[3,4,5] 解释:链表只有一个中间结点,值为 3 。示例 2:
![]()
输入:head = [1,2,3,4,5,6] 输出:[4,5,6] 解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。提示:
- 链表的结点数范围是
[1, 100]1 <= Node.val <= 100
2、解题思路
- 数组存储法:将所有节点存储到数组中,然后直接访问中间节点。
- 两次遍历法:第一次遍历计算链表长度,第二次遍历到中间节点。
- 快慢指针法:使用两个指针,慢指针每次移动一步,快指针每次移动两步,当快指针到达末尾时,慢指针指向中间节点。
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 <= 500piles.length是 偶数1 <= piles[i] <= 500sum(piles[i])是 奇数
2、解题思路
-
动态规划法:
- 定义
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 能赢。
- 定义
-
空间优化动态规划法:
- 使用一维数组
dp替代二维数组,优化空间复杂度。 - 状态转移方程:
dp[j] = max(piles[i] - dp[j], piles[j] - dp[j-1])。
- 使用一维数组
-
数学法:
- Alice 作为先手总能选择最优策略,所以直接返回
true。
- Alice 作为先手总能选择最优策略,所以直接返回
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 <= 1092 <= a, b <= 4 * 104
2、解题思路
-
二分查找:
- 神奇数字的分布满足单调性,可以二分查找第 n 个神奇数字。
- 对于一个数 mid,计算有多少个数字 ≤ mid 能被 a 或 b 整除:
cnt = mid/a + mid/b - mid/lcm(a,b)。 - 根据 cnt 与 n 的关系调整搜索范围。
-
数学周期法:
- 神奇数字的分布具有周期性,周期为
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 <= 1000 <= minProfit <= 1001 <= group.length <= 1001 <= group[i] <= 100profit.length == group.length0 <= profit[i] <= 100
2、解题思路
- 动态规划(三维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]。
- 动态规划(二维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),二维数组。
- 时间复杂度:
更多推荐
所有评论(0)