2025年第七届全国高校计算机能力挑战赛初赛 Java组 编程题汇总

编程题汇总(算法进阶练习)

整体难度说明

以下题目涵盖贪心、BFS、动态规划、KMP、并查集等经典算法,适配算法进阶阶段学习者。

难度可参考牛客竞赛的3至6题,适合巩固基础算法应用与复杂场景处理能力。

文章目录

  • 编程题汇总(算法进阶练习)
  • 一、魔法药水平分问题
    • [1. 魔法药水平分(通用版)](#1. 魔法药水平分(通用版))
    • [2. 魔法药水平分(特定容量版)](#2. 魔法药水平分(特定容量版))
  • 二、农田作物收获问题
    • [1. 农田作物收获(每日统计版)](#1. 农田作物收获(每日统计版))
    • [2. 农田作物收获(总量统计版)](#2. 农田作物收获(总量统计版))
  • 三、DNA片段查找问题
    • [1. DNA片段查找(计数+位置版)](#1. DNA片段查找(计数+位置版))
    • [2. DNA片段查找(位置分行版)](#2. DNA片段查找(位置分行版))
  • 四、城市通信网络问题
    • [1. 城市通信网络(查询带详情版)](#1. 城市通信网络(查询带详情版))
    • [2. 城市通信网络(简洁查询版)](#2. 城市通信网络(简洁查询版))

一、魔法药水平分问题

相似题目差异总结

  • 核心差异:第二题新增约束条件(药水总量S = 两只魔法瓶容量之和N+M,且S < 101),输出格式简化(无需输出"YES"前缀,仅输出次数);核心算法一致采用BFS搜索最短操作路径。
  • 核心算法:BFS遍历所有倒水状态(避免重复访问),通过状态转移寻找平分方案,时间复杂度O(SNM),适配小范围容量数据。
  • 优化点:使用哈希集合记录已访问状态,避免循环遍历;采用队列存储状态与操作步数,确保找到最短路径。

1. 魔法药水平分(通用版)

题目描述:在魔法学院里,巫师小Y发现了一种神奇的魔法药水,总量为S毫升。学院规定,每次使用药水前,必须将药水平均分给小Y和他的伙伴小Z。小Y手中有两只不同容量的魔法瓶,容量分别为N毫升和M毫升。三个容器(原药水容器、N毫升瓶、M毫升瓶)之间可以相互倒药水,但容器上没有刻度,无法精确控制倒出量(仅能倒满或倒尽)。现在需要判断是否能通过一系列倒水操作,将药水均分为两份(每份S/2毫升)。如果可以,请输出最少操作次数;如果不可能,则输出"NO"。

运行条件

  • 总时限:5000毫秒
  • 单组时限:1000毫秒
  • 总内存:320 MB
  • 单组内存:64 MB

输入格式

  • 一行包含三个正整数S、N、M(分别表示药水总量、两只魔法瓶的容量)。

输出格式

  • 如果可以平分,输出"YES",后跟一个空格,再输出最少操作次数;
  • 如果无法平分,输出"NO"。

输入输出样例

  • 样例1:
    输入

    plaintext 复制代码
    4 1 3

    输出

    plaintext 复制代码
    YES 3

    样例解释
    初始状态为(4,0,0)(原容器、N瓶、M瓶的药水容量);
    第一步:将原容器的药水倒入M瓶,状态变为(1,0,3);
    第二步:将M瓶的药水倒入N瓶,状态变为(1,1,2);
    第三步:将原容器的药水倒入N瓶,状态变为(0,2,2);
    此时已成功平分,共3步操作。

  • 样例2:
    输入

    plaintext 复制代码
    14 6 8

    输出

    plaintext 复制代码
    NO

    样例解释:无论经过多少次倒水操作,都无法将14毫升药水平分为两份7毫升,因此输出"NO"。

题解代码

java 复制代码
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int s = sc.nextInt();  // 药水总量
        int n = sc.nextInt();  // 第一只魔法瓶容量
        int m = sc.nextInt();  // 第二只魔法瓶容量
        sc.close();

        // 第一步:判断总量是否为偶数,奇数无法平分
        if (s % 2 != 0) {
            System.out.println("NO");
            return;
        }
        int target = s / 2;  // 目标平分量

        // BFS队列:存储状态(原容器容量、N瓶容量、M瓶容量、操作步数)
        Queue<int[]> queue = new LinkedList<>();
        // 哈希集合:记录已访问过的状态,避免重复遍历(状态用"a,b,c"字符串表示)
        Set<String> visited = new HashSet<>();

        // 初始状态:原容器满(s毫升),两只瓶子空(0毫升),操作步数0
        queue.add(new int[]{s, 0, 0, 0});
        visited.add(s + "," + 0 + "," + 0);

        // 定义所有可能的倒水方向:from容器倒向to容器(0=原容器,1=N瓶,2=M瓶)
        int[][] directions = {{0,1}, {0,2}, {1,0}, {1,2}, {2,0}, {2,1}};
        // 三个容器的最大容量
        int[] capacity = {s, n, m};

        int result = -1;  // 存储最少操作次数,初始为-1(表示未找到方案)

        // BFS遍历所有可能状态
        while (!queue.isEmpty()) {
            int[] current = queue.poll();
            int a = current[0];  // 原容器当前容量
            int b = current[1];  // N瓶当前容量
            int c = current[2];  // M瓶当前容量
            int steps = current[3];  // 当前操作步数

            // 检查是否达到平分条件:任意两个容器的容量之和为s(即各为s/2)
            if ((a == target && b == target) || (a == target && c == target) || (b == target && c == target)) {
                result = steps;
                break;  // BFS保证首次找到的是最短路径,直接退出
            }

            // 尝试所有倒水方向
            for (int[] dir : directions) {
                int from = dir[0];  // 倒水的源容器
                int to = dir[1];    // 倒水的目标容器

                // 复制当前状态,避免修改原状态
                int[] nextState = {a, b, c};
                // 计算可倒出的最大水量:取源容器当前水量和目标容器剩余容量的最小值
                int pourAmount = Math.min(nextState[from], capacity[to] - nextState[to]);

                // 执行倒水操作
                nextState[from] -= pourAmount;
                nextState[to] += pourAmount;

                // 将新状态转换为字符串,判断是否已访问
                String stateKey = nextState[0] + "," + nextState[1] + "," + nextState[2];
                if (!visited.contains(stateKey)) {
                    visited.add(stateKey);
                    // 将新状态加入队列,操作步数+1
                    queue.add(new int[]{nextState[0], nextState[1], nextState[2], steps + 1});
                }
            }
        }

        // 输出结果
        if (result != -1) {
            System.out.println("YES " + result);
        } else {
            System.out.println("NO");
        }
    }
}

2. 魔法药水平分(特定容量版)

题目描述:在魔法学院里,巫师小Y刚刚炼制了一瓶神奇的魔法药水,总量为S毫升。学院规定,每次使用魔法药水前,必须将药水平均分给小Y和他的伙伴小Z,以保证两人力量相等。小Y手中有两只不同容量的魔法瓶,容量分别为N毫升和M毫升,且药水总量恰好等于两只魔法瓶容量之和(S = N + M),其中S < 101,N > 0,M > 0。三个容器之间可以相互倒药水,但容器上没有刻度,仅能倒满或倒尽。现在需要判断是否能通过一系列倒水操作,将药水均分为两份。如果可以,请输出最少操作次数;如果不可能,则输出"NO"。

运行条件

  • 总时限:5000毫秒
  • 单组时限:1000毫秒
  • 总内存:320 MB
  • 单组内存:64 MB

输入格式

  • 一行包含三个正整数S、N、M(满足S = N + M,1 ≤ S < 101,N > 0,M > 0)。

输出格式

  • 如果可以平分,输出最少操作次数;
  • 如果无法平分,输出"NO"。

输入输出样例

  • 样例1:
    输入

    plaintext 复制代码
    4 1 3

    输出

    plaintext 复制代码
    3

    样例解释
    初始状态为(4,0,0)(原容器、N瓶、M瓶的药水容量);
    第一步:将原容器的药水倒入M瓶,状态变为(1,0,3);
    第二步:将M瓶的药水倒入N瓶,状态变为(1,1,2);
    第三步:将原容器的药水倒入N瓶,状态变为(0,2,2);
    此时已成功平分,共3步操作。

  • 样例2:
    输入

    plaintext 复制代码
    14 6 8

    输出

    plaintext 复制代码
    NO

    样例解释:无论经过多少次倒水操作,都无法将14毫升药水平分为两份7毫升,因此输出"NO"。

题解代码

java 复制代码
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int s = sc.nextInt();  // 药水总量(S = N + M)
        int n = sc.nextInt();  // 第一只魔法瓶容量
        int m = sc.nextInt();  // 第二只魔法瓶容量
        sc.close();

        // 前置校验:1. S必须等于N+M;2. S必须小于101;3. S必须为偶数(否则无法平分)
        if (s != n + m || s >= 101 || s % 2 != 0) {
            System.out.println("NO");
            return;
        }
        int target = s / 2;  // 目标平分量(每份的容量)

        // BFS队列:存储状态(原容器容量、N瓶容量、M瓶容量、操作步数)
        Queue<int[]> queue = new LinkedList<>();
        // 哈希集合:记录已访问过的状态,避免重复遍历
        Set<String> visited = new HashSet<>();

        // 初始状态:原容器满(s毫升),两只瓶子空(0毫升),操作步数0
        queue.add(new int[]{s, 0, 0, 0});
        visited.add(s + "," + 0 + "," + 0);

        // 定义所有可能的倒水方向(0=原容器,1=N瓶,2=M瓶)
        int[][] directions = {{0,1}, {0,2}, {1,0}, {1,2}, {2,0}, {2,1}};
        // 三个容器的最大容量
        int[] capacity = {s, n, m};

        int result = -1;  // 存储最少操作次数,初始为-1(未找到方案)

        // BFS遍历所有可能状态
        while (!queue.isEmpty()) {
            int[] current = queue.poll();
            int a = current[0];  // 原容器当前容量
            int b = current[1];  // N瓶当前容量
            int c = current[2];  // M瓶当前容量
            int steps = current[3];  // 当前操作步数

            // 检查是否达到平分条件:任意两个容器各存target毫升
            if ((a == target && b == target) || (a == target && c == target) || (b == target && c == target)) {
                result = steps;
                break;  // BFS保证最短路径,直接退出
            }

            // 尝试所有倒水方向
            for (int[] dir : directions) {
                int from = dir[0];  // 源容器
                int to = dir[1];    // 目标容器

                // 复制当前状态,执行倒水操作
                int[] nextState = {a, b, c};
                // 可倒水量 = 源容器当前水量 和 目标容器剩余容量 的最小值
                int pourAmount = Math.min(nextState[from], capacity[to] - nextState[to]);

                nextState[from] -= pourAmount;
                nextState[to] += pourAmount;

                // 检查新状态是否已访问
                String stateKey = nextState[0] + "," + nextState[1] + "," + nextState[2];
                if (!visited.contains(stateKey)) {
                    visited.add(stateKey);
                    queue.add(new int[]{nextState[0], nextState[1], nextState[2], steps + 1});
                }
            }
        }

        // 输出结果
        if (result != -1) {
            System.out.println(result);
        } else {
            System.out.println("NO");
        }
    }
}

二、农田作物收获问题

相似题目差异总结

  • 核心差异:第一题要求输出指定时间段内的每日收获量(T<100时输出0至T天,T≥100时输出T-100~T天);第二题仅要求输出第T天结束时的总收获量;核心算法一致采用贪心策略(优先种植生长周期最短的作物)。
  • 核心算法:贪心+模拟种植流程,优先选择生长周期短的作物以最大化收获次数,时间复杂度O((nm)log(nm) + T),适配大规模T值(最大1e6)。
  • 优化点:对作物生长周期排序,优先分配天数给短周期作物;用布尔数组标记已占用的天数,避免重复分配;预处理收获日期后统计每日/总收获量。

1. 农田作物收获(每日统计版)

题目描述:农场主有一片n×m的矩形农田,每个格子里种植着一种作物。每种作物有一个生长周期a[i][j](单位:天),表示从播种到成熟需要的天数,收获作物必须在作物成熟之后(即至少在播种后的第a[i][j]天)进行。农场主每天只能在一个格子里进行一次操作(每次播种或收获都需要消耗一天时间):要么播种新作物,要么收获已成熟的作物,收获后该格子会立即被清空。现在是第0天刚开始,所有格子都是空的。农场主希望制定一个种植计划,使得在第T天结束时,收获的作物总数量尽可能多。请计算在最多收获作物的情况下,最后一百零一天(从T-100天到第T天,若T<100则输出从0天到第T天)每一天的仓库作物数量。

运行条件

  • 总时限:5000毫秒
  • 单组时限:1000毫秒
  • 总内存:320 MB
  • 单组内存:64 MB

输入格式

  • 第一行包含三个整数n、m、T(1 ≤ n, m ≤ 100,1 ≤ T ≤ 100000);
  • 接下来n行,每行m个整数,表示每个格子作物的生长周期a[i][j](1 ≤ a[i][j] ≤ 100)。

输出格式

  • 当T ≥ 100时,一行输出101个整数(从第T-100天到第T天的每日收获量),每个数字间用空格隔开;
  • 当T < 100时,一行输出T+1个整数(从第0天到第T天的每日收获量),每个数字间用空格隔开。

输入输出样例

  • 样例1:
    输入

    plaintext 复制代码
    2 2 5
    1 2
    3 4

    输出

    plaintext 复制代码
    0 1 1 2 2 3

    样例解释
    农田共4个格子,作物生长周期排序后为[1,2,3,4],T=5(可操作天数为0~5天,共6天);
    贪心策略:优先种植生长周期最短的作物(a=1),最大化收获次数;
    操作序列:
    第0天:播种a=1的作物 → 第1天:收获(第1天收获量+1);
    第2天:播种a=1的作物 → 第3天:收获(第3天收获量+1);
    第4天:播种a=1的作物 → 第5天:收获(第5天收获量+1);
    每日收获量统计:第0天0,第1天1,第2天0,第3天1,第4天0,第5天1;
    由于每日仓库数量是累计值,最终输出为0 1 1 2 2 3。

  • 样例2:
    输入

    plaintext 复制代码
    2 2 5
    2 3
    4 5

    输出

    plaintext 复制代码
    0 0 1 1 2 3

    样例解释
    作物生长周期排序后为[2,3,4,5],T=5;
    操作序列(并行生长):
    第0天:播种a=2 → 第2天:收获(第2天收获量+1)→ 第3天:播种a=2 → 第5天:收获(第5天收获量+1);
    第1天:播种a=3 → 第4天:收获(第4天收获量+1);
    累计每日仓库数量:0 0 1 1 2 3。

题解代码

java 复制代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Main {
    public static void main(String[] args) throws IOException {
        // 使用BufferedReader加速输入(处理大规模输入更高效)
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] firstLine = br.readLine().split(" ");
        int n = Integer.parseInt(firstLine[0]);  // 农田行数
        int m = Integer.parseInt(firstLine[1]);  // 农田列数
        int T = Integer.parseInt(firstLine[2]);  // 目标天数

        // 将二维农田的生长周期转换为一维数组,方便排序
        int totalGrid = n * m;
        int[] growthCycles = new int[totalGrid];
        int idx = 0;
        for (int i = 0; i < n; i++) {
            String[] line = br.readLine().split(" ");
            for (int j = 0; j < m; j++) {
                growthCycles[idx++] = Integer.parseInt(line[j]);
            }
        }

        // 贪心核心:对生长周期排序,优先种植周期最短的作物(最大化收获次数)
        Arrays.sort(growthCycles);

        List<Integer> harvestDays = new ArrayList<>();  // 记录每次收获的具体天数
        boolean[] usedDays = new boolean[T + 1];       // 标记已被占用的天数(播种或收获)

        // 遍历每种作物,尝试安排种植和收获
        for (int cycle : growthCycles) {
            int seedDay = 0;  // 播种日期,初始从第0天开始查找
            while (true) {
                // 找到第一个未被占用的播种日期
                while (seedDay <= T && usedDays[seedDay]) {
                    seedDay++;
                }
                // 如果没有可用的播种日期,跳过该作物
                if (seedDay > T) {
                    break;
                }

                // 计算收获日期 = 播种日期 + 生长周期
                int harvestDay = seedDay + cycle;
                // 收获日期超过T天,无法收获,跳过该作物
                if (harvestDay > T) {
                    break;
                }

                // 标记播种日和收获日为已占用
                usedDays[seedDay] = true;
                usedDays[harvestDay] = true;

                // 记录收获日期
                harvestDays.add(harvestDay);

                // 下一次播种从当前收获日的下一天开始查找
                seedDay = harvestDay + 1;
            }
        }

        // 对收获日期排序,方便后续统计每日累计量
        Collections.sort(harvestDays);

        // 统计每日仓库累计收获量
        int[] dailyTotal = new int[T + 1];
        int count = 0;  // 累计收获次数
        for (int day : harvestDays) {
            count++;
            dailyTotal[day] = count;  // 标记该天的累计收获量
        }

        // 填充相邻天数的累计量(例如第2天收获1次,第3天无收获,则第3天累计量仍为1)
        for (int i = 1; i <= T; i++) {
            dailyTotal[i] = Math.max(dailyTotal[i], dailyTotal[i - 1]);
        }

        // 构建输出结果
        StringBuilder sb = new StringBuilder();
        if (T < 100) {
            // T<100时,输出0~T天的累计量
            for (int i = 0; i <= T; i++) {
                sb.append(dailyTotal[i]).append(" ");
            }
        } else {
            // T≥100时,输出T-100~T天的累计量
            for (int i = T - 100; i <= T; i++) {
                sb.append(dailyTotal[i]).append(" ");
            }
        }

        // 去除末尾多余空格,输出结果
        sb.setLength(sb.length() - 1);
        System.out.println(sb);
    }
}

2. 农田作物收获(总量统计版)

题目描述:农场主有一片n×m的矩形农田,每个格子里种植着一种作物。每种作物有一个生长周期a[i][j](单位:天),表示从播种到成熟需要的天数,收获作物必须在作物成熟之后(即至少在播种后的第a[i][j]天)进行。农场主每天只能在一个格子里进行一次操作(每次播种或收获都需要消耗一天时间):要么播种新作物,要么收获已成熟的作物,收获后该格子会立即被清空。现在是第0天刚开始,所有格子都是空的。农场主希望制定一个种植计划,使得在第T天结束时,收获的作物总数量尽可能多。请计算最多能收获的作物数量。

运行条件

  • 总时限:5000毫秒
  • 单组时限:1000毫秒
  • 总内存:320 MB
  • 单组内存:64 MB

输入格式

  • 第一行包含三个整数n、m、T(1 ≤ n, m ≤ 100,1 ≤ T ≤ 1000000);
  • 接下来n行,每行m个整数,表示每个格子作物的生长周期a[i][j](1 ≤ a[i][j] ≤ 100)。

输出格式

  • 一个整数,表示第T天结束时最多能收获的作物数量。

输入输出样例

  • 样例1:
    输入

    plaintext 复制代码
    2 2 5
    1 2
    3 4

    输出

    plaintext 复制代码
    3

    样例解释
    农田共4个格子,作物生长周期排序后为[1,2,3,4],T=5(可操作天数为0~5天,共6天);
    贪心策略:优先种植生长周期最短的作物(a=1),最大化收获次数;
    操作序列:
    第0天:播种a=1 → 第1天:收获(1次);
    第2天:播种a=1 → 第3天:收获(2次);
    第4天:播种a=1 → 第5天:收获(3次);
    其他作物因周期较长,无剩余天数安排种植,总收获量为3。

  • 样例2:
    输入

    plaintext 复制代码
    2 2 5
    2 3
    4 5

    输出

    plaintext 复制代码
    3

    样例解释
    作物生长周期排序后为[2,3,4,5],T=5;
    操作序列(并行生长):
    第0天:播种a=2 → 第2天:收获(1次)→ 第3天:播种a=2 → 第5天:收获(2次);
    第1天:播种a=3 → 第4天:收获(3次);
    总收获量为3次。

题解代码

java 复制代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) throws IOException {
        // BufferedReader加速输入,适配大规模T值(最大1e6)
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] firstLine = br.readLine().split(" ");
        int n = Integer.parseInt(firstLine[0]);  // 农田行数
        int m = Integer.parseInt(firstLine[1]);  // 农田列数
        int T = Integer.parseInt(firstLine[2]);  // 目标天数

        // 转换为一维数组存储所有作物的生长周期
        int totalGrid = n * m;
        int[] growthCycles = new int[totalGrid];
        int idx = 0;
        for (int i = 0; i < n; i++) {
            String[] line = br.readLine().split(" ");
            for (int j = 0; j < m; j++) {
                growthCycles[idx++] = Integer.parseInt(line[j]);
            }
        }

        // 贪心核心:排序后优先种植周期最短的作物
        Arrays.sort(growthCycles);

        boolean[] usedDays = new boolean[T + 1];  // 标记已占用的天数(播种/收获)
        int totalHarvest = 0;  // 总收获量

        // 遍历每种作物,安排种植和收获
        for (int cycle : growthCycles) {
            int seedDay = 0;  // 初始播种日从0开始
            while (true) {
                // 找到第一个未被占用的播种日
                while (seedDay <= T && usedDays[seedDay]) {
                    seedDay++;
                }
                // 无可用播种日,跳过该作物
                if (seedDay > T) {
                    break;
                }

                // 计算收获日 = 播种日 + 生长周期
                int harvestDay = seedDay + cycle;
                // 收获日超过T,无法收获,跳过
                if (harvestDay > T) {
                    break;
                }
                // 若收获日已被占用,尝试下一个播种日
                if (usedDays[harvestDay]) {
                    seedDay++;
                    continue;
                }

                // 标记播种日和收获日为已占用
                usedDays[seedDay] = true;
                usedDays[harvestDay] = true;
                totalHarvest++;  // 收获量+1

                // 下一次播种从当前收获日的下一天开始
                seedDay = harvestDay + 1;
            }
        }

        // 输出总收获量
        System.out.println(totalHarvest);
    }
}

三、DNA片段查找问题

相似题目差异总结

  • 核心差异:输出格式不同(第一题先输出匹配次数,再输出所有起始位置;第二题每行输出一个起始位置,无匹配时输出"NO");核心算法一致采用KMP算法(高效字符串匹配,避免暴力匹配的低效)。
  • 核心算法:KMP算法,通过预处理模式串生成LPS数组(最长前缀后缀数组),减少重复比较,时间复杂度O(|S| + |P|),适配大规模字符串(长度最大1e6)。
  • 优化点:使用BufferedReader/BufferedWriter处理输入输出,避免IO超时;LPS数组预处理优化模式串匹配过程,减少字符比较次数。

1. DNA片段查找(计数+位置版)

题目描述:在基因组分析中,经常需要快速定位短DNA片段(motif)在参考序列中的全部出现位置,以便进行下游注释或变异分析。DNA仅由A、C、G、T四种碱基组成。给定参考DNA序列S和目标DNA片段P,求出P在S中所有出现的起始下标(从1开始计数),允许重叠匹配。请输出匹配次数,若匹配次数大于0,需额外输出所有起始下标的升序序列。

运行条件

  • 总时限:5000毫秒
  • 单组时限:1000毫秒
  • 总内存:320 MB
  • 单组内存:64 MB

输入格式

  • 第一行:参考序列S(仅含A、C、G、T四种字符,1 ≤ |S| ≤ 1e6);
  • 第二行:目标片段P(仅含A、C、G、T四种字符,1 ≤ |P| ≤ 1e6)。

输出格式

  • 第一行输出一个整数k------匹配次数(可能为0);
  • 若k > 0:第二行输出k个整数(按升序排列),用空格分隔,表示每个匹配的起始下标(1-based);
  • 若k = 0:仅输出第一行的0(不输出第二行)。

输入输出样例

  • 样例1:
    输入

    plaintext 复制代码
    ACGTACGTACGT
    ACGT

    输出

    plaintext 复制代码
    3
    1 5 9

    样例解释:目标片段"ACGT"在参考序列中出现3次,起始下标分别为1、5、9(允许重叠匹配)。

  • 样例2:
    输入

    plaintext 复制代码
    ACGTAC
    ACGTGT

    输出

    plaintext 复制代码
    0

    样例解释:目标片段与参考序列无匹配,输出匹配次数0。

题解代码

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) throws IOException {
        // 使用BufferedReader处理大规模输入,避免IO超时
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String S = br.readLine();  // 参考DNA序列
        String P = br.readLine();  // 目标DNA片段

        // 调用KMP算法查找所有匹配位置
        List<Integer> matchPositions = kmpSearch(S, P);

        // 输出匹配次数
        System.out.println(matchPositions.size());
        // 若有匹配,输出所有起始位置(1-based)
        if (!matchPositions.isEmpty()) {
            StringBuilder sb = new StringBuilder();
            for (int pos : matchPositions) {
                sb.append(pos).append(" ");
            }
            // 去除末尾空格,输出结果
            System.out.println(sb.toString().trim());
        }
    }

    /**
     * KMP搜索算法:在文本text中查找模式串pattern的所有出现位置(允许重叠)
     * @param text 参考序列(文本串)
     * @param pattern 目标片段(模式串)
     * @return 所有匹配的起始下标(1-based)
     */
    private static List<Integer> kmpSearch(String text, String pattern) {
        List<Integer> matches = new ArrayList<>();
        int textLen = text.length();
        int patternLen = pattern.length();

        // 边界条件:模式串为空或长度大于文本串,直接返回空结果
        if (patternLen == 0 || patternLen > textLen) {
            return matches;
        }

        // 预处理模式串,生成LPS数组(最长前缀后缀数组)
        int[] lps = computeLPS(pattern);

        int i = 0;  // text的指针(当前匹配位置)
        int j = 0;  // pattern的指针(当前匹配位置)

        while (i < textLen) {
            // 字符匹配,两个指针同时后移
            if (text.charAt(i) == pattern.charAt(j)) {
                i++;
                j++;
            }

            // 模式串完全匹配,记录起始位置
            if (j == patternLen) {
                // 起始位置为1-based:text中匹配的起始索引 = i - j(0-based)+ 1
                matches.add(i - j + 1);
                // 利用LPS数组回溯,寻找下一个可能的匹配(允许重叠)
                j = lps[j - 1];
            }
            // 字符不匹配
            else if (i < textLen && text.charAt(i) != pattern.charAt(j)) {
                if (j != 0) {
                    // 模式串指针回溯到LPS[j-1]位置(避免重复比较)
                    j = lps[j - 1];
                } else {
                    // j=0时无前缀可回溯,文本串指针后移
                    i++;
                }
            }
        }

        return matches;
    }

    /**
     * 计算模式串的LPS数组(最长前缀后缀数组)
     * LPS[i]表示pattern[0..i]的最长真前缀(不等于自身的前缀),且该前缀也是后缀
     * @param pattern 模式串
     * @return LPS数组
     */
    private static int[] computeLPS(String pattern) {
        int patternLen = pattern.length();
        int[] lps = new int[patternLen];
        int len = 0;  // len表示当前最长匹配的前缀后缀长度(初始为0,因为单个字符无真前缀)

        int i = 1;
        while (i < patternLen) {
            if (pattern.charAt(i) == pattern.charAt(len)) {
                len++;
                lps[i] = len;
                i++;
            } else {
                if (len != 0) {
                    // 回溯到前一个可能的匹配长度
                    len = lps[len - 1];
                } else {
                    // len=0时无回溯可能,LPS[i]设为0,i后移
                    lps[i] = 0;
                    i++;
                }
            }
        }

        return lps;
    }
}

2. DNA片段查找(位置分行版)

题目描述:在生物信息学中,科学家常常需要在基因组序列中查找特定的DNA片段,例如查找已知功能基因的起始位置、定位关键突变区域、检测序列中的特定重复片段。DNA序列由四种碱基组成:A(腺嘌呤)、C(胞嘧啶)、G(鸟嘌呤)、T(胸腺嘧啶)。给定一条参考DNA序列s1和一个目标DNA片段s2,请找出s2在s1中的所有出现位置。如果s2在s1的区间[l, r]内完全匹配(即s1[l...r] == s2),则认为该片段在序列中出现一次,其起始位置为下标l(下标从1开始计数)。允许重叠匹配,请按升序输出所有匹配的起始位置,每行一个;若没有匹配,则输出"NO"。

运行条件

  • 总时限:5000毫秒
  • 单组时限:1000毫秒
  • 总内存:320 MB
  • 单组内存:64 MB

输入格式

  • 第一行:参考DNA序列s1(仅含A、C、G、T四种字符,1 ≤ |s1| ≤ 1e6);
  • 第二行:目标DNA片段s2(仅含A、C、G、T四种字符,1 ≤ |s2| ≤ 1e6)。

输出格式

  • 输出所有匹配位置,每行一个整数,表示目标片段在参考序列中的起始下标(1-based);
  • 若没有匹配,则输出"NO"。

输入输出样例

  • 样例1:
    输入

    plaintext 复制代码
    ACGTACGTACGT
    ACGT

    输出

    plaintext 复制代码
    1
    5
    9

    样例解释:目标片段"ACGT"在参考序列中出现3次,起始下标分别为1、5、9(允许重叠匹配)。

  • 样例2:
    输入

    plaintext 复制代码
    AAAAA
    AAA

    输出

    plaintext 复制代码
    1
    2
    3

    样例解释:目标片段"AAA"在参考序列中重叠出现3次,起始下标分别为1(覆盖1-3位)、2(覆盖2-4位)、3(覆盖3-5位)。

题解代码

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) throws IOException {
        // BufferedReader处理大规模输入,提升效率
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String s1 = br.readLine();  // 参考DNA序列
        String s2 = br.readLine();  // 目标DNA片段

        // KMP算法查找所有匹配位置
        List<Integer> matchPositions = kmpSearch(s1, s2);

        // 输出结果
        if (matchPositions.isEmpty()) {
            System.out.println("NO");
        } else {
            // 每行输出一个匹配位置(1-based)
            for (int pos : matchPositions) {
                System.out.println(pos);
            }
        }
    }

    /**
     * KMP搜索算法:在文本text中查找模式串pattern的所有出现位置(允许重叠)
     * @param text 参考序列(文本串)
     * @param pattern 目标片段(模式串)
     * @return 所有匹配的起始下标(1-based)
     */
    private static List<Integer> kmpSearch(String text, String pattern) {
        List<Integer> matches = new ArrayList<>();
        int textLen = text.length();
        int patternLen = pattern.length();

        // 边界条件:模式串为空或长度大于文本串,返回空结果
        if (patternLen == 0 || patternLen > textLen) {
            return matches;
        }

        // 预处理生成LPS数组,优化匹配效率
        int[] lps = computeLPS(pattern);

        int i = 0;  // text指针
        int j = 0;  // pattern指针

        while (i < textLen) {
            // 字符匹配,双指针后移
            if (text.charAt(i) == pattern.charAt(j)) {
                i++;
                j++;
            }

            // 模式串完全匹配,记录起始位置
            if (j == patternLen) {
                matches.add(i - j + 1);  // 转换为1-based下标
                j = lps[j - 1];  // 回溯寻找下一个匹配(允许重叠)
            }
            // 字符不匹配
            else if (i < textLen && text.charAt(i) != pattern.charAt(j)) {
                if (j != 0) {
                    j = lps[j - 1];  // 利用LPS回溯,避免重复比较
                } else {
                    i++;  // j=0时直接后移文本指针
                }
            }
        }

        return matches;
    }

    /**
     * 计算模式串的LPS数组(最长前缀后缀数组)
     * @param pattern 模式串
     * @return LPS数组
     */
    private static int[] computeLPS(String pattern) {
        int patternLen = pattern.length();
        int[] lps = new int[patternLen];
        int len = 0;  // 当前最长匹配的前缀后缀长度

        int i = 1;
        while (i < patternLen) {
            if (pattern.charAt(i) == pattern.charAt(len)) {
                len++;
                lps[i] = len;
                i++;
            } else {
                if (len != 0) {
                    len = lps[len - 1];  // 回溯到前一个可能的匹配长度
                } else {
                    lps[i] = 0;
                    i++;
                }
            }
        }

        return lps;
    }
}

四、城市通信网络问题

相似题目差异总结

  • 核心差异:查询操作的输出格式不同(第一题输出"Yes"后需附带两个城市编号,第二题仅输出"Yes"或"No");核心算法一致采用并查集(Disjoint Set Union,DSU),高效处理动态连通性问题。
  • 核心算法:并查集(路径压缩优化),实现合并(union)和查询(find)操作,时间复杂度近似O(α(n))(α为阿克曼函数的反函数,接近常数),适配小规模城市数量(n≤50)和操作数(q≤50)。
  • 优化点:路径压缩优化find操作,减少后续查询的时间开销;使用数组存储父节点,结构简洁高效。

1. 城市通信网络(查询带详情版)

题目描述:某个省份正在进行城市通信网络升级,共有n个城市,每个城市最初独立(互不连通)。网络升级过程中有两类操作:1. 建立连接操作:将两个城市建立连接,使它们属于同一个通信网络(连通分量);2. 互通查询操作:判断两个城市是否可以互相通信(即是否属于同一个连通分量)。请根据输入的操作序列,对每个查询操作输出结果:若两城市互通,输出"Yes"后跟两个城市编号(按查询时的顺序);若不互通,输出"No"。

运行条件

  • 总时限:5000毫秒
  • 单组时限:1000毫秒
  • 总内存:320 MB
  • 单组内存:64 MB

输入格式

  • 第一行包含两个整数n、q(2 ≤ n ≤ 50,0 ≤ q ≤ 50),分别表示城市数和操作数;
  • 接下来q行,每行包含三个整数op、x、y(1 ≤ x, y ≤ n):
    • op=1:建立连接操作,将城市x和城市y连接;
    • op=2:查询操作,询问城市x和城市y是否可以互通。

输出格式

  • 对于每个op=2的查询操作,输出一行:
    • 若两城市在同一网络,输出"Yes",后跟一个空格,再输出x和y(x与y之间用空格分隔);
    • 否则输出"No"。

输入输出样例

  • 样例1:
    输入

    plaintext 复制代码
    5 6
    1 1 2
    1 3 4
    2 1 3
    1 2 3
    2 1 3
    2 4 5

    输出

    plaintext 复制代码
    No
    Yes 1 3
    No

    样例解释

    1. 操作1 1 2:城市1与2连接,连通分量为{1,2};
    2. 操作1 3 4:城市3与4连接,连通分量为{3,4};
    3. 操作2 1 3:查询1和3,分属不同分量,输出"No";
    4. 操作1 2 3:城市2与3连接,连通分量合并为{1,2,3,4};
    5. 操作2 1 3:查询1和3,同属一个分量,输出"Yes 1 3";
    6. 操作2 4 5:查询4和5,5独立,输出"No"。
  • 样例2:
    输入

    plaintext 复制代码
    3 2
    1 1 1
    2 1 2

    输出

    plaintext 复制代码
    No

    样例解释

    1. 操作1 1 1:城市1与自身连接(自环),无实质影响,连通分量仍为{1}、{2}、{3};
    2. 操作2 1 2:查询1和2,分属不同分量,输出"No"。

题解代码

java 复制代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    // 并查集父节点数组:f[i]表示城市i的父节点(初始时父节点为自身)
    static int[] f;

    /**
     * 并查集查询操作(带路径压缩优化)
     * @param x 待查询的城市编号
     * @return 城市x所在连通分量的根节点
     */
    static int find(int x) {
        // 路径压缩:如果x的父节点不是自身,递归查找根节点,并将x的父节点直接指向根节点
        return f[x] == x ? x : (f[x] = find(f[x]));
    }

    public static void main(String[] args) throws Exception {
        // BufferedReader加速输入,处理多组操作
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line = br.readLine();
        if (line == null || line.isEmpty()) {
            return;
        }

        // 解析第一行的n(城市数)和q(操作数)
        StringTokenizer st = new StringTokenizer(line);
        int n = Integer.parseInt(st.nextToken());
        int q = Integer.parseInt(st.nextToken());

        // 初始化并查集:每个城市的父节点为自身
        f = new int[n + 1];  // 城市编号从1开始,数组大小为n+1
        for (int i = 1; i <= n; i++) {
            f[i] = i;
        }

        // 用StringBuilder拼接输出结果,减少IO次数
        StringBuilder sb = new StringBuilder();

        // 处理q个操作
        for (int i = 0; i < q; i++) {
            line = br.readLine();
            if (line == null || line.isEmpty()) {
                i--;  // 跳过空行,重新读取
                continue;
            }
            st = new StringTokenizer(line);
            int op = Integer.parseInt(st.nextToken());  // 操作类型
            int x = Integer.parseInt(st.nextToken());   // 城市x
            int y = Integer.parseInt(st.nextToken());   // 城市y

            if (op == 1) {
                // 建立连接操作:合并x和y所在的连通分量
                int rootX = find(x);  // 查找x的根节点
                int rootY = find(y);  // 查找y的根节点
                if (rootX != rootY) {
                    f[rootX] = rootY;  // 将rootX的父节点设为rootY,合并分量
                }
            } else if (op == 2) {
                // 查询操作:判断x和y是否在同一连通分量
                if (find(x) == find(y)) {
                    // 互通,输出"Yes x y"
                    sb.append("Yes ").append(x).append(" ").append(y).append('\n');
                } else {
                    // 不互通,输出"No"
                    sb.append("No").append('\n');
                }
            }
        }

        // 输出所有查询结果
        System.out.print(sb.toString());
    }
}

2. 城市通信网络(简洁查询版)

题目描述:某个省份有n个城市,这些城市之间可能逐渐建立通信网络。初始时,每个城市是一个独立的网络(互不连通)。接下来会有两类操作:1. 合并操作:将两个城市连接起来,使它们属于同一个网络(连通分量);2. 查询操作:询问两个城市是否属于同一个网络。请根据输入的操作序列,对每个查询操作输出结果:若两城市属于同一个网络,输出"Yes";否则输出"No"。注意:单个城市与自身一定属于同一个网络。

运行条件

  • 总时限:5000毫秒
  • 单组时限:1000毫秒
  • 总内存:320 MB
  • 单组内存:64 MB

输入格式

  • 第一行包含两个整数n、q(2 ≤ n ≤ 50,0 ≤ q ≤ 50),分别表示城市数和操作数;
  • 接下来q行,每行包含三个整数op、x、y(1 ≤ x, y ≤ n):
    • op=1:合并操作,将城市x和城市y连接;
    • op=2:查询操作,询问城市x和城市y是否属于同一个网络。

输出格式

  • 对于每个op=2的查询操作,输出一行:
    • 若两城市属于同一个网络,输出"Yes";
    • 否则输出"No"。

输入输出样例

  • 样例1:
    输入

    plaintext 复制代码
    5 6
    1 1 2
    1 3 4
    2 1 3
    1 2 3
    2 1 3
    2 4 5

    输出

    plaintext 复制代码
    No
    Yes
    No

    样例解释

    1. 操作1 1 2:城市1与2合并,连通分量{1,2};
    2. 操作1 3 4:城市3与4合并,连通分量{3,4};
    3. 操作2 1 3:查询1和3,不同分量→输出"No";
    4. 操作1 2 3:城市2与3合并,分量合并为{1,2,3,4};
    5. 操作2 1 3:查询1和3,同分量→输出"Yes";
    6. 操作2 4 5:查询4和5,5独立→输出"No"。
  • 样例2:
    输入

    plaintext 复制代码
    5 8
    1 2 2
    1 5 3
    2 4 4
    1 4 5
    2 4 3
    1 1 2
    2 4 2
    1 4 5

    输出

    plaintext 复制代码
    Yes
    Yes
    No

    样例解释

    1. 操作1 2 2:城市2自环,无影响;
    2. 操作1 5 3:城市5与3合并,分量{3,5};
    3. 操作2 4 4:查询4与自身→输出"Yes";
    4. 操作1 4 5:城市4与5合并,分量{3,4,5};
    5. 操作2 4 3:查询4与3,同分量→输出"Yes";
    6. 操作1 1 2:城市1与2合并,分量{1,2};
    7. 操作2 4 2:查询4与2,不同分量→输出"No";
    8. 操作1 4 5:已在同一分量,无影响。

题解代码

java 复制代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    // 并查集父节点数组:f[i]表示城市i的父节点
    static int[] f;

    /**
     * 并查集查询操作(带路径压缩优化)
     * @param x 待查询的城市编号
     * @return 城市x所在连通分量的根节点
     */
    static int find(int x) {
        // 路径压缩:直接将x的父节点指向根节点,减少后续查询开销
        return f[x] == x ? x : (f[x] = find(f[x]));
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line = br.readLine();
        if (line == null || line.isEmpty()) {
            return;
        }

        // 解析城市数n和操作数q
        StringTokenizer st = new StringTokenizer(line);
        int n = Integer.parseInt(st.nextToken());
        int q = Integer.parseInt(st.nextToken());

        // 初始化并查集:每个城市的父节点为自身
        f = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            f[i] = i;
        }

        StringBuilder sb = new StringBuilder();  // 拼接输出结果

        // 处理q个操作
        for (int i = 0; i < q; i++) {
            line = br.readLine();
            if (line == null || line.isEmpty()) {
                i--;
                continue;
            }
            st = new StringTokenizer(line);
            int op = Integer.parseInt(st.nextToken());
            int x = Integer.parseInt(st.nextToken());
            int y = Integer.parseInt(st.nextToken());

            if (op == 1) {
                // 合并操作:合并x和y所在的连通分量
                int rootX = find(x);
                int rootY = find(y);
                if (rootX != rootY) {
                    f[rootX] = rootY;  // 合并两个分量
                }
            } else if (op == 2) {
                // 查询操作:判断x和y是否同属一个分量
                if (find(x) == find(y)) {
                    sb.append("Yes").append('\n');
                } else {
                    sb.append("No").append('\n');
                }
            }
        }

        // 输出所有查询结果
        System.out.print(sb.toString());
    }
}
相关推荐
泉城老铁28 分钟前
Springboot对接mqtt
java·spring boot·后端
源码_V_saaskw32 分钟前
JAVA国际版同城跑腿源码快递代取帮买帮送同城服务源码支持Android+IOS+H5
android·java·ios·微信小程序
TT哇37 分钟前
消息推送机制——WebSocket
java·网络·websocket·网络协议
镜花水月linyi1 小时前
ConcurrentHashMap 深入解析:从0到1彻底掌握(1.3万字)
java·后端
极客Bob1 小时前
Java 集合操作完整清单(Java 8+ Stream API)
java
雨中飘荡的记忆1 小时前
Javassist实战指南
java
Knight_AL1 小时前
JWT 无状态认证深度解析:原理、优势
java·jwt
寒山李白1 小时前
IDEA中如何配置Java类注释(Java类注释信息配置,如作者、备注、时间等)
java
我要添砖java1 小时前
<JAVAEE> 多线程4-wait和notify方法
android·java·java-ee