P8218 【深进1.例1】求区间和
解题思路
前缀和数组:
prefixSum[i]表示数组 a 的前 (i) 项的和。- 通过
prefixSum[r] - prefixSum[l - 1]可以快速计算区间 ([l, r]) 的和。时间复杂度:
- 构建前缀和数组的时间复杂度是 (O(n))。
- 每次查询的时间复杂度是 (O(1))。
- 总体时间复杂度从原来的 (O(m \cdot n)) 降低到 (O(n + m))。
空间复杂度:
- 额外使用了一个长度为 (n + 1) 的前缀和数组,空间复杂度是 (O(n))。
这里首先使用常规思路解题,思路没错,但是会超时,使用前缀后之后,用空间换时间避免超时。
常规思路
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int[] a = new int[n];
for (int i = 0; i < a.length; i++) {
a[i] = input.nextInt();
}
int m = input.nextInt();
int ans = 0;
while (m-- > 0) {
int l = input.nextInt();
int r = input.nextInt();
for (int i = l; i <= r; i++) {
ans += a[i - 1];
}
System.out.println(ans);
ans = 0;
}
input.close();
}
}
前缀和(AC代码)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int[] a = new int[n];
int[] prefixSum = new int[n + 1]; // 前缀和数组,prefixSum[0] = 0
// 读取数组并计算前缀和
for (int i = 0; i < n; i++) {
a[i] = input.nextInt();
prefixSum[i + 1] = prefixSum[i] + a[i];
}
int m = input.nextInt();
while (m-- > 0) {
int l = input.nextInt();
int r = input.nextInt();
// 使用前缀和计算区间和
int ans = prefixSum[r] - prefixSum[l - 1];
System.out.println(ans);
}
input.close();
}
}
P1719 最大加权矩形
解题思路
枚举上下边界:
- 使用两个嵌套循环
top和bottom,分别表示矩形的上边界和下边界。- 在固定上下边界后,将矩阵压缩为一维数组
temp,其中temp[col]表示当前上下边界内第col列的累加和。Kadane 算法:
- 对压缩后的一维数组
temp使用 Kadane 算法,快速计算最大子数组和。- Kadane 算法的时间复杂度是 (O(n))。
更新最大值:
- 每次计算出当前上下边界内的最大子数组和后,更新全局最大值
maxSum。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int[][] a = new int[n][n];
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a.length; j++) {
a[i][j] = input.nextInt();
}
}
int maxSum = Integer.MIN_VALUE;
// 枚举上下边界
for (int top = 0; top < n; top++) {
int[] temp = new int[n]; // 用于存储列的累加和
for (int bottom = top; bottom < n; bottom++) {
// 计算当前上下边界内每列的累加和
for (int col = 0; col < n; col++) {
temp[col] += a[bottom][col];
}
// 使用 Kadane 算法计算一维数组的最大子数组和
maxSum = Math.max(maxSum, maxSubArraySum(temp));
}
}
System.out.println(maxSum);
input.close();
}
// 一维最大子数组和(Kadane 算法)
private static int maxSubArraySum(int[] arr) {
int maxSum = arr[0];
int currentSum = arr[0];
for (int i = 1; i < arr.length; i++) {
currentSum = Math.max(arr[i], currentSum + arr[i]);
maxSum = Math.max(maxSum, currentSum);
}
return maxSum;
}
}
P1314 [NOIP 2011 提高组] 聪明的质监员
解题思路
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());
// 读取矿石数量 n、区间数量 m 和目标值 s
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
long s = Long.parseLong(st.nextToken());
// 读取每个矿石的重量 w 和价值 v
int[] w = new int[n + 1];
int[] v = new int[n + 1];
for (int i = 1; i <= n; i++) {
st = new StringTokenizer(br.readLine());
w[i] = Integer.parseInt(st.nextToken());
v[i] = Integer.parseInt(st.nextToken());
}
// 读取每个区间的左右端点
int[][] intervals = new int[m][2];
for (int i = 0; i < m; i++) {
st = new StringTokenizer(br.readLine());
intervals[i][0] = Integer.parseInt(st.nextToken());
intervals[i][1] = Integer.parseInt(st.nextToken());
}
// 找到矿石的最大重量,用于二分查找的右边界
int max_w = 0;
for (int i = 1; i <= n; i++) {
if (w[i] > max_w) {
max_w = w[i];
}
}
// 初始化二分查找的左右边界
int left = 0;
int right = max_w + 1;
long minDiff = Long.MAX_VALUE; // 记录最小的 |s - y|
// 辅助数组,用于前缀和计算
long[] cnt = new long[n + 1]; // 记录满足条件的矿石数量的前缀和
long[] sum_v = new long[n + 1]; // 记录满足条件的矿石价值的前缀和
// 二分查找,寻找使 |s - y| 最小的参数 W
while (left <= right) {
int mid = (left + right) >>> 1; // 取中间值作为当前的 W
// 重置前缀和数组
Arrays.fill(cnt, 0);
Arrays.fill(sum_v, 0);
// 计算前缀和数组
for (int i = 1; i <= n; i++) {
if (w[i] >= mid) { // 如果当前矿石的重量满足条件
cnt[i] = cnt[i - 1] + 1; // 累加满足条件的矿石数量
sum_v[i] = sum_v[i - 1] + v[i]; // 累加满足条件的矿石价值
} else {
cnt[i] = cnt[i - 1]; // 不满足条件,数量保持不变
sum_v[i] = sum_v[i - 1]; // 不满足条件,价值保持不变
}
}
// 计算所有区间的检验值 y
long total = 0;
for (int i = 0; i < m; i++) {
int l = intervals[i][0];
int r = intervals[i][1];
// 计算区间 [l, r] 内满足条件的矿石数量和价值
long count = cnt[r] - cnt[l - 1];
long sum = sum_v[r] - sum_v[l - 1];
total += count * sum; // 累加到总检验值
}
// 计算当前检验值与目标值 s 的差值
long diff = Math.abs(total - s);
if (diff < minDiff) { // 更新最小差值
minDiff = diff;
}
// 根据当前检验值调整二分查找的范围
if (total > s) {
left = mid + 1; // 检验值过大,增大 W
} else {
right = mid - 1; // 检验值过小,减小 W
}
}
// 输出最小的 |s - y|
System.out.println(minDiff);
}
}
P2367 语文成绩
解题思路
- 差分数组:差分数组允许我们在O(1)时间内完成区间加减操作。差分数组
d的第i个元素表示原数组a中第i个元素与前一个元素的差值。- 区间操作处理:对于每个区间操作
(x, y, z),我们只需在差分数组的d[x]加上z,并在d[y+1]减去z。- 前缀和计算:通过计算差分数组的前缀和,我们可以得到每个学生的最终成绩,并找到其中的最小值。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer st = new StreamTokenizer(br);
st.nextToken();
int n = (int) st.nval;
st.nextToken();
int p = (int) st.nval;
int[] d = new int[n + 2];
// 构造差分数组
st.nextToken();
int aPrev = (int) st.nval;
d[1] = aPrev;
for (int i = 2; i <= n; i++) {
st.nextToken();
int aCurrent = (int) st.nval;
d[i] = aCurrent - aPrev;
aPrev = aCurrent;
}
// 处理区间操作
for (int i = 0; i < p; i++) {
st.nextToken();
int x = (int) st.nval;
st.nextToken();
int y = (int) st.nval;
st.nextToken();
int z = (int) st.nval;
d[x] += z;
d[y + 1] -= z;
}
// 计算前缀和并找最小值
int sum = 0;
int min = Integer.MAX_VALUE;
for (int i = 1; i <= n; i++) {
sum += d[i];
if (sum < min) {
min = sum;
}
}
System.out.println(min);
}
}
P3397 地毯
解题思路
- 二维差分数组:差分数组是一种用于高效处理区间更新操作的数据结构。通过记录区间的起点和终点的变化量,我们可以在最后通过前缀和每个点的最终覆盖次数。
- 差分操作:对于每个地毯覆盖的矩形区域,我们更新差分数组的四个角点,分别进行加1和减1操作。
- 前缀和计算:通过两次前缀和计算(先按行,再按列),我们可以将差分数组转换为每个点的实际覆盖次数。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int m = input.nextInt();
int[][] d = new int[n + 2][n + 2]; // 使用n+2的数组来避免越界
for (int k = 0; k < m; k++) {
int x1 = input.nextInt();
int y1 = input.nextInt();
int x2 = input.nextInt();
int y2 = input.nextInt();
// 应用差分数组的四个操作
d[x1][y1]++;
d[x1][y2 + 1]--;
d[x2 + 1][y1]--;
d[x2 + 1][y2 + 1]++;
}
// 计算行前缀和
for (int i = 1; i <= n + 1; i++) {
for (int j = 1; j <= n + 1; j++) {
d[i][j] += d[i][j - 1];
}
}
// 计算列前缀和
for (int j = 1; j <= n + 1; j++) {
for (int i = 1; i <= n + 1; i++) {
d[i][j] += d[i - 1][j];
}
}
// 输出结果
for (int i = 1; i <= n; i++) {
StringBuilder sb = new StringBuilder();
for (int j = 1; j <= n; j++) {
sb.append(d[i][j]);
if (j < n) {
sb.append(" ");
}
}
System.out.println(sb);
}
input.close();
}
}
P1496 火烧赤壁
解题思路
输入处理:将所有的区间存储在
List<int[]>中,每个区间用一个长度为 2 的数组表示。排序:按区间的起点升序排序,如果起点相同,则按终点升序排序。
区间合并:遍历排序后的区间列表,判断当前区间是否与前一个区间重叠:
- 如果不重叠,则将前一个区间的长度累加到总长度中,并更新当前区间为新的起点和终点。
- 如果重叠,则更新当前区间的结束点。
输出结果:最后将最后一个区间的长度累加到总长度中。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
// 用于存储所有区间
List<int[]> intervals = new ArrayList<>();
for (int i = 0; i < n; i++) {
int a = input.nextInt();
int b = input.nextInt();
intervals.add(new int[]{a, b});
}
// 按起点排序,如果起点相同按终点排序
Collections.sort(intervals, (o1, o2) -> o1[0] == o2[0] ? o1[1] - o2[1] : o1[0] - o2[0]);
// 合并区间并计算总长度
int totalLength = 0;
int start = intervals.get(0)[0];
int end = intervals.get(0)[1];
for (int i = 1; i < intervals.size(); i++) {
int[] current = intervals.get(i);
if (current[0] <= end) {
// 当前区间与前一个区间重叠或相连,更新结束点
end = Math.max(end, current[1]);
} else {
// 当前区间与前一个区间不重叠,累加长度并更新起点和终点
totalLength += end - start;
start = current[0];
end = current[1];
}
}
// 累加最后一个区间的长度
totalLength += end - start;
System.out.println(totalLength);
input.close();
}
}
P1955 [NOI2015] 程序自动分析
解题思路
- 初始化并查集:高效处理连通性问题,每个元素初始时是独立的集合。
- 处理相等约束:将相等的元素合并到同一个集合。
- 处理不等约束:
- 检查不等的元素是否已经连通。
- 如果连通,则违反约束,返回
NO。- 输出结果:如果所有约束都满足,返回
YES。
import java.util.*;
public class Main {
// Union-Find 数据结构,用于高效处理连通性问题
static class UnionFind {
private final Map<Integer, Integer> parent = new HashMap<>();
// 查找操作,带路径压缩优化
public int find(int x) {
if (!parent.containsKey(x)) { // 如果 x 不在 parent 中,初始化为自身
parent.put(x, x);
}
if (parent.get(x) != x) { // 路径压缩,将 x 的父节点直接指向根节点
parent.put(x, find(parent.get(x)));
}
return parent.get(x);
}
// 合并操作,将两个集合合并
public void union(int x, int y) {
int rootX = find(x); // 找到 x 的根节点
int rootY = find(y); // 找到 y 的根节点
if (rootX != rootY) { // 如果根节点不同,合并两个集合
parent.put(rootY, rootX);
}
}
// 判断两个元素是否在同一个集合中
public boolean connected(int x, int y) {
return find(x) == find(y);
}
}
// 处理单个测试用例,判断约束是否满足
public static String solveCase(List<int[]> constraints) {
UnionFind uf = new UnionFind();
List<int[]> notEqualPairs = new ArrayList<>(); // 存储不等约束的对
// 遍历所有约束
for (int[] constraint : constraints) {
int x = constraint[0];
int y = constraint[1];
int e = constraint[2]; // 约束类型 (1 表示相等,0 表示不等)
if (e == 1) {
uf.union(x, y); // 如果是相等约束,合并两个元素
} else {
notEqualPairs.add(new int[] { x, y }); // 如果是不等约束,加入列表
}
}
// 检查所有不等约束是否被违反
for (int[] pair : notEqualPairs) {
if (uf.connected(pair[0], pair[1])) { // 如果两个元素已经连通,则违反约束
return "NO";
}
}
return "YES";
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int t = input.nextInt();
StringBuilder result = new StringBuilder(); // 用于存储所有测试用例的结果
// 遍历每个测试用例
for (int i = 0; i < t; i++) {
int n = input.nextInt(); // 读取当前测试用例的约束数量
List<int[]> constraints = new ArrayList<>(); // 存储当前测试用例的约束
for (int j = 0; j < n; j++) {
int x = input.nextInt();
int y = input.nextInt();
int e = input.nextInt(); // 读取约束类型 (1 表示相等,0 表示不等)
constraints.add(new int[] { x, y, e }); // 将约束加入列表
}
String res = solveCase(constraints); // 处理当前测试用例
result.append(res).append("\n");
}
System.out.print(result);
input.close();
}
}
P1884 [USACO12FEB] Overplanting S
解题思路
离散化坐标:
- 由于坐标范围很大(
-10^8到10^8),直接操作会导致内存和时间复杂度过高。- 我们可以将所有矩形的横坐标和纵坐标进行离散化,映射到一个较小的索引范围。
扫描线算法:
- 按照矩形的横坐标(
x值)排序,依次处理每个矩形的左边界和右边界。- 使用一个差分数组或线段树来维护当前被覆盖的纵坐标区间。
计算面积:
- 每次扫描到一个新的横坐标时,根据当前的覆盖区间计算面积增量。
- 面积增量等于当前覆盖的纵坐标长度乘以横坐标的变化量。
import java.util.*;
public class Main {
static class Event implements Comparable<Event> {
int x, y1, y2, type; // x坐标,y区间,type=1表示矩形左边界,type=-1表示右边界
public Event(int x, int y1, int y2, int type) {
this.x = x;
this.y1 = y1;
this.y2 = y2;
this.type = type;
}
@Override
public int compareTo(Event other) {
return this.x - other.x; // 按x坐标排序
}
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
List<Event> events = new ArrayList<>();
Set<Integer> yCoords = new HashSet<>();
// 读取矩形信息
for (int i = 0; i < n; i++) {
int x1 = input.nextInt();
int y1 = input.nextInt();
int x2 = input.nextInt();
int y2 = input.nextInt();
// 确保y1 < y2
if (y1 > y2) {
int temp = y1;
y1 = y2;
y2 = temp;
}
// 添加事件
events.add(new Event(x1, y1, y2, 1)); // 左边界
events.add(new Event(x2, y1, y2, -1)); // 右边界
// 收集所有y坐标
yCoords.add(y1);
yCoords.add(y2);
}
// 离散化y坐标
List<Integer> sortedY = new ArrayList<>(yCoords);
Collections.sort(sortedY);
Map<Integer, Integer> yIndex = new HashMap<>();
for (int i = 0; i < sortedY.size(); i++) {
yIndex.put(sortedY.get(i), i);
}
// 按x坐标排序事件
Collections.sort(events);
// 差分数组,记录每个离散化y区间的覆盖次数
int[] count = new int[sortedY.size()];
long totalArea = 0;
int prevX = events.get(0).x;
// 扫描线处理
for (Event event : events) {
int currX = event.x;
// 计算当前覆盖的y区间长度
long coveredLength = 0;
for (int i = 0; i < count.length - 1; i++) {
if (count[i] > 0) {
coveredLength += sortedY.get(i + 1) - sortedY.get(i);
}
}
// 累加面积
totalArea += coveredLength * (currX - prevX);
// 更新差分数组
int y1Index = yIndex.get(event.y1);
int y2Index = yIndex.get(event.y2);
for (int i = y1Index; i < y2Index; i++) {
count[i] += event.type;
}
// 更新prevX
prevX = currX;
}
// 输出结果
System.out.println(totalArea);
input.close();
}
}
P2004 领地选择
解题思路
第七个测试点如果MLE,多试几次就能过。
二维前缀和:
- 构建一个二维前缀和数组
prefix,其中prefix[i][j]表示从地图左上角(1, 1)到(i, j)的矩形区域的土地价值总和。- 通过前缀和,可以在常数时间内计算任意矩形区域的总和。
滑动窗口计算最大值:
- 遍历所有可能的首都左上角坐标
(x, y),计算以(x, y)为左上角、边长为C的正方形区域的总价值。- 记录最大值及其对应的坐标。
优化:
- 使用二维前缀和可以将矩形区域的求和从
O(C^2)优化到O(1),整体复杂度为O(N * M)。
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 C = input.nextInt();
int[][] grid = new int[N + 1][M + 1]; // 地图,1-based 索引
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= M; j++) {
grid[i][j] = input.nextInt();
}
}
// 构建二维前缀和
int[][] prefix = new int[N + 1][M + 1];
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= M; j++) {
prefix[i][j] = grid[i][j]
+ prefix[i - 1][j]
+ prefix[i][j - 1]
- prefix[i - 1][j - 1];
}
}
// 滑动窗口寻找最大价值
int maxSum = Integer.MIN_VALUE;
int bestX = 0, bestY = 0;
for (int i = C; i <= N; i++) {
for (int j = C; j <= M; j++) {
// 计算以 (i, j) 为右下角,边长为 C 的正方形的总价值
int total = prefix[i][j]
- prefix[i - C][j]
- prefix[i][j - C]
+ prefix[i - C][j - C];
if (total > maxSum) {
maxSum = total;
bestX = i - C + 1;
bestY = j - C + 1;
}
}
}
// 输出结果
System.out.println(bestX + " " + bestY);
input.close();
}
}
P3017 [USACO11MAR] Brownie Slicing G
解题思路
前缀和计算
构建二维前缀和数组s,其中s[i][j]表示从(1,1)到(i,j)的子矩阵和。通过前缀和,可以快速计算任意子矩阵的和,例如行区间[now+1, i]和列j的和为:
(s[i][j] - s[i][j-1]) - (s[now][j] - s[now][j-1])。二分答案
在可能的范围[0, 矩阵总和]内进行二分查找,寻找最大的x,使得矩阵可以被切割成a行b列,每块的和均≥x。检查函数
check
- 逐行扫描:从第一行开始,累计当前行与上一次切割行之间的列差值。
- 动态切割:当累计值≥
x时切割一列,并统计当前行切割出的列数。若某行切割出至少b列,则记为该行有效,并更新切割位置。- 结果判定:若有效行数≥
a,则当前x可行。
import java.io.*;
import java.util.*;
public class Main {
static int r, c, a, b; // 矩阵行数、列数,目标切割成a行b列
static int[][] map = new int[501][501]; // 输入的矩阵数据(1-based)
static int[][] s = new int[501][501]; // 二维前缀和数组(1-based)
static int ans; // 存储最终结果
static boolean check(int x) {
int now = 0; // 记录上一次切割的行号(初始从第0行开始)
int num = 0; // 统计已切割的行数
for (int i = 1; i <= r; i++) { // 遍历每一行
int dis = 0; // 当前累计的差值
int sum = 0; // 当前行切割出的列数
for (int j = 1; j <= c; j++) {
// 计算第j列在[now+1, i]行之间的和:当前行j列前缀和 - 上一次切割行j列前缀和
int current = (s[i][j] - s[i][j - 1]) - (s[now][j] - s[now][j - 1]);
if (dis + current < x) { // 累加后仍不足x,继续累积
dis += current;
} else { // 累积足够,切割一列
sum++;
dis = 0; // 重置累计值
}
}
if (sum >= b) { // 当前行能切割出至少b列
now = i; // 更新切割行
num++; // 增加切割行计数
}
}
return num >= a; // 是否满足至少a行
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
r = Integer.parseInt(st.nextToken());
c = Integer.parseInt(st.nextToken());
a = Integer.parseInt(st.nextToken());
b = Integer.parseInt(st.nextToken());
// 读取矩阵数据(1-based)
for (int i = 1; i <= r; i++) {
st = new StringTokenizer(br.readLine());
for (int j = 1; j <= c; j++) {
map[i][j] = Integer.parseInt(st.nextToken());
}
}
// 计算二维前缀和数组s[i][j](1-based)
for (int i = 1; i <= r; i++) {
for (int j = 1; j <= c; j++) {
s[i][j] = s[i - 1][j] + s[i][j - 1] + map[i][j] - s[i - 1][j - 1];
}
}
int h = 0; // 二分左边界(最小可能值)
int t = s[r][c]; // 二分右边界(矩阵总和,最大可能值)
ans = 0;
// 二分查找寻找最大可行的x
while (h <= t) {
int mid = (h + t) / 2;
if (check(mid)) { // 当前mid可行,尝试更大的值
ans = mid;
h = mid + 1;
} else { // 不可行,减小阈值
t = mid - 1;
}
}
System.out.println(ans);
}
}
P3406 海底高铁
解题思路
输入处理:读取城市数量和访问顺序,以及每段铁路的票价信息。
差分数组:使用差分数组来高效统计每段铁路的经过次数。差分数组可以在O(1)时间内处理区间增操作,最后通过前缀和得到每段铁路的实际经过次数。
费用计算:根据每段铁路的经过次数,比较购买纸质车票和使用IC卡的总费用,选择较小的那个累加到总费用中。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
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]);
line = br.readLine().split(" ");
int[] P = new int[M];
for (int i = 0; i < M; i++) {
P[i] = Integer.parseInt(line[i]);
}
int[] d = new int[N + 2]; // 差分数组,索引0到N+1
for (int j = 0; j < M - 1; j++) {
int u = P[j];
int v = P[j + 1];
int l = Math.min(u, v);
int r = Math.max(u, v) - 1;
d[l]++;
if (r + 1 <= N) {
d[r + 1]--;
}
}
int[] cnt = new int[N + 1]; // 段i的次数是cnt[i]
int sum = 0;
for (int i = 1; i <= N - 1; i++) {
sum += d[i];
cnt[i] = sum;
}
long total = 0;
for (int i = 1; i <= N - 1; i++) {
line = br.readLine().split(" ");
int A = Integer.parseInt(line[0]);
int B = Integer.parseInt(line[1]);
int C = Integer.parseInt(line[2]);
int k = cnt[i];
if (k == 0) {
continue;
}
long cost1 = (long) k * A;
long cost2 = (long) k * B + C;
total += Math.min(cost1, cost2);
}
System.out.println(total);
}
}
P1083 [NOIP 2012 提高组] 借教室
解题思路
差分数组原理
- 核心思想:将区间操作转换为端点标记
当处理订单[l,r]增加d时:
diff[l] += d:表示从l开始所有元素增加ddiff[r+1] -= d:表示从r+1开始取消这个增加- 前缀和计算:最终通过计算diff数组的前缀和,得到每个位置的实际变化量
二分查找优化
- 搜索目标:第一个导致资源不足的订单(最小的非法订单号)
- 循环不变量:保持
[0,left)区间合法,[left,right)区间待检查- 终止条件:当
left == right时,left即为第一个非法订单索引大数处理方案
- 数据类型选择:
rest/diff/dArr使用long类型:防止处理1e9级数值时溢出current使用long类型:防止累加过程溢出int范围- 输入处理优化:使用
(long)st.nval直接读取浮点数转long,保留完整精度
import java.io.*;
import java.util.*;
public class Main {
static int n, m; // 天数、订单数
static long[] rest; // 每日可用教室数(1-based)
static long[] diff; // 差分数组(记录每日变化量)
static long[] dArr; // 订单需求量数组(long防溢出)
static int[] lArr; // 订单左端点数组
static int[] rArr; // 订单右端点数组
static boolean isValid(int x) {
Arrays.fill(diff, 0); // 重置差分数组
// 应用前x个订单到差分数组
for (int i = 0; i < x; i++) {
int l = lArr[i];
diff[l] += dArr[i]; // 区间起点增加需求
if (rArr[i] + 1 <= n) { // 防止越界
diff[rArr[i] + 1] -= dArr[i]; // 区间终点后一位减少需求
}
}
// 2. 计算每日累计需求并验证
long current = 0; // 使用long防止累加溢出
for (int i = 1; i <= n; i++) {
current += diff[i]; // 计算前缀和得到实际需求
if (current > rest[i]) { // 发现资源不足
return false;
}
}
return true;
}
public static void main(String[] args) throws IOException {
// 输入加速:使用StreamTokenizer处理大输入
StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
st.nextToken(); n = (int) st.nval; // 天数
st.nextToken(); m = (int) st.nval; // 订单数
// 初始化每日教室数量数组(注意1-based)
rest = new long[n + 2];
for (int i = 1; i <= n; i++) {
st.nextToken();
rest[i] = (long) st.nval;
}
// 存储订单数据(0-based)
dArr = new long[m]; // 需求值存在大数,必须用long
lArr = new int[m]; // 区间左端点(1-based)
rArr = new int[m]; // 区间右端点(1-based)
for (int i = 0; i < m; i++) {
st.nextToken(); dArr[i] = (long) st.nval; // 处理大整数
st.nextToken(); lArr[i] = (int) st.nval;
st.nextToken(); rArr[i] = (int) st.nval;
}
// 初始化差分数组(大小与rest对齐)
diff = new long[n + 2];
// 快速检查:所有订单都合法时直接输出
if (isValid(m)) {
System.out.println(0);
return;
}
// 二分查找核心逻辑(左闭右开区间)
int left = 0, right = m;
while (left < right) {
int mid = (left + right) >>> 1; // 无符号右移防溢出
if (isValid(mid + 1)) { // 检查前mid+1个订单是否合法
left = mid + 1; // 合法则尝试更大的值
} else {
right = mid; // 非法则缩小右边界
}
}
// 输出第一个非法订单编号(从1开始)
System.out.println("-1");
System.out.println(left + 1);
}
}
P2882 [USACO07MAR] Face The Right Way G
解题思路
贪心策略:从左到右遍历每个位置,如果发现当前牛朝后,则立即进行一次翻转操作。这样可以确保后面的处理不会影响到已经处理过的位置。
队列维护:使用队列来记录翻转操作的结束位置,以便在处理后续位置时能够正确跟踪当前翻转的影响。
二分查找优化:通过遍历所有可能的 K 值,找到使得操作次数最少的 K。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
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));
int N = Integer.parseInt(br.readLine());
int[] state = new int[N];
// 读取每一行状态,将 'B' 转换为 1,其他字符转换为 0
for (int i = 0; i < N; i++) {
String line = br.readLine().trim();
state[i] = line.charAt(0) == 'B' ? 1 : 0;
}
// 初始化答案变量,ansK 表示最优的 K 值,ansM 表示最小的翻转次数
int ansK = N;
int ansM = Integer.MAX_VALUE;
for (int K = 1; K <= N; K++) {
Deque<Integer> queue = new ArrayDeque<>(); // 用于记录当前翻转区间的结束位置
int current = 0; // 当前翻转状态的累积值
int cnt = 0; // 当前翻转次数
boolean valid = true; // 标记当前 K 是否有效
// 遍历状态数组
for (int i = 1; i <= N; i++) {
// 移除已经过期的翻转区间
while (!queue.isEmpty() && queue.peekFirst() <= i) {
queue.pollFirst();
current--;
}
int idx = i - 1; // 当前索引(从 0 开始)
int cs = state[idx] ^ (current % 2); // 计算当前状态是否需要翻转
if (cs == 1) { // 如果需要翻转
// 如果翻转区间超出数组范围,则当前 K 无效
if (i + K - 1 > N) {
valid = false;
break;
}
cnt++; // 增加翻转次数
int end = i + K; // 计算翻转区间的结束位置
queue.addLast(end); // 将结束位置加入队列
current++; // 更新当前翻转状态
}
}
// 如果当前 K 有效,更新最优解
if (valid) {
// 如果翻转次数更少,或者翻转次数相同但 K 更小,则更新答案
if (cnt < ansM || (cnt == ansM && K < ansK)) {
ansM = cnt;
ansK = K;
}
}
}
System.out.println(ansK + " " + ansM);
}
}
P4552 [Poetize6] IncDec Sequence
解题思路
- 输入处理:使用
BufferedReader读取输入,将每个元素存储在数组a中。- 差分计算:遍历数组,计算相邻元素的差值
diff,并分别累加到正数总和pos或负数绝对值总和neg。- 最少操作次数:取
pos和neg的最大值,因为每次操作可以处理一个正数和一个负数单位,剩余部分需要单独处理。- 可能的结果数:计算
pos和neg的差的绝对值加1,因为剩余的操作可以调整最终值的不同可能性。
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());
long[] a = new long[n];
for (int i = 0; i < n; i++) {
a[i] = Long.parseLong(br.readLine());
}
// 计算差分数组中正差和负差的绝对值和
long pos = 0; // 存储所有正差的总和(a[i] - a[i-1] > 0)
long neg = 0; // 存储所有负差的绝对值总和(a[i] - a[i-1] < 0)
for (int i = 1; i < n; i++) {
long diff = a[i] - a[i - 1]; // 计算当前元素与前一个元素的差值
if (diff > 0) {
pos += diff; // 累加正差
} else {
neg += -diff; // 累加负差的绝对值(取反后相加)
}
}
// 计算最少操作次数:等于正差和负差中较大的那个(每次操作可以抵消一个正差和一个负差)
long operations = Math.max(pos, neg);
// 计算可能的结果种数:正差与负差的差值绝对值 + 1(剩余的操作次数可以自由分配到不同位置)
long variations = Math.abs(pos - neg) + 1;
System.out.println(operations); // 输出最少操作次数
System.out.println(variations); // 输出可能的结果种数
}
}
P3029 [USACO11NOV] Cow Lineup S
解题思路
数据结构定义:
Cow类用于存储每头牛的坐标和品种,并实现Comparable接口以便根据坐标排序。输入处理与排序:
- 使用
BufferedReader读取输入数据,将每头牛的信息存入数组,并统计所有不同品种。- 根据牛的坐标对数组进行排序,确保后续滑动窗口处理的有序性。
滑动窗口逻辑:
- 初始化:左指针
left、品种计数器count、最小窗口长度minLength和品种计数哈希表breedCount。- 右指针移动:遍历每头牛(作为窗口右端点),更新品种计数。当某品种首次出现时,增加计数器。
- 窗口收缩:当窗口包含所有品种时,不断右移左指针以缩小窗口,并更新最小窗口长度。左指针移动时,减少对应品种的计数,若某品种计数归零则减少计数器。
import java.io.*;
import java.util.*;
public class Main {
// 定义Cow类,存储牛的坐标和品种,并实现Comparable接口以便排序
static class Cow implements Comparable<Cow> {
int x;
int breed;
public Cow(int x, int breed) {
this.x = x;
this.breed = breed;
}
// 按x坐标升序排序
@Override
public int compareTo(Cow o) {
return Integer.compare(this.x, o.x);
}
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
Cow[] cows = new Cow[n];
Set<Integer> breeds = new HashSet<>(); // 用于统计所有不同的品种
// 读取输入并初始化牛的数组
for (int i = 0; i < n; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
int x = Integer.parseInt(st.nextToken());
int breed = Integer.parseInt(st.nextToken());
cows[i] = new Cow(x, breed);
breeds.add(breed);
}
Arrays.sort(cows); // 按x坐标排序
int k = breeds.size(); // 不同品种的总数
if (k == 1) { // 特殊情况:只有一种品种,无需移动
System.out.println(0);
return;
}
int left = 0; // 滑动窗口左指针
int count = 0; // 当前窗口中不同品种的数量
long minLength = Long.MAX_VALUE; // 最小窗口长度
Map<Integer, Integer> breedCount = new HashMap<>(); // 记录窗口中各品种的出现次数
for (int right = 0; right < n; right++) {
int currentBreed = cows[right].breed;
// 更新当前品种的计数
breedCount.put(currentBreed, breedCount.getOrDefault(currentBreed, 0) + 1);
if (breedCount.get(currentBreed) == 1) { // 首次出现该品种,增加计数器
count++;
}
// 当窗口包含所有品种时,尝试缩小窗口以找到最小长度
while (count == k) {
// 计算当前窗口的x坐标差,并更新最小值
minLength = Math.min(minLength, cows[right].x - cows[left].x);
int leftBreed = cows[left].breed;
// 左指针右移,更新品种计数
breedCount.put(leftBreed, breedCount.get(leftBreed) - 1);
if (breedCount.get(leftBreed) == 0) { // 该品种在窗口中不再存在,减少计数器
count--;
}
left++;
}
}
System.out.println(minLength);
}
}
P1904 天际线
解题思路
事件处理:
- 每个建筑生成两个事件:左边缘(开始)和右边缘(结束)。
- 事件按x坐标升序排序,同一x下开始事件优先处理,确保正确的覆盖顺序。
高度管理:
- 使用
TreeMap维护当前活动的高度及其出现次数,便于快速获取最大值(lastKey())。轮廓线生成:
- 遍历处理所有事件,每次处理同一x的所有事件后检查当前最大高度。
- 仅当高度变化时记录转折点,避免冗余点。
输出格式:
- 结果列表按顺序拼接为字符串,符合题目要求的交替x和y坐标格式。
import java.io.*;
import java.util.*;
public class Main {
static class Event {
int x;
int h;
boolean isStart;
public Event(int x, int h, boolean isStart) {
this.x = x;
this.h = h;
this.isStart = isStart;
}
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
List<Event> events = new ArrayList<>();
String line;
// 读取所有建筑数据并生成事件列表
while ((line = br.readLine()) != null && !line.isEmpty()) {
StringTokenizer st = new StringTokenizer(line);
int L = Integer.parseInt(st.nextToken());
int H = Integer.parseInt(st.nextToken());
int R = Integer.parseInt(st.nextToken());
events.add(new Event(L, H, true)); // 开始事件
events.add(new Event(R, H, false)); // 结束事件
}
// 事件排序:按x升序,同一x下开始事件优先
Collections.sort(events, (a, b) -> {
if (a.x != b.x) return a.x - b.x;
// 开始事件排在结束事件前面
if (a.isStart && !b.isStart) return -1;
if (!a.isStart && b.isStart) return 1;
return 0;
});
TreeMap<Integer, Integer> heightCount = new TreeMap<>();
List<Integer> result = new ArrayList<>();
int prevHeight = 0;
int i = 0;
while (i < events.size()) {
int currentX = events.get(i).x;
// 处理同一x的所有事件
int j = i;
while (j < events.size() && events.get(j).x == currentX) {
Event event = events.get(j);
if (event.isStart) {
// 添加高度计数
heightCount.put(event.h, heightCount.getOrDefault(event.h, 0) + 1);
} else {
// 减少高度计数,若为0则移除
int cnt = heightCount.getOrDefault(event.h, 0);
if (cnt == 1) {
heightCount.remove(event.h);
} else if (cnt > 1) {
heightCount.put(event.h, cnt - 1);
}
}
j++;
}
// 计算当前最大高度
int currHeight = heightCount.isEmpty() ? 0 : heightCount.lastKey();
if (currHeight != prevHeight) {
result.add(currentX);
result.add(currHeight);
prevHeight = currHeight;
}
i = j; // 移动到下一个x的事件
}
// 构建输出字符串
StringBuilder sb = new StringBuilder();
for (int k = 0; k < result.size(); k++) {
if (k > 0) sb.append(" ");
sb.append(result.get(k));
}
System.out.println(sb);
}
}
P4375 [USACO18OPEN] Out of Sorts G
解题思路
- 数据结构定义:
Data类用于保存元素的值(val)和原始位置(num),并实现Comparable接口以支持排序。- 输入处理:读取输入数据并构建
Data数组,使用1-based索引以匹配原C++代码逻辑。- 排序操作:使用
Arrays.sort对数组的1到n部分进行排序,排序规则按值和原始位置升序排列。- 标记数组与计数:
vis数组用于标记元素的原始位置是否被处理。cnt记录当前需要处理的元素数,ans记录最大的cnt值,即最大“moo”次数。- 遍历与更新逻辑:遍历排序后的数组,根据元素原始位置和当前位置的关系更新计数,并维护最大计数值。
import java.util.*;
// 定义数据结构,保存元素的值和原始位置
class Data implements Comparable<Data> {
int val;
int num;
public Data(int val, int num) {
this.val = val;
this.num = num;
}
// 实现比较方法:先按值升序,值相同则按原始位置升序
@Override
public int compareTo(Data other) {
if (this.val != other.val) {
return Integer.compare(this.val, other.val);
} else {
return Integer.compare(this.num, other.num);
}
}
}
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
// 使用1-based索引,数组大小为n+1以便直接使用1到n的索引
Data[] a = new Data[n + 1];
for (int i = 1; i <= n; i++) {
int val = input.nextInt();
a[i] = new Data(val, i); // 记录元素的原始位置(从1开始)
}
// 对数组的1到n部分进行排序
Arrays.sort(a, 1, n + 1);
// 初始化访问标记数组,记录每个位置是否被处理过
boolean[] vis = new boolean[n + 2]; // 防止越界
int cnt = 0;
int ans = 1; // 至少会有一个moo输出
for (int i = 1; i <= n; i++) {
// 如果当前元素的原始位置在当前位置之后,需要处理
if (i < a[i].num) {
cnt++;
}
// 如果当前位置已被访问过,减少计数(可能已处理完毕)
if (vis[i]) {
cnt--;
}
// 标记当前元素的原始位置已被处理
vis[a[i].num] = true;
// 更新最大计数,即最大的moo次数
ans = Math.max(ans, cnt);
}
System.out.println(ans);
input.close();
}
}
P5937 [CEOI 1999] Parity Game
解题思路
- Query类:存储每个查询的x、y坐标和奇偶性z。
- 并查集实现:
find函数:路径压缩优化,确保快速查找根节点。union函数:合并两个集合。- 离散化处理:
- 使用TreeSet收集所有出现的坐标点并排序。
- 将坐标映射到连续的索引,缩小数据范围。
- 处理查询:
- 将每个查询的坐标转换为离散化后的索引。
- 根据奇偶性类型(z),合并对应的节点并检查矛盾。若发现矛盾立即输出当前查询索引。
- 输出结果:若所有查询均无矛盾,输出查询总数m。
import java.util.*;
import java.io.*;
public class Main {
static class Query {
int x, y, z;
Query(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
}
static int[] parent;
static int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
static void union(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
parent[fx] = fy;
}
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
int m = Integer.parseInt(br.readLine());
Query[] queries = new Query[m];
Set<Integer> points = new TreeSet<>();
// 读取所有查询并收集坐标点
for (int i = 0; i < m; i++) {
String[] parts = br.readLine().split(" ");
int x = Integer.parseInt(parts[0]) - 1; // 转换为前缀差形式
int y = Integer.parseInt(parts[1]);
int z = parts[2].equals("odd") ? 1 : 0;
queries[i] = new Query(x, y, z);
points.add(x);
points.add(y);
}
// 离散化处理
List<Integer> sorted = new ArrayList<>(points);
int size = sorted.size();
parent = new int[size * 2 + 2]; // 每个点对应两个节点
// 初始化并查集
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
}
// 处理每个查询
for (int i = 0; i < m; i++) {
Query q = queries[i];
int x = Collections.binarySearch(sorted, q.x);
int y = Collections.binarySearch(sorted, q.y);
x = x < 0 ? -x - 1 : x; // 处理binarySearch的返回值
y = y < 0 ? -y - 1 : y;
int xSame = x;
int xDiff = x + size;
int ySame = y;
int yDiff = y + size;
if (q.z == 0) { // even: x和y奇偶性相同
if (find(xSame) == find(yDiff)) {
System.out.println(i);
return;
}
union(xSame, ySame);
union(xDiff, yDiff);
} else { // odd: x和y奇偶性不同
if (find(xSame) == find(ySame)) {
System.out.println(i);
return;
}
union(xSame, yDiff);
union(xDiff, ySame);
}
}
System.out.println(m);
}
}
2276

被折叠的 条评论
为什么被折叠?



