java 洛谷题单【算法2-2】常见优化技巧

P1102 A-B 数对

解题思路

  1. 输入读取与初始化

    • 使用 Scanner 读取输入。
    • n 表示数组的长度,c 表示目标差值。
    • 使用一个 HashMap 存储数组中每个数字及其出现的次数,方便快速查找。
    • 数组 a 用于存储输入的数字。
  2. 构建哈希映射

    • 遍历数组,将每个数字存入 HashMap 中,键为数字,值为该数字出现的次数。
    • 使用 map.getOrDefault(a[i], 0) 方法,确保即使键不存在也能返回默认值 0
  3. 查找满足条件的数对

    • 遍历数组中的每个数字 a[i],检查是否存在 a[i] + c
    • 如果存在 a[i] + c,则将其出现次数累加到结果 ans 中。
  4. 输出结果

    • 最终输出符合条件的数对数量 ans
import java.util.HashMap;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);

        int n = input.nextInt();  // 数组长度
        int c = input.nextInt();  // 目标差值

        HashMap<Integer, Integer> map = new HashMap<>();  // 用于存储数字及其出现次数
        int[] a = new int[n];  // 存储输入的数组
        
        // 读取数组输入
        for (int i = 0; i < n; i++) {
            a[i] = input.nextInt();
            // 将数字存储在哈希映射中
            map.put(a[i], map.getOrDefault(a[i], 0) + 1);
        }

        long ans = 0;  // 用于记录符合条件的对数

        // 遍历数组,寻找每个 a[i] 对应的 a[i] + c 或 a[i] - c 是否存在
        for (int i = 0; i < n; i++) {
            // 检查是否存在 a[i] + c
            if (map.containsKey(a[i] + c)) {
                ans += map.get(a[i] + c);  // 统计满足条件的对数
            }
        }

        // 输出符合条件的对数
        System.out.println(ans);
    }
}

P1638 逛画展

解题思路

  1. 滑动窗口

    • 使用两个指针 left 和 right 表示当前窗口的左右边界。
    • 通过移动 right 扩大窗口,直到窗口内包含所有的名师编号。
    • 然后移动 left 缩小窗口,尝试找到更短的区间。
  2. 哈希表统计

    • 使用一个哈希表 count 记录当前窗口内每个名师编号的出现次数。
    • 使用一个变量 uniqueCount 记录当前窗口内包含的不同名师编号的数量。
  3. 判断条件

    • 当 uniqueCount == m 时,说明当前窗口已经包含了所有的名师编号。
    • 记录当前窗口的长度,并尝试更新最优解。
  4. 输出结果

    • 输出最短区间的起始位置和结束位置 [a, b],注意题目要求输出的是 1-based 索引。
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);

        int n = input.nextInt(); 
        int m = input.nextInt();
        int[] paintings = new int[n];
        for (int i = 0; i < n; i++) {
            paintings[i] = input.nextInt();
        }

        // 滑动窗口 + 哈希表
        Map<Integer, Integer> count = new HashMap<>();
        int uniqueCount = 0; // 当前窗口内的不同名师数量
        int left = 0, minLength = Integer.MAX_VALUE;
        int start = 0, end = 0; // 最优区间的起始和结束位置

        for (int right = 0; right < n; right++) {
            // 扩大窗口
            int current = paintings[right];
            count.put(current, count.getOrDefault(current, 0) + 1);
            if (count.get(current) == 1) {
                uniqueCount++; // 新增一个名师编号
            }

            // 缩小窗口
            while (uniqueCount == m) {
                // 更新最优解
                if (right - left + 1 < minLength) {
                    minLength = right - left + 1;
                    start = left;
                    end = right;
                }

                // 移动左指针
                int leftValue = paintings[left];
                count.put(leftValue, count.get(leftValue) - 1);
                if (count.get(leftValue) == 0) {
                    uniqueCount--; // 移除一个名师编号
                }
                left++;
            }
        }

        System.out.println((start + 1) + " " + (end + 1));
        input.close();
    }
}

P1115 最大子段和

解题思路

  • Kadane算法思想​:

    • 维护两个变量:current(当前子段和)和max(全局最大值)。
    • 对于每个元素,决定是将其加入当前子段还是以该元素为起点重新开始。
    • 通过遍历数组一次,时间复杂度为O(n)。
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        String[] parts = br.readLine().split(" ");
        int[] a = new int[n];
        for (int i = 0; i < n; i++) {
            a[i] = Integer.parseInt(parts[i]);
        }
        int current = a[0];
        int max = a[0];
        for (int i = 1; i < n; i++) {
            current = Math.max(current + a[i], a[i]); // 更新当前子段和​
            max = Math.max(max, current);            // 更新全局最大值
        }
        System.out.println(max);
    }
}

P7072 [CSP-J2020] 直播获奖

解题思路

该问题的核心是实时计算每次新增成绩后的获奖分数线。由于成绩范围为0-600,可以利用计数排序的思想,高效维护每个分数出现的次数,并通过累加的方式快速确定当前前k大的分数。

  1. 输入处理​:

    • 使用 BufferedReader 读取输入数据,StringTokenizer 分割字符串。
    • 读取选手总数 n 和获奖率 w,接着读取所有选手的成绩存入数组 scores
  2. 计数数组初始化​:

    • count 数组大小为601(索引0到600),用于记录每个分数出现的次数。
  3. 遍历处理每个成绩​:

    • 更新当前分数 s 的计数。
    • 计算当前已处理人数 p = i + 1
    • 根据公式计算当前计划获奖人数 k,并确保至少1人获奖。
    • 从高分到低分(600到0)累加人数,直到总和≥k,此时的分数即为分数线。
  4. 结果输出​:

    • 使用 StringBuilder 高效拼接结果字符串,最后去除末尾空格并输出。
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int w = Integer.parseInt(st.nextToken());
        int[] scores = new int[n];
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < n; i++) {
            scores[i] = Integer.parseInt(st.nextToken());
        }
        
        int[] count = new int[601];
        StringBuilder sb = new StringBuilder();
        
        for (int i = 0; i < n; i++) {
            int s = scores[i];
            count[s]++;
            int p = i + 1;
            int k = Math.max(1, (p * w) / 100);
            
            int sum = 0;
            int line = 0;
            for (int j = 600; j >= 0; j--) {
                sum += count[j];
                if (sum >= k) {
                    line = j;
                    break;
                }
            }
            sb.append(line).append(' ');
        }
        System.out.println(sb.toString().trim());
    }
}

P2671 [NOIP 2015 普及组] 求和

解题思路

  1. 输入处理与分组​:

    • 读取输入的格子数量、颜色种类、每个格子的数字和颜色。
    • 根据颜色将格子分组,并在每个颜色组内进一步按位置的奇偶性分为奇数列和偶数列。
  2. 排序处理​:

    • 对每个颜色组内的奇数列和偶数列按位置进行排序,以便后续处理。
  3. 计算贡献​:

    • 对于每个奇数列或偶数列,计算所有可能的三元组贡献之和。通过数学公式优化,避免双重循环,将时间复杂度从O(n²)降为O(n)。
import java.util.*;

public class Main {
    static class Node {
        int pos;
        int num;

        public Node(int pos, int num) {
            this.pos = pos;
            this.num = num;
        }
    }

    static final int MOD = 10007;

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int m = input.nextInt();
        int[] number = new int[n];
        for (int i = 0; i < n; i++) {
            number[i] = input.nextInt();
        }
        int[] color = new int[n];
        for (int i = 0; i < n; i++) {
            color[i] = input.nextInt();
        }

        // 按颜色分组,奇偶分别存储
        Map<Integer, List<Node>> colorOddMap = new HashMap<>();
        Map<Integer, List<Node>> colorEvenMap = new HashMap<>();
        for (int i = 0; i < n; i++) {
            int c = color[i];
            int pos = i + 1;
            int num = number[i];
            Node node = new Node(pos, num);
            if (pos % 2 == 1) {
                colorOddMap.computeIfAbsent(c, k -> new ArrayList<>()).add(node);
            } else {
                colorEvenMap.computeIfAbsent(c, k -> new ArrayList<>()).add(node);
            }
        }

        // 对每个颜色的奇偶列表排序
        for (List<Node> list : colorOddMap.values()) {
            list.sort(Comparator.comparingInt(a -> a.pos));
        }
        for (List<Node> list : colorEvenMap.values()) {
            list.sort(Comparator.comparingInt(a -> a.pos));
        }

        long total = 0;
        // 处理奇列表
        for (List<Node> list : colorOddMap.values()) {
            total = (total + processList(list)) % MOD;
        }
        // 处理偶列表
        for (List<Node> list : colorEvenMap.values()) {
            total = (total + processList(list)) % MOD;
        }

        System.out.println(total % MOD);
        input.close();
    }

    private static long processList(List<Node> list) {
        int size = list.size();
        if (size < 2) {
            return 0;
        }

        // 计算sum1 = sum(pos * num)
        long sum1 = 0;
        for (Node node : list) {
            sum1 += (long) node.pos * node.num;
        }
        sum1 %= MOD;

        // sum_total = (size-1) * sum1 mod MOD
        long sumTotal = ((size - 1) % MOD) * sum1 % MOD;

        // 计算后缀和
        long[] sumNum = new long[size + 1];
        long[] sumPos = new long[size + 1];
        for (int i = size - 1; i >= 0; i--) {
            sumNum[i] = sumNum[i + 1] + list.get(i).num;
            sumPos[i] = sumPos[i + 1] + list.get(i).pos;
        }

        // 计算sum2
        long sum2 = 0;
        for (int i = 0; i < size; i++) {
            Node node = list.get(i);
            int currentPos = node.pos;
            int currentNum = node.num;
            long term1 = (currentPos * sumNum[i + 1]) % MOD;
            long term2 = (currentNum * sumPos[i + 1]) % MOD;
            sum2 = (sum2 + term1 + term2) % MOD;
        }

        return (sumTotal + sum2) % MOD;
    }
}

P4147 玉蟾宫

解题思路

该问题需要在给定的网格中找到最大的全'F'矩形面积。核心思路是逐行构建高度数组,然后利用单调栈高效计算每行对应的直方图最大面积。具体步骤如下:

  1. 输入处理​:

    • 读取网格的行数 n 和列数 m
    • 逐行读取网格数据,存储为字符矩阵 grid
  2. 动态维护高度数组​:

    • 对每一行,更新高度数组 heights。若当前格子是'F',则高度递增;否则重置为0。
  3. 单调栈求最大矩形面积​:

    • 对每一行的高度数组,使用单调栈算法计算最大矩形面积。
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int m = input.nextInt();
        input.nextLine(); // 跳过n和m后的换行符

        char[][] grid = new char[n][m];
        for (int i = 0; i < n; i++) {
            String line = input.nextLine();
            // 分割所有空白符,包括多个空格或制表符
            String[] tokens = line.split("\\s+");
            for (int j = 0; j < m; j++) {
                grid[i][j] = tokens[j].charAt(0);
            }
        }

        int[] heights = new int[m];
        int maxArea = 0;
        for (int i = 0; i < n; i++) {
            // 更新当前行的高度数组
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == 'F') {
                    heights[j]++;
                } else {
                    heights[j] = 0;
                }
            }
            // 计算当前行的最大矩形面积
            maxArea = Math.max(maxArea, largestRectangleArea(heights));
        }

        System.out.println(maxArea * 3);
        input.close();
    }

    // 单调栈计算直方图最大矩形面积
    private static int largestRectangleArea(int[] heights) {
        Stack<Integer> stack = new Stack<>();
        int maxArea = 0;
        int n = heights.length;

        for (int i = 0; i <= n; i++) {
            // 当前高度(i=n时视为高度0以处理剩余栈元素)
            int currentHeight = (i == n) ? 0 : heights[i];
            // 维护单调递增栈
            while (!stack.isEmpty() && currentHeight < heights[stack.peek()]) {
                int height = heights[stack.pop()];
                // 左边界为栈顶元素,右边界为当前索引i
                int left = stack.isEmpty() ? -1 : stack.peek();
                int width = i - left - 1;
                maxArea = Math.max(maxArea, height * width);
            }
            stack.push(i);
        }

        return maxArea;
    }
}

P2866 [USACO06NOV] Bad Hair Day S

解题思路

本题的目标是计算一排牛中每头牛能看到的右边连续较矮的牛的数量之和。核心思路是使用单调栈高效地找到每个元素右边第一个大于等于它的位置,从而避免暴力枚举带来的高时间复杂度。

  • 栈的作用​:维护一个从栈底到栈顶单调递增的索引序列,对应身高递减。这保证了栈顶元素始终是当前牛右边第一个身高不低于它的牛的位置。
  • 弹出条件​:当遇到当前牛的身高大于栈顶索引对应的牛的身高时,说明栈顶的牛会被当前牛挡住,后续计算无需考虑这些被挡住的牛。
  • 计算可见数​:若栈为空,当前牛能看到右边所有牛;否则,可见数为栈顶索引与当前索引之间的牛的数量。
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int[] h = new int[n]; 
        for (int i = 0; i < n; i++) {
            h[i] = Integer.parseInt(br.readLine());
        }

        Deque<Integer> stack = new ArrayDeque<>(); // 单调栈,存储索引
        long sum = 0; // 总和可能很大,用long存储
        // 从右向左遍历每头牛
        for (int i = n - 1; i >= 0; i--) {
            // 弹出栈中所有比当前牛矮的索引,这些牛会被当前牛挡住
            while (!stack.isEmpty() && h[stack.peek()] < h[i]) {
                stack.pop();
            }
            int c; // 当前牛能看到的牛的数量
            if (stack.isEmpty()) {
                // 栈空说明右边所有牛都比当前牛矮
                c = (n - 1 - i); // 计算右边所有牛的个数
            } else {
                // 栈顶是右边第一个不低于当前牛的位置,中间的牛都可见
                c = stack.peek() - i - 1;
            }
            sum += c; // 累加到总和
            stack.push(i); // 将当前索引入栈
        }

        System.out.println(sum);
    }
}

P1950 长方形

解题思路

该问题的核心是统计所有由'.'组成的矩形的数量。关键思路是逐行维护一个高度数组,记录每个位置向上连续的'.'数目,然后利用单调栈高效计算每行的贡献。

  1. 高度数组更新​:逐行遍历,更新每个位置向上的连续'.'数目,遇到'*'则重置为0。
  2. 单调栈计算贡献​:
    • 左边界计算​:维护递增栈,找到左边第一个比当前高度小的位置。
    • 右边界计算​:维护递增栈,找到右边第一个比当前高度小的位置。
    • 累计贡献​:每个位置的贡献为高度乘以其左右边界的跨度,累加得到每行的总和。
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] nm = br.readLine().split(" ");
        int n = Integer.parseInt(nm[0]);
        int m = Integer.parseInt(nm[1]);
        char[][] grid = new char[n][m];
        for (int i = 0; i < n; i++) {
            String line = br.readLine().replaceAll("\\s+", "");
            for (int j = 0; j < m; j++) {
                grid[i][j] = line.charAt(j);
            }
        }

        int[] heights = new int[m];
        long total = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == '.') {
                    heights[j]++;
                } else {
                    heights[j] = 0;
                }
            }
            total += calculateRowSum(heights);
        }
        System.out.println(total);
    }

    private static long calculateRowSum(int[] heights) {
        int m = heights.length;
        Deque<Integer> stack = new ArrayDeque<>();
        int[] left = new int[m];
        Arrays.fill(left, -1);

        // 计算left数组,左边第一个比当前小的位置
        for (int i = 0; i < m; i++) {
            while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
                stack.pop();
            }
            if (!stack.isEmpty()) {
                left[i] = stack.peek();
            }
            stack.push(i);
        }

        stack.clear();
        int[] right = new int[m];
        Arrays.fill(right, m);

        // 计算right数组,右边第一个比当前小的位置
        for (int i = m - 1; i >= 0; i--) {
            while (!stack.isEmpty() && heights[stack.peek()] > heights[i]) {
                stack.pop();
            }
            if (!stack.isEmpty()) {
                right[i] = stack.peek();
            }
            stack.push(i);
        }

        long sum = 0;
        for (int i = 0; i < m; i++) {
            sum += (long) heights[i] * (i - left[i]) * (right[i] - i);
        }
        return sum;
    }
}

P2032 扫描

解题思路

核心思路是使用单调队列维护当前窗口中的最大值候选元素。​

  1. 单调队列维护​:

    • 队列清理​:在遍历每个元素时,先移除队列中不在当前窗口范围内的索引(即小于 i - k + 1 的索引)。
    • 维护单调性​:从队列尾部移除所有对应值小于当前元素的索引,确保队列中的元素对应值单调递减。
    • 索引入队​:将当前元素的索引加入队列尾部。
  2. 窗口最大值查询​:

    • 当遍历到第 i 个元素且 i >= k - 1 时,说明窗口已形成,此时队列头部的索引对应的值即为当前窗口的最大值,直接输出。
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int k = Integer.parseInt(st.nextToken());
        int[] nums = new int[n];
        int idx = 0;
        
        // 读取数组元素,处理多行输入情况
        while (idx < n) {
            if (!st.hasMoreTokens()) {
                st = new StringTokenizer(br.readLine());
            }
            nums[idx++] = Integer.parseInt(st.nextToken());
        }
        
        Deque<Integer> deque = new ArrayDeque<>();
        PrintWriter pw = new PrintWriter(System.out);
        
        for (int i = 0; i < n; i++) {
            // 移除队首不在当前窗口中的元素
            while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
                deque.pollFirst();
            }
            
            // 移除队尾所有小于当前元素的索引
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }
            
            deque.offerLast(i); // 将当前索引入队
            
            // 当窗口形成时输出队首元素对应的值
            if (i >= k - 1) {
                pw.println(nums[deque.peekFirst()]);
            }
        }
        pw.flush();
    }
}

P2216 [HAOI2007] 理想的正方形

解题思路

核心思路是分阶段滑动窗口极值计算

1. 行方向极值预处理
  • 使用两个双端队列(maxDeque 和 minDeque)分别维护当前窗口的最大值和最小值。
  • 遍历行中的每个元素,动态更新队列,确保队列头部始终是窗口极值的索引。
  • 结果存储为 maxRow 和 minRow,其中 maxRow[i][j] 表示第 i 行从列 j 开始的横向窗口的最大值,minRow 同理。
2. 列方向极值计算与差值求解
  • 逐列提取数据​:对每列 j,从 maxRow 和 minRow 中提取该列的极值数据,生成临时数组 colMax(纵向最大值列)和 colMin(纵向最小值列)。
  • 纵向窗口处理​:对 colMax 和 colMin 分别进行长度为 n 的纵向滑动窗口处理,得到 maxWindow(纵向窗口最大值)和 minWindow(纵向窗口最小值)。
  • 即时计算差值​:遍历每个纵向窗口的结果,计算 maxWindow 和 minWindow 的差值,并更新全局最小差值 minDiff
3. 内存与效率优化
  • 避免显式转置​:通过逐列提取数据代替矩阵转置,减少中间矩阵的内存占用。
  • 即时释放内存​:每列处理完成后,临时数组(如 colMaxcolMin)可立即释放,避免同时存储大规模数据。
  • 单调队列优化​:行和列的极值计算均通过单调队列实现,确保时间复杂度为 ​O(a×b)​,适用于大规模矩阵。
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int a = Integer.parseInt(st.nextToken());
        int b = Integer.parseInt(st.nextToken());
        int n = Integer.parseInt(st.nextToken());
        int[][] mat = new int[a][b];
        for (int i = 0; i < a; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j < b; j++) {
                mat[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        // 处理行方向的最大值和最小值
        int[][] maxRow = new int[a][b - n + 1];
        int[][] minRow = new int[a][b - n + 1];
        for (int i = 0; i < a; i++) {
            processRow(mat[i], maxRow[i], minRow[i], n);
        }

        int minDiff = Integer.MAX_VALUE;

        // 逐列处理,避免转置
        for (int j = 0; j < maxRow[0].length; j++) {
            // 提取当前列的max和min数据
            int[] colMax = new int[a];
            int[] colMin = new int[a];
            for (int i = 0; i < a; i++) {
                colMax[i] = maxRow[i][j];
                colMin[i] = minRow[i][j];
            }

            // 处理当前列的滑动窗口
            int[] maxWindow = processWindow(colMax, n, true);
            int[] minWindow = processWindow(colMin, n, false);

            // 计算差值并更新最小值
            for (int i = 0; i < maxWindow.length; i++) {
                int diff = maxWindow[i] - minWindow[i];
                minDiff = Math.min(minDiff, diff);
            }
        }

        System.out.println(minDiff);
    }

    private static void processRow(int[] row, int[] maxRow, int[] minRow, int k) {
        Deque<Integer> maxDeque = new ArrayDeque<>();
        Deque<Integer> minDeque = new ArrayDeque<>();
        for (int j = 0; j < row.length; j++) {
            // 维护最大值队列
            while (!maxDeque.isEmpty() && maxDeque.peekFirst() < j - k + 1) {
                maxDeque.pollFirst();
            }
            while (!maxDeque.isEmpty() && row[maxDeque.peekLast()] <= row[j]) {
                maxDeque.pollLast();
            }
            maxDeque.offerLast(j);

            // 维护最小值队列
            while (!minDeque.isEmpty() && minDeque.peekFirst() < j - k + 1) {
                minDeque.pollFirst();
            }
            while (!minDeque.isEmpty() && row[minDeque.peekLast()] >= row[j]) {
                minDeque.pollLast();
            }
            minDeque.offerLast(j);

            if (j >= k - 1) {
                int idx = j - k + 1;
                maxRow[idx] = row[maxDeque.peekFirst()];
                minRow[idx] = row[minDeque.peekFirst()];
            }
        }
    }

    private static int[] processWindow(int[] data, int windowSize, boolean isMax) {
        Deque<Integer> deque = new ArrayDeque<>();
        int[] res = new int[data.length - windowSize + 1];
        for (int i = 0; i < data.length; i++) {
            while (!deque.isEmpty() && deque.peekFirst() < i - windowSize + 1) {
                deque.pollFirst();
            }
            if (isMax) {
                while (!deque.isEmpty() && data[deque.peekLast()] <= data[i]) {
                    deque.pollLast();
                }
            } else {
                while (!deque.isEmpty() && data[deque.peekLast()] >= data[i]) {
                    deque.pollLast();
                }
            }
            deque.offerLast(i);
            if (i >= windowSize - 1) {
                res[i - windowSize + 1] = data[deque.peekFirst()];
            }
        }
        return res;
    }
}

UVA11572 唯一的雪花 Unique Snowflakes

解题思路

该题的提交需要绑定UVa OJ的账号。绑定流程参考:洛谷日报-注册UVa OJ 、博客园-胖乎乎的王老师。注意:如果无法在Edge中打开UVa官网,请使用Google或者Firefox打开。

该问题要求找到最长的连续子数组,其中所有元素都是唯一的。使用滑动窗口(双指针)和哈希表的方法来实现高效解决:

  1. ​滑动窗口:使用两个指针leftright维护当前窗口,确保窗口内元素唯一。
  2. ​哈希表​:记录每个元素最后一次出现的位置,以便快速判断是否重复并调整窗口。
import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        List<Integer> input = new ArrayList<>();
        String line;
        while ((line = br.readLine()) != null) {
            StringTokenizer st = new StringTokenizer(line);
            while (st.hasMoreTokens()) {
                input.add(Integer.parseInt(st.nextToken()));
            }
        }
        int ptr = 0;
        int T = input.get(ptr++);
        while (T-- > 0) {
            int n = input.get(ptr++);
            int[] arr = new int[n];
            for (int i = 0; i < n; i++) {
                arr[i] = input.get(ptr++);
            }
            System.out.println(maxUniqueSnowflakes(arr));
        }
    }

    private static int maxUniqueSnowflakes(int[] arr) {
        int max = 0;
        int left = 0;
        Map<Integer, Integer> lastSeen = new HashMap<>();
        for (int right = 0; right < arr.length; right++) {
            int num = arr[right];
            if (lastSeen.containsKey(num) && lastSeen.get(num) >= left) {
                left = lastSeen.get(num) + 1;
            }
            lastSeen.put(num, right);
            max = Math.max(max, right - left + 1);
        }
        return max;
    }
}

P4653 [CEOI 2017] Sure Bet

解题思路

该题需要我们在选择灯泡时,系统随机选择A类或B类灯泡点亮后的最小收益尽可能大。我们可以通过贪心策略和二分查找来优化这个过程。

  1. 排序和前缀和​:首先将A类和B类灯泡按权值降序排列,计算前缀和数组sumAsumB,分别表示前i个最大权值的A类和B类灯泡的总权值。
  2. 预处理最大值数组​:预处理数组max_b,其中max_b[j]表示前j个B类灯泡的总权值减去j后的最大值。
  3. 二分查找​:使用二分查找确定最大可能的最小收益。对于每个候选值x,检查是否存在i和j使得选择i个A类和j个B类灯泡时,两种类型的最小收益至少为x。
import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        // 读取输入
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        if (n == 0) {  // 处理n=0的特殊情况
            System.out.println("0.0000");
            return;
        }

        List<Double> A = new ArrayList<>();
        List<Double> B = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            String[] parts = br.readLine().split(" ");
            double a = Double.parseDouble(parts[0]);
            double b = Double.parseDouble(parts[1]);
            A.add(a);
            B.add(b);
        }

        // 将A和B分别按降序排列,优先选择权值大的灯泡
        Collections.sort(A, Collections.reverseOrder());
        Collections.sort(B, Collections.reverseOrder());

        // 计算前缀和数组:sumA[i]表示前i个A类灯泡的总权值 - i(每个灯泡成本为1)
        double[] sumA = new double[n + 1];
        // sumB同理
        double[] sumB = new double[n + 1];
        for (int i = 1; i <= n; i++) {
            sumA[i] = sumA[i - 1] + A.get(i - 1);
            sumB[i] = sumB[i - 1] + B.get(i - 1);
        }

        // 预处理maxB数组:maxB[j]表示前j个B类灯泡的总权值 - j后的最大值(前缀最大值)
        double[] maxB = new double[n + 1];
        double currentMax = Double.NEGATIVE_INFINITY;
        for (int j = 0; j <= n; j++) {
            double val = sumB[j] - j;     // 选择j个B类灯泡的净收益
            if (val > currentMax) {       // 维护前缀最大值
                currentMax = val;
            }
            maxB[j] = currentMax;
        }

        // 二分查找寻找最大可能的最小收益x
        double left = -1e9;   // 二分下界
        double right = 1e9;   // 二分上界
        // 进行100次二分迭代保证精度(比判断差值更高效)
        for (int iter = 0; iter < 100; iter++) {
            double mid = (left + right) / 2;
            // 检查是否存在i和j使得两种情况的收益都至少为mid
            if (isPossible(sumA, maxB, n, mid)) {
                left = mid;   // 可能达到mid,尝试更大的值
            } else {
                right = mid;  // 无法达到mid,降低上界
            }
        }

        // 四舍五入到四位小数输出(注意处理浮点精度)
        System.out.printf("%.4f\n", Math.round(left * 10000.0) / 10000.0);
    }

    // 判断是否存在选择i个A类灯泡和j个B类灯泡的方案,使得两种情况的收益都至少为x
    private static boolean isPossible(double[] sumA, double[] maxB, int n, double x) {
        // 遍历所有可能的A类灯泡选择数量i
        for (int i = 0; i <= n; i++) {
            // A类的净收益:总权值 - i个灯泡成本 - x >= 0
            double aI = sumA[i] - i - x;
            if (aI < 0) continue;  // 不满足条件,跳过

            // 计算允许的B类灯泡最大数量jMax = floor(aI)
            int jMax = (int) Math.floor(aI);
            jMax = Math.min(jMax, n);  // 不能超过总灯泡数
            jMax = Math.max(jMax, 0);   // 防止负数

            // 检查是否存在j <= jMax,使得B类的净收益 >= i + x
            // 利用预处理好的maxB数组快速查询
            if (maxB[jMax] >= i + x) {
                return true;  // 存在满足条件的组合
            }
        }
        return false;  // 遍历完所有i仍不满足条件
    }
}

P3143 [USACO16OPEN] Diamond Collector S

解题思路

该题需要找到两个不重叠的展示柜,使得每个展示柜中的钻石大小相差不超过K,并且两个展示柜中的钻石数量总和最大。思路如下:

  1. 排序:首先将所有钻石的大小排序,以便后续处理。

  2. 预处理最长左窗口:对于每个右端点i,找到最左边的j,使得窗口[j, i]中的钻石大小差不超过K,并记录每个右端点的窗口长度。

  3. 预处理最长右窗口:对于每个左端点i,找到最右边的j,使得窗口[i, j]中的钻石大小差不超过K,并记录每个左端点的窗口长度。

  4. 预处理前缀最大值和后缀最大值:分别计算前缀最大值数组(每个位置之前的最大窗口长度)和后缀最大值数组(每个位置之后的最大窗口长度)。

  5. 计算最大总和:遍历所有可能的分割点,计算左边和右边窗口的最大长度之和,找出最大值。

import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int k = Integer.parseInt(st.nextToken());
        if (n < 2) {
            System.out.println(0);
            return;
        }
        int[] s = new int[n];
        for (int i = 0; i < n; i++) {
            s[i] = Integer.parseInt(br.readLine());
        }
        Arrays.sort(s);

        // 预处理max_left_len数组
        int[] maxLeftLen = new int[n];
        int j = 0;
        for (int i = 0; i < n; i++) {
            while (s[i] - s[j] > k) {
                j++;
            }
            maxLeftLen[i] = i - j + 1;
        }

        // 预处理max_right_len数组
        int[] maxRightLen = new int[n];
        j = n - 1;
        for (int i = n - 1; i >= 0; i--) {
            while (j > i && s[j] - s[i] > k) {
                j--;
            }
            maxRightLen[i] = j - i + 1;
        }

        // 计算前缀最大值数组leftMax
        int[] leftMax = new int[n];
        leftMax[0] = maxLeftLen[0];
        for (int i = 1; i < n; i++) {
            leftMax[i] = Math.max(leftMax[i - 1], maxLeftLen[i]);
        }

        // 计算后缀最大值数组rightMax
        int[] rightMax = new int[n];
        rightMax[n - 1] = maxRightLen[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            rightMax[i] = Math.max(maxRightLen[i], rightMax[i + 1]);
        }

        int ans = 0;
        for (int i = 0; i < n - 1; i++) {
            ans = Math.max(ans, leftMax[i] + rightMax[i + 1]);
        }

        System.out.println(ans);
    }
}

P7910 [CSP-J 2021] 插入排序

解题思路

  1. Pair类​:存储元素的值和原下标,并实现Comparable接口,按值升序、原下标升序排序。
  2. 初始化​:读取输入数据,初始化原始数组a和排序后的列表sorted
  3. 修改操作​:
    • 遍历sorted列表找到旧元素的位置并删除。
    • 插入新元素到正确位置,保持列表有序。
  4. 查询操作​:
    • 使用二分查找计算比目标值小的元素数量。
    • 在等于目标值的元素区间内,查找原下标小于目标位置的元素数量。
  5. 二分查找辅助函数​:实现bisectLeftbisectRightbisectIndex,用于快速定位插入点和统计元素数量。
import java.util.*;
import java.io.*;

// 自定义Pair类,存储元素值和原下标,并实现Comparable接口以便排序
class Pair implements Comparable<Pair> {
    int value;
    int index;

    public Pair(int value, int index) {
        this.value = value;
        this.index = index;
    }

    @Override
    public int compareTo(Pair o) {
        if (this.value != o.value) {
            return Integer.compare(this.value, o.value);
        } else {
            return Integer.compare(this.index, o.index);
        }
    }
}

public class Main {
    // 原始数组,存储当前每个元素的值
    static int[] a;
    // 排序后的列表,按值升序、原下标升序排列
    static List<Pair> sorted = new ArrayList<>();

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter out = new PrintWriter(System.out);

        String[] firstLine = br.readLine().split(" ");
        int n = Integer.parseInt(firstLine[0]);
        int Q = Integer.parseInt(firstLine[1]);

        a = new int[n + 1]; // 原下标从1开始
        String[] aValues = br.readLine().split(" ");
        for (int i = 1; i <= n; i++) {
            a[i] = Integer.parseInt(aValues[i - 1]);
        }

        // 初始化sorted列表
        for (int i = 1; i <= n; i++) {
            sorted.add(new Pair(a[i], i));
        }
        Collections.sort(sorted);

        for (int q = 0; q < Q; q++) {
            String[] op = br.readLine().split(" ");
            if (op[0].equals("1")) { // 修改操作
                int x = Integer.parseInt(op[1]);
                int v = Integer.parseInt(op[2]);
                modify(x, v);
            } else { // 查询操作
                int x = Integer.parseInt(op[1]);
                int ans = query(x);
                out.println(ans);
            }
        }

        out.flush();
    }

    // 处理修改操作,将原下标x的元素值更新为v
    private static void modify(int x, int v) {
        // 找到旧元素在sorted中的位置
        int oldIndex = -1;
        for (int i = 0; i < sorted.size(); i++) {
            if (sorted.get(i).index == x) {
                oldIndex = i;
                break;
            }
        }
        // 删除旧元素
        sorted.remove(oldIndex);
        // 插入新元素到正确位置
        Pair newPair = new Pair(v, x);
        int insertPos = bisectLeft(sorted, newPair);
        sorted.add(insertPos, newPair);
        // 更新原数组
        a[x] = v;
    }

    // 处理查询操作,返回原下标x的元素在排序后的位置
    private static int query(int x) {
        int v = a[x];
        // 计算比v小的元素数量
        Pair keyStart = new Pair(v, Integer.MIN_VALUE);
        int start = bisectLeft(sorted, keyStart);
        int lessCount = start;

        // 计算等于v且原下标小于x的元素数量
        int equalLessCount = 0;
        if (start < sorted.size() && sorted.get(start).value == v) {
            // 找到等于v的区间[start, end)
            Pair keyEnd = new Pair(v, Integer.MAX_VALUE);
            int end = bisectRight(sorted, keyEnd);
            // 在[start, end)区间内查找原下标小于x的元素数量
            equalLessCount = bisectIndex(start, end, x) - start;
        }

        return lessCount + equalLessCount + 1;
    }

    // 在sorted列表的[start, end)区间内,找到第一个原下标>=x的位置
    private static int bisectIndex(int start, int end, int targetIndex) {
        int low = start;
        int high = end;
        while (low < high) {
            int mid = (low + high) / 2;
            int midIndex = sorted.get(mid).index;
            if (midIndex < targetIndex) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }

    // 实现bisect_left功能,找到插入点
    private static int bisectLeft(List<Pair> list, Pair key) {
        int low = 0;
        int high = list.size();
        while (low < high) {
            int mid = (low + high) / 2;
            Pair midVal = list.get(mid);
            if (midVal.compareTo(key) < 0) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }

    // 实现bisect_right功能,找到插入点
    private static int bisectRight(List<Pair> list, Pair key) {
        int low = 0;
        int high = list.size();
        while (low < high) {
            int mid = (low + high) / 2;
            Pair midVal = list.get(mid);
            if (midVal.compareTo(key) <= 0) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }
}

P1578 奶牛浴场

解题思路

该题实际上是需要我们在给定的二维区域(边界为 [0, N] × [0, M])和 K 个障碍点的情况下,找到最大的无障碍矩形区域

1. 添加边界点
  • 在输入的 K 个障碍点基础上,添加四个角落的边界点:(N,0)(N,M)(0,M)(0,0)
  • 目的​:确保最大矩形可能紧贴区域边界的情况被覆盖。
2. 按 X 轴排序后处理横向扩展
  • 排序​:将所有点按 x 坐标升序排列(x 相同则按 y 排序)。
  • 遍历每个点​:对每个点 a[i],向和向两个方向寻找可能的矩形:
    1. 向右扩展​(j = i+1 开始):
      • 维护上下边界 h1(上边界)和 h2(下边界)。
      • 若遇到障碍点,调整上下边界以缩小可能的矩形高度。
      • 计算当前横向长度 v = a[j].x - a[i].x,并更新最大面积。
    2. 向左扩展​(j = i-1 开始):
      • 类似向右扩展,但横向长度为 v = a[i].x - a[j].x
  • 剪枝优化​:若当前可能的面积已小于已记录的最大值,则提前跳出循环。
3. 按 Y 轴排序后处理垂直扩展
  • 排序​:将所有点按 y 坐标升序排列。
  • 遍历相邻点​:计算相邻两点 a[i] 和 a[i+1] 之间的垂直距离 (a[i+1].y - a[i].y)
  • 最大垂直矩形​:由于水平方向无约束,宽度为 N,面积为 N * (a[i+1].y - a[i].y)
import java.util.Arrays;
import java.util.Scanner;

// 定义一个节点类,用于存储坐标信息
class Node {
    int x, y;

    // 构造函数,用于初始化节点的 x 和 y 坐标
    Node(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

public class Main {
    // 用于数组的最大长度
    static final int MAX = 5000 + 7;
    // 用于存储所有的节点信息
    static Node[] a = new Node[MAX];
    // N 和 M 可能表示区域的边界,K 表示节点数量,ans 用于存储最终结果
    static int N, M, K, ans;

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        N = input.nextInt();
        M = input.nextInt();
        K = input.nextInt();

        for (int i = 1; i <= K; i++) {
            int x = input.nextInt();
            int y = input.nextInt();
            a[i] = new Node(x, y);
        }

        a[++K] = new Node(N, 0);
        a[++K] = new Node(N, M);
        a[++K] = new Node(0, M);
        a[++K] = new Node(0, 0);

        // 按照 x 坐标升序排序,如果 x 坐标相同,则按照 y 坐标升序排序
        Arrays.sort(a, 1, K + 1, (n1, n2) -> {
            if (n1.x != n2.x) {
                return n1.x - n2.x;
            }
            return n1.y - n2.y;
        });

        for (int i = 1; i <= K; i++) {
            // 初始化 h1 为 M,h2 为 0,v 为 N - a[i].x,fa 为 0
            // h1 和 h2 用于记录当前的上下边界,v 用于记录当前的横向长度,fa 用于标记是否遇到特殊情况
            int h1 = M, h2 = 0, v = N - a[i].x, fa = 0;

            for (int j = i + 1; j <= K; j++) {
                if (a[j].y <= h1 && a[i].y >= h2) {
                    // 如果当前可能的矩形面积小于等于已记录的最大面积,则跳出循环
                    if (v * (h1 - h2) <= ans) {
                        break;
                    }
                    // 更新最大面积
                    ans = Math.max(ans, (h1 - h2) * (a[j].x - a[i].x));
                    // 如果当前节点的 y 坐标等于当前节点的 y 坐标
                    if (a[j].y == a[i].y) {
                        // 标记为遇到特殊情况
                        fa = 1;
                        break;
                    }
                    // 如果当前节点的 y 坐标大于当前节点的 y 坐标,更新 h1 为较小值
                    if (a[j].y > a[i].y) {
                        h1 = Math.min(h1, a[j].y);
                    } else {
                        // 否则更新 h2 为较大值
                        h2 = Math.max(h2, a[j].y);
                    }
                }
            }
            // 如果没有遇到特殊情况,更新最大面积
            if (fa == 0) {
                ans = Math.max(ans, v * (h1 - h2));
            }
            // 重新初始化 h1、h2、v 和 fa
            h1 = M;
            h2 = 0;
            v = a[i].x;
            fa = 0;
            // 从当前节点的前一个节点开始向前遍历
            for (int j = i - 1; j >= 1; j--) {
                // 如果当前节点的 y 坐标在 h1 和 h2 之间
                if (a[j].y <= h1 && a[i].y >= h2) {
                    // 如果当前可能的矩形面积小于等于已记录的最大面积,则跳出循环
                    if (v * (h1 - h2) <= ans) {
                        break;
                    }
                    // 更新最大面积
                    ans = Math.max(ans, (h1 - h2) * (a[i].x - a[j].x));
                    // 如果当前节点的 y 坐标等于当前节点的 y 坐标
                    if (a[j].y == a[i].y) {
                        // 标记为遇到特殊情况
                        fa = 1;
                        break;
                    }
                    // 如果当前节点的 y 坐标大于当前节点的 y 坐标,更新 h1 为较小值
                    if (a[j].y > a[i].y) {
                        h1 = Math.min(h1, a[j].y);
                    } else {
                        // 否则更新 h2 为较大值
                        h2 = Math.max(h2, a[j].y);
                    }
                }
            }
            // 如果没有遇到特殊情况,更新最大面积
            if (fa == 0) {
                ans = Math.max(ans, v * (h1 - h2));
            }
        }

        // 按照 y 坐标升序排序
        Arrays.sort(a, 1, K + 1, (n1, n2) -> n1.y - n2.y);
        // 遍历所有节点,更新最大面积
        for (int i = 1; i < K; i++) {
            ans = Math.max(ans, (a[i + 1].y - a[i].y) * N);
        }

        System.out.println(ans);
        input.close();
    }
}

P3467 [POI 2008] PLA-Postering

解题思路

这个问题需要我们找到最少数量的矩形海报,以覆盖所有建筑的北立面。每个海报必须覆盖连续的区间,并且其高度必须至少是该区间内所有建筑的最大高度。可以使用单调栈来解决这个问题。

  1. 排序和单调栈:我们使用单调递增栈来维护当前处理的高度序列。每次遇到一个新的高度时,弹出所有比当前高度大的栈顶元素,并记录弹出的次数。

  2. 合并相同高度:如果当前高度与栈顶高度相同,则可以合并到同一个海报中,无需增加计数。

  3. 统计结果:最终的结果为弹出的次数加上栈中剩余的元素数目,这表示所有需要分开的区域数量。

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int[] heights = new int[n];
        for (int i = 0; i < n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            st.nextToken(); // 宽度不需要,只取高度
            heights[i] = Integer.parseInt(st.nextToken());
        }

        Deque<Integer> stack = new ArrayDeque<>();
        int count = 0;

        for (int h : heights) {
            while (!stack.isEmpty() && stack.peek() > h) {
                stack.pop();
                count++;
            }
            if (stack.isEmpty() || stack.peek() < h) {
                stack.push(h);
            }
        }

        System.out.println(count + stack.size());
    }
}

P1886 滑动窗口 /【模板】单调队列

解题思路

解决滑动窗口中的最大值和最小值问题,我们可以使用单调队列来高效地维护窗口内的元素。

  1. 单调队列维护最小值​:使用一个单调递增队列来维护窗口内的最小值。队列中保存元素的索引,保证队列中的元素值严格递增。当新元素进入窗口时,移除队列中所有比它大的元素,因为这些元素不可能成为后续窗口的最小值。
  2. 单调队列维护最大值​:使用一个单调递减队列来维护窗口内的最大值。队列中保存元素的索引,保证队列中的元素值严格递减。当新元素进入窗口时,移除队列中所有比它小的元素,因为这些元素不可能成为后续窗口的最大值。
  3. 窗口范围检查​:每次处理新元素时,检查队列头部元素是否超出当前窗口范围,若超出则移除,确保队列中的元素始终在当前窗口内。
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int k = Integer.parseInt(st.nextToken());
        int[] a = new int[n];
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < n; i++) {
            a[i] = Integer.parseInt(st.nextToken());
        }

        // 处理最小值
        Deque<Integer> minDeque = new ArrayDeque<>();
        List<Integer> minResult = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            // 移除超出窗口的元素
            while (!minDeque.isEmpty() && minDeque.peekFirst() < i - k + 1) {
                minDeque.pollFirst();
            }
            // 移除比当前元素大的元素
            while (!minDeque.isEmpty() && a[i] <= a[minDeque.peekLast()]) {
                minDeque.pollLast();
            }
            minDeque.offerLast(i);
            if (i >= k - 1) {
                minResult.add(a[minDeque.peekFirst()]);
            }
        }

        // 处理最大值
        Deque<Integer> maxDeque = new ArrayDeque<>();
        List<Integer> maxResult = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            // 移除超出窗口的元素
            while (!maxDeque.isEmpty() && maxDeque.peekFirst() < i - k + 1) {
                maxDeque.pollFirst();
            }
            // 移除比当前元素小的元素
            while (!maxDeque.isEmpty() && a[i] >= a[maxDeque.peekLast()]) {
                maxDeque.pollLast();
            }
            maxDeque.offerLast(i);
            if (i >= k - 1) {
                maxResult.add(a[maxDeque.peekFirst()]);
            }
        }

        // 输出结果
        StringBuilder sb = new StringBuilder();
        for (int num : minResult) {
            sb.append(num).append(' ');
        }
        sb.setLength(sb.length() - 1);
        sb.append('\n');
        for (int num : maxResult) {
            sb.append(num).append(' ');
        }
        sb.setLength(sb.length() - 1);
        bw.write(sb.toString());
        bw.flush();
    }
}

P2880 [USACO07JAN] Balanced Lineup G

解题思路

该题需要解决多次查询区间内最大值和最小值的差值问题。采用稀疏表(ST表)数据结构。

  1. 输入处理:​​ 读取牛的数量n和查询次数q,随后读取每头牛的身高存入数组h。
  2. 预处理ST表:​
    • 构建两个二维数组st_max和st_min,分别存储区间最大值和最小值。
    • 初始层(j=0)每个区间的长度为1,最大值和最小值即为该位置的元素。
    • 对于每一层j,将区间分为两半,分别取前半段和后半段的最值,合并后得到当前层的最值。
  3. 处理查询:​
    • 对于每个查询区间[a, b],转换为0-based的索引[L, R]。
    • 计算区间长度对应的幂次k,将区间分为两个重叠的2^k长度的区间,分别查询最值后取结果。
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter out = new PrintWriter(System.out);
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int q = Integer.parseInt(st.nextToken());
        int[] h = new int[n];
        
        for (int i = 0; i < n; i++) {
            h[i] = Integer.parseInt(br.readLine());
        }

        // 预处理ST表
        int log_max = 0;
        while ((1 << (log_max + 1)) <= n) {
            log_max++;
        }
        int[][] st_max = new int[log_max + 1][n];
        int[][] st_min = new int[log_max + 1][n];
        
        for (int i = 0; i < n; i++) {
            st_max[0][i] = h[i];
            st_min[0][i] = h[i];
        }
        
        for (int j = 1; j <= log_max; j++) {
            for (int i = 0; i + (1 << j) <= n; i++) {
                int prev = 1 << (j - 1);
                st_max[j][i] = Math.max(st_max[j - 1][i], st_max[j - 1][i + prev]);
                st_min[j][i] = Math.min(st_min[j - 1][i], st_min[j - 1][i + prev]);
            }
        }

        // 处理查询
        for (int i = 0; i < q; i++) {
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            int L = a - 1;
            int R = b - 1;
            int len = R - L + 1;
            int k = 31 - Integer.numberOfLeadingZeros(len);
            
            int max_val = Math.max(st_max[k][L], st_max[k][R - (1 << k) + 1]);
            int min_val = Math.min(st_min[k][L], st_min[k][R - (1 << k) + 1]);
            out.println(max_val - min_val);
        }
        out.flush();
    }
}

P1714 切蛋糕

解题思路

这题需要找到给定数组中长度不超过 m 的连续子数组,使得它们的和最大。可以通过使用前缀和和单调队列解决。

  1. 前缀和数组:计算前缀和数组,以便快速计算任意子数组的和。

  2. 单调队列维护:使用单调队列来维护当前窗口内的最小前缀和值,从而快速找到每个右端点对应的最大子数组和。

  3. 滑动窗口处理:遍历每个右端点,维护队列中的元素在有效窗口范围内,并更新最大值。

import java.io.*;
import java.util.ArrayDeque;
import java.util.Deque;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] line = br.readLine().split(" ");
        int n = Integer.parseInt(line[0]);
        int m = Integer.parseInt(line[1]);
        
        int[] p = new int[n];
        String[] nums = br.readLine().split(" ");
        for (int i = 0; i < n; i++) {
            p[i] = Integer.parseInt(nums[i]);
        }
        
        long[] s = new long[n + 1];
        for (int i = 1; i <= n; i++) {
            s[i] = s[i - 1] + p[i - 1];
        }
        
        Deque<Integer> deque = new ArrayDeque<>();
        deque.addLast(0);
        long max = s[1] - s[0];  // 初始化为第一个元素的值
        
        for (int r = 1; r <= n; r++) {
            // 移除超出窗口的左端点
            while (!deque.isEmpty() && deque.peekFirst() < r - m) {
                deque.pollFirst();
            }
            
            // 计算当前最大值
            if (!deque.isEmpty()) {
                long current = s[r] - s[deque.peekFirst()];
                if (current > max) {
                    max = current;
                }
            }
            
            // 维护单调递增队列
            while (!deque.isEmpty() && s[r] <= s[deque.peekLast()]) {
                deque.pollLast();
            }
            deque.addLast(r);
        }
        
        System.out.println(max);
    }
}

P1725 琪露诺

解题思路

该题需要找到最大冰冻指数,考虑到每个位置的决策依赖之前的位置状态以及滑动窗口的最大值,使用dp+单调队列。

  1. 输入处理​:读取输入的N、L、R和冰冻指数数组A。数组A的大小为N+1,对应编号0到N的格子。

  2. 初始化动态规划数组​:dp[i]表示到达第i个格子时的最大冰冻指数。初始时,只有dp[0] = 0(起始位置),其余设为最小值。

  3. 单调队列维护​:使用双端队列维护当前可到达的候选位置,确保队列中的元素(位置索引)对应的dp值单调递减,从而快速获取区间最大值。

  4. 状态转移​:对于每个位置i,通过队列找到可跳转到i的候选位置中的最大值,更新dp[i]。队列维护窗口范围为[i-R, i-L],确保时间复杂度为O(N)。

  5. 结果计算​:遍历所有可能直接跳到对岸的位置(即满足i + R >= N+1的位置),取最大的dp值作为最终结果。

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int L = Integer.parseInt(st.nextToken());
        int R = Integer.parseInt(st.nextToken());

        int[] A = new int[N + 1];
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i <= N; i++) {
            A[i] = Integer.parseInt(st.nextToken());
        }

        int[] dp = new int[N + 1];
        Arrays.fill(dp, Integer.MIN_VALUE);
        dp[0] = 0; // 初始位置在0号格子,冰冻指数为0

        Deque<Integer> deque = new ArrayDeque<>();

        for (int i = 0; i <= N; i++) {
            // 将可以跳到当前i的候选j(即j_end = i - L)加入队列
            if (i >= L) {
                int jEnd = i - L;
                // 维护队列单调递减,确保队列末尾的元素的dp值 >= 当前jEnd的dp值
                while (!deque.isEmpty() && dp[jEnd] >= dp[deque.peekLast()]) {
                    deque.pollLast();
                }
                deque.addLast(jEnd);
            }

            // 移除队列中不在当前窗口[i-R, i-L]范围内的元素
            int windowLeft = i - R;
            while (!deque.isEmpty() && deque.peekFirst() < windowLeft) {
                deque.pollFirst();
            }

            // 计算dp[i]
            if (i == 0) {
                continue; // dp[0]已经初始化
            }
            if (i >= L) {
                if (!deque.isEmpty()) {
                    dp[i] = dp[deque.peekFirst()] + A[i];
                } // 否则保持为MIN_VALUE
            } // 否则保持为MIN_VALUE
        }

        // 寻找最终的最大值:所有满足i + R >= N+1的dp[i]
        int result = Integer.MIN_VALUE;
        int lower = Math.max(0, (N + 1) - R);
        for (int i = lower; i <= N; i++) {
            if (dp[i] > result) {
                result = dp[i];
            }
        }

        System.out.println(result);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HeShen.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值