【蓝桥杯】练习题目合集(自用)-3

往期:
【蓝桥杯】练习题目合集(自用)-1
【蓝桥杯】练习题目合集(自用)-2

题单

  1. 离散化 🔜

  2. 贪心

  3. DFS


数学问题

模拟 / 思维 / 枚举

经典问题

小蓝的旅行计划 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 机加工,目标是最小化总完工时间)的最优排序算法。其核心思想是将工件分为两类,并分别按特定顺序排列,从而保证整体顺序最优。

image

使用法二实现该算法
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个线段中,找出最多的线段,使得它们互相不交叉。
  • 类似问题:活动安排问题

image

点击查看代码
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】的时间限制更大,所以需要二分查找。

image

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【反悔贪心】

第一道省选题🥰,好难😅

image

贪心策略(大佬题解):
image

点击查看代码
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皇后】【经典】

image

  • 用数组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

image

点击查看代码
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【剪枝】【回溯】

image

点击查看代码
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)
image

点击查看代码
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");
		}
	}
}
posted @ 2026-03-11 17:53  idle_life  阅读(65)  评论(0)    收藏  举报