算法分析与设计程序题汇总
第二章 递归法
1.切正方形
题目描述
小明有一些矩形的材料,他要从这些矩形材料中切割出一些正方形。
当他面对一块矩形材料时,他总是切出当前矩形内能得到的最大正方形,切割完成后剩余部分仍为矩形,重复该切割操作,直到整块材料全部切为正方形为止。
例如,对于一块尺寸为 \(5 × 3\) 的矩形材料,会依次切出3×3、2×2、1×1、1×1,一共得到4个正方形。
现有一块矩形材料,两边长度分别为 2019 和 324,请求出最终一共可以切出多少个正方形。
解题思路
- 设矩形长为a,宽为b,始终保证a大于b
- 每次能切出 a整除b 个边长为b的正方形
- 更新矩形尺寸为 (b , a对b取余)
- 循环执行,直到短边长度为0,累加所有数量即为总数
题目出处
蓝桥杯省赛填空题真题
题解
点击查看Python代码
def helper(a, b):
global count
if a == 0 or b == 0:
return
if a > b:
count += a // b
a = a % b
helper(a, b)
else:
count += b // a
b = b % a
helper(a, b)
if __name__ == "__main__":
count = 0
a, b = 2019, 324
helper(a, b)
print(count)
点击查看Java代码
public class 切正方形 {
static int count=0;
public static void main(String[] args) {
int a=2019;
int b=324;
helper(a,b);
System.out.println(count);
}
private static void helper(int a, int b) {
if(a==0||b==0){
return;
}
if(a>b){
count+=a/b;
a=a%b;
helper(a,b);
}else{
count+=b/a;
b=b%a;
helper(a,b);
}
}
}
第三章 穷举法
1. 开心的金明
题目描述
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 N 元钱就行”。今天一早金明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的 N 元。于是,他把每件物品规定了一个重要度,分为 5 等:用整数 1-5 表示,第 5 等最重要。他还从因特网上查到了每件物品的价格(都是整数元)。他希望在不超过 N 元(可以等于 N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第 j 件物品的价格为 v_j,重要度为 w_j,共选中了 k 件物品,编号依次为 j_1,j_2,…,j_k,则所求的总和为:

请你帮助金明设计一个满足要求的购物单。
输入格式
第一行,为 2 个正整数,用一个空格隔开:n,m(n<3×10^4,m<25)其中 n 表示总钱数,m 为希望购买物品的个数。
从第 2 行到第 m+1 行,第 j 行给出了编号为 j-1 的物品的基本数据,每行有 2 个非负整数 v,p(其中 v 表示该物品的价格 (v <= 10^4),p 表示该物品的重要度(1<=p<=5))。
输出格式
1 个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<10^8)。
输入输出样例 #1
输入 #1
1000 5
800 2
400 5
300 5
400 3
200 2
输出 #1
3900
说明/提示
NOIP 2006 普及组 第二题。
题解
点击查看Python代码
import sys
def main():
# 读取输入
data = sys.stdin.read().strip().split()
if not data:
return
n = int(data[0]) # 总钱数
m = int(data[1]) # 物品个数
# dp[j] 表示花费不超过 j 元时能获得的最大价格×重要度总和
dp = [0] * (n + 1)
idx = 2
for _ in range(m):
v = int(data[idx]) # 物品价格
p = int(data[idx + 1]) # 重要度
idx += 2
val = v * p # 该物品的价值 = 价格 × 重要度
# 0/1 背包:逆序更新,保证每个物品最多选一次
for j in range(n, v - 1, -1):
# 不选当前物品:dp[j] 保持不变
# 选当前物品:dp[j - v] + val
dp[j] = max(dp[j], dp[j - v] + val)
# 输出不超过总钱数的最大价值
print(dp[n])
if __name__ == "__main__":
main()
点击查看Java代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 输入总钱数 n 和物品个数 m
int n = sc.nextInt(); // n < 30000
int m = sc.nextInt(); // m < 25
/*
* dp[j] 表示:在总花费不超过 j 元的前提下,能够获得的最大“价格×重要度”总和。
* 数组长度为 n+1,下标从 0 到 n,dp[0]=0 表示花费 0 元时总价值为 0。
*/
int[] dp = new int[n + 1];
// 依次处理每一个物品
for (int i = 0; i < m; i++) {
int v = sc.nextInt(); // 物品价格 v (v <= 10000)
int p = sc.nextInt(); // 物品重要度 p (1~5)
int val = v * p; // 该物品的价值 = 价格 × 重要度
/*
* 0/1 背包问题(每种物品最多选一件)
* 为了保证每个物品只被考虑一次,内层循环必须倒序更新 dp 数组。
* 如果正序更新,同一个物品可能会被重复使用多次,变成完全背包。
*
* j 从 n 递减到 v,表示当前考虑花费不超过 j 元的情况。
* 对于每个 j,有两种选择:
* 1. 不选当前物品 → dp[j] 保持不变
* 2. 选当前物品 → 总价值变为 dp[j - v] + val
* 取两者中的较大值。
*/
for (int j = n; j >= v; j--) {
dp[j] = Math.max(dp[j], dp[j - v] + val);
}
}
// dp[n] 即为总花费不超过 n 元时的最大总价值
System.out.println(dp[n]);
sc.close();
}
}
2. 连续自然数和
题目描述
对一个给定的自然数M,求出所有的连续的自然数段(连续个数大于1),这些连续的自然数段中的全部数之和为M。
例子:1998+1999+2000+2001+2002 = 10000,所以从1998到2002的一个自然数段为M=10000的一个解。
输入格式
包含一个整数的单独一行给出\(M\)的值(10 <= M <= 2000000)。
输出格式
每行两个自然数,给出一个满足条件的连续自然数段中的第一个数和最后一个数,两数之间用一个空格隔开。
所有输出按照起点从小到大升序排列,题目保证输入数据至少存在一组合法解。
样例输入
10000
21334
2000000
样例输出
18 142
297 328
388 412
1998 2002
5332 5335
923 2202
2888 3512
7685 7940
15938 16062
79988 80012
399998 400002
题解
点击查看Pyhton代码
import math
M = int(input()) # 输入目标总和
# 最大可能的连续长度 k 满足: 1+2+...+(k-1) < M
# 即 k(k-1)/2 < M => k < (1 + sqrt(1+8M)) / 2
max_k = int((1 + math.sqrt(1 + 8 * M)) / 2)
# 枚举长度从大到小,这样起点会自动从小到大(段越长起点越小)
for k in range(max_k, 1, -1):
# 连续 k 个数: a, a+1, ..., a+k-1
# 总和 = k*a + k*(k-1)/2 = M
# => a = (2M - k(k-1)) / (2k)
numerator = 2 * M - k * (k - 1)
# 分子必须为正,且能被 2k 整除
if numerator > 0 and numerator % (2 * k) == 0:
a = numerator // (2 * k) # 起点
b = a + k - 1 # 终点
print(a, b)
点击查看Java代码
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int M = sc.nextInt(); // 目标和
List<int[]> results = new ArrayList<>(); // 存储 (起点, 终点)
// 连续自然数段至少包含 2 个数,段长度 k 最大满足 k*(k+1)/2 <= M
// 解得 k <= sqrt(2M),因此枚举 k = 2 ~ floor(sqrt(2M))
int maxK = (int) Math.sqrt(2 * M);
for (int k = 2; k <= maxK; k++) {
// 等差数列求和公式:S = k * a + k*(k-1)/2 = M
// 两边乘 2:2M = 2k*a + k(k-1) => 2k*a = 2M - k(k-1)
// 变形得:2M / k = 2a + (k-1) ,所以 2M 必须能被 k 整除
if (2 * M % k != 0) continue; // 不能整除则跳过
int temp = 2 * M / k - (k - 1); // temp = 2a
if (temp <= 0 || temp % 2 != 0) continue; // a 必须为正整数且 temp 为偶数
int a = temp / 2; // 段的起始数
int b = a + k - 1; // 段的结束数
results.add(new int[]{a, b}); // 保存解
}
// 因为 k 从小到大枚举,而 k 越小起始数 a 越大,所以直接输出会得到降序
// 题目要求按起点升序,因此逆序输出 results
for (int i = results.size() - 1; i >= 0; i--) {
System.out.println(results.get(i)[0] + " " + results.get(i)[1]);
}
sc.close();
}
}
第四章 分治算法
1. 循环赛日程表问题
题目描述
设有 n = 2^k 名选手进行网球循环赛,满足以下规则:
- 每个选手必须与其他所有选手各比赛一次
- 每个选手每天只能比赛一场
- 比赛在
n-1天内完成
请使用分治法生成一个 n × n 的比赛日程表。
日程表中第 i 行第 j 列表示第 i 个选手在第 j 天的比赛对手。
分治规则
- 初始条件(2×2)
1 2
2 1
- 分治填充规则
- 右上角:左上角 + 当前块大小的一半
- 左下角:左上角 + 当前块大小的一半
- 右下角:与左上角相同
- 依次填充 4×4 → 8×8 的日程表。
输入
无固定输入,固定输出 8×8 的循环赛日程表。
输出
输出一个 8×8 的整数矩阵,表示循环赛日程表。
样例输出
[1, 2, 3, 4, 5, 6, 7, 8]
[2, 1, 4, 3, 6, 5, 8, 7]
[3, 4, 1, 2, 7, 8, 5, 6]
[4, 3, 2, 1, 8, 7, 6, 5]
[5, 6, 7, 8, 1, 2, 3, 4]
[6, 5, 8, 7, 2, 1, 4, 3]
[7, 8, 5, 6, 3, 4, 1, 2]
[8, 7, 6, 5, 4, 3, 2, 1]
题解
点击查看Pyhton代码
def build_table(n):
"""
递归分治构造 n×n 循环赛日程表(n 为 2 的幂)
"""
# 初始化 n×n 全零矩阵
table = [[0] * n for _ in range(n)]
# 递归出口:2×2 基础情况
if n == 2:
table[0][0] = 1
table[0][1] = 2
table[1][0] = 2
table[1][1] = 1
return table
half = n // 2
# 递归生成左上角子表
top_left = build_table(half)
# 根据规则填充四个象限
for i in range(half):
for j in range(half):
# 左上角
table[i][j] = top_left[i][j]
# 右上角 = 左上角 + half
table[i][j + half] = top_left[i][j] + half
# 左下角 = 左上角 + half
table[i + half][j] = top_left[i][j] + half
# 右下角 = 左上角(不变)
table[i + half][j + half] = top_left[i][j]
return table
# 生成 8×8 日程表并输出
schedule = build_table(8)
for row in schedule:
print(' '.join(f"{num:2d}" for num in row))
点击查看Java代码
public class Schedule {
/**
* 递归分治构造 n×n 循环赛日程表
* @param n 选手个数(必须是2的幂)
* @return n×n 日程表
*/
public static int[][] buildTable(int n) {
int[][] table = new int[n][n];
// 基础情况:2×2
if (n == 2) {
table[0][0] = 1;
table[0][1] = 2;
table[1][0] = 2;
table[1][1] = 1;
return table;
}
int half = n / 2;
int[][] topLeft = buildTable(half); // 递归生成左上角子表
// 填充四个象限
for (int i = 0; i < half; i++) {
for (int j = 0; j < half; j++) {
// 左上角
table[i][j] = topLeft[i][j];
// 右上角 = 左上角 + half
table[i][j + half] = topLeft[i][j] + half;
// 左下角 = 左上角 + half
table[i + half][j] = topLeft[i][j] + half;
// 右下角 = 左上角
table[i + half][j + half] = topLeft[i][j];
}
}
return table;
}
public static void main(String[] args) {
int n = 8;
int[][] schedule = buildTable(n);
for (int[] row : schedule) {
for (int num : row) {
System.out.printf("%2d ", num);
}
System.out.println();
}
}
}
2. L型骨牌覆盖
问题描述
有一个大小为 2^k × 2^k(k>0) 的正方形棋盘,棋盘中恰好有一个方格是特殊方格。
现在需要使用 L 型骨牌 覆盖除特殊方格外的所有棋盘方格,要求:
- L 型骨牌可以任意旋转
- 骨牌之间不能重叠
- 所有非特殊方格必须被完全覆盖
请使用分治法设计算法实现覆盖,并输出覆盖后的棋盘。
输入说明
程序中直接给定:
- 棋盘大小
size = 8 - 特殊方格坐标
(tx, ty),从 0 开始编号
输出说明
输出覆盖完成的 8×8 棋盘:
- 数字
0表示特殊方格 - 其他数字表示L 型骨牌编号
- 要求数字右对齐、整齐格式化输出
输出格式
// 打印棋盘
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
System.out.printf("%2d ", board[i][j]);
}
System.out.println();
}
算法要求
使用分治法递归实现:
- 将当前棋盘划分为 4 个等大的子棋盘
- 判断特殊方格位于哪个子棋盘
- 对没有特殊方格的子棋盘,在交界处放置一个 L 型骨牌
- 递归处理 4 个子棋盘
- 直到子棋盘大小为 1 时结束递归
题解
点击查看Pyhton代码
# 棋盘覆盖问题 - 分治法
size = 8 # 棋盘大小 2^k x 2^k,这里 8x8
tx = 3 # 特殊方格横坐标(从0开始)
ty = 3 # 特殊方格纵坐标
board = [[0]*size for _ in range(size)] # 棋盘
tile = 1 # L型骨牌编号(从1开始)
# 分治覆盖函数
# tr, tc:左上角行、列
# dr, dc:特殊方格行、列
# size:当前子棋盘大小
def chessboard(tr, tc, dr, dc, size):
global tile
if size == 1: # 递归出口:1x1 无需覆盖
return
s = size // 2 # 分割为 4 个 1/2 子棋盘
t = tile # 当前骨牌编号
# ===== 1. 覆盖左上角子棋盘 =====
# 特殊方格在左上
if dr < tr + s and dc < tc + s:
chessboard(tr, tc, dr, dc, s)
# 特殊方格不在左上 → 右下角设为临时特殊方格
else:
board[tr + s - 1][tc + s - 1] = t
chessboard(tr, tc, tr + s - 1, tc + s - 1, s)
# ===== 2. 覆盖右上角子棋盘 =====
if dr < tr + s and dc >= tc + s:
chessboard(tr, tc + s, dr, dc, s)
else:
board[tr + s - 1][tc + s] = t
chessboard(tr, tc + s, tr + s - 1, tc + s, s)
# ===== 3. 覆盖左下角子棋盘 =====
if dr >= tr + s and dc < tc + s:
chessboard(tr + s, tc, dr, dc, s)
else:
board[tr + s][tc + s - 1] = t
chessboard(tr + s, tc, tr + s, tc + s - 1, s)
# ===== 4. 覆盖右下角子棋盘 =====
if dr >= tr + s and dc >= tc + s:
chessboard(tr + s, tc + s, dr, dc, s)
else:
board[tr + s][tc + s] = t
chessboard(tr + s, tc + s, tr + s, tc + s, s)
tile += 1 # 一个L型骨牌用完,编号+1
# 开始覆盖
chessboard(0, 0, tx, ty, size)
# 打印棋盘(对齐输出)
for i in range(size):
for j in range(size):
print(f"{board[i][j]:2d}", end=" ")
print()
点击查看Java代码
public class ChessBoardCover {
// 棋盘尺寸 2^k,此处为8
private static final int SIZE = 8;
// 特殊方格坐标 tx, ty
private static final int tx = 3;
private static final int ty = 3;
// 棋盘二维数组,存储骨牌编号,0代表特殊方格
private static int[][] board = new int[SIZE][SIZE];
// L型骨牌全局编号,从1开始自增
private static int tile = 1;
/**
* 分治递归覆盖棋盘
* @param tr 当前子棋盘左上角行号
* @param tc 当前子棋盘左上角列号
* @param dr 当前子棋盘内特殊方格行号
* @param dc 当前子棋盘内特殊方格列号
* @param size 当前子棋盘边长
*/
public static void chessBoard(int tr, int tc, int dr, int dc, int size) {
// 递归终止条件:1*1棋盘,无需铺骨牌
if (size == 1) {
return;
}
// 当前子棋盘对半分割后的边长
int s = size / 2;
// 暂存当前L骨牌编号,四块子棋盘共用同一块L骨牌
int t = tile;
// ========== 1. 处理左上角子棋盘 ==========
if (dr < tr + s && dc < tc + s) {
// 特殊方格在左上子棋盘内,直接递归处理
chessBoard(tr, tc, dr, dc, s);
} else {
// 无特殊方格,将左上子棋盘右下角设为临时特殊格,铺当前L骨牌
board[tr + s - 1][tc + s - 1] = t;
chessBoard(tr, tc, tr + s - 1, tc + s - 1, s);
}
// ========== 2. 处理右上角子棋盘 ==========
if (dr < tr + s && dc >= tc + s) {
// 特殊方格在右上子棋盘内
chessBoard(tr, tc + s, dr, dc, s);
} else {
// 右上子棋盘左下角设临时特殊格
board[tr + s - 1][tc + s] = t;
chessBoard(tr, tc + s, tr + s - 1, tc + s, s);
}
// ========== 3. 处理左下角子棋盘 ==========
if (dr >= tr + s && dc < tc + s) {
// 特殊方格在左下子棋盘内
chessBoard(tr + s, tc, dr, dc, s);
} else {
// 左下子棋盘右上角设临时特殊格
board[tr + s][tc + s - 1] = t;
chessBoard(tr + s, tc, tr + s, tc + s - 1, s);
}
// ========== 4. 处理右下角子棋盘 ==========
if (dr >= tr + s && dc >= tc + s) {
// 特殊方格在右下子棋盘内
chessBoard(tr + s, tc + s, dr, dc, s);
} else {
// 右下子棋盘左上角设临时特殊格
board[tr + s][tc + s] = t;
chessBoard(tr + s, tc + s, tr + s, tc + s, s);
}
// 当前L骨牌四块子棋盘全部处理完毕,编号自增,下一块新骨牌
tile++;
}
public static void main(String[] args) {
// 初始化特殊方格,标记为0
board[tx][ty] = 0;
// 从整个棋盘左上角(0,0)开始分治覆盖
chessBoard(0, 0, tx, ty, SIZE);
// 格式化打印棋盘,数字占2位对齐输出
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
System.out.printf("%2d ", board[i][j]);
}
// 换行
System.out.println();
}
}
}
第五章 回溯法
1. 活字印刷
题目描述
你有一套活字字模 tiles,其中每个字模上都刻有一个字母 tiles[i]。返回你可以印出的非空字母序列的数目。
注意:本题中,每个活字字模只能使用一次。
示例
示例 1:
输入:"AAB"
输出:8
解释:可能的序列为 "A", "B", "AA", "AB", "BA", "AAB", "ABA", "BAA"。
示例 2:
输入:"AAABBC"
输出:188
示例 3:
输入:"V"
输出:1
提示
1 <= tiles.length <= 7tiles由大写英文字母组成
题解
点击查看Pyhton代码
class Solution:
def numTilePossibilities(self, tiles: str) -> int:
# 排序,用于跳过重复字符
tiles = sorted(tiles)
n = len(tiles)
used = [False] * n
def dfs():
res = 0
for i in range(n):
# 重复字符剪枝,和C++逻辑完全一致
if i > 0 and tiles[i] == tiles[i-1] and not used[i-1]:
continue
if not used[i]:
used[i] = True
res += 1 + dfs()
used[i] = False
return res
return dfs()
# 测试入口
if __name__ == "__main__":
sol = Solution()
print(sol.numTilePossibilities("AAB"))
print(sol.numTilePossibilities("AAABBC"))
print(sol.numTilePossibilities("V"))
点击查看Java代码
import java.util.Arrays;
public class Solution {
public int numTilePossibilities(String tiles) {
// 字符串转字符数组并排序,去重核心前提
char[] arr = tiles.toCharArray();
Arrays.sort(arr);
boolean[] used = new boolean[arr.length];
return dfs(arr, used);
}
private int dfs(char[] tiles, boolean[] used) {
int res = 0;
for (int i = 0; i < tiles.length; i++) {
// 去重:当前字符和前一个相同,且前一个未使用,跳过
if (i > 0 && tiles[i] == tiles[i - 1] && !used[i - 1]) {
continue;
}
if (!used[i]) {
used[i] = true;
// 当前选1个 + 后续递归所有组合
res += 1 + dfs(tiles, used);
used[i] = false; // 回溯撤销
}
}
return res;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.numTilePossibilities("AAB"));
System.out.println(sol.numTilePossibilities("AAABBC"));
System.out.println(sol.numTilePossibilities("V"));
}
}
2. 跳房子(Hopscotch)
题目描述
牛以一种非传统的方式玩跳房子游戏。它们不是跳入一条线性的数字盒子中,而是创建了一个与 x 轴和 y 轴平行的 5x5 数字网格。
然后它们灵巧地跳到网格中的任何一个数字上,然后向前、向后、向右或向左(永远不会斜着)跳到网格中的另一个数字上。它们再次跳跃(规则相同)到另一个数字(可能是之前已经访问过的数字)。
通过总共五次网格内的跳跃,它们的跳跃形成了一个六位数(可能像 000201 这样的有前导零的数字)。
请确定以这种方式可以生成的不同整数的数量。
输入
第 1 至 5 行:网格,每行五个整数。
输出
第 1 行:可以构造的不同整数的数量。
样例输入
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 2 1
1 1 1 1 1
样例输出
15
提示
输出详情:
可以构造出以下 15 种不同的序列,没有其他可能的值:
111111、111112、111121、111211、111212、112111、112121、121111、121112、121211、121212、211111、211121、212111、212121。
题解
点击查看Pyhton代码
def main():
# 读取 5x5 网格
grid = [list(map(int, input().split())) for _ in range(5)]
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] # 下、上、右、左
result = set() # 存储不同序列
# 深度优先搜索
def dfs(x, y, step, path):
# step 已收集的数字个数 (0~5)
if step == 6:
result.add(path) # 收集完整序列
return
# 尝试四个方向
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < 5 and 0 <= ny < 5:
dfs(nx, ny, step + 1, path + str(grid[nx][ny]))
# 从每个格子开始搜索
for i in range(5):
for j in range(5):
dfs(i, j, 1, str(grid[i][j])) # 起点步数为1,路径字符串包含起点数字
print(len(result))
if __name__ == "__main__":
main()
点击查看Java代码
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class Hopscotch {
static int[][] grid = new int[5][5];
static Set<String> set = new HashSet<>();
static int[] dx = {1, -1, 0, 0};
static int[] dy = {0, 0, 1, -1};
// 深度优先搜索
static void dfs(int x, int y, int step, String path) {
if (step == 6) {
set.add(path); // 保存完整序列
return;
}
for (int d = 0; d < 4; d++) {
int nx = x + dx[d];
int ny = y + dy[d];
if (nx >= 0 && nx < 5 && ny >= 0 && ny < 5) {
dfs(nx, ny, step + 1, path + grid[nx][ny]);
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 读取网格
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
grid[i][j] = sc.nextInt();
}
}
// 从每个格子出发
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
dfs(i, j, 1, Integer.toString(grid[i][j]));
}
}
System.out.println(set.size());
sc.close();
}
}
第六章 分支限界法
1. 岛屿个数
题目描述
小蓝得到了一副大小为 M × N 的格子地图,可以将其视作一个只包含字符 0(代表海水)和 1(代表陆地)的二维数组,地图之外可以视作全部是海水,每个岛屿由在上、下、左、右四个方向上相邻的 1 相连接而形成。
在岛屿 A 所占据的格子中,如果可以从中选出 k 个不同的格子,使得他们的坐标能够组成一个这样的排列:(x0,y0),(x1,y1),…,(xk−1,yk−1),其中 (x (i+1)%k , y (i+1)%k) 是由 (xi, yi) 通过上、下、左、右移动一次得来的(0 ≤ i ≤ k−1),此时这 k 个格子就构成了一个「环」。如果另一个岛屿 B 所占据的格子全部位于这个「环」内部,此时我们将岛屿 B 视作是岛屿 A 的子岛屿。若 B 是 A 的子岛屿,C 又是 B 的子岛屿,那 C 也是 A 的子岛屿。
请问这个地图上共有多少个岛屿?在进行统计时不需要统计子岛屿的数目。
输入格式
第一行一个整数 T,表示有 T 组测试数据。
接下来输入 T 组数据。对于每组数据,第一行包含两个用空格分隔的整数 M, N 表示地图大小;接下来输入 M 行,每行包含 N 个字符,字符只可能是 0 或 1。
输出格式
对于每组数据,输出一行,包含一个整数表示答案。
输入输出样例
输入
2
5 5
01111
11001
10101
10001
11111
5 6
111111
100001
010101
100001
111111
输出
1
3
样例说明
对于第一组数据,包含两个岛屿,下面用不同的数字进行了区分:
01111
11001
10201
10001
11111
岛屿 2 在岛屿 1 的「环」内部,所以岛屿 2 是岛屿 1 的子岛屿,答案为 1。
对于第二组数据,包含三个岛屿,下面用不同的数字进行了区分:
111111
100001
020301
100001
111111
注意岛屿 3 并不是岛屿 1 或者岛屿 2 的子岛屿,因为岛屿 1 和岛屿 2 中均没有「环」。
数据范围
对于 30% 的评测用例,1 ≤ M,N ≤ 10。
对于 100% 的评测用例,1 ≤ T ≤ 10,1 ≤ M,N ≤ 50。
题解
点击查看Pyhton代码
from collections import deque
import sys
def solve():
# 快速读取所有输入
data = sys.stdin.read().split()
t = int(data[0]) # 测试数据组数
idx = 1
results = []
# 8 个方向:上、右上、右、右下、下、左下、左、左上
dir8 = [(-1, -1), (-1, 0), (-1, 1),
(0, -1), (0, 1),
(1, -1), (1, 0), (1, 1)]
# 4 个方向:上下左右
dir4 = [(-1, 0), (0, 1), (1, 0), (0, -1)]
for _ in range(t):
m = int(data[idx]); n = int(data[idx+1]); idx += 2
# 读取地图,每行是一个字符串
grid = [list(data[idx + i]) for i in range(m)]
idx += m
# marked 数组:标记与边界连通的海水格子
marked = [[False] * n for _ in range(m)]
# ---------- 步骤1:从边界上的 '0' 开始 8 方向 BFS,标记所有外部海水 ----------
q = deque()
for i in range(m):
for j in range(n):
# 边界上的海水格子作为 BFS 起点
if (i == 0 or i == m-1 or j == 0 or j == n-1) and grid[i][j] == '0' and not marked[i][j]:
q.append((i, j))
marked[i][j] = True
while q:
x, y = q.popleft()
for dx, dy in dir8:
nx, ny = x + dx, y + dy
# 不越界、未被标记、且是海水
if 0 <= nx < m and 0 <= ny < n and not marked[nx][ny] and grid[nx][ny] == '0':
marked[nx][ny] = True
q.append((nx, ny))
# ---------- 步骤2:将内部海水(未标记的 '0')变成陆地 ----------
for i in range(m):
for j in range(n):
if grid[i][j] == '0' and not marked[i][j]:
grid[i][j] = '1' # 变成陆地,与包围它的岛屿合并
# ---------- 步骤3:用 4 方向 BFS 统计剩余的岛屿数量 ----------
visited = [[False] * n for _ in range(m)]
islands = 0
for i in range(m):
for j in range(n):
if grid[i][j] == '1' and not visited[i][j]:
islands += 1
q = deque()
q.append((i, j))
visited[i][j] = True
while q:
x, y = q.popleft()
for dx, dy in dir4:
nx, ny = x + dx, y + dy
if 0 <= nx < m and 0 <= ny < n and not visited[nx][ny] and grid[nx][ny] == '1':
visited[nx][ny] = True
q.append((nx, ny))
results.append(str(islands))
sys.stdout.write("\n".join(results))
if __name__ == "__main__":
solve()
点击查看Java代码
import java.util.*;
public class Main {
// 8 个方向对应的行列变化(包括对角线)
static int[] dx8 = {-1, -1, 0, 1, 1, 1, 0, -1};
static int[] dy8 = {0, 1, 1, 1, 0, -1, -1, -1};
// 4 个方向(仅上下左右)
static int[] dx4 = {-1, 0, 1, 0};
static int[] dy4 = {0, 1, 0, -1};
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt(); // 测试数据组数
while (T-- > 0) {
int m = sc.nextInt();
int n = sc.nextInt();
char[][] grid = new char[m][n];
for (int i = 0; i < m; i++) {
grid[i] = sc.next().toCharArray(); // 读取一行字符串,转为字符数组
}
// ---------- 步骤1:从边界上的 '0' 开始 8 方向 BFS,标记所有外部海水 ----------
boolean[][] marked = new boolean[m][n]; // 标记与边界连通的海水
Queue<int[]> q = new LinkedList<>();
// 所有边界海水格子入队
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if ((i == 0 || i == m-1 || j == 0 || j == n-1) && grid[i][j] == '0' && !marked[i][j]) {
q.offer(new int[]{i, j});
marked[i][j] = true;
}
}
}
// 8 方向 BFS 扩散,标记所有与边界连通的海水
while (!q.isEmpty()) {
int[] cur = q.poll();
int x = cur[0], y = cur[1];
for (int d = 0; d < 8; d++) {
int nx = x + dx8[d];
int ny = y + dy8[d];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && !marked[nx][ny] && grid[nx][ny] == '0') {
marked[nx][ny] = true;
q.offer(new int[]{nx, ny});
}
}
}
// ---------- 步骤2:将内部海水(未标记的 '0')变成陆地 ----------
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '0' && !marked[i][j]) {
grid[i][j] = '1'; // 变成陆地,与包围它的岛屿合并
}
}
}
// ---------- 步骤3:用 4 方向 BFS 统计剩余的岛屿数量 ----------
boolean[][] visited = new boolean[m][n];
int islands = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1' && !visited[i][j]) {
islands++;
q = new LinkedList<>();
q.offer(new int[]{i, j});
visited[i][j] = true;
// 4 方向 BFS 遍历整个岛屿
while (!q.isEmpty()) {
int[] cur = q.poll();
int x = cur[0], y = cur[1];
for (int d = 0; d < 4; d++) {
int nx = x + dx4[d];
int ny = y + dy4[d];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && !visited[nx][ny] && grid[nx][ny] == '1') {
visited[nx][ny] = true;
q.offer(new int[]{nx, ny});
}
}
}
}
}
}
System.out.println(islands);
}
sc.close();
}
}
2. 原始森林迷宫
题目背景
原始森林中有很多树,如线段树、后缀树和红黑树等,你掌握了所有的树吗?别担心,本问题不会谈论树,而是介绍原始森林中的一些动物。
-
第一种是金刚:金刚是一种危险的动物,如果你遇到金刚,你会死的。
-
第二种是野狗:它不像金刚那么危险,但它会咬你,被咬两次之后你也会死的。
Amaze 是一个女孩,她不幸迷失于原始森林中。Magicpig 非常担心她,他要到原始森林找她。Magicpig 知道遇到金刚会死,被野狗咬两次也会死,他是多么可怜!
题目描述
输入的第 1 行是单个数字 t(0 ≤ t ≤ 20),表示测试用例的数目。
每个测试用例:
-
第一行给出一个整数
n(0 ≤ n ≤ 30),表示原始森林是一个n×n的单元矩阵。 -
接下来
n行,每行一个长度为n的字符串,表示地图,其中:-
p表示 Magicpig(起点) -
a表示 Amaze(终点) -
r表示道路(可通行) -
k表示金刚(遇到即死亡,不可通行) -
d表示野狗(遇到会被咬一次,累计被咬两次即死亡)
-
Magicpig 只能在上、下、左、右 4 个方向移动。
对于每个测试用例,如果 Magicpig 能够在存活的前提下找到 Amaze,则输出 Yes,否则输出 No。
输入格式
-
第 1 行:单个整数
t,表示测试用例数。 -
每个测试用例:
-
第 1 行:整数
n。 -
接下来
n行:每行n个字符,表示地图。
-
输出格式
对于每个测试用例,输出一行 Yes 或 No。
输入样例
4
3
pkk
rrd
rda
3
prr
kkk
rra
4
prrr
rrrr
rrrr
arrr
5
prrrr
ddddd
ddddd
rrrrr
rrrra
输出样例
Yes
No
Yes
No
题目分析
这是一道典型的带状态的 BFS 迷宫搜索题,和普通迷宫的区别在于:
-
除了位置,还需要记录当前的被咬次数(0 或 1),因为被咬两次就会死亡,不能再继续移动。
-
遇到金刚
k时,直接死亡,不能通过。 -
遇到野狗
d时,被咬次数 +1,只有当次数 < 2 时才能继续移动。
因此,BFS 的状态需要包含三个信息:(x, y, biteCount),其中 biteCount 表示当前已经被咬的次数(0 或 1)。
解题思路(核心)
-
状态定义:使用三维数组
visited[x][y][bite]记录是否访问过(x,y)位置且被咬bite次的状态,避免重复搜索。 -
队列元素:存储
(x, y, biteCount)。 -
移动规则:
-
遇到
k:直接跳过,不加入队列。 -
遇到
d:如果biteCount == 1,跳过;否则新状态为(nx, ny, 1)。 -
遇到
r或p或a:新状态为(nx, ny, biteCount)。
-
-
终止条件:在 BFS 过程中,只要到达
a的位置,就直接返回Yes。
题解
点击查看Pyhton代码
from collections import deque
import sys
def solve():
# 读取所有输入,按空白字符分割
data = sys.stdin.read().split()
t = int(data[0]) # 测试用例数
idx = 1
results = []
# 四个方向:上、右、下、左
dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)]
for _ in range(t):
n = int(data[idx]); idx += 1
grid = []
for __ in range(n):
grid.append(data[idx]); idx += 1
# 1. 寻找起点 'p'
sx = sy = -1
for i in range(n):
for j in range(n):
if grid[i][j] == 'p':
sx, sy = i, j
break
if sx != -1:
break
# visited[x][y][bites] bites 取值 0 或 1
visited = [[[False] * 2 for _ in range(n)] for __ in range(n)]
q = deque()
q.append((sx, sy, 0)) # (x, y, 已咬次数)
visited[sx][sy][0] = True
found = False
while q:
x, y, bites = q.popleft()
# 到达终点
if grid[x][y] == 'a':
found = True
break
# 尝试四个方向移动
for dx, dy in dirs:
nx, ny = x + dx, y + dy
# 边界检查
if nx < 0 or nx >= n or ny < 0 or ny >= n:
continue
cell = grid[nx][ny]
# 金刚:不能走
if cell == 'k':
continue
new_bites = bites
# 野狗:被咬次数 +1
if cell == 'd':
new_bites = bites + 1
# 如果咬两次(new_bites >= 2),不能走这个格子
if new_bites >= 2:
continue
# 其他格子:'r', 'a', 'p', 以及有效咬次数的 'd' 都可以走
# 但需要检查该状态是否已经访问过
if not visited[nx][ny][new_bites]:
visited[nx][ny][new_bites] = True
q.append((nx, ny, new_bites))
results.append("Yes" if found else "No")
# 统一输出结果
sys.stdout.write("\n".join(results))
if __name__ == "__main__":
solve()
点击查看Java代码
import java.util.*;
public class Main {
static class State {
int x, y, bite;
State(int x, int y, int bite) {
this.x = x;
this.y = y;
this.bite = bite;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
List<String> results = new ArrayList<>();
// 四个方向:上、右、下、左
int[] dx = {-1, 0, 1, 0};
int[] dy = {0, 1, 0, -1};
while (T-- > 0) {
int n = sc.nextInt();
char[][] grid = new char[n][n];
for (int i = 0; i < n; i++) {
grid[i] = sc.next().toCharArray();
}
// 找到起点 'p'
int sx = -1, sy = -1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 'p') {
sx = i; sy = j;
break;
}
}
if (sx != -1) break;
}
// visited[x][y][bite] : bite 取值 0 或 1
boolean[][][] visited = new boolean[n][n][2];
Queue<State> q = new LinkedList<>();
q.offer(new State(sx, sy, 0));
visited[sx][sy][0] = true;
boolean found = false;
while (!q.isEmpty()) {
State cur = q.poll();
int x = cur.x, y = cur.y, bite = cur.bite;
// 到达 Amaze
if (grid[x][y] == 'a') {
found = true;
break;
}
for (int d = 0; d < 4; d++) {
int nx = x + dx[d];
int ny = y + dy[d];
if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
char cell = grid[nx][ny];
// 金刚不可走
if (cell == 'k') continue;
int newBite = bite;
// 野狗:咬次数 +1
if (cell == 'd') {
newBite = bite + 1;
if (newBite >= 2) continue; // 被咬两次 → 死亡
}
if (!visited[nx][ny][newBite]) {
visited[nx][ny][newBite] = true;
q.offer(new State(nx, ny, newBite));
}
}
}
results.add(found ? "Yes" : "No");
}
for (String res : results) {
System.out.println(res);
}
sc.close();
}
}
第七章 动态规划法
1.鸡蛋掉落-两枚鸡蛋
题目描述
给你 2 枚相同 的鸡蛋,和一栋从第 1 层到第 n 层共有 n 层楼的建筑。(1 <= n <= 1000)
已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都 会碎 ,从 f 楼层或比它低 的楼层落下的鸡蛋都 不会碎 。
每次操作,你可以取一枚 没有碎 的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?
示例 1:
输入:n = 2
输出:2
解释:我们可以将第一枚鸡蛋从 1 楼扔下,然后将第二枚从 2 楼扔下。
如果第一枚鸡蛋碎了,可知 f = 0;
如果第二枚鸡蛋碎了,但第一枚没碎,可知 f = 1;
否则,当两个鸡蛋都没碎时,可知 f = 2。
示例 2:
输入:n = 100
输出:14
解释:
一种最优的策略是:
- 将第一枚鸡蛋从 9 楼扔下。如果碎了,那么 f 在 0 和 8 之间。将第二枚从 1 楼扔下,然后每扔一次上一层楼,在 8 次内找到 f 。总操作次数 = 1 + 8 = 9 。
- 如果第一枚鸡蛋没有碎,那么再把第一枚鸡蛋从 22 层扔下。如果碎了,那么 f 在 9 和 21 之间。将第二枚鸡蛋从 10 楼扔下,然后每扔一次上一层楼,在 12 次内找到 f 。总操作次数 = 2 + 12 = 14 。
- 如果第一枚鸡蛋没有再次碎掉,则按照类似的方法从 34, 45, 55, 64, 72, 79, 85, 90, 94, 97, 99 和 100 楼分别扔下第一枚鸡蛋。
不管结果如何,最多需要扔 14 次来确定 f 。
题解
点击查看Pyhton代码
import sys
def twoEggDrop(n: int) -> int:
"""
计算两枚鸡蛋、n 层楼时的最小最坏情况操作次数。
算法:动态规划,dp[i] 表示 i 层楼的最优解。
"""
# 创建一个长度为 n+1 的列表,dp[0] 默认为 0(0 层楼不需要操作)
dp = [0] * (n + 1)
# 边界条件:只有 1 层楼时,只需扔 1 次即可(无论碎不碎,都能确定 f)
if n >= 1:
dp[1] = 1
# 从 2 层楼开始,依次计算到 n 层楼
for i in range(2, n + 1):
# 先将当前 dp[i] 初始化为一个很大的数,以便后续取最小值
dp[i] = float('inf')
# 尝试第一次从第 j 层扔下鸡蛋,j 可以从 1 到 i
for j in range(1, i + 1):
# 情况1:鸡蛋碎了
# 那么临界楼层 f 一定小于 j,我们需要用剩下的 1 枚鸡蛋测试 1..j-1 层
# 由于只剩 1 枚鸡蛋,只能从 1 层开始逐层向上试,最坏情况需要试 j-1 次
# 加上第一次扔的 1 次,总共 j 次
broken = j
# 情况2:鸡蛋没碎
# 那么 f 一定 >= j,上面的 i-j 层(j+1 到 i)还需要测试
# 此时我们仍然拥有 2 枚鸡蛋(因为没碎),所以需要 1 + dp[i-j] 次
# 其中 dp[i-j] 是解决上面 i-j 层楼所需的最优操作次数
not_broken = 1 + dp[i - j]
# 因为无法控制结果是碎还是不碎,所以必须考虑最坏情况,取两者最大值
worst = max(broken, not_broken)
# 选择所有 j 中,使最坏情况最小的那个作为 dp[i] 的值
if worst < dp[i]:
dp[i] = worst
# 返回 n 层楼的结果
return dp[n]
def main() -> None:
"""
读取输入:一行或多行,包含多个整数(空格或换行分隔)。
对每个整数 n,调用 twoEggDrop(n) 计算结果,并输出一行一个结果。
"""
# 从标准输入读取全部内容,按空白字符分割(支持换行和空格)
data = sys.stdin.read().strip().split()
# 如果没有输入,直接返回
if not data:
return
# 对每个输入值,转换成整数,计算 twoEggDrop,然后转为字符串存入列表
results = [str(twoEggDrop(int(x))) for x in data]
# 将所有结果用换行连接,输出到标准输出
sys.stdout.write("\n".join(results))
if __name__ == "__main__":
main()
点击查看Java代码
import java.util.Scanner;
public class EggDrop {
/**
* 计算两枚鸡蛋、n 层楼的最小最坏情况操作次数。
* 动态规划:dp[i] 表示 i 层楼的最优解。
*
* @param n 楼层数(1 <= n <= 1000)
* @return 最小操作次数
*/
public static int twoEggDrop(int n) {
// dp[i] 数组,长度 n+1,dp[0] 自动为 0
int[] dp = new int[n + 1];
// 边界:只有 1 层楼,只需扔 1 次即可
if (n >= 1) {
dp[1] = 1;
}
// 从 2 层楼开始递推
for (int i = 2; i <= n; i++) {
// 初始化为一个很大的数,用于求最小值
dp[i] = Integer.MAX_VALUE;
// 尝试第一次从第 j 层扔下,j 从 1 到 i
for (int j = 1; j <= i; j++) {
// 情况1:鸡蛋碎了 → 剩余 1 枚鸡蛋,测试下面 j-1 层,共 j 次
int broken = j;
// 情况2:鸡蛋没碎 → 剩余 2 枚鸡蛋,测试上面 i-j 层,共 1 + dp[i-j] 次
int notBroken = 1 + dp[i - j];
// 最坏情况取最大值
int worst = Math.max(broken, notBroken);
// 取所有 j 中最小的最坏情况
if (worst < dp[i]) {
dp[i] = worst;
}
}
}
return dp[n];
}
public static void main(String[] args) {
// 创建 Scanner 对象读取标准输入
Scanner sc = new Scanner(System.in);
// 循环读取所有整数(支持空格或换行分隔)
while (sc.hasNextInt()) {
int n = sc.nextInt(); // 读取一个整数楼层数
int ans = twoEggDrop(n); // 调用算法函数计算
System.out.println(ans); // 输出结果,每个结果占一行
}
sc.close(); // 关闭 Scanner
}
}
2. 接龙数列
题目描述
对于一个长度为 \(K\) 的整数数列:\(A_{1},A_{2},\ldots,A_{K}\),我们称之为接龙数列当且仅当 \(A_{i}\) 的首位数字恰好等于 \(A_{i-1}\) 的末位数字(\(2 \leq i \leq K\))。
例如 \(12,23,35,56,61,11\) 是接龙数列;\(12,23,34,56\) 不是接龙数列,因为 \(56\) 的首位数字不等于 \(34\) 的末位数字。所有长度为 \(1\) 的整数数列都是接龙数列。
现在给定一个长度为 \(N\) 的数列 \(A_{1},A_{2},\ldots,A_{N}\),请你计算最少从中删除多少 个数,可以使剩下的序列是接龙序列?
输入格式
第一行包含一个整数 \(N\)。
第二行包含 \(N\) 个整数 \(A_{1},A_{2},\ldots,A_{N}\)。
输出格式
一个整数代表答案。
输入输出样例 #1
输入 #1
5
11 121 22 12 2023
输出 #1
1
说明/提示
【样例说明】
删除 \(22\),剩余 \(11,121,12,2023\) 是接龙数列。
【评测用例规模与约定】
对于 \(20 \%\) 的数据,\(1 \leq N \leq 20\)。
对于 \(50 \%\) 的数据,\(1 \leq N \leq 10^4\)。
对于 \(100 \%\) 的数据,\(1 \leq N \leq 10^{5}\),\(1 \leq A_{i} \leq 10^{9}\)。所有 \(A_{i}\) 保证不包含前导 0。
蓝桥杯 2023 省赛 B 组 E 题。
题解
点击查看Pyhton代码
import sys
def solve() -> None:
data = sys.stdin.read().strip().split()
if not data:
return
n = int(data[0])
# 读取接下来的 n 个数
nums = list(map(int, data[1:1+n]))
# dp[d] 表示当前以数字 d 结尾的最长接龙子序列长度
dp = [0] * 10
for x in nums:
# 提取首位和末位数字
s = str(x)
first = int(s[0]) # 首位
last = int(s[-1]) # 末位
# 当前数字可以接在任意以 first 结尾的序列后面
# 新的长度为 dp[first] + 1
# 同时自身可作为长度为 1 的新序列
dp[last] = max(dp[last], dp[first] + 1)
# 最长接龙子序列长度
longest = max(dp)
# 最少删除数量 = 总数 - 最长长度
print(n - longest)
if __name__ == "__main__":
solve()
点击查看Java代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = sc.nextInt();
}
sc.close();
// dp[d] 表示当前以数字 d 结尾的最长接龙子序列长度
int[] dp = new int[10];
for (int x : nums) {
// 提取首位和末位数字
String s = Integer.toString(x);
int first = s.charAt(0) - '0'; // 首位
int last = s.charAt(s.length() - 1) - '0'; // 末位
// 当前数字可接在任意以 first 结尾的序列后面
// 新长度为 dp[first] + 1,与原来的 dp[last] 取最大值
dp[last] = Math.max(dp[last], dp[first] + 1);
}
// 求最长接龙子序列长度
int longest = 0;
for (int len : dp) {
if (len > longest) longest = len;
}
// 最少删除数量
System.out.println(n - longest);
}
}
第八章 贪心法
1. 最低加油次数
题目描述
汽车从起点出发驶向目的地,该目的地位于出发位置东面 target 英里处。
沿途有加油站,用数组 stations 表示。其中 stations[i] = [positioni, fueli] 表示第 i 个加油站位于出发位置东面 positioni 英里处,并且有 fueli 升汽油。
假设汽车油箱的容量是无限的,其中最初有 startFuel 升燃料。它每行驶 1 英里就会用掉 1 升汽油。当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。
为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 -1。
注意:如果汽车到达加油站时剩余燃料为 0,它仍然可以在那里加油。如果汽车到达目的地时剩余燃料为 0,仍然认为它已经到达目的地。
示例
示例 1
输入:target = 1, startFuel = 1, stations = []
输出:0
解释:可以在不加油的情况下到达目的地。
示例 2
输入:target = 100, startFuel = 1, stations = [[10,100]]
输出:-1
解释:无法抵达目的地,甚至无法到达第一个加油站。
示例 3
输入:target = 100, startFuel = 10, stations = [[10,60],[20,30],[30,30],[60,40]]
输出:2
解释:
出发时有 10 升燃料。
开车来到距起点 10 英里处的加油站,消耗 10 升燃料。将汽油从 0 升加到 60 升。
然后,从 10 英里处的加油站开到 60 英里处的加油站(消耗 50 升燃料),并将汽油从 10 升加到 50 升。然后开车抵达目的地。
沿途在两个加油站停靠,所以返回 2。
提示
1 <= target, startFuel <= 10^90 <= stations.length <= 5001 <= positioni < positioni+1 < target1 <= fueli < 10^9
题解
点击查看Java代码
import java.util.PriorityQueue;
class Main {
/**
* 计算到达目的地所需最少加油次数,贪心+大根堆解法
* @param target 终点距离起点的英里数
* @param startFuel 汽车初始油量
* @param stations 加油站数组,每个元素 [位置, 油量],位置严格升序
* @return 最少加油次数;无法到达返回 -1
*/
public int minRefuelStops(int target, int startFuel, int[][] stations) {
// 大根堆:存储路过的加油站油量,每次取出油量最大的站点补油(贪心核心)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
// 记录加油总次数
int refuelCount = 0;
// 上一个经过的位置(初始为起点0)
int lastPosition = 0;
// 当前油箱剩余油量
int currentFuel = startFuel;
// 加油站总数量
int stationNum = stations.length;
// 循环遍历所有加油站 + 终点(i == stationNum 代表终点target)
for (int i = 0; i <= stationNum; i++) {
// 判断当前遍历到的位置:前n轮是加油站,最后一轮是终点
int currentPosition = i < stationNum ? stations[i][0] : target;
// 计算从上一个位置开到当前位置消耗的里程
int driveDistance = currentPosition - lastPosition;
// 行驶消耗对应油量,剩余油量减少
currentFuel -= driveDistance;
// 油量不足,回溯取之前经过油量最大的加油站补油
while (currentFuel < 0 && !maxHeap.isEmpty()) {
// 取出最大油量加入油箱
currentFuel += maxHeap.poll();
// 加油次数+1
refuelCount++;
}
// 补完所有路过站点的油依旧不够,直接判定无法到达
if (currentFuel < 0) {
return -1;
}
// 如果当前不是终点,把本站油量存入堆,更新上一站位置
if (i < stationNum) {
// 将当前加油站油量加入大根堆,后续缺油时取用
maxHeap.offer(stations[i][1]);
// 更新上一个途经位置为当前加油站
lastPosition = currentPosition;
}
}
// 成功抵达终点,返回最少加油次数
return refuelCount;
}
// 程序入口测试方法
public static void main(String[] args) {
Main solution = new Main();
// 示例3测试用例,预期输出2
int res = solution.minRefuelStops(100, 10, new int[][]{{10,60},{20,30},{30,30},{60,40}});
System.out.println(res);
}
}
2. 零钱兑换
题目描述
给你一个整数数组 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] <= 2^31 - 10 <= amount <= 10^4
题解
点击查看Java代码
import java.util.Arrays;
class Main {
/**
* LeetCode 322 零钱兑换:完全背包DP解法
* @param coins 硬币面额数组,每种硬币可无限使用
* @param amount 需要凑出的目标总金额
* @return 凑成amount的最少硬币数;无法凑出返回-1
*/
public int coinChange(int[] coins, int amount) {
// 定义一个不可能达到的最大值:最多全用1元硬币,最多amount枚,amount+1等价无穷大
int impossibleMax = amount + 1;
// dp数组:dp[i]代表凑出金额i需要的最少硬币数量
int[] dp = new int[amount + 1];
// 初始化dp数组全部填充为不可能的数值,代表初始状态无法凑出
Arrays.fill(dp, impossibleMax);
// 边界条件:金额0不需要任何硬币,硬币数量为0
dp[0] = 0;
// 外层循环:遍历从1到amount的所有金额
for (int targetMoney = 1; targetMoney <= amount; targetMoney++) {
// 内层循环:枚举每一种硬币面额
for (int coin : coins) {
// 当前硬币面额不能超过当前要凑的金额,否则无法选取该硬币
if (coin <= targetMoney) {
// 状态转移:不选当前硬币dp[targetMoney] 、选当前硬币dp[targetMoney - coin] + 1 取较小值
dp[targetMoney] = Math.min(dp[targetMoney], dp[targetMoney - coin] + 1);
}
}
}
// 如果dp[amount]仍大于amount,说明没有合法组合,返回-1;否则返回最少硬币数
return dp[amount] > amount ? -1 : dp[amount];
}
public static void main(String[] args) {
// 创建解题对象
Main solution = new Main();
// 测试用例1:coins=[1,2,5], amount=11,预期输出3
int result = solution.coinChange(new int[]{1,2,5}, 11);
System.out.println(result);
}
}
浙公网安备 33010602011771号