代码随想录题单题解笔记(Java)· 下
本文是代码随想录笔记下篇(DP / 单调栈 / 图论 / 附录)。
接中篇。本篇覆盖动态规划、单调栈、图论(卡码网)以及 n 数之和模板。
目录
10. 动态规划 (Dynamic Programming)
核心思想: 基础 → 背包 → 打家劫舍 → 股票 → 子序列。
题目列表
动态规划基础
509. 斐波那契数 (Fibonacci Number) 简单 标签:递归, 记忆化搜索, 动态规划
70. 爬楼梯 (Climbing Stairs) 简单 标签:记忆化搜索, 数学, 动态规划
746. 使用最小花费爬楼梯 (Min Cost Climbing Stairs) 简单 标签:数组, 动态规划
62. 不同路径 (Unique Paths) 中等 标签:数学, 动态规划, 组合数学
63. 不同路径 II (Unique Paths II) 中等 标签:数组, 动态规划, 矩阵
343. 整数拆分 (Integer Break) 中等 标签:数学, 动态规划
96. 不同的二叉搜索树 (Unique Binary Search Trees) 中等 标签:树, 二叉搜索树, 数学, 动态规划, 二叉树
01 背包
416. 分割等和子集 (Partition Equal Subset Sum) 中等 标签:数组, 动态规划
1049. 最后一块石头的重量 II (Last Stone Weight II) 中等 标签:数组, 动态规划
494. 目标和 (Target Sum) 中等 标签:数组, 动态规划, 回溯
474. 一和零 (Ones and Zeroes) 中等 标签:数组, 字符串, 动态规划
完全背包
518. 零钱兑换 II (Coin Change II) 中等 标签:数组, 动态规划
377. 组合总和 Ⅳ (Combination Sum IV) 中等 标签:数组, 动态规划
322. 零钱兑换 (Coin Change) 中等 标签:广度优先搜索, 数组, 动态规划
279. 完全平方数 (Perfect Squares) 中等 标签:广度优先搜索, 数学, 动态规划
139. 单词拆分 (Word Break) 中等 标签:字典树, 记忆化搜索, 数组, 哈希表, 字符串, 动态规划
打家劫舍系列
198. 打家劫舍 (House Robber) 中等 标签:数组, 动态规划
213. 打家劫舍 II (House Robber II) 中等 标签:数组, 动态规划
337. 打家劫舍 III (House Robber III) 中等 标签:树, 深度优先搜索, 动态规划, 二叉树
股票系列
121. 买卖股票的最佳时机 (Best Time to Buy and Sell Stock) 简单 标签:数组, 动态规划
123. 买卖股票的最佳时机 III (Best Time to Buy and Sell Stock III) 困难 标签:数组, 动态规划
188. 买卖股票的最佳时机 IV (Best Time to Buy and Sell Stock IV) 困难 标签:数组, 动态规划
309. 最佳买卖股票时机含冷冻期 (Best Time to Buy and Sell Stock with Cooldown) 中等 标签:数组, 动态规划
714. 买卖股票的最佳时机含手续费 (Best Time to Buy and Sell Stock with Transaction Fee) 中等 标签:数组, 动态规划
子序列系列
300. 最长递增子序列 (Longest Increasing Subsequence) 中等 标签:数组, 二分查找, 动态规划
674. 最长连续递增序列 (Longest Continuous Increasing Subsequence) 简单 标签:数组
718. 最长重复子数组 (Maximum Length of Repeated Subarray) 中等 标签:数组, 二分查找, 动态规划, 滑动窗口, 哈希函数, 滚动哈希
1143. 最长公共子序列 (Longest Common Subsequence) 中等 标签:字符串, 动态规划
1035. 不相交的线 (Uncrossed Lines) 中等 标签:数组, 动态规划
392. 判断子序列 (Is Subsequence) 简单 标签:双指针, 字符串, 动态规划
115. 不同的子序列 (Distinct Subsequences) 困难 标签:字符串, 动态规划
583. 两个字符串的删除操作 (Delete Operation for Two Strings) 中等 标签:字符串, 动态规划
72. 编辑距离 (Edit Distance) 中等 标签:字符串, 动态规划
647. 回文子串 (Palindromic Substrings) 中等 标签:字符串, 动态规划
516. 最长回文子序列 (Longest Palindromic Subsequence) 中等 标签:字符串, 动态规划
509. 斐波那契数 (Fibonacci Number)
递归记忆化数学动态规划简单
题目
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
PLAINTEXT
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。
示例 1:
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
0 <= n <= 30
思路简述F(n) = F(n-1) + F(n-2)。用两个变量滚动前进,空间 O(1);勿写朴素递归(指数时间)。
参考代码
JAVA
class Solution {
public int fib(int n) {
if (n <= 1) {
return n;
}
int prev2 = 0; // F(i-2)
int prev1 = 1; // F(i-1)
for (int i = 2; i <= n; i++) {
int cur = prev1 + prev2;
prev2 = prev1;
prev1 = cur;
}
return prev1; // F(n)
}
}
复杂度
时间:O(n)
空间:O(1)
70. 爬楼梯 (Climbing Stairs)
记忆化数学动态规划简单
题目
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶
提示:
1 <= n <= 45
思路简述
到第 i 阶:dp[i] = dp[i-1] + dp[i-2](最后一步爬 1 或 2),与斐波那契同构。两变量滚动即可。
参考代码
JAVA
class Solution {
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
int prev1 = 1; // 到第 i-2 阶的方法数
int prev2 = 2; // 到第 i-1 阶的方法数
for (int i = 3; i <= n; i++) {
int cur = prev2 + prev1;
prev1 = prev2;
prev2 = cur;
}
return prev2;
}
}
复杂度
时间:O(n)
空间:O(1)
746. 使用最小花费爬楼梯 (Min Cost Climbing Stairs)
数组动态规划简单
题目
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。 - 支付 15 ,向上爬两个台阶,到达楼梯顶部。 总花费为 15 。
示例 2:
输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。 - 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。 - 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。 - 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。 - 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。 - 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。 - 支付 1 ,向上爬一个台阶,到达楼梯顶部。 总花费为 6 。
提示:
2 <= cost.length <= 10000 <= cost[i] <= 999
思路简述dp[i]:到达第 i 阶并支付 cost[i] 的最低花费。dp[i] = cost[i] + min(dp[i-1], dp[i-2])。楼顶在数组之外,答案为 min(dp[n-1], dp[n-2])。两变量滚动。
参考代码
JAVA
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int prev2 = cost[0]; // 到达 i-2 并付费的最低花费
int prev1 = cost[1]; // 到达 i-1 并付费的最低花费
for (int i = 2; i < n; i++) {
int cur = cost[i] + Math.min(prev2, prev1);
prev2 = prev1;
prev1 = cur;
}
// 可从最后一阶或倒数第二阶迈向楼顶,无需再付楼顶费用
return Math.min(prev2, prev1);
}
}
复杂度
时间:O(n)
空间:O(1)
62. 不同路径 (Unique Paths)
数学动态规划组合数学中等
题目
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:

输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 3. 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
提示:
1 <= m, n <= 100题目数据保证答案小于等于
2 109
思路简述
从左上到右下共需 m+n-2 步,其中恰 m-1 步向下(或 n-1 步向右),答案为组合数 C(m+n-2, m-1)。也可用 DP:dp[j] += dp[j-1] 滚动优化。
参考代码
JAVA
class Solution {
public int uniquePaths(int m, int n) {
// C(m+n-2, m-1),边乘边除保证整除、控制溢出
long ans = 1;
int a = m - 1;
int b = n - 1;
if (a > b) { // 取较小者为循环次数
int t = a;
a = b;
b = t;
}
for (int i = 1; i <= a; i++) {
ans = ans * (b + i) / i;
}
return (int) ans;
}
}
dp[j] 表示当前行到第 j 列的路径数:首行全 1,然后 dp[j] += dp[j-1]。时间 O(mn),空间 O(n)。无障碍时组合数更优。
复杂度
时间:O(min(m, n))
空间:O(1)
63. 不同路径 II (Unique Paths II)
数组动态规划矩阵中等
题目
给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角(即 grid[0][0])。机器人尝试移动到 右下角(即 grid[m - 1][n - 1])。机器人每次只能向下或者向右移动一步。
网格中的障碍物和空位置分别用 1 和 0 来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。
返回机器人能够到达右下角的不同路径数量。
测试用例保证答案小于等于 2 * 109。
示例 1:

输入:obstacleGrid =
[[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。 从左上角到右下角一共有 2 条不同的路径: 1. 向右 -> 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 -> 向右
示例 2:

输入:obstacleGrid =
[[0,1],[0,0]]
输出:1
提示:
m == obstacleGrid.lengthn == obstacleGrid[i].length1 <= m, n <= 100obstacleGrid[i][j]为0或1
思路简述
与 62 相同递推,遇障碍路径数为 0。一维滚动:dp[j] 表示到当前行第 j 列;障碍处置 0,否则 dp[j] += dp[j-1]。起点是障碍则直接返回 0。
参考代码
JAVA
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
if (obstacleGrid[0][0] == 1) {
return 0; // 起点被堵
}
int[] dp = new int[n]; // dp[j]:到当前行第 j 列的路径数
dp[0] = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (obstacleGrid[i][j] == 1) {
dp[j] = 0; // 障碍:不可达
continue;
}
if (j > 0) {
dp[j] += dp[j - 1]; // 从左边来;dp[j] 原值是从上边滚下来的
}
}
}
return dp[n - 1];
}
}
复杂度
时间:O(mn)
空间:O(n)
343. 整数拆分 (Integer Break)
数学动态规划中等
题目
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
示例 1:
输入:n = 2
输出:1
解释:2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入:n = 10
输出:36
解释:10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
提示:
2 <= n <= 58
思路简述
拆分因子均值靠近 e≈2.718 时乘积最大,实际用 3 最优、2 次之;避免出现 1(3+1 改成 2+2)。n≤3 特判(至少拆两段)。也可用 DP:dp[i] = max(j*(i-j), j*dp[i-j])。
参考代码
JAVA
class Solution {
public int integerBreak(int n) {
if (n <= 3) {
return n - 1; // 2→1+1;3→1+2
}
int ans = 1;
// 尽量拆出 3;剩下 4 时拆成 2+2(因为 3+1 更差)
while (n > 4) {
ans *= 3;
n -= 3;
}
ans *= n; // 剩余 2、3 或 4
return ans;
}
}
dp[i] 为拆分 i 的最大乘积;对每个 j∈[1,i) 取 max(j*(i-j), j*dp[i-j])。时间 O(n²),空间 O(n)。
复杂度
时间:O(n/3) ≈ O(n),亦可
Math.pow做到 O(1)空间:O(1)
96. 不同的二叉搜索树 (Unique Binary Search Trees)
树二叉搜索树数学动态规划二叉树中等
题目
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:

输入:n = 3
输出:5
示例 2:
输入:n = 1
输出:1
提示:
1 <= n <= 19
思路简述
以 i 为根时,左子树 1..i-1、右子树 i+1..n,种数相乘再对所有根求和:G(n) = Σ G(j-1)·G(n-j),即卡特兰数。可用 DP O(n²),或通项递推 O(n)。
参考代码
JAVA
class Solution {
public int numTrees(int n) {
// 卡特兰数:C_0=1,C_{i+1} = C_i * 2(2i+1)/(i+2)
long ans = 1;
for (int i = 0; i < n; i++) {
ans = ans * 2 * (2L * i + 1) / (i + 2);
}
return (int) ans;
}
}
JAVA
int[] dp = new int[n + 1]; // dp[i]:i 个结点的 BST 种数
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) { // j 为根
dp[i] += dp[j - 1] * dp[i - j]; // 左 j-1 个 × 右 i-j 个
}
}
return dp[n];
时间 O(n²),空间 O(n)。
复杂度
时间:O(n)
空间:O(1)
416. 分割等和子集 (Partition Equal Subset Sum)
数组动态规划中等
题目
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 2001 <= nums[i] <= 100
思路简述
总和为奇数则不可能。目标变为:能否选出子集使和为 sum/2。01 背包:dp[j] 表示能否凑出和 j;每个数只用一次,内层从大到小滚动。
参考代码
JAVA
class Solution {
public boolean canPartition(int[] nums) {
int total = 0;
for (int num : nums) {
total += num;
}
if (total % 2 != 0) {
return false; // 奇数无法平分
}
int target = total / 2;
boolean[] dp = new boolean[target + 1]; // dp[j]:能否凑出和 j
dp[0] = true;
for (int num : nums) {
// 倒序:保证每个 num 在本轮只被用一次
for (int j = target; j >= num; j--) {
dp[j] = dp[j] || dp[j - num];
}
}
return dp[target];
}
}
复杂度
时间:O(n·sum)
空间:O(sum)
1049. 最后一块石头的重量 II (Last Stone Weight II)
数组动态规划中等
题目
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果
x == y,那么两块石头都会被完全粉碎;如果
x != y,那么重量为x的石头将会完全粉碎,而重量为y的石头新重量为y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
示例 1:
输入:stones = [2,7,4,1,8,1]
输出:1
解释:组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1], 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1], 组合 2 和 1,得到 1,所以数组转化为 [1,1,1], 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
示例 2:
输入:stones = [31,26,33,21,40]
输出:5
提示:
1 <= stones.length <= 301 <= stones[i] <= 100
思路简述
石头对撞等价于把石头分成两堆,差的绝对值即剩余重量。要最小化 |正堆 - 负堆| = |sum - 2·子集和|,即 01 背包:在容量 sum/2 内尽量装满,取最接近的子集和。
参考代码
JAVA
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int stone : stones) {
sum += stone;
}
int target = sum / 2; // 一堆尽量靠近 sum/2
boolean[] dp = new boolean[target + 1]; // dp[j]:能否凑出重量 j
dp[0] = true;
for (int stone : stones) {
// 01 背包:倒序,每块石头只用一次
for (int j = target; j >= stone; j--) {
dp[j] = dp[j] || dp[j - stone];
}
}
// 找最大的可凑 j,剩余重量 = sum - 2j
for (int j = target; j >= 0; j--) {
if (dp[j]) {
int ans = sum - 2 * j;
return ans;
}
}
return 0;
}
}
复杂度
时间:O(n·sum)
空间:O(sum)
494. 目标和 (Target Sum)
数组动态规划回溯中等
题目
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,
nums = [2, 1],可以在2之前添加'+',在1之前添加'-',然后串联起来得到表达式"+2-1"。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1
输出:1
提示:
1 <= nums.length <= 200 <= nums[i] <= 10000 <= sum(nums[i]) <= 1000-1000 <= target <= 1000
思路简述
设正号集合和为 P、负号集合和为 N,则 P+N=sum,P-N=target ⇒ P=(sum+target)/2。化为:选若干数(每个最多一次)使和为 P 的 01 背包求方案数。需保证 sum+target 为偶数且 P≥0。
参考代码
JAVA
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num : nums) {
sum += num;
}
// P = (sum + target) / 2,无法整除或越界则无解
if ((sum + target) % 2 != 0 || Math.abs(target) > sum) {
return 0;
}
int pack = (sum + target) / 2;
int[] dp = new int[pack + 1]; // dp[j]:凑出 j 的方案数
dp[0] = 1;
for (int num : nums) {
// 01 背包倒序:每个数只用一次
for (int j = pack; j >= num; j--) {
dp[j] += dp[j - num];
}
}
int ans = dp[pack];
return ans;
}
}
复杂度
时间:O(n·sum)
空间:O(sum)
474. 一和零 (Ones and Zeroes)
数组字符串动态规划中等
题目
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
提示:
1 <= strs.length <= 6001 <= strs[i].length <= 100strs[i]仅由'0'和'1'组成1 <= m, n <= 100
思路简述
每个字符串最多用一次,代价是 (零的个数, 一的个数),价值为 1(选中个数)。二维 01 背包:dp[i][j] 为最多 i 个 0、j 个 1 时能选的最多字符串数;两维都 倒序。
参考代码
JAVA
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
// dp[i][j]:最多 i 个 0、j 个 1 时的最大子集长度
int[][] dp = new int[m + 1][n + 1];
for (String str : strs) {
int zeros = 0;
int ones = 0;
for (char c : str.toCharArray()) {
if (c == '0') {
zeros++;
} else {
ones++;
}
}
// 01 背包:两维均倒序,保证每个字符串只用一次
for (int i = m; i >= zeros; i--) {
for (int j = n; j >= ones; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1);
}
}
}
int ans = dp[m][n];
return ans;
}
}
复杂度
时间:O(len·m·n + Σ|str|),len 为字符串个数
空间:O(m·n)
518. 零钱兑换 II (Coin Change II)
数组动态规划中等
题目
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据 保证 最终 结果符合 32 位 带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1
示例 2:
输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。
示例 3:
输入:amount = 10, coins = [10]
输出:1
提示:
1 <= coins.length <= 3001 <= coins[i] <= 5000coins中的所有值 互不相同0 <= amount <= 5000
思路简述
完全背包求 组合数:dp[j] 为凑出金额 j 的方案数。dp[0]=1;外层物品、内层容量正序,保证「先币后额」只计组合、不计排列(与 377 外层额内层币求排列相对)。
参考代码
JAVA
class Solution {
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1]; // dp[j]:凑出 j 的组合数
dp[0] = 1; // 凑出 0 只有一种:什么都不选
// 外层硬币:每种币可重复用,且顺序无关 → 组合
for (int coin : coins) {
for (int j = coin; j <= amount; j++) {
dp[j] += dp[j - coin]; // 用当前币凑 j
}
}
int ans = dp[amount];
return ans;
}
}
复杂度
时间:O(n·amount)
空间:O(amount)
377. 组合总和 Ⅳ (Combination Sum IV)
数组动态规划中等
题目
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入:nums = [9], target = 3
输出:0
提示:
1 <= nums.length <= 2001 <= nums[i] <= 1000nums中的所有元素 互不相同1 <= target <= 1000进阶:如果给定的数组中含有负数会发生什么?问题会产生何种变化?如果允许负数出现,需要向题目中添加哪些限制条件?
思路简述
数字可重复用,且 顺序不同算不同方案((1,3) 与 (3,1) 都算)→ 完全背包求 排列数。必须 外层容量、内层物品:先凑出 i-num,再在末尾接上 num,从而把不同顺序都计进去。与 518 外层物品、内层容量(只计组合)相对。
参考代码
JAVA
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1]; // dp[i]:凑出 i 的排列数
dp[0] = 1; // 空排列
// 外层容量:保证考虑顺序
for (int i = 1; i <= target; i++) {
for (int num : nums) {
if (i >= num) {
dp[i] += dp[i - num]; // 末尾接上 num
}
}
}
int ans = dp[target];
return ans;
}
}
复杂度
时间:O(target·n)
空间:O(target)
322. 零钱兑换 (Coin Change)
广度优先搜索数组动态规划中等
题目
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
提示:
1 <= coins.length <= 121 <= coins[i] <= 231 - 10 <= amount <= 10^4
思路简述
完全背包最少件数:dp[j] 为凑出金额 j 的最少硬币数。每种币可重复用,内层正序;凑不出则保持大初值,最后返回 -1。
参考代码
JAVA
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1]; // dp[j]:凑出 j 的最少硬币数
Arrays.fill(dp, amount + 1); // 不可能达到的大值
dp[0] = 0;
for (int coin : coins) {
// 正序:同一硬币可多次使用
for (int j = coin; j <= amount; j++) {
dp[j] = Math.min(dp[j], dp[j - coin] + 1);
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
复杂度
时间:O(n·amount),n 为硬币种数
空间:O(amount)
279. 完全平方数 (Perfect Squares)
广度优先搜索数学动态规划中等
题目
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 10^4
思路简述
完全背包:物品为 1²,2²,…,求凑成 n 的最少个数。dp[i]:和为 i 的最少完全平方数个数;dp[i] = min(dp[i], dp[i-j*j]+1)。
参考代码
JAVA
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1]; // dp[i]:和为 i 的最少个数
Arrays.fill(dp, n + 1); // 足够大的初值
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j * j <= i; j++) {
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
}
四平方和定理可 O(√n) 判定答案只能是 1/2/3/4,不必 DP;背包章以 DP 为主即可。
复杂度
时间:O(n√n)
空间:O(n)
139. 单词拆分 (Word Break)
字典树记忆化数组哈希表字符串动态规划中等
题目
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入:s = "leetcode", wordDict = ["leet", "code"]
输出:true
解释:返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入:s = "applepenapple", wordDict = ["apple", "pen"]
输出:true
解释:返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。 注意,你可以重复使用字典中的单词。
示例 3:
输入:s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出:false
提示:
1 <= s.length <= 3001 <= wordDict.length <= 10001 <= wordDict[i].length <= 20s和wordDict[i]仅由小写英文字母组成wordDict中的所有字符串 互不相同
思路简述
把单词当物品、字符串前缀当容量:dp[i] 表示 s[0..i) 能否被拆出。单词可重复用,属 完全背包。转移:若某前缀 dp[j] 为真且 s[j..i) 在字典中,则 dp[i]=true。内层只扫不超过最长单词的长度。
参考代码
JAVA
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
Set<String> dict = new HashSet<>(wordDict); // 判断子串是否在字典中
int maxLen = 0;
for (String word : wordDict) {
maxLen = Math.max(maxLen, word.length());
}
boolean[] dp = new boolean[n + 1]; // dp[i]:前 i 个字符能否拆出
dp[0] = true; // 空串可拆
for (int i = 1; i <= n; i++) {
// 只枚举长度 ≤ 最长单词的切分点
int start = Math.max(0, i - maxLen);
for (int j = i - 1; j >= start; j--) {
if (dp[j] && dict.contains(s.substring(j, i))) {
dp[i] = true;
break; // 能拆即可,不必再试别的 j
}
}
}
boolean ans = dp[n];
return ans;
}
}
复杂度
时间:O(n·L·L),L 为最长单词长度(子串哈希),n ≤ 300、L ≤ 20
空间:O(n + Σ|word|)
198. 打家劫舍 (House Robber)
数组动态规划中等
题目
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
1 <= nums.length <= 1000 <= nums[i] <= 400
思路简述
线性 DP:对第 i 间房,选或不选——dp[i] = max(dp[i-1], dp[i-2] + nums[i])。用两个变量 滚动 即可:prev1 为到 i-2 的最优,prev2 为到 i-1 的最优。
参考代码
JAVA
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) {
return nums[0];
}
// prev1:偷到 i-2 的最高金额;prev2:偷到 i-1 的最高金额
int prev1 = nums[0];
int prev2 = Math.max(nums[0], nums[1]);
for (int i = 2; i < n; i++) {
// 不偷 i 用 prev2;偷 i 则加上 prev1
int curr = Math.max(prev2, prev1 + nums[i]);
prev1 = prev2;
prev2 = curr;
}
int ans = prev2;
return ans;
}
}
复杂度
时间:O(n)
空间:O(1)
213. 打家劫舍 II (House Robber II)
数组动态规划中等
题目
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3]
输出:3
提示:
1 <= nums.length <= 1000 <= nums[i] <= 1000
思路简述
首尾相邻,不能同时偷。拆成两段线性打家劫舍:只偷 [0..n-2] 或只偷 [1..n-1],取较大值。子问题仍用 198 滚动 DP。用奇偶长度特判末尾无法正确处理「首尾互斥」。
参考代码
JAVA
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) {
return nums[0];
}
// 不偷最后一家 / 不偷第一家,取更优
int ans = Math.max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
return ans;
}
// 在闭区间 [left, right] 上做线性打家劫舍
private int robRange(int[] nums, int left, int right) {
int prev1 = 0; // 到 i-2 的最优
int prev2 = 0; // 到 i-1 的最优
for (int i = left; i <= right; i++) {
int curr = Math.max(prev2, prev1 + nums[i]);
prev1 = prev2;
prev2 = curr;
}
return prev2;
}
}
复杂度
时间:O(n),两段各扫一遍
空间:O(1)
337. 打家劫舍 III (House Robber III)
树深度优先搜索动态规划二叉树中等
题目
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 1:

输入:root = [3,2,3,null,3,null,1]
输出:7
解释:小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:

输入:root = [3,4,5,1,3,null,1]
输出:9
解释:小偷一晚能够盗取的最高金额 4 + 5 = 9
提示:
树的节点数在
[1, 10^4]范围内0 <= Node.val <= 10^4
思路简述
树形打家劫舍:每个节点返回两态——偷 / 不偷。偷当前则左右只能取「不偷」;不偷当前则左右各自取两态较大值。后序 DFS 一次即可。
若把下标 1 定义成「子树整体最优」而非「必须偷当前」,递推也能过,但语义易混;建议固定为「偷 / 不偷」两态。
参考代码
JAVA
class Solution {
public int rob(TreeNode root) {
int[] res = dfs(root); // res[0] 不偷根;res[1] 偷根
int ans = Math.max(res[0], res[1]);
return ans;
}
// 返回 {不偷当前, 偷当前} 时该子树能得到的最大金额
private int[] dfs(TreeNode node) {
if (node == null) {
return new int[] { 0, 0 };
}
int[] left = dfs(node.left);
int[] right = dfs(node.right);
// 不偷当前:左右可选偷或不偷
int skip = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
// 偷当前:左右都不能偷
int take = node.val + left[0] + right[0];
return new int[] { skip, take };
}
}
复杂度
时间:O(n),每个节点访问一次
空间:O(h),递归栈深为树高
121. 买卖股票的最佳时机 (Best Time to Buy and Sell Stock)
数组动态规划简单
题目
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 10^50 <= prices[i] <= 10^4
思路简述
只允许买卖一次:从左到右维护历史 最低买入价,每天用「今日价 − 最低价」更新最大利润。
参考代码
JAVA
class Solution {
public int maxProfit(int[] prices) {
int minPrice = prices[0]; // 迄今最低买入价
int ans = 0;
for (int price : prices) {
if (price < minPrice) {
minPrice = price; // 更低的买入点
} else {
// 今天卖出相对历史最低的利润
ans = Math.max(ans, price - minPrice);
}
}
return ans;
}
}
复杂度
时间:O(n)
空间:O(1)
123. 买卖股票的最佳时机 III (Best Time to Buy and Sell Stock III)
数组动态规划困难
题目
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。 随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。 因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
示例 4:
输入:prices = [1]
输出:0
提示:
1 <= prices.length <= 10^50 <= prices[i] <= 10^5
思路简述
最多两笔交易,用四个变量 滚动:第一次持有 / 第一次空仓 / 第二次持有 / 第二次空仓。每天按「买 → 卖」更新,保证同一天不重复用同一笔现金。
参考代码
JAVA
class Solution {
public int maxProfit(int[] prices) {
// buy1/sell1:完成至多 1 笔;buy2/sell2:完成至多 2 笔
int buy1 = Integer.MIN_VALUE / 2; // 持有第 1 股时的最大利润(可为负)
int sell1 = 0;
int buy2 = Integer.MIN_VALUE / 2;
int sell2 = 0;
for (int price : prices) {
buy1 = Math.max(buy1, -price); // 第一次买入
sell1 = Math.max(sell1, buy1 + price); // 第一次卖出
buy2 = Math.max(buy2, sell1 - price); // 第二次买入(用 sell1 的现金)
sell2 = Math.max(sell2, buy2 + price); // 第二次卖出
}
int ans = sell2; // 至多两笔,最终一定不持仓更优
return ans;
}
}
复杂度
时间:O(n)
空间:O(1)
188. 买卖股票的最佳时机 IV (Best Time to Buy and Sell Stock IV)
数组动态规划困难
题目
给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
示例 2:
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。 随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
提示:
1 <= k <= 1001 <= prices.length <= 10000 <= prices[i] <= 1000
思路简述
123 的推广:buy[j] / sell[j] 表示至多完成第 j 笔交易时持仓 / 空仓的最大利润。k >= n/2 时交易次数实际上不受限,退化为 122 累加上涨段。
参考代码
JAVA
class Solution {
public int maxProfit(int k, int[] prices) {
int n = prices.length;
if (n == 0) {
return 0;
}
// 交易次数足够多:等价无限次买卖
if (k >= n / 2) {
int ans = 0;
for (int i = 1; i < n; i++) {
if (prices[i] > prices[i - 1]) {
ans += prices[i] - prices[i - 1];
}
}
return ans;
}
int[] buy = new int[k + 1]; // buy[j]:第 j 笔买入后持仓
int[] sell = new int[k + 1]; // sell[j]:第 j 笔卖出后空仓
Arrays.fill(buy, Integer.MIN_VALUE / 2);
for (int price : prices) {
for (int j = 1; j <= k; j++) {
buy[j] = Math.max(buy[j], sell[j - 1] - price);
sell[j] = Math.max(sell[j], buy[j] + price);
}
}
int ans = sell[k];
return ans;
}
}
复杂度
时间:O(n·k);k 很大时 O(n)
空间:O(k)
309. 最佳买卖股票时机含冷冻期 (Best Time to Buy and Sell Stock with Cooldown)
数组动态规划中等
题目
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:prices = [1,2,3,0,2]
输出:3
解释:对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
示例 2:
输入:prices = [1]
输出:0
提示:
1 <= prices.length <= 50000 <= prices[i] <= 1000
思路简述
无限次买卖,但卖出后次日冷冻。三态滚动:hold 持仓,sold 今日刚卖出(进入冷冻),rest 空仓且非冷冻(可买)。买入只能从 rest 转移。
参考代码
JAVA
class Solution {
public int maxProfit(int[] prices) {
int hold = Integer.MIN_VALUE / 2; // 持仓
int sold = 0; // 今日卖出 → 明日冷冻
int rest = 0; // 空仓且可买
for (int price : prices) {
int prevHold = hold;
int prevSold = sold;
int prevRest = rest;
hold = Math.max(prevHold, prevRest - price); // 继续持 / 今日买
sold = prevHold + price; // 今日卖出
rest = Math.max(prevRest, prevSold); // 继续空仓 / 冷冻结束
}
int ans = Math.max(sold, rest); // 结束时不应持仓
return ans;
}
}
复杂度
时间:O(n)
空间:O(1)
714. 买卖股票的最佳时机含手续费 (Best Time to Buy and Sell Stock with Transaction Fee)
贪心数组动态规划中等
题目
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例 1:
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润: 在此处买入 prices[0] = 1 在此处卖出 prices[3] = 8 在此处买入 prices[4] = 4 在此处卖出 prices[5] = 9 总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
示例 2:
输入:prices = [1,3,7,5,10,3], fee = 3
输出:6
提示:
1 <= prices.length <= 5 1041 <= prices[i] < 5 1040 <= fee < 5 104
思路简述
无限次买卖,每笔卖出扣手续费。两态:hold 持仓,cash 空仓。卖出时用 hold + price - fee;与 122 同骨架,只多一项 扣费。
参考代码
JAVA
class Solution {
public int maxProfit(int[] prices, int fee) {
int hold = Integer.MIN_VALUE / 2; // 持仓时的最大利润
int cash = 0; // 空仓时的最大利润
for (int price : prices) {
int prevHold = hold;
int prevCash = cash;
hold = Math.max(prevHold, prevCash - price); // 买入
cash = Math.max(prevCash, prevHold + price - fee); // 卖出并扣手续费
}
int ans = cash;
return ans;
}
}
复杂度
时间:O(n)
空间:O(1)
300. 最长递增子序列 (Longest Increasing Subsequence)
数组二分查找动态规划中等
题目
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500-10^4 <= nums[i] <= 10^4进阶:你能将算法的时间复杂度降低到
O(n log(n))吗?
思路简述
维护数组 tails:tails[len-1] = 所有长度为 len 的递增子序列中 结尾最小 的那个值(结尾越小,后面越容易接上)。扫每个 num,二分找第一个 >= num 的位置替换;若在末尾则最长长度 +1。
参考代码
JAVA
class Solution {
public int lengthOfLIS(int[] nums) {
// tails[k]:长度为 k+1 的递增子序列中,结尾的最小值
int[] tails = new int[nums.length];
int size = 0; // tails 当前有效长度 = 当前 LIS 长度
for (int num : nums) {
int left = 0;
int right = size;
// 下界:第一个 >= num 的下标
while (left < right) {
int mid = left + (right - left) / 2;
if (tails[mid] < num) {
left = mid + 1;
} else {
right = mid;
}
}
tails[left] = num; // 用更小的结尾替换,或接到末尾
if (left == size) {
size++; // 扩展了最长长度
}
}
int ans = size;
return ans;
}
}
dp[i] = 以 nums[i] 结尾的 LIS 长度;dp[i] = max(dp[j])+1(j<i 且 nums[j]<nums[i])。更直观,但进阶要求用上面的二分。
复杂度
时间:O(n log n)
空间:O(n)
674. 最长连续递增序列 (Longest Continuous Increasing Subsequence)
数组简单
题目
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。 尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
提示:
1 <= nums.length <= 10^4-109 <= nums[i] <= 10^9
思路简述
要求 连续 严格递增:双指针扫,遇非递增就结算当前段长度并重置左端;扫完再结算最后一段。与 300 不同,不能跳着选。
参考代码
JAVA
class Solution {
public int findLengthOfLCIS(int[] nums) {
int ans = 1;
int left = 0; // 当前连续递增段起点
for (int right = 1; right < nums.length; right++) {
if (nums[right] <= nums[right - 1]) {
// 断掉:结算 [left, right) 并开新段
ans = Math.max(ans, right - left);
left = right;
}
}
// 最后一段可能一直递增到末尾
ans = Math.max(ans, nums.length - left);
return ans;
}
}
复杂度
时间:O(n)
空间:O(1)
718. 最长重复子数组 (Maximum Length of Repeated Subarray)
数组二分查找动态规划滑动窗口哈希函数滚动哈希中等
题目
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
示例 2:
输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5
提示:
1 <= nums1.length, nums2.length <= 10000 <= nums1[i], nums2[i] <= 100
思路简述
求最长 公共子数组(必须连续)。dp[i][j] = 以 nums1[i-1]、nums2[j-1] 结尾 的公共子数组长度:相等则 dp[i-1][j-1]+1,否则 0。答案是所有 dp[i][j] 的最大值,不是 dp[m][n](那是 1143 子序列)。
参考代码
JAVA
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
// dp[i][j]:以 nums1[i-1]、nums2[j-1] 结尾的最长公共子数组长度
int[][] dp = new int[m + 1][n + 1];
int ans = 0;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
ans = Math.max(ans, dp[i][j]);
}
// 不等则保持 0:公共子数组在此处断开
}
}
return ans;
}
}
子数组连续:不等就断成 0。子序列可不连续:不等时取 max(dp[i-1][j], dp[i][j-1])。
复杂度
时间:O(m·n)
空间:O(m·n);可滚成一维 O(min(m,n))
1143. 最长公共子序列 (Longest Common Subsequence)
字符串动态规划中等
题目
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,
"ace"是"abcde"的子序列,但"aec"不是"abcde"的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
提示:
1 <= text1.length, text2.length <= 1000text1和text2仅由小写英文字符组成。
思路简述
二维 DP:dp[i][j] = text1[0..i) 与 text2[0..j) 的 LCS 长度。当前字符相等则走对角线 +1;否则取「丢掉 text1 末位」与「丢掉 text2 末位」的较大值。与 1035 不相交的线 转移完全相同。
参考代码
JAVA
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
// dp[i][j]:text1 前 i 个字符与 text2 前 j 个字符的 LCS 长度
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
// 两端字符可同时选入公共子序列
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
// 至少丢掉其中一个串的当前字符
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
int ans = dp[m][n];
return ans;
}
}
转移只依赖上一行与左侧,可滚成一维 dp[j],用临时变量保存「左上角」dp[i-1][j-1],额外空间 O(min(m,n))。
复杂度
时间:O(m·n)
空间:O(m·n)(一维优化后 O(min(m,n)))
1035. 不相交的线 (Uncrossed Lines)
数组动态规划中等
题目
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:
nums1[i] == nums2[j]且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
示例 1:

输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。 但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。
示例 2:
输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3
示例 3:
输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2
提示:
1 <= nums1.length, nums2.length <= 5001 <= nums1[i], nums2[j] <= 2000
思路简述
不相交连线 = 保持相对顺序的匹配 → 等价于求两数组的 最长公共子序列(1143)。转移相同:相等则对角线 +1,否则取上/左较大值。
参考代码
JAVA
class Solution {
public int maxUncrossedLines(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
// dp[i][j]:nums1[0..i) 与 nums2[0..j) 的 LCS 长度
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1; // 连一条线
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
int ans = dp[m][n];
return ans;
}
}
复杂度
时间:O(m·n)
空间:O(m·n)
392. 判断子序列 (Is Subsequence)
双指针字符串动态规划简单
题目
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1:
输入:s = "abc", t = "ahbgdc"
输出:true
示例 2:
输入:s = "axc", t = "ahbgdc"
输出:false
提示:
进阶:如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
致谢:
特别感谢 @pbrother 添加此问题并且创建所有测试用例。
0 <= s.length <= 1000 <= t.length <= 10^4两个字符串都只由小写字符组成。
思路简述
单次询问用双指针:在 t 上从左扫,依次匹配 s 的字符。进阶(固定 t、大量 s)则预处理 nxt[i][c] = 从位置 i 起字符 c 第一次出现的下标,每个 s 只需 O(|s|) 跳转。
参考代码
JAVA
class Solution {
public boolean isSubsequence(String s, String t) {
int i = 0; // s 的匹配进度
int j = 0; // t 的扫描下标
while (i < s.length() && j < t.length()) {
if (s.charAt(i) == t.charAt(j)) {
i++; // 匹配到 s 的下一字符
}
j++;
}
boolean ans = i == s.length(); // s 全部匹配完
return ans;
}
}
从后往前建 nxt[i][26]:nxt[i][c] 表示下标 ≥ i 时字符 c 最早出现位置(没有则为 n)。对每个 s 从 i=-1 起反复 i = nxt[i+1][c],遇到 n 则失败。预处理 O(|t|·26),单次查询 O(|s|)。
JAVA
// 进阶写法(与上题双指针二选一)
int n = t.length();
int[][] nxt = new int[n + 1][26];
Arrays.fill(nxt[n], n);
for (int i = n - 1; i >= 0; i--) {
nxt[i] = nxt[i + 1].clone();
nxt[i][t.charAt(i) - 'a'] = i;
}
int i = -1;
for (char c : s.toCharArray()) {
i = nxt[i + 1][c - 'a'];
if (i == n) {
return false;
}
}
return true;
复杂度
时间:双指针 O(|s|+|t|);进阶预处理 O(|t|·Σ),单次查询 O(|s|),Σ=26
空间:双指针 O(1);进阶 O(|t|·Σ)
115. 不同的子序列 (Distinct Subsequences)
字符串动态规划困难
题目
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。
测试用例保证结果在 32 位有符号整数范围内。
示例 1:
输入:s = "rabbbit", t = "rabbit"
输出:3
解释:如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。 rabbbit rabbbit rabbbit
示例 2:
输入:s = "babgbag", t = "bag"
输出:5
解释:如下所示, 有 5 种可以从 s 中得到 "bag" 的方案。 babgbag babgbag babgbag babgbag babgbag
提示:
1 <= s.length, t.length <= 1000s和t由英文字母组成
思路简述dp[i][j] = 用 s 前 i 个字符构成 t 前 j 个字符的 方案数。dp[i][0]=1(空串一种)。相等时:用当前字符匹配 dp[i-1][j-1] + 不用当前字符 dp[i-1][j];不等则只能不用。
参考代码
JAVA
class Solution {
public int numDistinct(String s, String t) {
int m = s.length();
int n = t.length();
// dp[i][j]:s[0..i) 的子序列中等于 t[0..j) 的个数
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
dp[i][0] = 1; // 构成空串只有 1 种:全删
}
for (int j = 1; j <= n; j++) {
// i 从 j 起:s 长度不足 j 时不可能构成 t 前 j 个
for (int i = j; i <= m; i++) {
if (s.charAt(i - 1) == t.charAt(j - 1)) {
// 用 s[i-1] 匹配 t[j-1] + 不用 s[i-1]
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j]; // 只能跳过 s[i-1]
}
}
}
int ans = dp[m][n];
return ans;
}
}
复杂度
时间:O(m·n)
空间:O(m·n)
583. 两个字符串的删除操作 (Delete Operation for Two Strings)
字符串动态规划中等
题目
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入:word1 = "sea", word2 = "eat"
输出:2
解释:第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
示例 2:
输入:word1 = "leetcode", word2 = "etco"
输出:4
提示:
1 <= word1.length, word2.length <= 500word1和word2只包含小写英文字母
思路简述
只允许删除:要使两串相同,应保留 最长公共子序列,其余全删。步数 = m + n - 2·LCS。LCS 转移与 1143 相同。
参考代码
JAVA
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// dp[i][j]:word1[0..i) 与 word2[0..j) 的 LCS 长度
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
int ans = m + n - 2 * dp[m][n]; // 两边非 LCS 字符都删掉
return ans;
}
}
72 可插/删/换;本题只能删。也可用编辑距离思路:dp[i][j] 为最少删除次数,相等继承,不等取 min(删 word1, 删 word2)+1。
复杂度
时间:O(m·n)
空间:O(m·n)
72. 编辑距离 (Edit Distance)
字符串动态规划中等
题目
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u')
提示:
0 <= word1.length, word2.length <= 500word1和word2由小写英文字母组成
思路简述dp[i][j] = 把 word1 前 i 个字符改成 word2 前 j 个字符的最少操作。相等则继承对角线;否则在 删 / 插 / 换 三种里取 min 再 +1。边界:dp[i][0]=i(全删),dp[0][j]=j(全插)。
参考代码
JAVA
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// dp[i][j]:word1[0..i) → word2[0..j) 的最少操作数
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
dp[i][0] = i; // 删掉 word1 前 i 个字符
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j; // 插入 word2 前 j 个字符
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1]; // 无需操作
} else {
// 删 word1[i-1] / 在 word1 插入 word2[j-1] / 替换
dp[i][j] = Math.min(
Math.min(dp[i - 1][j], dp[i][j - 1]),
dp[i - 1][j - 1]) + 1;
}
}
}
int ans = dp[m][n];
return ans;
}
}
复杂度
时间:O(m·n)
空间:O(m·n)
647. 回文子串 (Palindromic Substrings)
双指针字符串动态规划中等
题目
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
示例 1:
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
提示:
1 <= s.length <= 1000s由小写英文字母组成
思路简述
枚举回文中心并向两边扩展:奇数中心 (i,i)、偶数中心 (i,i+1),能扩一次就多一个回文子串。也可用区间 DP:dp[i][j] 表示 s[i..j] 是否回文。
参考代码
JAVA
class Solution {
public int countSubstrings(String s) {
int ans = 0;
int n = s.length();
for (int i = 0; i < n; i++) {
ans += expand(s, i, i); // 奇数长度:中心 s[i]
ans += expand(s, i, i + 1); // 偶数长度:中心在 s[i] 与 s[i+1] 之间
}
return ans;
}
// 从 [left, right] 向两边扩展,返回以该中心得到的回文个数
private int expand(String s, int left, int right) {
int count = 0;
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
count++;
left--;
right++;
}
return count;
}
}
dp[i][j] = s[i..j] 是否回文。转移:两端字符相同,且(长度 ≤ 2 或 dp[i+1][j-1])。按长度从小到大填,为 true 则 ans++。时间 O(n²),空间 O(n²)。
JAVA
class Solution {
public int countSubstrings(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n]; // dp[i][j]:s[i..j] 是否回文
int ans = 0;
for (int i = n - 1; i >= 0; i--) { // i 倒序:先算更短的内侧
for (int j = i; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) {
// 长度 1~2 直接回文;更长则看去掉两端后是否回文
if (j - i <= 1 || dp[i + 1][j - 1]) {
dp[i][j] = true;
ans++;
}
}
}
}
return ans;
}
}
枚举所有 [i,j] 再双指针判回文,正确但 O(n³);优先中心扩展或上面的 DP。
复杂度
时间:O(n²)(中心扩展 / DP 同阶)
空间:中心扩展 O(1);DP O(n²)
516. 最长回文子序列 (Longest Palindromic Subsequence)
字符串动态规划中等
题目
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
提示:
1 <= s.length <= 1000s仅由小写英文字母组成
思路简述
区间 DP:dp[l][r] = s[l..r] 的最长回文子序列长度。两端相等则 dp[l+1][r-1]+2;否则取丢掉左端或右端的较大值。按区间长度(或右端点增大)填表。
参考代码
JAVA
class Solution {
public int longestPalindromeSubseq(String s) {
int n = s.length();
// dp[l][r]:s[l..r] 的最长回文子序列长度
int[][] dp = new int[n][n];
for (int i = 0; i < n; i++) {
dp[i][i] = 1; // 单字符
}
// 右端点 r 增大;左端点 l 从 r 往左扫,保证依赖已算
for (int r = 1; r < n; r++) {
for (int l = r - 1; l >= 0; l--) {
if (s.charAt(l) == s.charAt(r)) {
// 长度 2:中间为空,结果为 2;更长则夹住内侧
dp[l][r] = (r - l == 1) ? 2 : dp[l + 1][r - 1] + 2;
} else {
dp[l][r] = Math.max(dp[l + 1][r], dp[l][r - 1]);
}
}
}
int ans = dp[0][n - 1];
return ans;
}
}
647 问「子串是否回文」(连续);本题问「子序列最长回文」(可删字符)。转移形似,一个是 bool,一个是长度。
复杂度
时间:O(n²)
空间:O(n²)
11. 单调栈 (Monotonic Stack)
核心思想: 下一个更大/更小元素、接雨水与柱状图。
题目列表
739. 每日温度 (Daily Temperatures) 中等 标签:栈, 数组, 单调栈
496. 下一个更大元素 I (Next Greater Element I) 简单 标签:栈, 数组, 哈希表, 单调栈
503. 下一个更大元素 II (Next Greater Element II) 中等 标签:栈, 数组, 单调栈
42. 接雨水 (Trapping Rain Water) 困难 标签:栈, 数组, 双指针, 动态规划, 单调栈
84. 柱状图中最大的矩形 (Largest Rectangle in Histogram) 困难 标签:栈, 数组, 单调栈
739. 每日温度 (Daily Temperatures)
栈数组单调栈中等
题目
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
示例 1:
输入:temperatures = [73,74,75,71,69,72,76,73]
输出:[1,1,4,2,1,1,0,0]
示例 2:
输入:temperatures = [30,40,50,60]
输出:[1,1,1,0]
示例 3:
输入:temperatures = [30,60,90]
输出:[1,1,0]
提示:
1 <= temperatures.length <= 10^530 <= temperatures[i] <= 100
思路简述
求每个位置右侧第一个更大温度的距离 → 单调递减栈(存下标)。当前温度高于栈顶,则栈顶的「下一个更高」就是今天,弹出并填 i - j;否则入栈等待。
参考代码
JAVA
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] ans = new int[n]; // 默认 0:后面没有更高
Deque<Integer> stack = new ArrayDeque<>(); // 存下标,对应温度单调不增
for (int i = 0; i < n; i++) {
// 今天比栈顶更热:结算栈顶那天
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int j = stack.pop();
ans[j] = i - j; // 要等的天数
}
stack.push(i); // 今天尚未找到更热天,入栈
}
return ans;
}
}
复杂度
时间:O(n),每个下标最多进栈出栈一次
空间:O(n)
496. 下一个更大元素 I (Next Greater Element I)
栈数组哈希表单调栈简单
题目nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。
给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中nums1 是 nums2 的子集。
对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1 。
返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。
示例 1:
输入:nums1 = [4,1,2], nums2 = [1,3,4,2].
输出:[-1,3,-1]
解释:nums1 中每个值的下一个更大元素如下所述: - 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。 - 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。 - 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
示例 2:
输入:nums1 = [2,4], nums2 = [1,2,3,4].
输出:[3,-1]
解释:nums1 中每个值的下一个更大元素如下所述: - 2 ,用加粗斜体标识,nums2 = [1,2,3,4]。下一个更大元素是 3 。 - 4 ,用加粗斜体标识,nums2 = [1,2,3,4]。不存在下一个更大元素,所以答案是 -1 。
提示:
1 <= nums1.length <= nums2.length <= 10000 <= nums1[i], nums2[i] <= 10^4nums1和nums2中所有整数 互不相同nums1中的所有整数同样出现在nums2中进阶:你可以设计一个时间复杂度为
O(nums1.length + nums2.length)的解决方案吗?
思路简述
先在 nums2 上用 单调递减栈 求出每个数右侧第一个更大值,存进哈希表;再按 nums1 查表,没有则为 -1。元素互不相同,可用值当键。
参考代码
JAVA
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Map<Integer, Integer> nextGreater = new HashMap<>(); // 值 → 右侧下一个更大
Deque<Integer> stack = new ArrayDeque<>(); // 存 nums2 的值,单调不增
for (int num : nums2) {
while (!stack.isEmpty() && stack.peek() < num) {
nextGreater.put(stack.pop(), num); // 栈顶的下一个更大就是 num
}
stack.push(num);
}
int[] ans = new int[nums1.length];
for (int i = 0; i < nums1.length; i++) {
ans[i] = nextGreater.getOrDefault(nums1[i], -1);
}
return ans;
}
}
复杂度
时间:O(n1 + n2)
空间:O(n2)
503. 下一个更大元素 II (Next Greater Element II)
栈数组单调栈中等
题目
给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。
数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。
示例 1:
输入:nums = [1,2,1]
输出:[2,-1,2]
解释:第一个 1 的下一个更大的数是 2; 数字 2 找不到下一个更大的数; 第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
示例 2:
输入:nums = [1,2,3,4,3]
输出:[2,3,4,-1,4]
提示:
1 <= nums.length <= 10^4-109 <= nums[i] <= 10^9
思路简述
循环数组:把下标空间「拉直」扫两遍(i = 0 .. 2n-1,取 i % n)。仍用 递减栈 存下标;第二遍只用来给第一遍未结算的元素找环绕后的更大值,不必再入栈。
参考代码
JAVA
class Solution {
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
Arrays.fill(ans, -1); // 默认没有下一个更大
Deque<Integer> stack = new ArrayDeque<>(); // 存下标,对应值单调不增
for (int i = 0; i < 2 * n; i++) {
int idx = i % n;
while (!stack.isEmpty() && nums[stack.peek()] < nums[idx]) {
ans[stack.pop()] = nums[idx];
}
// 第一圈入栈等待;第二圈只负责结算,避免重复压入同一下标
if (i < n) {
stack.push(idx);
}
}
return ans;
}
}
复杂度
时间:O(n)
空间:O(n)
42. 接雨水 (Trapping Rain Water)
栈数组双指针动态规划单调栈困难
题目
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
提示:
n == height.length1 <= n <= 2 1040 <= height[i] <= 10^5
思路简述
每个位置接水量 = min(左最高, 右最高) - height[i]。双指针从两端向中间:较矮一侧先结算(另一侧必有不低于它的墙),维护 maxLeft / maxRight,一步步累加。
参考代码
JAVA
class Solution {
public int trap(int[] height) {
int left = 0;
int right = height.length - 1;
int maxLeft = 0; // 已扫过的左侧最高
int maxRight = 0; // 已扫过的右侧最高
int ans = 0;
while (left < right) {
// 较矮一侧决定水位上界
if (height[left] <= height[right]) {
maxLeft = Math.max(maxLeft, height[left]);
ans += maxLeft - height[left];
left++;
} else {
maxRight = Math.max(maxRight, height[right]);
ans += maxRight - height[right];
right--;
}
}
return ans;
}
}
递增栈存下标;遇更高柱弹出栈顶作坑底,左右墙取 min(当前, 新栈顶),面积按宽度累加。时间同为 O(n),空间 O(n)。
复杂度
时间:O(n)
空间:O(1)
84. 柱状图中最大的矩形 (Largest Rectangle in Histogram)
栈数组单调栈困难
题目
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例 1:

输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10
示例 2:

输入:heights = [2,4]
输出:4
提示:
1 <= heights.length <=1050 <= heights[i] <= 10^4
思路简述
对每根柱,面积 = 高度 ×(右侧第一个更矮 − 左侧第一个更矮 − 1)。用 单调递增栈 存下标;左右加高度 0 的哨兵,避免空栈与扫尾未结算。
参考代码
JAVA
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
// 左右哨兵高度 0:保证栈底安全,并在结尾强制弹空
int[] h = new int[n + 2];
System.arraycopy(heights, 0, h, 1, n);
Deque<Integer> stack = new ArrayDeque<>();
stack.push(0); // 左哨兵下标
int ans = 0;
for (int i = 1; i < n + 2; i++) {
// 当前更矮:栈顶柱的右边界就是 i
while (h[i] < h[stack.peek()]) {
int height = h[stack.pop()];
// 弹出后栈顶 = 左侧第一个更矮(或等矮)柱
int width = i - stack.peek() - 1;
ans = Math.max(ans, height * width);
}
stack.push(i);
}
return ans;
}
}
复杂度
时间:O(n),每个下标进栈出栈各一次
空间:O(n)
12. 图论 (Graph) · 卡码网
核心思想: 深搜广搜、并查集、拓扑、最小生成树与最短路;题目在 卡码网,适合练 ACM 输入输出。
题目列表
98. 所有可达路径 (All Reachable Paths) 中等 标签:图, 深度优先搜索, ACM
99. 岛屿数量 (Number of Islands) 中等 标签:图, 深度优先搜索, 广度优先搜索, ACM
100. 岛屿的最大面积 (Max Area of Island) 中等 标签:图, 深度优先搜索, 广度优先搜索, ACM
101. 孤岛的总面积 (Total Area of Isolated Islands) 中等 标签:图, 深度优先搜索, 广度优先搜索, ACM
102. 沉没孤岛 (Sink Isolated Islands) 中等 标签:图, 深度优先搜索, 广度优先搜索, ACM
103. 水流问题 (Pacific Atlantic Water Flow) 中等 标签:图, 深度优先搜索, 广度优先搜索, ACM
104. 建造最大岛屿 (Making A Large Island) 困难 标签:图, 深度优先搜索, 并查集, ACM
106. 岛屿的周长 (Island Perimeter) 简单 标签:图, 数组, 矩阵, ACM
110. 字符串接龙 (Word Ladder) 困难 标签:图, 广度优先搜索, 字符串, ACM
105. 有向图的完全可达性 (Complete Reachability in Directed Graph) 中等 标签:图, 深度优先搜索, 广度优先搜索, ACM
107. 寻找存在的路径 (Find Existing Path) 中等 标签:图, 并查集, ACM
108. 冗余连接 (Redundant Connection) 中等 标签:图, 深度优先搜索, 并查集, ACM
109. 冗余连接 II (Redundant Connection II) 困难 标签:图, 深度优先搜索, 并查集, ACM
53. 寻宝 (Treasure Hunt) 中等 标签:图, 最小生成树, ACM
117. 软件构建 (Software Build) 中等 标签:图, 拓扑排序, ACM
47. 参加科学大会 (Attend Science Conference) 中等 标签:图, 最短路, ACM
94. 城市间货物运输 I (City Freight Transport I) 中等 标签:图, 最短路, ACM
95. 城市间货物运输 II (City Freight Transport II) 中等 标签:图, 最短路, ACM
96. 城市间货物运输 III (City Freight Transport III) 中等 标签:图, 最短路, ACM
97. 小明逛公园 (Xiaoming Visits the Park) 困难 标签:图, 最短路, ACM
126. 骑士的攻击 (Knight Attack) 困难 标签:图, 最短路, 启发式搜索, ACM
98. 所有可达路径 (All Reachable Paths)
图深度优先搜索ACM中等
题目
给定一个有 n 个节点的有向无环图,节点编号从 1 到 n。请编写一个函数,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。输入描述:第一行包含两个整数 N,M,表示图中拥有 N 个节点,M 条边后续 M 行,每行包含两个整数 s 和 t,表示图中的 s 节点与 t 节点中有一条路径。输出描述:输出所有的可达路径,路径中所有节点之间空格隔开,每条路径独占一行,存在多条路径,路径输出的顺序可任意。如果不存在任何一条路径,则输出 -1。注意输出的序列中,最后一个节点后面没有空格! 例如正确的答案是 1 3 5,而不是 1 3 5, 5后面没有空格!
示例 1:
输入:
5 5
1 3
3 5
1 2
2 4
4 5
输出:
1 3 5
1 2 4 5
提示:
用例解释:有五个节点,其中的从 1 到达 5 的路径有两个,分别是 1 -> 3 -> 5 和 1 -> 2 -> 4 -> 5。因为拥有多条路径,所以输出结果为:1 3 5 1 2 4 5或1 2 4 5 1 3 5 都算正确。数据范围:
思路简述
有向无环图,求 1 到 n 的全部路径:邻接表存图,从 1 出发 DFS + 回溯。走到 n 就收录当前路径;DAG 无环,不必 vis。无路径则输出 -1。路径末节点后不能有空格。
参考代码
JAVA
import java.util.*;
public class Main {
static List<List<Integer>> graph;
static List<Integer> path;
static List<List<Integer>> ans; // 所有 1→n 路径
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
graph = new ArrayList<>();
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}
for (int i = 0; i < m; i++) {
int s = in.nextInt();
int t = in.nextInt();
graph.get(s).add(t); // 有向边 s → t
}
path = new ArrayList<>();
ans = new ArrayList<>();
path.add(1);
dfs(1, n);
if (ans.isEmpty()) {
System.out.println(-1);
} else {
for (List<Integer> p : ans) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < p.size(); i++) {
if (i > 0) {
sb.append(' ');
}
sb.append(p.get(i));
}
System.out.println(sb); // 行末无空格
}
}
in.close();
}
static void dfs(int cur, int n) {
if (cur == n) { // 到达终点,记录一条完整路径
ans.add(new ArrayList<>(path));
return;
}
for (int nxt : graph.get(cur)) {
path.add(nxt);
dfs(nxt, n);
path.remove(path.size() - 1); // 回溯
}
}
}
复杂度
时间:O(路径条数 × 路径长度),最坏与搜索树规模同阶;N≤100、M≤500
空间:O(N + M + 答案)
99. 岛屿数量 (Number of Islands)
图深度优先搜索广度优先搜索ACM中等
题目
给定一个由 1(陆地)和 0(水)组成的矩阵,你需要计算岛屿的数量。岛屿由水平方向或垂直方向上相邻的陆地连接而成,并且四周都是水域。你可以假设矩阵外均被水包围。输入描述:第一行包含两个整数 N, M,表示矩阵的行数和列数。后续 N 行,每行包含 M 个数字,数字为 1 或者 0。输出描述:输出一个整数,表示岛屿的数量。如果不存在岛屿,则输出 0。
示例 1:
输入:
4 5
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1
输出:
3
提示:
根据测试案例中所展示,岛屿数量共有 3 个,所以输出 3。数据范围:1 <= N, M <= 50
思路简述
网格连通块计数:遇到未访问陆地就 DFS/BFS 淹没 整座岛(改成 0 或 vis),岛屿数 +1。四连通。N、M≤50,扫一遍即最优。
参考代码
JAVA
import java.util.*;
public class Main {
static int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
static int n, m;
static int[][] grid;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
grid = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
grid[i][j] = in.nextInt();
}
}
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) { // 新岛屿
dfs(i, j);
ans++;
}
}
}
System.out.println(ans);
in.close();
}
// 把 (i,j) 所在岛屿全部淹成 0,避免重复计数
static void dfs(int i, int j) {
grid[i][j] = 0;
for (int[] d : dirs) {
int x = i + d[0];
int y = j + d[1];
if (x >= 0 && x < n && y >= 0 && y < m && grid[x][y] == 1) {
dfs(x, y);
}
}
}
}
复杂度
时间:O(n·m),每格进出一次
空间:O(n·m),最坏递归栈(也可用 BFS 队列)
100. 岛屿的最大面积 (Max Area of Island)
图深度优先搜索广度优先搜索ACM中等
题目
给定一个由 1(陆地)和 0(水)组成的矩阵,计算岛屿的最大面积。岛屿面积的计算方式为组成岛屿的陆地的总数。岛屿由水平方向或垂直方向上相邻的陆地连接而成,并且四周都是水域。你可以假设矩阵外均被水包围。输入描述:第一行包含两个整数 N, M,表示矩阵的行数和列数。后续 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。输出描述:输出一个整数,表示岛屿的最大面积。如果不存在岛屿,则输出 0。
示例 1:
输入:
4 5
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1
输出:
4
提示:
样例输入中,岛屿的最大面积为 4。数据范围:1 <= M, N <= 50。
思路简述
与 99 同一套淹没:DFS 返回当前连通块格子数,全局取 最大面积。无岛则 0。
参考代码
JAVA
import java.util.*;
public class Main {
static int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
static int n, m;
static int[][] grid;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
grid = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
grid[i][j] = in.nextInt();
}
}
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
ans = Math.max(ans, dfs(i, j));
}
}
}
System.out.println(ans);
in.close();
}
// 返回从 (i,j) 出发淹没的陆地格数
static int dfs(int i, int j) {
grid[i][j] = 0;
int area = 1;
for (int[] d : dirs) {
int x = i + d[0];
int y = j + d[1];
if (x >= 0 && x < n && y >= 0 && y < m && grid[x][y] == 1) {
area += dfs(x, y);
}
}
return area;
}
}
复杂度
时间:O(n·m)
空间:O(n·m),递归栈
101. 孤岛的总面积 (Total Area of Isolated Islands)
图深度优先搜索广度优先搜索ACM中等
题目
给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被陆地单元格包围。孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。现在你需要计算所有孤岛的总面积,岛屿面积的计算方式为组成岛屿的陆地的总数。输入描述:第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0。输出描述:输出一个整数,表示所有孤岛的总面积,如果不存在孤岛,则输出 0。
示例 1:
输入:
4 5
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1
输出:
1
提示:
在矩阵中心部分的岛屿,因为没有任何一个单元格接触到矩阵边缘,所以该岛屿属于孤岛,总面积为 1。数据范围:1 <= M, N <= 50。
思路简述
孤岛 = 不贴边的陆地连通块。先从四条边界上的陆地 DFS,把「能走到海边」的岛全部淹没;剩下的 1 就是孤岛,再扫一遍求和。
参考代码
JAVA
import java.util.*;
public class Main {
static int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
static int n, m;
static int[][] grid;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
grid = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
grid[i][j] = in.nextInt();
}
}
// 从四条边界把「非孤岛」淹没成 0
for (int i = 0; i < n; i++) {
if (grid[i][0] == 1) {
dfs(i, 0);
}
if (grid[i][m - 1] == 1) {
dfs(i, m - 1);
}
}
for (int j = 0; j < m; j++) {
if (grid[0][j] == 1) {
dfs(0, j);
}
if (grid[n - 1][j] == 1) {
dfs(n - 1, j);
}
}
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
ans++;
}
}
}
System.out.println(ans);
in.close();
}
static void dfs(int i, int j) {
grid[i][j] = 0;
for (int[] d : dirs) {
int x = i + d[0];
int y = j + d[1];
if (x >= 0 && x < n && y >= 0 && y < m && grid[x][y] == 1) {
dfs(x, y);
}
}
}
}
复杂度
时间:O(n·m)
空间:O(n·m)
102. 沉没孤岛 (Sink Isolated Islands)
图深度优先搜索广度优先搜索ACM中等
题目
给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被水域单元格包围。孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。现在你需要将所有孤岛“沉没”,即将孤岛中的所有陆地单元格(1)转变为水域单元格(0)。输入描述:第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。输出描述:输出将孤岛“沉没”之后的岛屿矩阵。
注意:每个元素后面都有一个空格。
示例 1:
输入:
4 5
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1
输出:
1 1 0 0 0
1 1 0 0 0
0 0 0 0 0
0 0 0 1 1
提示:
将孤岛沉没。数据范围:1 <= M, N <= 50。
思路简述
先把贴边的岛标记成 2(或先淹没再还原):从边界 DFS,能走到边的陆地改成 2;再把剩余 1 改成 0(沉没孤岛),2 改回 1。输出时 每个数字后都带空格。
参考代码
JAVA
import java.util.*;
public class Main {
static int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
static int n, m;
static int[][] grid;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
grid = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
grid[i][j] = in.nextInt();
}
}
// 边界陆地标记为 2:这些岛不能沉
for (int i = 0; i < n; i++) {
if (grid[i][0] == 1) {
dfs(i, 0);
}
if (grid[i][m - 1] == 1) {
dfs(i, m - 1);
}
}
for (int j = 0; j < m; j++) {
if (grid[0][j] == 1) {
dfs(0, j);
}
if (grid[n - 1][j] == 1) {
dfs(n - 1, j);
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
grid[i][j] = 0; // 孤岛沉没
} else if (grid[i][j] == 2) {
grid[i][j] = 1; // 靠边岛还原
}
}
}
for (int i = 0; i < n; i++) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < m; j++) {
sb.append(grid[i][j]).append(' '); // 题面要求每个元素后都有空格
}
System.out.println(sb);
}
in.close();
}
static void dfs(int i, int j) {
grid[i][j] = 2;
for (int[] d : dirs) {
int x = i + d[0];
int y = j + d[1];
if (x >= 0 && x < n && y >= 0 && y < m && grid[x][y] == 1) {
dfs(x, y);
}
}
}
}
复杂度
时间:O(n·m)
空间:O(n·m)
103. 水流问题 (Pacific Atlantic Water Flow)
图深度优先搜索广度优先搜索ACM中等
题目
现有一个 N × M 的矩阵,每个单元格包含一个数值,这个数值代表该位置的相对高度。矩阵的左边界和上边界被认为是第一组边界,而矩阵的右边界和下边界被视为第二组边界。矩阵模拟了一个地形,当雨水落在上面时,水会根据地形的倾斜向低处流动,但只能从较高或等高的地点流向较低或等高并且相邻(上下左右方向)的地点。我们的目标是确定那些单元格,从这些单元格出发的水可以达到第一组边界和第二组边界。输入描述:第一行包含两个整数 N 和 M,分别表示矩阵的行数和列数。后续 N 行,每行包含 M 个整数,表示矩阵中的每个单元格的高度。输出描述:输出共有多行,每行输出两个整数,用一个空格隔开,表示可达第一组边界和第二组边界的单元格的坐标,输出顺序任意。
示例 1:
输入:
5 5
1 3 1 2 4
1 2 1 3 2
2 4 7 2 1
4 5 6 1 1
1 4 1 2 1
输出:
0 4
1 3
2 2
3 0
3 1
3 2
4 0
4 1
提示:
图中的蓝色方块上的雨水既能流向第一组边界,也能流向第二组边界。所以最终答案为所有蓝色方块的坐标。数据范围:1 <= M, N <= 100。
思路简述
水只能从高/等高流向低/等高。正向枚举每个格子会爆。改为 从边界反向搜:左+上为第一组边界,右+下为第二组;反向只能走到高度 ≥ 当前的格子。两组都能到的格子即答案,坐标 0-index,顺序任意。
参考代码
JAVA
import java.util.*;
public class Main {
static int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
static int n, m;
static int[][] height;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
height = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
height[i][j] = in.nextInt();
}
}
boolean[][] first = new boolean[n][m]; // 能流到第一组边界(左、上)
boolean[][] second = new boolean[n][m]; // 能流到第二组边界(右、下)
for (int i = 0; i < n; i++) {
dfs(i, 0, first);
dfs(i, m - 1, second);
}
for (int j = 0; j < m; j++) {
dfs(0, j, first);
dfs(n - 1, j, second);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (first[i][j] && second[i][j]) {
System.out.println(i + " " + j);
}
}
}
in.close();
}
// 反向:从边界往「高度不低于当前」的格子走
static void dfs(int i, int j, boolean[][] vis) {
vis[i][j] = true;
for (int[] d : dirs) {
int x = i + d[0];
int y = j + d[1];
if (x < 0 || x >= n || y < 0 || y >= m || vis[x][y]) {
continue;
}
if (height[x][y] < height[i][j]) { // 反向不能走到更低
continue;
}
dfs(x, y, vis);
}
}
}
复杂度
时间:O(n·m)
空间:O(n·m)
104. 建造最大岛屿 (Making A Large Island)
图深度优先搜索并查集ACM困难
题目
给定一个由 1(陆地)和 0(水)组成的矩阵,你最多可以将矩阵中的一格水变为一块陆地,在执行了此操作之后,矩阵中最大的岛屿面积是多少。岛屿面积的计算方式为组成岛屿的陆地的总数。岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设矩阵外均被水包围。输入描述:第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。输出描述:输出一个整数,表示最大的岛屿面积。
示例 1:
输入:
4 5
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1
输出:
6
提示:
对于上面的案例,有两个位置可将 0 变成 1,使得岛屿的面积最大,即 6。数据范围:1 <= M, N <= 50。
思路简述
最多把一格 0 改成 1。先给每座岛编号并记下面积;再枚举每个 0,把四周不同岛的面积相加 +1。还要考虑「不填水」时已有的最大岛。关键是 邻岛去重(同一岛可能贴着这个 0 的两边)。
参考代码
JAVA
import java.util.*;
public class Main {
static int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
static int n, m;
static int[][] grid;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
grid = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
grid[i][j] = in.nextInt();
}
}
// islandId 从 2 起,避免和 0/1 冲突;area[id] 为该岛面积
int[] area = new int[n * m + 2];
int id = 2;
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
area[id] = dfs(i, j, id);
ans = Math.max(ans, area[id]); // 不填水时的最大岛
id++;
}
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] != 0) {
continue;
}
Set<Integer> neigh = new HashSet<>(); // 同一岛只加一次
for (int[] d : dirs) {
int x = i + d[0];
int y = j + d[1];
if (x >= 0 && x < n && y >= 0 && y < m && grid[x][y] > 1) {
neigh.add(grid[x][y]);
}
}
int sum = 1; // 当前这格水变陆地
for (int island : neigh) {
sum += area[island];
}
ans = Math.max(ans, sum);
}
}
System.out.println(ans);
in.close();
}
static int dfs(int i, int j, int id) {
grid[i][j] = id;
int cnt = 1;
for (int[] d : dirs) {
int x = i + d[0];
int y = j + d[1];
if (x >= 0 && x < n && y >= 0 && y < m && grid[x][y] == 1) {
cnt += dfs(x, y, id);
}
}
return cnt;
}
}
复杂度
时间:O(n·m)
空间:O(n·m)
106. 岛屿的周长 (Island Perimeter)
图数组矩阵ACM简单
题目
给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设矩阵外均被水包围。在矩阵中恰好拥有一个岛屿,假设组成岛屿的陆地边长都为 1,请计算海岸线,即:岛屿的周长。岛屿内部没有水域。输入描述:第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。输出描述:输出一个整数,表示岛屿的周长。
示例 1:
输入:
5 5
0 0 0 0 0
0 1 0 1 0
0 1 1 1 0
0 1 1 1 0
0 0 0 0 0
输出:
14
提示:
岛屿的周长为 14。数据范围:1 <= M, N <= 50。
思路简述
恰好一座岛且内部无湖:每块陆地贡献 4 条边,每对相邻陆地共用边要减 2。扫格子统计陆地数与「右/下」相邻对数即可,==周长 = 4×陆地 − 2×相邻==。
参考代码
JAVA
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
int[][] grid = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
grid[i][j] = in.nextInt();
}
}
int land = 0;
int adj = 0; // 水平或垂直相邻的陆地对数
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 0) {
continue;
}
land++;
if (j + 1 < m && grid[i][j + 1] == 1) {
adj++;
}
if (i + 1 < n && grid[i + 1][j] == 1) {
adj++;
}
}
}
int ans = 4 * land - 2 * adj;
System.out.println(ans);
in.close();
}
}
复杂度
时间:O(n·m)
空间:O(n·m)(读入网格;若边读边算可为 O(m))
110. 字符串接龙 (Word Ladder)
图广度优先搜索字符串ACM困难
题目
字典 strList 中从字符串 beginStr 和 endStr 的转换序列是一个按下述规格形成的序列:1. 序列中第一个字符串是 beginStr。2. 序列中最后一个字符串是 endStr。3. 每次转换只能改变一个字符。4. 转换过程中的中间字符串必须是字典 strList 中的字符串,且strList里的每个字符串只用使用一次。给你两个字符串 beginStr 和 endStr 和一个字典 strList,找到从 beginStr 到 endStr 的最短转换序列中的字符串数目。如果不存在这样的转换序列,返回 0。输入描述:第一行包含一个整数 N,表示字典 strList 中的字符串数量。
第二行包含两个字符串,用空格隔开,分别代表 beginStr 和 endStr。
后续 N 行,每行一个字符串,代表 strList 中的字符串。输出描述:输出一个整数,代表从 beginStr 转换到 endStr 需要的最短转换序列中的字符串数量。如果不存在这样的转换序列,则输出 0。
示例 1:
输入:
6
abc def
efc
dbc
ebc
dec
dfc
yhn
输出:
4
提示:
从 startStr 到 endStr,在 strList 中最短的路径为 abc -> dbc -> dec -> def,所以输出结果为 4,如图:数据范围:2 <= N <= 500
思路简述
单词差一位为一条边,求 begin→end 最短转换序列长度(含两端)。无权图用 BFS;字典放进 HashSet,出队单词从集合删除保证每词只用一次。每次枚举改一个字母,比两两比单词更快。
参考代码
JAVA
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
String beginStr = in.next();
String endStr = in.next();
Set<String> dict = new HashSet<>();
for (int i = 0; i < n; i++) {
dict.add(in.next());
}
if (beginStr.equals(endStr)) {
System.out.println(1);
in.close();
return;
}
Queue<String> q = new ArrayDeque<>();
q.offer(beginStr);
dict.remove(beginStr); // 起点已在队列,避免回头
int ans = 1; // 序列长度,含 beginStr
while (!q.isEmpty()) {
int size = q.size();
ans++;
for (int k = 0; k < size; k++) {
String cur = q.poll();
char[] arr = cur.toCharArray();
for (int i = 0; i < arr.length; i++) {
char old = arr[i];
for (char c = 'a'; c <= 'z'; c++) {
if (c == old) {
continue;
}
arr[i] = c;
String nxt = new String(arr);
// 终点可以不在字典里(样例 endStr=def 就不在 strList 中)
if (nxt.equals(endStr)) {
System.out.println(ans);
in.close();
return;
}
if (!dict.contains(nxt)) {
continue;
}
q.offer(nxt);
dict.remove(nxt); // 每个词只用一次
}
arr[i] = old;
}
}
}
System.out.println(0);
in.close();
}
}
复杂度
时间:O(N · L · 26),L 为单词长度
空间:O(N · L)
105. 有向图的完全可达性 (Complete Reachability in Directed Graph)
图深度优先搜索广度优先搜索ACM中等
题目
给定一个有向图,包含 N 个节点,节点编号分别为 1,2,...,N。现从 1 号节点开始,如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。输入描述:第一行包含两个正整数,表示节点数量 N 和边的数量 K。
后续 K 行,每行两个正整数 s 和 t,表示从 s 节点有一条边单向连接到 t 节点。输出描述:如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。
示例 1:
输入:
4 4
1 2
2 1
1 3
2 4
输出:
1
提示:
从 1 号节点可以到达任意节点,输出 1。数据范围:1 <= N <= 100; 1 <= K <= 2000。
思路简述
从 1 出发 DFS/BFS,能访问到的点数等于 N 则输出 1,否则 -1。这是「染色/遍历」而非「找路径」,不用回溯(与 98 对比)。
参考代码
JAVA
import java.util.*;
public class Main {
static List<List<Integer>> graph;
static boolean[] vis;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int k = in.nextInt();
graph = new ArrayList<>();
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}
for (int i = 0; i < k; i++) {
int s = in.nextInt();
int t = in.nextInt();
graph.get(s).add(t);
}
vis = new boolean[n + 1];
dfs(1);
int cnt = 0;
for (int i = 1; i <= n; i++) {
if (vis[i]) {
cnt++;
}
}
int ans = (cnt == n) ? 1 : -1;
System.out.println(ans);
in.close();
}
static void dfs(int u) {
vis[u] = true;
for (int v : graph.get(u)) {
if (!vis[v]) {
dfs(v);
}
}
}
}
复杂度
时间:O(N + K)
空间:O(N + K)
107. 寻找存在的路径 (Find Existing Path)
图并查集ACM中等
题目
给定一个包含 n 个节点的无向图中,节点编号从 1 到 n (含 1 和 n )。你的任务是判断是否有一条从节点 source 出发到节点 destination 的路径存在。输入描述:第一行包含两个正整数 N 和 M,N 代表节点的个数,M 代表边的个数。后续 M 行,每行两个正整数 s 和 t,代表从节点 s 与节点 t 之间有一条边。最后一行包含两个正整数,代表起始节点 source 和目标节点 destination。输出描述:输出一个整数,代表是否存在从节点 source 到节点 destination 的路径。如果存在,输出 1;否则,输出 0。
示例 1:
输入:
5 4
1 2
1 3
2 4
3 4
1 4
输出:
1
提示:
数据范围:1 <= M, N <= 100。
思路简述
无向图连通性:边加入 并查集,最后判断 source 与 destination 是否同一根。路径存在当且仅当同一连通分量。
参考代码
JAVA
import java.util.*;
public class Main {
static int[] parent;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
parent = new int[n + 1];
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
for (int i = 0; i < m; i++) {
int s = in.nextInt();
int t = in.nextInt();
union(s, t);
}
int source = in.nextInt();
int destination = in.nextInt();
int ans = (find(source) == find(destination)) ? 1 : 0;
System.out.println(ans);
in.close();
}
static int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
static void union(int a, int b) {
int ra = find(a);
int rb = find(b);
if (ra != rb) {
parent[rb] = ra;
}
}
}
复杂度
时间:O(M · α(N))
空间:O(N)
108. 冗余连接 (Redundant Connection)
图深度优先搜索并查集ACM中等
题目
有一个图,它是一棵树,他是拥有 n 个节点(节点编号1到n)和 n - 1 条边的连通无环无向图,例如如图:现在在这棵树上的基础上,添加一条边(依然是n个节点,但有n条边),使这个图变成了有环图,如图:先请你找出冗余边,删除后,使该图可以重新变成一棵树。输入描述:第一行包含一个整数 N,表示图的节点个数和边的个数。后续 N 行,每行包含两个整数 s 和 t,表示图中 s 和 t 之间有一条边。输出描述:输出一条可以删除的边。如果有多个答案,请删除标准输入中最后出现的那条边。
示例 1:
输入:
3
1 2
2 3
1 3
输出:
1 3
提示:
图中的 1 2,2 3,1 3 等三条边在删除后都能使原图变为一棵合法的树。但是 1 3 由于是标准输出里最后出现的那条边,所以输出结果为 1 3数据范围:1 <= N <= 1000.
思路简述
树加一条边成环。按输入顺序把边丢进并查集:若两端已连通,这条边就是成环边。多解时要删 输入中最后出现 的那条,按序处理自然得到它。
参考代码
JAVA
import java.util.*;
public class Main {
static int[] parent;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
parent = new int[n + 1];
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
int ansS = 0;
int ansT = 0;
for (int i = 0; i < n; i++) {
int s = in.nextInt();
int t = in.nextInt();
if (find(s) == find(t)) { // 已连通再加边 → 成环
ansS = s;
ansT = t;
} else {
union(s, t);
}
}
System.out.println(ansS + " " + ansT);
in.close();
}
static int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
static void union(int a, int b) {
int ra = find(a);
int rb = find(b);
if (ra != rb) {
parent[rb] = ra;
}
}
}
复杂度
时间:O(N · α(N))
空间:O(N)
109. 冗余连接 II (Redundant Connection II)
图深度优先搜索并查集ACM困难
题目
有一种有向树,该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。有向树拥有 n 个节点和 n - 1 条边。如图:现在有一个有向图,有向图是在有向树中的两个没有直接链接的节点中间添加一条有向边。如图:输入一个有向图,该图由一个有着 n 个节点(节点编号 从 1 到 n),n 条边,请返回一条可以删除的边,使得删除该条边之后该有向图可以被当作一颗有向树。输入描述:第一行输入一个整数 N,表示有向图中节点和边的个数。后续 N 行,每行输入两个整数 s 和 t,代表这是 s 节点连接并指向 t 节点的单向边。输出描述:输出一条可以删除的边,若有多条边可以删除,请输出标准输入中最后出现的一条边。
示例 1:
输入:
3
1 2
1 3
2 3
输出:
2 3
提示:
在删除 2 3 后有向图可以变为一棵合法的有向树,所以输出 2 3数据范围:1 <= N <= 1000.
思路简述
有向树 + 一条多余有向边。分三种:① 某点入度 2 且无环,删指向它的、更靠后的边;② 入度 2 且有环,必须删那条会留环的边;③ 没有入度 2,则一定有向环,删 构成环的最后一条边。用并查集判断删边后是否仍成环。
参考代码
JAVA
import java.util.*;
public class Main {
static int n;
static int[] parent;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n = in.nextInt();
int[][] edges = new int[n][2];
int[] inDegree = new int[n + 1];
for (int i = 0; i < n; i++) {
edges[i][0] = in.nextInt();
edges[i][1] = in.nextInt();
inDegree[edges[i][1]]++;
}
// 指向入度为 2 的点的边,倒序收集(优先删靠后的)
List<Integer> cand = new ArrayList<>();
for (int i = n - 1; i >= 0; i--) {
if (inDegree[edges[i][1]] == 2) {
cand.add(i);
}
}
if (!cand.isEmpty()) {
// 先试删更靠后的 cand.get(0)
if (isTreeAfterRemove(edges, cand.get(0))) {
System.out.println(edges[cand.get(0)][0] + " " + edges[cand.get(0)][1]);
} else {
System.out.println(edges[cand.get(1)][0] + " " + edges[cand.get(1)][1]);
}
} else {
// 无入度 2:删构成有向环的那条边
removeCycleEdge(edges);
}
in.close();
}
static void init() {
parent = new int[n + 1];
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
}
static int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
static boolean same(int a, int b) {
return find(a) == find(b);
}
static void union(int a, int b) {
int ra = find(a);
int rb = find(b);
if (ra != rb) {
parent[rb] = ra;
}
}
// 删掉下标 deleteIdx 的边后,剩余边是否无环(有向树)
static boolean isTreeAfterRemove(int[][] edges, int deleteIdx) {
init();
for (int i = 0; i < n; i++) {
if (i == deleteIdx) {
continue;
}
if (same(edges[i][0], edges[i][1])) {
return false; // 仍成环
}
union(edges[i][0], edges[i][1]);
}
return true;
}
static void removeCycleEdge(int[][] edges) {
init();
for (int i = 0; i < n; i++) {
if (same(edges[i][0], edges[i][1])) {
System.out.println(edges[i][0] + " " + edges[i][1]);
return;
}
union(edges[i][0], edges[i][1]);
}
}
}
复杂度
时间:O(N · α(N))
空间:O(N)
53. 寻宝 (Treasure Hunt)
图最小生成树ACM中等
题目
在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。不同岛屿之间,路途距离不同,国王希望你可以规划建公路的方案,如何可以以最短的总公路距离将 所有岛屿联通起来(注意:这是一个无向图)。给定一张地图,其中包括了所有的岛屿,以及它们之间的距离。以最小化公路建设长度,确保可以链接到所有岛屿。输入描述:第一行包含两个整数V 和 E,V代表顶点数,E代表边数 。顶点编号是从1到V。例如:V=2,一个有两个顶点,分别是1和2。接下来共有 E 行,每行三个整数 v1,v2 和 val,v1 和 v2 为边的起点和终点,val代表边的权值。输出描述:输出联通所有岛屿的最小路径总距离。
示例 1:
输入:
7 11
1 2 1
1 3 1
1 5 2
2 6 1
2 4 2
2 3 2
3 4 1
4 5 1
5 6 2
5 7 1
6 7 1
输出:
6
提示:
数据范围: 2 <= V <= 10000; 1 <= E <= 100000; 0 <= val <= 10000;如下图,可见将所有的顶点都访问一遍,总距离最低是6.
思路简述
连通所有岛且总权最小 → 最小生成树。稀疏图(V≤1e4,E≤1e5)用 Kruskal:边按权排序,并查集加边直到 V−1 条。数据量大,用 BufferedReader。
参考代码
JAVA
import java.io.*;
import java.util.*;
public class Main {
static int[] parent;
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int v = Integer.parseInt(st.nextToken());
int e = Integer.parseInt(st.nextToken());
int[][] edges = new int[e][3]; // [u, v, w]
for (int i = 0; i < e; i++) {
st = new StringTokenizer(br.readLine());
edges[i][0] = Integer.parseInt(st.nextToken());
edges[i][1] = Integer.parseInt(st.nextToken());
edges[i][2] = Integer.parseInt(st.nextToken());
}
Arrays.sort(edges, (a, b) -> Integer.compare(a[2], b[2]));
parent = new int[v + 1];
for (int i = 1; i <= v; i++) {
parent[i] = i;
}
int ans = 0;
int used = 0;
for (int[] edge : edges) {
int a = find(edge[0]);
int b = find(edge[1]);
if (a == b) {
continue;
}
parent[b] = a;
ans += edge[2];
used++;
if (used == v - 1) {
break;
}
}
System.out.println(ans);
}
static int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
}
稠密图更适合 Prim。本题边相对少,Kruskal 更合适。
复杂度
时间:O(E log E)
空间:O(V + E)
117. 软件构建 (Software Build)
图拓扑排序ACM中等
题目
某个大型软件项目的构建系统拥有 N 个文件,文件编号从 0 到 N - 1,在这些文件中,某些文件依赖于其他文件的内容,这意味着如果文件 A 依赖于文件 B,则必须在处理文件 A 之前处理文件 B (0 <= A, B <= N - 1)。请编写一个算法,用于确定文件处理的顺序。输入描述:第一行输入两个正整数 N, M。表示 N 个文件之间拥有 M 条依赖关系。后续 M 行,每行两个正整数 S 和 T,表示 T 文件依赖于 S 文件。输出描述:输出共一行,如果能处理成功,则输出文件顺序,用空格隔开。如果不能成功处理(相互依赖),则输出 -1。
示例 1:
输入:
5 4
0 1
0 2
1 3
2 4
输出:
0 1 2 3 4
提示:
文件依赖关系如下:所以,文件处理的顺序除了示例中的顺序,还存在0 2 4 1 30 2 1 3 4等等合法的顺序。数据范围:0 <= N <= 10 51 <= M <= 10 9每行末尾无空格。
思路简述
文件依赖是有向图:S→T 表示先处理 S 再处理 T。求一种合法顺序 = 拓扑排序(Kahn):不断取入度为 0 的点,删出边。结果个数 < N 说明有环,输出 -1。顺序不唯一。N 可达 1e5,用邻接表 + 快读。
参考代码
JAVA
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) {
graph.add(new ArrayList<>());
}
int[] inDegree = new int[n];
for (int i = 0; i < m; i++) {
st = new StringTokenizer(br.readLine());
int s = Integer.parseInt(st.nextToken());
int t = Integer.parseInt(st.nextToken());
graph.get(s).add(t); // 先 S 后 T
inDegree[t]++;
}
Queue<Integer> q = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) {
q.offer(i);
}
}
List<Integer> order = new ArrayList<>();
while (!q.isEmpty()) {
int cur = q.poll();
order.add(cur);
for (int nxt : graph.get(cur)) {
inDegree[nxt]--;
if (inDegree[nxt] == 0) {
q.offer(nxt);
}
}
}
if (order.size() != n) {
System.out.println(-1);
} else {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
if (i > 0) {
sb.append(' ');
}
sb.append(order.get(i));
}
System.out.println(sb); // 行末无空格
}
}
}
题面写 M <= 1e9 无法按边存图;实际按读入的 M 条边建邻接表。有环则 order.size() < n。
复杂度
时间:O(N + M)
空间:O(N + M)
47. 参加科学大会 (Attend Science Conference)
图最短路ACM中等
题目
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。小明的起点是第一个车站,终点是最后一个车站。然而,途中的各个车站之间的道路状况、交通拥堵程度以及可能的自然因素(如天气变化)等不同,这些因素都会影响每条路径的通行时间。小明希望能选择一条花费时间最少的路线,以确保他能够尽快到达目的地。输入描述:第一行包含两个正整数,第一个正整数 N 表示一共有 N 个公共汽车站,第二个正整数 M 表示有 M 条公路。接下来为 M 行,每行包括三个整数,S、E 和 V,代表了从 S 车站可以单向直达 E 车站,并且需要花费 V 单位的时间。输出描述:输出一个整数,代表小明从起点到终点所花费的最小时间。
示例 1:
输入:
7 9
1 2 1
1 3 4
2 3 2
2 4 5
3 4 2
4 5 3
2 6 4
5 7 4
6 7 9
输出:
12
提示:
能够到达的情况:如下图所示, 起始车站为 1 号车站,终点车站为 7 号车站, 绿色路线为最短的路线,路线总长度为 12,则输出 12。不能到达的情况:如下图所示,当从起始车站不能到达终点车站时,则输出 -1。数据范围:1 <= N <= 500; 1 <= M <= 5000;
思路简述
车站 1 到 N 的最短时间,边权为正 → Dijkstra。堆优化 O((N+M) log N);不可达输出 -1。
参考代码
JAVA
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
List<List<int[]>> graph = new ArrayList<>();
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}
for (int i = 0; i < m; i++) {
int s = in.nextInt();
int e = in.nextInt();
int v = in.nextInt();
graph.get(s).add(new int[]{e, v}); // 有向边 s→e,耗时 v
}
final int INF = 0x3f3f3f3f;
int[] dist = new int[n + 1];
Arrays.fill(dist, INF);
dist[1] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
pq.offer(new int[]{0, 1}); // {当前距离, 节点}
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int d = cur[0];
int u = cur[1];
if (d > dist[u]) {
continue; // 过期状态
}
for (int[] edge : graph.get(u)) {
int nxt = edge[0];
int w = edge[1];
if (dist[nxt] > dist[u] + w) {
dist[nxt] = dist[u] + w;
pq.offer(new int[]{dist[nxt], nxt});
}
}
}
int ans = dist[n] == INF ? -1 : dist[n];
System.out.println(ans);
in.close();
}
}
复杂度
时间:O((N + M) log N)
空间:O(N + M)
94. 城市间货物运输 I (City Freight Transport I)
图最短路ACM中等
题目
某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。网络中的道路都有各自的运输成本和政府补贴, 道路的权值计算方式为:运输成本 - 政府补贴 。权值为正表示扣除了政府补贴后运输货物仍需支付的费用;权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。请找出从城市 1 到城市 n 的所有可能路径中,综合政府补贴后的最低运输成本。如果最低运输成本是一个负数,它表示在遵循最优路径的情况下,运输过程中反而能够实现盈利。城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。输入描述:第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v (单向图)。输出描述:如果能够从城市 1 到连通到城市 n, 请输出一个整数,表示运输成本。如果该整数是负数,则表示实现了盈利。如果从城市 1 没有路径可达城市 n,请输出 "unconnected"。
示例 1:
输入:
6 7
5 6 -2
1 2 1
5 3 1
2 5 2
2 4 -3
4 6 4
1 3 5
输出:
1
提示:
示例中最佳路径是从 1 -> 2 -> 5 -> 6,路上的权值分别为 1 2 -2,最终的最低运输成本为 1 + 2 + (-2) = 1。示例 2:4 2 1 2 -1 3 4 -1在此示例中,无法找到一条路径从 1 通往 4,所以此时应该输出 "unconnected"。数据范围:1 <= n <= 1000; 1 <= m <= 10000;-100 <= v <= 100;
思路简述
有向图、边权可负,但保证无负环:不能用普通 Dijkstra。用 Bellman-Ford / SPFA 求 1 到 n 最短路;不可达输出 unconnected。
参考代码
JAVA
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
int[][] edges = new int[m][3];
for (int i = 0; i < m; i++) {
edges[i][0] = in.nextInt();
edges[i][1] = in.nextInt();
edges[i][2] = in.nextInt();
}
final int INF = 0x3f3f3f3f;
int[] dist = new int[n + 1];
Arrays.fill(dist, INF);
dist[1] = 0;
// n-1 轮松弛;无负环时最短路最多 n-1 条边
for (int i = 1; i <= n - 1; i++) {
boolean updated = false;
for (int[] e : edges) {
int s = e[0];
int t = e[1];
int v = e[2];
if (dist[s] == INF) {
continue; // 尚未从 1 到达 s
}
if (dist[t] > dist[s] + v) {
dist[t] = dist[s] + v;
updated = true;
}
}
if (!updated) {
break;
}
}
if (dist[n] == INF) {
System.out.println("unconnected");
} else {
int ans = dist[n];
System.out.println(ans);
}
in.close();
}
}
复杂度
时间:O(n·m)
空间:O(n + m)
95. 城市间货物运输 II (City Freight Transport II)
图最短路ACM中等
题目
某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。网络中的道路都有各自的运输成本和政府补贴, 道路的权值计算方式为:运输成本 - 政府补贴 。权值为正表示扣除了政府补贴后运输货物仍需支付的费用;权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。然而,在评估从城市 1 到城市 n 的所有可能路径中综合政府补贴后的最低运输成本时,存在一种情况: 图中可能出现负权回路。 负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。为了避免货物运输商采用负权回路这种情况无限的赚取政府补贴,算法还需检测这种特殊情况。请找出从城市 1 到城市 n 的所有可能路径中,综合政府补贴后的最低运输成本。同时能够检测并适当处理负权回路的存在。城市 1 到城市 n 之间可能会出现没有路径的情况。输入描述:第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v。输出描述:如果没有发现负权回路,则输出一个整数,表示从城市 1 到城市 n 的最低运输成本(包括政府补贴)。如果该整数是负数,则表示实现了盈利。如果发现了负权回路的存在,则输出 "circle"。如果从城市 1 无法到达城市 n,则输出 "unconnected"。
示例 1:
输入:
4 4
1 2 -1
2 3 1
3 1 -1
3 4 1
输出:
circle
提示:
路径中存在负权回路,从 1 -> 2 -> 3 -> 1,总权值为 -1,理论上货物运输商可以在该回路无限循环赚取政府补贴,所以输出 "circle" 表示已经检测出了该种情况。数据范围:1 <= n <= 1000; 1 <= m <= 10000;-100 <= v <= 100;
思路简述
在 94 基础上可能有负环。Bellman-Ford 先做 n−1 轮,再多做 1 轮:若还能松弛,说明从 1 可达负环,输出 circle;否则若 dist[n] 仍为无穷则 unconnected,否则输出最短路。
参考代码
JAVA
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
int[][] edges = new int[m][3];
for (int i = 0; i < m; i++) {
edges[i][0] = in.nextInt();
edges[i][1] = in.nextInt();
edges[i][2] = in.nextInt();
}
final int INF = 0x3f3f3f3f;
int[] dist = new int[n + 1];
Arrays.fill(dist, INF);
dist[1] = 0;
for (int i = 1; i <= n - 1; i++) {
for (int[] e : edges) {
int s = e[0];
int t = e[1];
int v = e[2];
if (dist[s] != INF && dist[t] > dist[s] + v) {
dist[t] = dist[s] + v;
}
}
}
boolean circle = false;
for (int[] e : edges) {
int s = e[0];
int t = e[1];
int v = e[2];
if (dist[s] != INF && dist[t] > dist[s] + v) {
circle = true; // 第 n 轮仍能松弛 → 1 可达负环
break;
}
}
if (circle) {
System.out.println("circle");
} else if (dist[n] == INF) {
System.out.println("unconnected");
} else {
int ans = dist[n];
System.out.println(ans);
}
in.close();
}
}
复杂度
时间:O(n·m)
空间:O(n + m)
96. 城市间货物运输 III (City Freight Transport III)
图最短路ACM中等
题目
某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。网络中的道路都有各自的运输成本和政府补贴, 道路的权值计算方式为:运输成本 - 政府补贴。 权值为正表示扣除了政府补贴后运输货物仍需支付的费用;权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。请计算在最多经过 k 个城市的条件下,从城市 src 到城市 dst 的最低运输成本。输入描述:第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v。最后一行包含三个正整数,src、dst、和 k,src 和 dst 为城市编号,从 src 到 dst 经过的城市数量限制。输出描述:输出一个整数,表示从城市 src 到城市 dst 的最低运输成本,如果无法在给定经过城市数量限制下找到从 src 到 dst 的路径,则输出 "unreachable",表示不存在符合条件的运输方案。
示例 1:
输入:
6 7
1 2 1
2 4 -3
2 5 2
1 3 5
3 5 1
4 6 4
5 6 -2
2 6 1
输出:
0
提示:
从 2 -> 5 -> 6 中转一站,运输成本为 0。1 <= n <= 1000;1 <= m <= 10000;-100 <= v <= 100;
思路简述
「最多经过 k 个城市」= 最多 k 个中转 = 最多 k+1 条边(与力扣 787 相同)。对边做 k+1 轮松弛,每轮用上一轮的 dist 拷贝,避免同轮多次中转。不可达输出 unreachable。
参考代码
JAVA
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
int[][] edges = new int[m][3];
for (int i = 0; i < m; i++) {
edges[i][0] = in.nextInt();
edges[i][1] = in.nextInt();
edges[i][2] = in.nextInt();
}
int src = in.nextInt();
int dst = in.nextInt();
int k = in.nextInt();
final int INF = 0x3f3f3f3f;
int[] dist = new int[n + 1];
Arrays.fill(dist, INF);
dist[src] = 0;
// 最多 k 个中转点 → 最多 k+1 条边
for (int step = 0; step <= k; step++) {
int[] prev = dist.clone(); // 只用上一轮结果,防止本轮连跳
for (int[] e : edges) {
int s = e[0];
int t = e[1];
int v = e[2];
if (prev[s] != INF && dist[t] > prev[s] + v) {
dist[t] = prev[s] + v;
}
}
}
if (dist[dst] == INF) {
System.out.println("unreachable");
} else {
int ans = dist[dst];
System.out.println(ans);
}
in.close();
}
}
复杂度
时间:O(k·m)
空间:O(n + m)
97. 小明逛公园 (Xiaoming Visits the Park)
图最短路ACM困难
题目
小明喜欢去公园散步,公园内布置了许多的景点,相互之间通过小路连接,小明希望在观看景点的同时,能够节省体力,走最短的路径。给定一个公园景点图,图中有 N 个景点(编号为 1 到 N),以及 M 条双向道路连接着这些景点。每条道路上行走的距离都是已知的。小明有 Q 个观景计划,每个计划都有一个起点 start 和一个终点 end,表示他想从景点 start 前往景点 end。由于小明希望节省体力,他想知道每个观景计划中从起点到终点的最短路径长度。
请你帮助小明计算出每个观景计划的最短路径长度。输入描述:第一行包含两个整数 N, M, 分别表示景点的数量和道路的数量。接下来的 M 行,每行包含三个整数 u, v, w,表示景点 u 和景点 v 之间有一条长度为 w 的双向道路。接下里的一行包含一个整数 Q,表示观景计划的数量。接下来的 Q 行,每行包含两个整数 start, end,表示一个观景计划的起点和终点。输出描述:对于每个观景计划,输出一行表示从起点到终点的最短路径长度。如果两个景点之间不存在路径,则输出 -1。
示例 1:
输入:
7 3
2 3 4
3 6 6
4 7 8
2
2 3
3 4
输出:
4
-1
提示:
从 2 到 3 的路径长度为 4,3 到 4 之间并没有道路。1 <= N, M, Q <= 1000.1 <= w <= 10000.
思路简述
无向正权、多组最短路查询。N≤1000,Floyd 的 O(N³) 在 Java 易超时。对每个不同起点跑一次 堆优化 Dijkstra 并缓存;不可达输出 -1。
参考代码
JAVA
import java.util.*;
public class Main {
static final int INF = 0x3f3f3f3f;
static List<List<int[]>> graph;
static int n;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n = in.nextInt();
int m = in.nextInt();
graph = new ArrayList<>();
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}
for (int i = 0; i < m; i++) {
int u = in.nextInt();
int v = in.nextInt();
int w = in.nextInt();
graph.get(u).add(new int[]{v, w});
graph.get(v).add(new int[]{u, w});
}
int[][] cache = new int[n + 1][]; // cache[s] = 从 s 出发的 dist
int q = in.nextInt();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < q; i++) {
int start = in.nextInt();
int end = in.nextInt();
if (cache[start] == null) {
cache[start] = dijkstra(start);
}
int ans = cache[start][end];
sb.append(ans == INF ? -1 : ans).append('\n');
}
System.out.print(sb);
in.close();
}
static int[] dijkstra(int src) {
int[] dist = new int[n + 1];
Arrays.fill(dist, INF);
dist[src] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
pq.offer(new int[]{0, src});
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int d = cur[0];
int u = cur[1];
if (d > dist[u]) {
continue;
}
for (int[] e : graph.get(u)) {
int v = e[0];
int w = e[1];
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.offer(new int[]{dist[v], v});
}
}
}
return dist;
}
}
复杂度
时间:O(Q · (N + M) log N),相同起点只算一次
空间:O(N² + M),最坏每个点都当过起点
126. 骑士的攻击 (Knight Attack)
图最短路启发式搜索ACM困难
题目
在象棋中,马和象的移动规则分别是“马走日”和“象走田”。现给定骑士的起始坐标和目标坐标,要求根据骑士的移动规则,计算从起点到达目标点所需的最短步数。棋盘大小 1000 x 1000(棋盘的 x 和 y 坐标均在 [1, 1000] 区间内,包含边界)。输入描述:第一行包含一个整数 n,表示测试用例的数量,1 <= n <= 100。接下来的 n 行,每行包含四个整数 a1, a2, b1, b2,分别表示骑士的起始位置 (a1, a2) 和目标位置 (b1, b2)。输出描述:输出共 n 行,每行输出一个整数,表示骑士从起点到目标点的最短路径长度。
示例 1:
输入:
6
5 2 5 4
1 1 2 2
1 1 8 8
1 1 8 7
2 1 3 3
4 6 4 6
输出:
2
4
6
5
1
0
提示:
骑士移动规则如图,红色是起始位置,黄色是骑士可以走的地方。
思路简述
1000×1000 棋盘上「马走日」,多组最短步数。无权图 BFS 最坏 1e6×100 组偏紧,用 A\*:启发函数取欧氏距离 / √5(单步最长约 √5),可采纳。起点即终点为 0。
参考代码
JAVA
import java.util.*;
public class Main {
static final int[][] DIRS = {
{1, 2}, {1, -2}, {-1, 2}, {-1, -2},
{2, 1}, {2, -1}, {-2, 1}, {-2, -1}
};
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
int a1 = in.nextInt();
int a2 = in.nextInt();
int b1 = in.nextInt();
int b2 = in.nextInt();
int ans = astar(a1, a2, b1, b2);
sb.append(ans).append('\n');
}
System.out.print(sb);
in.close();
}
// 可采纳启发:骑士每步最多走 √5
static int heuristic(int x, int y, int tx, int ty) {
int dx = x - tx;
int dy = y - ty;
return (int) Math.ceil(Math.sqrt(dx * dx + dy * dy) / Math.sqrt(5.0));
}
static int astar(int sx, int sy, int tx, int ty) {
if (sx == tx && sy == ty) {
return 0;
}
int[][] best = new int[1001][1001]; // 到达该格的最少步
for (int i = 0; i <= 1000; i++) {
Arrays.fill(best[i], Integer.MAX_VALUE);
}
best[sx][sy] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
// {f, g, x, y},f = g + h
pq.offer(new int[]{heuristic(sx, sy, tx, ty), 0, sx, sy});
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int g = cur[1];
int x = cur[2];
int y = cur[3];
if (g != best[x][y]) {
continue; // 过期状态
}
if (x == tx && y == ty) {
return g;
}
for (int[] d : DIRS) {
int nx = x + d[0];
int ny = y + d[1];
if (nx < 1 || nx > 1000 || ny < 1 || ny > 1000) {
continue;
}
int ng = g + 1;
if (ng < best[nx][ny]) {
best[nx][ny] = ng;
pq.offer(new int[]{ng + heuristic(nx, ny, tx, ty), ng, nx, ny});
}
}
}
return -1; // 棋盘连通,理论上走不到
}
}
复杂度
时间:最坏 O(棋盘格数 × log),A* 通常远小于全图 BFS
空间:O(1000²) 每组一份访问标记
附录 A. n 数之和通用模板 (N-Sum Template)
适用于「在数组中找所有不重复的 n 元组,使其和为 target」(如两数/三数/四数之和)。思路:先排序;n == 2 时用双指针;n > 2 时枚举第一个数并递归求 (n-1) 数之和;同层跳过相同值去重;求和用 long 防溢出。
用法: 主函数只调 nSum(nums, n, target)。
JAVA
import java.util.*;
/** n 数之和通用模板:返回所有和为 target 的不重复 n 元组 */
class NSumTemplate {
/**
* @param nums 原数组(会被排序,调用方若需保留原序请先拷贝)
* @param n 几数之和,n >= 2
* @param target 目标和
*/
public List<List<Integer>> nSum(int[] nums, int n, int target) {
Arrays.sort(nums);
return dfs(nums, n, (long) target, 0);
}
/**
* 从 nums[start..] 中选 n 个数,使其和为 target
*/
private List<List<Integer>> dfs(int[] nums, int n, long target, int start) {
List<List<Integer>> ans = new ArrayList<>();
int len = nums.length;
// 至少还要剩 n 个数
if (n < 2 || len - start < n) {
return ans;
}
// 剪枝:当前区间最小 / 最大 n 个数之和越界则无解
long minSum = 0;
long maxSum = 0;
for (int i = 0; i < n; i++) {
minSum += nums[start + i];
maxSum += nums[len - 1 - i];
}
if (target < minSum || target > maxSum) {
return ans;
}
// 递归边界:两数之和 → 双指针
if (n == 2) {
int left = start;
int right = len - 1;
while (left < right) {
long sum = (long) nums[left] + nums[right];
if (sum < target) {
left++;
} else if (sum > target) {
right--;
} else {
ans.add(Arrays.asList(nums[left], nums[right]));
left++;
right--;
while (left < right && nums[left] == nums[left - 1]) {
left++;
}
while (left < right && nums[right] == nums[right + 1]) {
right--;
}
}
}
return ans;
}
// n > 2:枚举第一个数,递归求 (n-1) 数之和
for (int i = start; i <= len - n; i++) {
if (i > start && nums[i] == nums[i - 1]) {
continue; // 同层去重
}
List<List<Integer>> subLists = dfs(nums, n - 1, target - nums[i], i + 1);
for (List<Integer> sub : subLists) {
List<Integer> path = new ArrayList<>(n);
path.add(nums[i]);
path.addAll(sub);
ans.add(path);
}
}
return ans;
}
// -------- 力扣封装示例 --------
// 15. 三数之和
public List<List<Integer>> threeSum(int[] nums) {
return nSum(nums, 3, 0);
}
// 18. 四数之和
public List<List<Integer>> fourSum(int[] nums, int target) {
return nSum(nums, 4, target);
}
}
复杂度(粗估)
时间:约 O(n^{k-1}),k 为几数之和(两数指针收成 O(n))
空间:O(k) 递归栈 + 答案列表

