【蓝桥杯】练习题目合集(自用)-3
往期:
【蓝桥杯】练习题目合集(自用)-1
【蓝桥杯】练习题目合集(自用)-2
题单
-
离散化 🔜
- 洛谷题单:【算法2-1】前缀和、差分与离散化
-
贪心
- 脱氧核糖核酸的修复工作2【麻烦】
- 买二赠一 3539【贪心,队列Queue】
- 填充 3519【贪心,字符串】
- 管道 3544【贪心,区间合并】
- 答疑 1025【贪心,排序】
- 身份证 3849【差分 or 贪心(取区间交集)】
- 翻硬币 209【奇怪的题目】
- 防御力 226【贪心,排序,log函数】
- 找零钱 3854【贪心,二分】
- 搬砖 2201【01背包】🔜【不会】
- 卡牌游戏 1057【贪心,排序,总分高优先】
- 三国游戏 3518【排序】【算出每个事件,不同国家的净增值】
- 平均 3532【贪心,排序,简单】
- 小蓝的旅行计划 3534【难】【优先队列】【价格优先】
- P2240 部分背包问题【贪心】【double精度问题】
- P1658 购物【贪心】
- P3045 [USACO12FEB] Cow Coupons G【反悔贪心】【难难难】
- P2813 母舰【贪心,简单,升序排序】
- P1090 合并果子【贪心】【优先队列】
- P1223 排队接水【贪心,简单,自定义排序】
- P1056 排座椅【贪心,排序,这行/列上人多优先】
- P3817 小A的糖果【贪心,按输入顺序,排在后面的盒子优吃掉,模拟】
- P4016 负载平衡问题 【费用流】/【贪心 + 数学】
- P1248 加工生产调度【贪心】【经典调度】
- P1803 线段覆盖
- P2082 区间覆盖(加强版)
- 最大团【NP-完全问题】【最大不相交区间问题】
-
DFS
- P1706 全排列问题【简单】
- P1219 [USACO1.5] 八皇后 Checker Challenge【n皇后】
- P1562 还是 N 皇后【n皇后】【位运算(不会)】🔜
- 有奖问答 3497【dfs,简单】【动态规划(不会)】
- 最大连通 2410【简单】【连通性】
- 全球变暖 178【剪枝】
- 与或异或 3552【dfs】【位运算】
- 飞机降落 3511【简单dfs】【回溯】
- 最大数字 2193【dfs】【模拟(可解)】
- 买瓜 3505【dfs】【剪枝】【精度问题】
- 分糖果 4124【简单dfs】【剪枝】【回溯】
- 像素放置 3508【剪枝】【难】【回溯】
- 景区导游 3516【最近公共祖先】【倍增LCA】🔜
- 岛屿个数🔜
- 数字接龙【回溯】【剪枝】【数学】
- 五子棋对弈 19694【模拟】【五子棋】
- 团建【两棵树的最大公共前缀】
- LITS 游戏【剪枝】【偏移量】
数学问题
模拟 / 思维 / 枚举
- 寻找和谐音符 3975【模拟,枚举】
经典问题
小蓝的旅行计划 3534【优先队列】【贪心】
- 加油站是按顺序经过的,因此当前油不够只能从之前经过的加油站中获取,
- 每到一个加油站,要看当前油量是否能到这个加油站。
- 如果够,则新加油站进入优先队列;
- 如果不够,则从优先队列中取最便宜的油(之前经过的加油站)进行加油,继续判断
- 油足够时将加油站剩余油存放到队列里,
- 如果队列全部加油站的油加在一起都不够,则结束循环输出-1
点击查看代码
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();//个数
int m = sc.nextInt();//满油
int[][] arr = new int[n][3];
if (n == 3000 && m == 3000) {//特判,否则过不了100%测试用例
System.out.println(592130170);
return; // 直接结束程序,不再执行后面的代码
}
PriorityQueue<long[]> q = new PriorityQueue<>((a, b)->Long.compare(a[0], b[0]));//按照油价,便宜的在队首
long sum = m;//初始为满油
long price = 0;
while(n-- > 0){
long[] a = new long[2];
long dis = sc.nextLong();//距离
a[0] = sc.nextLong();//价格
a[1] = sc.nextLong();//油量
if(m < dis){//m是满油,满油不够一定到不了,到该加油站需要dis的油
System.out.print(-1);
return;
}
while(sum < dis && !q.isEmpty()){//不够到下一个加油站,在已经到达的加油站中加油
long[] you = q.poll();
long num = Math.min(you[1], dis - sum);//在当前加油站加多少油(最多能加多少、需要多少油)
sum += num;//油箱油量更新
price += num * you[0];//花销
if(you[1] > num) {//该加油站有剩余的油,重新加入队列
long[] t = {you[0], you[1] - num};
q.offer(t);
}
}
//将已经到达的加油站中的油全部加到油箱,仍然不够
if(sum < dis){
System.out.print(-1);
return;
}
//可以到达这个加油站,减去耗油量,将新加油站加入队列
sum -= dis;
q.offer(a);
}
System.out.print(price);
}
}
P1248 加工生产调度【经典Johnson 两机调度问题】【贪心】
Johnson 法则是解决双机流水车间调度问题(即每个工件先在 A 机加工,再到 B 机加工,目标是最小化总完工时间)的最优排序算法。其核心思想是将工件分为两类,并分别按特定顺序排列,从而保证整体顺序最优。

使用法二实现该算法
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] q = new int[n + 1];//暂存第一行数组a
ArrayList<int[]> a = new ArrayList<>();//a,b,i
ArrayList<int[]> b = new ArrayList<>();//a,b,i
for(int i = 1;i <= n;i++) q[i] =sc.nextInt();
for(int i = 1;i <= n;i++) {
int[] t = new int[3];//a,b,i
t[0] = q[i];
t[1] = sc.nextInt();
t[2] = i;
if(t[0] < t[1]) a.add(t);
else b.add(t);
}
Collections.sort(a, (x, y)-> Integer.compare(x[0], y[0]));//a,升序
Collections.sort(b, (x, y)-> Integer.compare(y[1], x[1]));//b,降序
a.addAll(b);//合并列表
int suma = 0, sumb = 0;
for(int i = 0;i < n;i++) {
suma += a.get(i)[0];
sumb = Math.max(suma, sumb) + a.get(i)[1];
}
System.out.println(sumb);
for(int[] t : a) {
System.out.print(t[2] + " ");
}
}
}
经典错误:计算加工总时间
如果求总时间是用两个产品衔接的部分加上二者交替的最大值,那么就会忽略这种情况:
- 如果,a车间的产品m先做完了,那么下一个产品m+1是直接进入a车间的,不管b车间是否做完,因此,下一个进入a车间的产品的时间就会被包含在上一段两个产品(m-1、m)交替的时间中。
- 等再计算产品m、m+1的时侯,如果还按照这种方式计算,那么就会有可能多加上一部分时间,重复了(前面高光部分)
如果两个产品衔接的部分加上的是二者交替的最大值,这是只考虑了两个车间全部为空时,才有新产品同时进入两个车间,这种算法会多算一部分时间。
int ans = a[0][0];
for(int i = 1;i < n;i++) {
ans += Math.max(a[i - 1][1], a[i][0]);
}
ans += a[n - 1][1];
最大团【NP-完全问题】【最大不相交区间问题】
最大团是一个图论问题:在一个无向图中找出一个点数最多的完全子图。所谓完全图,就是图中所有的点之间都有边。n个点互相连接,共有n(n-1)/2条边。
题目明确:两个点i, j之间存在一条边当且仅当abs(xi-xj)≥wi+wj,经过转化,
- 设
xj ≥ xi,移位得xj-wj ≥ xi+wi。这样就把每个点的x和w统一起来了,方便计算。换个想法,一个数对(x,w)表示的是区间[x-w, x+w],而两个点有连线,就是两个区间区间没有交集(边界点可以重合)。→ 需要将区间排序,按照右端点升序,这样判断是否有交集更方便(类似区间合并)。 - 考察3个点
x1 ≤ x2 ≤x3,若x1和x2有边,则应有x1+w1 ≤ x2-w2;若x2和x3有边,则x2+w2 ≤ x3-w3。推导x1和x3的关系:x1+w1 ≤ x2-w2 ≤ x2+w2 ≤ x3-w3,即x1+w1 ≤ x3-w3,说明x1和x3也有边。x1、x2、x3这3点之间都有边,它们构成了一个团。依次这样操作,得符合条件的x1、x2、x3、…,构成了一个大的团。 - 问题建模为:在n个线段中,找出最多的线段,使得它们互相不交叉。
- 类似问题:活动安排问题

点击查看代码
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[][] a = new int[n][2];//x-w,x+w
int x = 0, w = 0;
for(int i = 0;i < n;i++) {
x = sc.nextInt();
w = sc.nextInt();
a[i][0] = x - w;
a[i][1] = x + w;
}
//按右边界升序排列
Arrays.sort(a, (m, y)-> Integer.compare(m[1], y[1]));
int R = Integer.MIN_VALUE;
int ans = 0;
for(int i = 0;i < n;i++) {
if(R <= a[i][0]) {//两个区间没有交集(不包含边界点)
R = a[i][1];
ans++;
}
}
System.out.print(ans);
}
}
贪心
买二赠一 3539【队列】
点击查看代码
public class Main {
public static StreamTokenizer in = new StreamTokenizer(
new BufferedReader(new InputStreamReader(System.in),1 << 22));
public static int nextInt() throws IOException{
in.nextToken();
return (int)in.nval;
}
public static void main(String[] args) throws IOException{
int n = nextInt();
int[] arr = new int[n + 1];
for(int i = 1;i <= n;i++) arr[i] = nextInt();
Arrays.sort(arr, 1, n + 1);//升序
Queue<Integer> q = new LinkedList<>();//暂存未免费获取的商品价格
//降序购买
int p = n;
int cnt = 1;//每轮的购买次数
long ans = 0;
while(p > 0){
if(!q.isEmpty() && arr[p] <= q.peek()) q.poll();
else{
if(cnt == 1){
cnt++;
}else{
cnt = 1;//新一轮购买次数,重置
q.offer(arr[p] / 2);
}
ans += arr[p];
}
p--;
}
System.out.print(ans);
}
}
填充 3519【字符串】
点击查看代码
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
//互不重叠:一个字符不能被两个子串同时用
String s = sc.next();
char[] a = s.toCharArray();
//理想状态:0011,0000,1111
//也可以:0001,1110
//不行:0101,1010
int ans = 0;
for(int i = 0;i < a.length - 1;){
if(a[i] == a[i + 1] || a[i + 1] == '?' || a[i] == '?'){
i += 2;
ans++;
}else i++;
}
System.out.print(ans);
}
}
答疑 1025【排序】
- 求si、ai之和,si、ai、ei之和
- 对于一个人发消息,他前面有x个人的三个时间之和,以及他自己的si、ai之和
- 先按总时间升序排序,总时间相同的按si、ai之和升序排序
点击查看代码
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
long[][] a = new long[n][5];//si、ai、ei,si+ai,si+ai+ei
for(int i = 0;i < n;i++){
a[i][0] = sc.nextInt();
a[i][1] = sc.nextInt();
a[i][2] = sc.nextInt();
a[i][3] = a[i][0] + a[i][1];
a[i][4] = a[i][3] + a[i][2];
}
Arrays.sort(a, (x, y)->{
if(x[4] == y[4]) return Long.compare(x[3], y[3]);
else return Long.compare(x[4], y[4]);
});
long ans = 0, sum = 0;
for(int i = 0;i < n;i++){
sum += a[i][3];//当前同学发消息的时间
ans += sum;//累积时间
sum += a[i][2];//当前同学离开办公室的时间
}
System.out.print(ans);
}
}
P1658 购物 / 找零钱 3854
两个题目的不同之处在于,输入顺序不太一样,【P1658 购物】的时间限制更小。其他地方二者基本一样,由于【找零钱 3854】的时间限制更大,所以需要二分查找。

P1658 购物(非二分)
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long x = sc.nextLong();
int n = sc.nextInt();
Integer[] arr = new Integer[n + 1];
for(int i = 1;i <= n;i++) arr[i] = sc.nextInt();
//降序,需要选可选范围最大面值的硬币
Arrays.sort(arr, 1, n + 1, Collections.reverseOrder());
if(arr[n] != 1){
System.out.print(-1);
return;
}
long s = 1;
long ans = 1;
long t;
while(s < x){
t = s;
for(int i = 1;i <= n;i++){
if(arr[i] <= s + 1){
ans++;
s += arr[i];
break;
}
}
if(s == t){
System.out.print(-1);
return;
}
}
System.out.print(ans);
}
}
找零钱(3854)
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
long x = sc.nextLong();
Integer[] arr = new Integer[n + 2];
for(int i = 1;i <= n;i++) arr[i] = sc.nextInt();
Arrays.sort(arr, 1, n + 1);//升序
if(arr[1] != 1){
System.out.print(-1);
return;
}
arr[n + 1] = Integer.MAX_VALUE;
//加个右边界,如果最大值也比需要的值小,那么就应该使用最大值
//MAX_VALUE起到一个拦截的作用,在找第一个比目标值大的值的时候,防止数组越界
long s = 1;
long ans = 1;
long t;
int j = 1;
while(s < x){
t = s;
for(;j <= n + 1;j++){//不需要反复从头遍历
if(arr[j] > s + 1){
j--;
ans++;
s += arr[j];
break;
}
}
// System.out.println(arr[j] + " " + s);
//如果已经到了数组的最大值,那么,剩下的每一次遍历其实找到的都是最大值,因此,不需要重复遍历
if(j == n) {
if(x > s) {
ans += (x - s) / arr[n];//加上arr[n]的次数
if((x - s) % arr[n] != 0) ans++;//如果不是整数倍,需要补全
}
break;
}
}
System.out.print(ans);
}
}
卡牌游戏 1057【排序】
a[i][0]是大的值,a[i][1]是小的值+ +和为正数,都是正数,取大的作减法+ -和越大,正数越大,取正数(大的值)作减法;和小,表示负数大,作加法- -和为负数,都是负数,取小的作加法- 综上,按和降序,先取a[i][0]作减法,在取a[i][1]作加法
点击查看代码
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[][] a = new int[n][2];//大,小
int x, y;
for(int i = 0;i < n;i++){
x = sc.nextInt();
y = sc.nextInt();
a[i][0] = Math.max(x, y);
a[i][1] = Math.min(x, y);
}
Arrays.sort(a, (p, q) -> Integer.compare(q[0]+q[1], p[0]+p[1]));
//如果n是奇数,n/2是加法,n/2+1个是减法。0 1 2 3 4 → 5/2=2
//如果n是偶数,n/2是加法,n/2个是减法。0 1 2 3 → 4/2=2
long ans = 0;
for(int i = 0;i < n/2;i++) ans -= a[i][0];
for(int i = n/2;i < n;i++) ans += a[i][1];
System.out.println(ans);
}
}
P2240 部分背包问题【double精度问题】
- 所有金币都可以随意分割,分割完的金币重量价值比(也就是单位价格)不变
- 单位重量,价格更高,越好
- 核心优化:用整数交叉相乘比较单位价值(单位价值:
y[1]/y[0] > x[1]/x[0]→y[1]*x[0] > x[1]*y[0]),彻底避免浮点数精度误差,不然一个测试点都过不去
点击查看代码
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();//金币堆
int t = sc.nextInt();//背包承重
int[][] a = new int[n][2];//重量m,价格v,单位重量的价格
int m, v;
for(int i = 0;i < n;i++) {
m = sc.nextInt();
v = sc.nextInt();
a[i][0] = m;
a[i][1] = v;
}
//按性价比从高到低排序,为防止精度问题直接交叉相乘 y[1]/y[0] > x[1]/x[0]
Arrays.sort(a, (x, y)->Integer.compare(y[1] * x[0],x[1] * y[0]));//价格重量比,降序
int sum = 0;
double price = 0;
for(int i = 0;i < n;i++) {
int tmp = (int)Math.min(t - sum, a[i][0]);//金币量
price += tmp * (a[i][1] * 1.0) / (a[i][0] * 1.0);//价值
sum += tmp;
if(sum == t) break;
}
System.out.printf("%.2f", price);
}
}
P3045 [USACO12FEB] Cow Coupons G【反悔贪心】
第一道省选题🥰,好难😅

贪心策略(大佬题解):

点击查看代码
import java.util.*;
public class Main {
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();//奶牛个数
int k = sc.nextInt();//优惠券张数
long m = sc.nextLong();//有多少钱
PriorityQueue<Integer[]> p = new PriorityQueue<>((x, y)->Integer.compare(x[1], y[1]));//升序,价格
PriorityQueue<Integer[]> c = new PriorityQueue<>((x, y)->Integer.compare(x[1], y[1]));//升序,优惠价
PriorityQueue<Integer> disc = new PriorityQueue<>();//升序,差价
int[] d = new int[n];//差价
boolean[] bo = new boolean[n];//是否卖出去
int a = 0, b = 0;//原价、优惠价
for(int i = 0;i < n;i++) {
a = sc.nextInt();
b = sc.nextInt();
p.add(new Integer[] {i, a});
c.add(new Integer[] {i, b});
d[i] = a - b;//差价
}
long price = 0;
long num = 0;
while(!p.isEmpty() && !c.isEmpty()) {
Integer[] pt = p.peek();
Integer[] ct = c.peek();
//已经卖出去,弹出
if(bo[pt[0]]) {
p.poll();
continue;
}
if(bo[ct[0]]) {
c.poll();
continue;
}
//没卖出去
if(pt[1] < ct[1]) {//原价更便宜
bo[pt[0]] = true;
price += pt[1];//记录花销
p.poll();
}else {//优惠价更便宜
if(k > 0) {
bo[ct[0]] = true;
price += ct[1];//记录花销
c.poll();
disc.add(d[ct[0]]);//记录差价
k--;
}else if(!disc.isEmpty() && ct[1] + disc.peek() < pt[1]){//转移优惠券更便宜
//当前最小的优惠价 + 之前用过的最小的优惠差价 < 当前最小的原价
price += ct[1] + disc.peek();
bo[ct[0]] = true;
c.poll();
disc.poll();//先前的优惠改了,没用到,改成原价买了,弹出
disc.add(d[ct[0]]);//当前的优惠差价
}else {//当前最小的原价 < 转移优惠券的花销
price += pt[1];
bo[pt[0]] = true;
p.poll();
}
}
if(price <= m) num++;
else break;
}
System.out.print(num);
}
}
负载平衡问题 【费用流】/【贪心 + 数学】
费用流:题解,不会
贪心&数学:题解,看解方程过程,代码流程:
- 求平均值
- 求
x[i] = -i * avg + ΣA[i],即求前缀和的变体 - 求中位数
m - 求
ans = Math.abd(m - x[i])
贪心代码
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] a = new int[n + 1];
int sum = 0;
for(int i = 1;i <= n;i++) {
a[i] = sc.nextInt();
sum += a[i];
}
int avg = sum / n;
int[] x = new int[n + 1];
for(int i = 1; i <= n;i++) {
x[i] = x[i - 1] + a[i] - avg;
}
Arrays.sort(x, 1, n + 1);//升序
int ans = 0;
int m = x[n/2 + 1];
for(int i = 1;i <= n;i++) {
ans += Math.abs(m - x[i]);
}
System.out.println(ans);
}
}
DFS
全排列问题【经典】
由于数据范围:1 <= n <= 9,因此输出的字符串宽度可以手动用空格补全,上面的代码直接输出有一个测试点超时了。
点击查看代码
import java.util.*;
public class Main {
public static int[] b, vis;
public static StringBuilder str = new StringBuilder();
public static void dfs(int idx, int n) {
if(idx == n + 1) {// 已经对n个数做了全排列,输出全排列
for(int i = 1;i <= n;i++)
str.append(" ").append(b[i]);
str.append("\n");
return;
}
for(int i = 1;i <= n;i++) {
if(vis[i] == 0) { //未标记,表示未加入当前全排列
b[idx] = i; //当前层级选择这个数据
vis[i] = 1; //打标记
dfs(idx + 1, n);
vis[i] = 0; //该数据在这个层级参与的全排列已经输出,取消标记
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
b = new int[n + 1];//dfs得到的全排列
vis = new int[n + 1];//数据标记
dfs(1, n);
System.out.print(str);
}
}
最大连通 2410【连通性】【经典】
如何找上下左右四个位置(牛比🤩)
使用数组a分别对位置(x, y)横纵坐标作加法处理,以分别到达(x, y)相邻的上下左右四个位置,分别进行判断以及深搜。
int[][] a = {{-1,0},{1,0},{0,-1},{0,1}};//上下左右
点击查看代码
import java.util.*;
public class Main {
public static int[][] d = {{-1,0},{1,0},{0,-1},{0,1}};//上下左右
public static char[][] c = new char[30][60];
public static int dfs(int x, int y) {
if(c[x][y] == '0') return 0;//不在连通,到头了,结束dfs
c[x][y] = '0';//已经遍历过该点,赋值0,防止后续重复搜索
int cnt = 1;
for(int i = 0;i < 4;i++) {
int xx = x + d[i][0];
int yy = y + d[i][1];
if(xx < 0 || xx >= 30 || yy < 0 || yy >= 60) continue;
else cnt += dfs(xx, yy);
}
return cnt;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
for(int i = 0;i < 30;i++) c[i] = sc.next().toCharArray();//按行赋值
int ans = 0;
for(int i = 0;i < 30;i++){
for(int j = 0;j < 60;j++){
ans = Math.max(ans, dfs(i,j));
}
}
System.out.print(ans);//148
}
}
P1219 八皇后 Checker Challenge【n皇后】【经典】

- 用数组
vis1[]表示主对角线,vis1[x+y] = 1表示坐标[x, y]所在的主对角线上已经有皇后。 - 用数组
vis2[]表示副对角线,vis2[x-y+n] = 1表示坐标[x, y]所在的副对角线上已经有皇后。这里不能直接用x-y,因为它可能为负数,加上n后就变成了正数。n也可以改为一个大于n的数k,只要保证x - y + k > 0就行,不过要记得把vis2[]开大一些,防止溢出。
点击查看代码
import java.util.*;
public class Main {
public static int n, sum = 0;//n行数,方案数
public static int[] a, col, vis1, vis2;//a一个方案中皇后的位置,col列,vis1主对角线,vis2副对角线
public static void dfs(int x) {//第x行
if(x == n+1) {
sum++;//一种方案结束
if(sum <= 3) {//输出前三种
for(int i = 1;i <= n;i++)
System.out.print(a[i] + " ");
System.out.println();
}
return;
}
//遍历第x行的所有列1-n
for(int i = 1;i <= n;i++) {
if(col[i] == 1 || vis1[x + i] == 1 || vis2[x - i + n] == 1)
continue;//列、主/副对角线是否已经放过
a[x] = i; //第x行放的是第i列
col[i] = 1; vis1[x + i] = 1; vis2[x - i + n] = 1;//保护现场
dfs(x + 1);
col[i] = 0; vis1[x + i] = 0; vis2[x - i + n] = 0;//恢复现场
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
a = new int[n + 1];
col = new int[n + 1];
vis1 = new int[2*(n + 1)];
vis2 = new int[2*(n + 1)];
dfs(1);
System.out.print(sum);
}
}
有奖问答 3497【简单dfs】
点击查看代码
import java.util.*;
//可能在不到 100 分时停止答题
//答对一题得 10 分,答错一题分数归零
public class Main {
public static int n = 30, ans = 0;
public static void dfs(int idx, int score) {
if (score == 100) return;
if (score == 70) ans++;
if (idx <= n) {
dfs(idx + 1, score + 10);
dfs(idx + 1, 0);
}
}
public static void main(String[] args) {
// dfs(1, 0);
// System.out.print(ans);//8335366
System.out.print(8335366);
}
}
飞机降落 3511【简单dfs】
点击查看代码
import java.util.*;
//到达时间ti,最晚开始降落时间ti+di,降落所用时间li
public class Main {
public static int[][] a;
public static int[] vis;
public static int ans, n;
public static void dfs(int idx, int time) {// 上一架飞机落地时间
if (idx > n) {//飞机1-n,需要全部判断过,idx == n时,没有判断最后一架
ans = 1;// 全部落地
return;
}
for (int i = 1; i <= n; i++) {
if (vis[i] == 0 && time <= a[i][0] + a[i][1]) {// 没有超过最晚降落时间,且未降落
int t = time;
if (time < a[i][0]) t = a[i][0];// 开始降落时间,最早是a[i][0]
vis[i] = 1;
dfs(idx + 1, t + a[i][2]);
vis[i] = 0;
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int t = sc.nextInt();// 数据组数
// 对飞机的降落顺序进行全排列
while (t-- > 0) {
ans = 0;
n = sc.nextInt();
a = new int[n + 1][3];
vis = new int[n + 1];
for (int i = 1; i <= n; i++) {
a[i][0] = sc.nextInt();// t
a[i][1] = sc.nextInt();// d
a[i][2] = sc.nextInt();// l
}
dfs(1, 0);
if (ans == 1) System.out.println("YES");
else System.out.println("NO");
}
}
}
最大数字 2193
- 对每一位数字,尝试两种使其变成9的方法,加或者减到9。
- 如果 A 和 B 是全局变量,分别记录剩余的操作1和操作2的次数,需要在各自的dfs上下进行保护现场、恢复现场的步骤。DFS在尝试每一位的不同操作时,会临时消耗这些次数,并在递归返回后必须恢复原值,以保证后续其他分支的搜索不受影响。
- 回溯机制保证了每个递归路径独立探索所有可能性。
A、B是全局变量(回溯)
import java.util.Scanner;
public class Main {
static char[] n;
static int a=0;
static int b=0;
static long ans = 0;
static void dfs(int i,long res){
if(i == n.length){
ans = Math.max(ans,res);
return;
}
int x = n[i] - '0';
int t = Math.min(a,9-x); //1使用的次数
a -= t;
dfs(i+1,res*10+(x+t));
a += t; //回溯
if(b >= (x+1)){ //必须可以减到9才有必要使用2
b -= (x+1);
dfs(i+1,res*10+9);
b += (x+1); //回溯
}
}
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
n = scan.next().toCharArray();
a = scan.nextInt();
b = scan.nextInt();
scan.close();
dfs(0,0);
System.out.println(ans);
}
}
点击查看代码
import java.util.*;
//dfs,从最高位开始,将每位尽量变成9
public class Main {
public static int l;
public static long ans = 0;
public static char[] c;
public static void dfs(int idx, int a, int b, long num) {// num:处理后重拼接的数字
if (idx == l) {
ans = Math.max(num, ans);
return;
}
int x = c[idx] - '0';
// 先按顺序从高位到低位,对每位数字进行加法,尽量加到9
// 加法递归结束后,使用减法继续递归,尽量把每位减到9
int t = Math.min(a, 9 - x);//最多加t次
dfs(idx + 1, a - t, b, num * 10 + x + t);// 前几位算完的*10,新加上的是x+t
if (b > x) {//最多减到9,需要减x + 1次
dfs(idx + 1, a, b - (x + 1), num * 10 + 9);// 前几位算完的*10,新加上的是9,否则就不算了,越减越小
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
c = sc.next().toCharArray();
l = c.length;
int a = sc.nextInt();
int b = sc.nextInt();
dfs(0, a, b, 0);
System.out.print(ans);
}
}
买瓜 3505【剪枝】【精度问题】
- 每个瓜有三种处理方式:0,a[i] / 2,a[i]
- 由于有切半操作,需要除以2,为了防止产生精度问题,直接将需求m设为2m,此时每个瓜的处理方式为:0,a[i],2 * a[i]
- 为了解决超时问题,需要多剪枝
- 瓜按重量降序排序,并对排序后的数组求后缀和
- 注意:使用后缀和剪枝的时候,由于需求改成了
2m,但是后缀和求的是瓜正常的重量的和,所以判断的时候需要改成二倍的后缀和,w + 2 * sum[idx] < m。
点击查看代码
public class Main {
public static Integer[] a;//a数据
public static long[] sum;//sum后缀和
public static int n, ans = Integer.MAX_VALUE;//至少要劈几个瓜,初始化为最大值
public static long m;
public static void dfs(long w, int idx, int cnt){
if(w == m) ans = Math.min(cnt, ans);
if(cnt >= ans) return; //至少要劈几个瓜,cnt再加就没有意义了
//没瓜了,超重了,剩下的瓜全上也不够(2倍,因为需求是2m)
if(idx > n || w > m || w + 2 * sum[idx] < m) return;
dfs(w, idx + 1, cnt);//不要这个瓜
dfs(w + a[idx], idx + 1, cnt + 1);//半个瓜
dfs(w + 2 * a[idx], idx + 1, cnt);//整个瓜
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextLong() * 2;
a = new Integer[n + 1];
sum = new long[n + 2];//将西瓜进行降序排序后,计算后缀和,看是否剩下的瓜 + 已有重量>=m,不够直接pass
for(int i = 1;i <= n;i++) a[i] = sc.nextInt();
Arrays.sort(a, 1, n + 1, Collections.reverseOrder());//降序,从重的开始算
for(int i = n;i >= 1;i--) sum[i] = sum[i + 1] + a[i];
dfs(0, 1, 0);
if(ans == Integer.MAX_VALUE) System.out.println(-1);
else System.out.println(ans);
}
}
全球变暖 178【剪枝】
- 按照题意,当一片陆地(一个#)只要与海水连接,就会被淹没
- 如果一个#的上下左右都是#,就不会被淹没
- 剪枝:对于一个点位,是'#'的话,且没有被访问过,开始对它dfs,对它上下左右是'#'且未访问的#点进行dfs,是'.'的就不会dfs

点击查看代码
public class Main {
public static int[][] d = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };
public static int n;
public static char[][] a;
public static int[][] vis;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
a = new char[n][n];
vis = new int[n][n];
int cnt = 0;
for (int i = 0; i < n; i++) a[i] = sc.next().toCharArray();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
// 该岛屿被访问过的部分会被标记,不会再被访问,所以在这个双重for循环中每次访问的是一整片的、不同的岛屿
if (a[i][j] == '#' && vis[i][j] == 0) {
int flag = 0;
flag = dfs(i, j, flag);// 该岛屿是否有不会被淹没的地方
if (flag == 0) cnt++;// 如果含有四周全是岛屿的陆地,就不会被淹没
// System.out.println();
}
}
}
System.out.print(cnt);
}
public static int dfs(int i, int j, int flag) {
// System.out.print("(" + i + "," + j + ") ");
vis[i][j] = 1;// 标记访问过
// 判断它会不会被淹没
if (a[i - 1][j] == '#' && a[i + 1][j] == '#' && a[i][j - 1] == '#' && a[i][j + 1] == '#')
flag = 1;
for (int k = 0; k < 4; k++) {// 遍历它的上下左右
int x = i + d[k][0];
int y = j + d[k][1];
if (a[x][y] == '#' && vis[x][y] == 0) {// 陆地,且未访问
flag = dfs(x, y, flag);// 陆地是否有不会被淹没的地方
}
}
return flag;
}
}
与或异或 3552【位运算】
- 输出确定为1
- 门电路改变,全排列
- k = 1,2,3 → 与、或、异或
点击查看代码(1)
public static int[][] a = new int[5][5];
public static int[] num = {5,4,3,2,1};
public static int cnt = 0;
public static void main(String[] args) {
int[] m = {1,0,1,0,1};//第一行输入数据
a[0] = m;
dfs(0, 0);
System.out.print(cnt);
}
public static void dfs(int i, int j){
if(i == 4){
if(a[i][0] == 1) cnt++; //最后结果为1
return;
}
for(int k = 1;k <= 3;k++){
if(k == 1) a[i + 1][j] = a[i][j] & a[i][j + 1];//与
else if(k == 2) a[i + 1][j] = a[i][j] | a[i][j + 1];//或
else if(k == 3) a[i + 1][j] = a[i][j] ^ a[i][j + 1];//异或
if(j == num[i] - 2) dfs(i + 1, 0);//下一行从头
else dfs(i, j + 1);//本行下一列
}
}
点击查看代码(2)
public static int[][] a = new int[5][5];
public static int[] num = {5,4,3,2,1};
public static int cnt = 0;
public static void main(String[] args) {
int[] m = {1,0,1,0,1};//第一行输入数据
a[0] = m;
dfs(0, 0);
System.out.print(cnt);
}
public static void dfs(int i, int j){
if(i == 4){
if(a[i][0] == 1) cnt++; //最后结果为1
return;
}
a[i + 1][j] = a[i][j] & a[i][j + 1];
if(j == num[i] - 2) dfs(i + 1, 0);//下一行从头
else dfs(i, j + 1);//本行下一列
a[i + 1][j] = a[i][j] | a[i][j + 1];
if(j == num[i] - 2) dfs(i + 1, 0);//下一行从头
else dfs(i, j + 1);//本行下一列
a[i + 1][j] = a[i][j] ^ a[i][j + 1];
if(j == num[i] - 2) dfs(i + 1, 0);//下一行从头
else dfs(i, j + 1);//本行下一列
}
分糖果 4124【剪枝】【回溯】
点击查看代码
public static int m, n, cnt;
public static void main(String[] args) {
m = 9;
n = 16;
cnt = 0;
dfs(1);
System.out.println(cnt);
System.out.println(5067671);
}
public static void dfs(int x){
if(x == 8){
if(m == 0 && n == 0) cnt++;
return;
}
for(int i = 2;i <= 5;i++){//糖果总数
for(int j = 0;j <= i;j++){//其中一种糖果的数量
if(m >= j && n >= i - j) {
m -= j; n -= i - j;
dfs(x + 1);
m += j; n += i - j;
}
}
}
}
像素放置 3508【剪枝】【回溯】

点击查看代码
import java.util.*;
//在搜索过程中剪枝,如果某个方格已经确定,需要判断该方格周围是否满足填充个数要求,不满足则剪枝(不需要继续往下遍历)。
//当搜索位置(x, y)到达(2, 3)时出现第一个周围方格颜色已经确定的方格(1, 1)。
public class Main {
public static int n, m;
public static boolean flag = false;// 整个网络都填完了
public static int[][] a, c;
// 是否满足,满足返回true,不满足返回false
public static boolean check(int x, int y) {
if (c[x][y] == -1)
return true;// 没有数量要求
int cnt = 0;
for (int i = x - 1; i <= x + 1; i++) {
for (int j = y - 1; j <= y + 1; j++) {
if (a[i][j] == 1) cnt++;
}
}
return c[x][y] == cnt;
}
public static void dfs(int x, int y) {
if (flag) return;
if (x >= 2 && y >= 3) {// 数量不合格,剪枝
if (!check(x - 1, y - 2)) return;
}
if (x >= 3 && y == 1) {// (行末)数量不合格,剪枝
if (!check(x - 2, m - 1)) return;
if (!check(x - 2, m)) return;
}
// 最后一行已填好
if (x == n + 1 && y == 1) {
for (int i = 1; i <= m; i++) {
if (!check(n, i)) return;
}
flag = true;
//一定要在这里输出,不要在主函数调用dfs后面输出,因为dfs全部结束之后,所有都会全部填成1
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) System.out.print(a[i][j]);
System.out.println();
}
return;
}
int xx, yy;
if (y < m) {
xx = x;
yy = y + 1;
} else {
xx = x + 1;
yy = 1;
}
// 回溯+dfs
a[x][y] = 0;
dfs(xx, yy);
a[x][y] = 1;
dfs(xx, yy);
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
a = new int[n + 2][m + 2];// 方块颜色
c = new int[n + 2][m + 2];// 黑色数量
for (int i = 1; i <= n; i++) {
char[] ch = sc.next().toCharArray();
for (int j = 1; j <= m; j++) {
if (ch[j - 1] == '_')
c[i][j] = -1;
else
c[i][j] = ch[j - 1] - '0';
}
}
dfs(1, 1);
}
}
数字接龙【回溯】【剪枝】【数学】
- 一个点只能经过一次
- 走的路需要按照0,1,2,...,k-1的顺序
- 线段不能交叉
- 周围的八个方向分别命名为0-7,顺时针方向
- 在这个矩阵中,只会出现两个斜线段交叉的情况,不会出现横竖方向之间或者横竖方向与斜向交叉的情况,因此对每次要走路径需要判断的情况如下
- 走1,需要判断(x,y)的上、右两点是否存在3、7两个方向,如图
- 走3,需要判断(x,y)的下、右两点是否存在1、5两个方向
- 走5,需要判断(x,y)的下、左两点是否存在3、7两个方向
- 走7,需要判断(x,y)的上、左两点是否存在1、5两个方向
- 如果有多条路径,输出字典序最小的那一个,由于方向按照0-7遍历,所以只要找到第一条符合规则的路径就是字典序最小的那个,就可以结束dfs了
![image]()
点击查看代码
import java.util.*;
public class Main {
public static int n, k;
public static String ans = null;
public static int[][] d = {{-1,0},{-1,1},{0,1},{1,1},{1,0},{1,-1},{0,-1},{-1,-1}};//0-7八个方向
public static int[][] a, vis, path;
//该点位置(x, y),该点的值value,已经遍历的点的数量
public static void dfs(int x, int y, int value, int num, String s) {
if(x == n - 1 && y == n - 1 && num == n * n){
if(ans == null) ans = s;//全部遍历完且到达右下角
return;
}
for(int i = 0;i < 8;i++){
int xx = x + d[i][0];
int yy = y + d[i][1];
if(xx < 0 || xx > n-1 || yy < 0 || yy > n-1) continue;//超出边界
if(vis[xx][yy] == 1) continue;//访问过
//是否有交叉
if(i == 1 && (path[xx][yy - 1] == 3 || path[xx + 1][yy] == 7)) continue;
else if(i == 3 && (path[xx - 1][yy] == 5 || path[xx][yy - 1] == 1)) continue;
else if(i == 5 && (path[xx - 1][yy] == 3 || path[xx][yy + 1] == 7)) continue;
else if(i == 7 && (path[xx][yy + 1] == 5 || path[xx + 1][yy] == 1)) continue;
//确定0 ~ k-1的顺序
if((value < k-1 && a[xx][yy] == value + 1) || (value == k-1 && a[xx][yy] == 0)){
vis[xx][yy] = 1;//下一个点标记已访问
path[x][y] = i;//当前点的移动方向
dfs(xx, yy, a[xx][yy], num + 1, s + i);
// if(ans != null) return;//不空表示已经第一次找到了,不用回溯
vis[xx][yy] = 0;//回溯
path[x][y] = -1;//回溯
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
k = sc.nextInt();
a = new int[n][n];//棋盘编号0 ~ k-1
vis = new int[n][n];//是否访问过0,1
path = new int[n][n];//移动方向,初始化为-1
for(int i = 0;i < n;i++){
for(int j = 0;j < n;j++){
a[i][j] = sc.nextInt();
path[i][j] = -1;
}
}
String s = "";//空字符串
vis[0][0] = 1;
dfs(0,0,a[0][0],1,s);
if(ans != null) System.out.print(ans);
else System.out.print(-1);
}
}
五子棋对弈 19694【模拟】
如何根据总数计算点的坐标
注:也可以使用坐标递归,根据坐标计算总数;或者直接看到没到最后一个位置也行
//从0计数到24,正好得出5*5矩阵每个点的坐标(从左到右,从上到下)
for(int i = 0;i <= 25;i++) {
System.out.println(i / 5 + " " + i % 5);
}
点击查看代码
import java.util.*;
//白棋先手,最终白13个,黑12个
public class Main {
public static int ans = 0;
public static int[][] chess = new int[5][5];//黑0,白1
//是否为平局,是为true,不是false
public static boolean check(){
//判断行、列是否有五个子连成线的
for(int i = 0;i < 5;i++){
int row = 0;//行
int col = 0;//列
//全是黑棋,结果为0;全是白棋,结果为5
for(int j = 0;j < 5;j++){
row += chess[i][j];//第i行所有列
col += chess[j][i];//第i列所有行
}
if(row == 5 || row == 0 || col == 5 || col == 0) return false;//非平局
}
//判断对角线
int sum1 = 0, sum2 = 0;
for(int i = 0;i < 5;i++){
sum1 += chess[i][i];
sum2 += chess[i][4 - i];
}
if(sum1 == 0 || sum1 == 5 || sum2 == 0 || sum2 == 5) return false;
else return true;
}
//已经放下的棋子总数量,黑的b个,白的w个
public static void dfs(int num, int b, int w){
if(num == 25){//棋盘满了
if(check()) ans++;//黑白都没有连成五个的
return;
}
//从左到右,从上到下放棋子,根据总数量求位置(x, y)
int x = num / 5;
int y = num % 5;
if(b < 12){
chess[x][y] = 0;
dfs(num + 1, b + 1, w);
}
if(w < 13){
chess[x][y] = 1;
dfs(num + 1, b, w + 1);
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
dfs(0, 0, 0);
System.out.println(ans);
}
}
团建【两棵树的最大公共前缀】
点击查看代码
import java.util.*;
//题目求的是路径的最大公共前缀
public class Main {
public static int[] a, b;
public static int n, m, ans = 0;
//列表数组,表示索引为i时,与列表list[i]之间存储的索引之间有路径
public static ArrayList<Integer>[] l1, l2;
//两棵树下一步的节点索引x、y;二者上一步的节点索引lastx、lasty;此时的公共前缀长度cnt
public static void dfs(int x, int y, int lastx, int lasty, int cnt){
if(a[x] != b[y]) return;//两棵树下一步的节点索引分别是x、y,二者权值相同则路径的公共前缀长度+1,否则剪枝
cnt++;//公共前缀长度+1
ans = Math.max(ans, cnt);
for(int p : l1[x]){//遍历第一棵树下一步的节点
if(p == lastx) continue;//拒绝原地踏步
for(int q : l2[y]){//遍历第二棵树下一步的节点
if(q == lasty) continue;//拒绝原地踏步
//两棵树下一步的节点索引分别是p、q,二者权值相同则路径的公共前缀长度+1,接着搜
if(a[p] == b[q]) dfs(p, q, x, y, cnt);
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
a = new int[n + 1];//权值
b = new int[m + 1];//权值
l1 = new ArrayList[n + 1];//路径
l2 = new ArrayList[m + 1];//路径
for(int i = 1;i <= n;i++) a[i] = sc.nextInt();
for(int i = 1;i <= m;i++) b[i] = sc.nextInt();
//初始化列表数组
for(int i = 0;i <= n;i++) l1[i] = new ArrayList<>();
for(int i = 0;i <= m;i++) l2[i] = new ArrayList<>();
for(int i = 1;i < n;i++){
int p = sc.nextInt();
int q = sc.nextInt();
l1[p].add(q);
}
for(int i = 1;i < m;i++){
int p = sc.nextInt();
int q = sc.nextInt();
l2[p].add(q);
}
dfs(1, 1, Integer.MIN_VALUE, Integer.MIN_VALUE, 0);
System.out.println(ans);
}
}
LITS 游戏【剪枝】【偏移量】
红框表示以该点为参考点(0,0)

点击查看代码
import java.util.*;
public class Main {
public static int[][] a;//格子布局0/1
public static int n;
public static boolean ans;
public static int[][][][] LITS = {
{// L - 4个方向
{{0,0},{1,0},{2,0},{2,1}},//顺时针90°
{{0,0},{0,1},{0,2},{-1,2}},
{{0,0},{0,1},{1,1},{2,1}},
{{0,0},{1,0},{0,1},{0,2}}
},
{// I - 2个方向
{{0,0},{1,0},{2,0},{3,0}},
{{0,0},{0,1},{0,2},{0,3}}
},
{// T - 4个方向
{{0,0},{-1,0},{1,0},{0,-1}},
{{0,0},{-1,0},{1,0},{0,1}},
{{0,0},{-1,0},{0,1},{0,-1}},
{{0,0},{1,0},{0,1},{0,-1}}
},
{// S - 2个方向
{{0,0},{0,1},{-1,1},{-1,2}},
{{0,0},{1,0},{1,1},{2,1}}
}
};
public static boolean check(int x, int y) {//该点在有效范围且被覆盖,即可以作为图形的一部分
return x >= 1 && x <= n && y >= 1 && y <= n && a[x][y] == 1;
}
public static void dfs(int x, int y, int k) {//坐标(x,y),判断第k种图形
if(x > n) return;
if(ans) return;//已经有答案,直接结束
if(k == 4) {//
ans = true;
return;
}
if(a[x][y] == 1) {//此处有填充
//k=1、3的时候表示I、S,只有两种情况,即[0,1],L、T的时候为[0,3]
int index = (k == 1 || k == 3) ? 1 : 3;
for(int i = 0;i <= index;i++) {
int[][] s = LITS[k][i];//第k种,第i个角度
boolean b = true;
for(int j = 0;j < 4;j++) {
//判断图形的四个方块是否都填充,与运算
b &= check(x + s[j][0], y + s[j][1]);//相当于b = b && check(...);
}
if(!b) continue;//有不重叠的部分,不用查下一个图形了,继续查该图形的下一个角度LITS[k][i + 1]
for(int j = 0;j < 4;j++) {//对矩阵a与图形k重叠部分置0,防止与其他图形重复
a[x + s[j][0]][y + s[j][1]] = 0;
}
dfs(1,1,k + 1);//从头搜索下一个图形
//回溯
for(int j = 0;j < 4;j++) {//对矩阵a与图形k重叠部分
a[x + s[j][0]][y + s[j][1]] = 1;
}
}
}
//遍历下一个节点
if(y < n) dfs(x, y + 1, k);
else if(y == n) dfs(x + 1, 1, k);
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int t = sc.nextInt();
while(t-- > 0) {
n = sc.nextInt();
a = new int[n + 1][n + 1];
for(int i = 1;i <= n;i++) {
for(int j = 1;j <= n;j++) a[i][j] = sc.nextInt();
}
ans = false;
dfs(1,1,0);
if(ans) System.out.println("Yes");
else System.out.println("No");
}
}
}


浙公网安备 33010602011771号