备战蓝桥杯,用JAVA刷洛谷算法题单:【算法2-1】前缀和、差分与离散化

参考

【算法2-1】前缀和、差分与离散化 - 题单 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

image-20240313154845340

前缀和

前缀和:一般用于解决求区间和的问题

P8218 【深进1.例1】求区间和

前缀和:用一个数组表示前i个数的和

  • 下标:		0 1 2 3 4  5
    
  • 如数组a:	0 1 5 2 4  7
    
  • 则数组b:	0 1 6 8 12 19
    
  • [l,r]的和=b[r]-b[l-1]
    

递推公式b[i] = b[i-1]+a[i]

区间和:[l,r]= b[r]-b[l-1]

详细可见题解如下图

image-20240313160142453

一次AC

package _2_1;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class P8218 {
	private static StreamTokenizer in;
	private static PrintWriter out;

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

		int n = nextInt();
		int[] nums = new int[n + 1];
		// algo 前缀和:用一个数组表示前i个数的和
		/*
		 * 	下标:		0 1 2 3 4  5
		 *  如数组a:	0 1 5 2 4  7
		 *  则数组b:	0 1 6 8 12 19
		 *  [l,r]的和=b[r]-b[l-1]
		 */
		
		int[] sums = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			nums[i] = nextInt();
			sums[i] = sums[i - 1] + nums[i];
		}
		int m = nextInt();
		while (m-- > 0) {
			int l = nextInt();
			int r = nextInt();
			out.println(sums[r]-sums[l-1]);
		}

		out.flush();
		out.close();
	}

	public static int nextInt() throws IOException {
		in.nextToken();
		return (int) in.nval;
	}
}

P1719 最大加权矩形

二维前缀和:

递推公式b[i][j] = b[i-1][j]+b[i][j-1]-b[i-1][j-1]+a[i][j]

image-20240313161347680

看上图,每个格子表示b[i][j],那么求得当前b(从11到ij这一部分的和)可以无非就是上一行的b(表示绿蓝这个部分的和)+左一列的b(表示绿黄这一部分的和)-重叠的左上角b(表示绿色这一部分的和)+当前值(a[i][j])

得到这个递推公式之后,如果要知道以b[x][y]为左上角和b[i][j]为右下角的矩阵的区间和(想象上图11是xy),只需要减去这个有颜色框的上面部分和左面部分,再加上重复剪掉的部分,如下图所示

image-20240313163519194

区间和:[x,y]~[i,j]=b[i][j]-b[x-1][j]-b[i][y-1]+b[x-1][y-1]

区间和:[x1,y1]~[x2,y2]=b[x2][y2]-b[x1-1][y2]-b[x2][y1-1]+b[x1-1][y1-1]

这里求区间和就要用四重循环咯(所以这道题n不大),注意ij不能xy小,从xy开始遍历,而不是+1,否则就成右下角开始了,少了同一行同一列的遍历。

package _2_1;

import java.util.Scanner;

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

		int n = scanner.nextInt();
		int[][] nums = new int[n + 1][n + 1];

		// algo 前缀和:二维
		// 设b[i][j] 表示以11为左上角,ij为右下角点的这个矩形的所有元素的和
		// 我们可以画图来看就能得到如下的公式
		// 递推公式 b[i][j] = b[i-1][j]+b[i][j-1]-b[i-1][j-1]+a[i][j]
		// 区间和xy,ij b[i][j]-b[x-1][j]-b[i][y-1]+b[x-1][y-1]

		int[][] sum = new int[n + 1][n + 1];
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= n; j++) {
				nums[i][j] = scanner.nextInt();
				sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + nums[i][j];
			}
		}

		int max = Integer.MIN_VALUE;
		for (int x = 1; x <= n; x++) {
			for (int y = 1; y <= n; y++) {
				// 设定ij分别比xy大(当然同一行同一列是满足的,所以从xy开始遍历,而不是+1,要不然就是右下角开始了,少了一行一列)
				for (int i = x; i <= n; i++) {
					for (int j = y; j <= n; j++) {
						max = Math.max(max, sum[i][j] - sum[x - 1][j] - sum[i][y - 1] + sum[x - 1][y - 1]);
					}
				}
			}
		}

		System.out.println(max);

		scanner.close();
	}
}

差分

差分:就是当前的值和前一个值得差值。通常差分用于得到多次对连续区间修改值操作后的结果值(区间修改+单点求值,树状数组也可以做)

P2367 语文成绩

递推公式b[i] = a[i]-a[i-1]

修改区间b[l]+k,b[r+1]-k

还原数组:a[i] = b[i]+a[i-1],就是递推公式移相

package _2_1;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;

public class P2367 {
	private static StreamTokenizer in;

	public static void main(String[] args) throws IOException {
		in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
		int n = nextInt();
		int p = nextInt();
		int[] nums = new int[n + 1];
		int[] diffs = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			nums[i] = nextInt();
			// algo 差分
			/*
			 * 问题:一个连续区间,加上同一个数 解决:差分数组b[i] = a[i]-a[i-1] 对于区间[l,r]对差分进行修改:b[l]+k,b[r+1]-k
			 * 还原数组 a[i] = b[i]+a[i-1]
			 * 
			 */
			// 1、计算差分
			diffs[i] = nums[i] - nums[i - 1];
		}
		while (p-- > 0) {
			int x = nextInt();
			int y = nextInt();
			int z = nextInt();
			// 2、修改区间的首末差分值
			diffs[x] += z;
			if (y + 1 <= n)
				diffs[y + 1] -= z;
		}
		// 3、还原数组
		int min = Integer.MAX_VALUE;
		for (int i = 1; i <= n; i++) {
			nums[i] = diffs[i] + nums[i - 1];
			min = Math.min(min, nums[i]);
		}

		System.out.println(min);
	}

	public static int nextInt() throws IOException {
		in.nextToken();
		return (int) in.nval;
	}
}

P1083 [NOIP2012 提高组] 借教室

二分+差分

package luogu;

import java.util.Arrays;
import java.util.Scanner;

public class P1083 {
	private static int[] starts;
	private static int[] ends;
	private static int[] nums;
	private static int[] diffs;
	private static int[] tempNums;
	private static int[] availableNums;
	private static int n;
	private static int m;

	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		n = scanner.nextInt();
		m = scanner.nextInt();
		availableNums = new int[n + 5];
		tempNums = new int[n + 5];
		diffs = new int[n + 5];
		nums = new int[m + 5];
		starts = new int[m + 5];
		ends = new int[m + 5];
//		初始化原数组
		for (int i = 1; i <= n; i++) {
			availableNums[i] = scanner.nextInt();
		}
//		记录需求
		for (int i = 1; i <= m; i++) {
			nums[i] = scanner.nextInt();
			starts[i] = scanner.nextInt();
			ends[i] = scanner.nextInt();
		}
//		注意题目要求还要找出需要修改的订单,所以需要根据元数据判断有没有不够租界的,
//		如果每改一次差分就要算一次元数据,则差分无意义
//		如果算完全部差分再算元数据,则找不出需要修改的订单
//		所以使用二分,分区间来算这一部分的临时差分和临时元数据,并判断够不够数量
		int left = 0;
		int right = m + 1;
//		如何找边界值?看极端值m=1,则符合判断0+1<2,m=1进行一次判断,
//		如果符合要求则left=mid,继续往后判断
//		最终如果left==订单数,表示判断到了最后都没问题,还要判断一次m,
//		否则在中间有问题时,right表示出问题的地方
		while (left + 1 < right) {
			int mid = left + (right - left) / 2;
			if (check(mid))
				left = mid;
			else
				right = mid;
		}
		if (left == m) {
			if (check(m)) {
				System.out.println(0);
			}
		} else {
			System.out.println(-1);
			System.out.println(right);
		}
		scanner.close();
	}

//	判断区间内是否够租界
//	注意练习题目,这里的区间能使中间某一个部分(因为天数要连续的算下来),所以区间始终是从1开始
	public static boolean check(int mid) {
//		初始化差分数组和临时结果数据
		Arrays.fill(diffs, 0);
		Arrays.fill(tempNums, 0);
//		计算区间差分
		for (int i = 1; i <= mid; i++) {
			diffs[starts[i]] += nums[i];
			diffs[ends[i] + 1] -= nums[i];
		}
//		还原结果数据,判断是否足够数量
		for (int i = 1; i <= n; i++) {
			tempNums[i] = tempNums[i - 1] + diffs[i];
//			tempsNums表示满足当前要求所需的数量,如果大于可租借数,则表示该区间内有需要修改的订单
			if (tempNums[i] > availableNums[i]) {
				return false;
			}
		}
		return true;
	}
}

P3397 地毯

二维差分

递推公式 b[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1]

修改区间:[x,y]~[i,j] b[x][y]+=k,b[x][j+1]-=k,b[i+1][y]-=k,b[i+1][j+1]+=k

修改区间:[x1,y1]~[x2,y2] b[x1][y1]+=k,b[x1][y2+1]-=k,b[x2+1][y1]-=k,b[x2+1][y2+1]+=k

还原数组: a[i][j]=a[i-1][j]a[i][j-1]-a[i-1][j-1]+b[i][j],就是递推公式移相

第一反应:啊好长,前缀和可以用图像记比较形象,这个有点抽象,但这俩怎么这么像呢?看下面

二维差分 - 知乎 (zhihu.com)

里面提到一点:原矩阵是差分矩阵的前缀和。我敲确实啊!那来比对一下两者的递推公式

  • 一维前缀和递推公式:b[i] = b[i-1]+a[i]

  • 一维差分递推公式:b[i] = a[i]-a[i-1]

不就是前缀和的a变成了差分的b吗!二维也是如此。

  • 二维前缀和递推公式:b[i][j] = b[i-1][j]+b[i][j-1]-b[i-1][j-1]+a[i][j]
  • 二维差分递推公式: b[i][j] = a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1]

那就舒服了,记个前缀和的不就行了,前缀和有图像好记(快去看我画的图)

至于修改区间的推导(我懒)看参考吧。

package _2_1;

import java.util.Scanner;

public class P3397 {
	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		int n = scanner.nextInt();
		int m = scanner.nextInt();

		int[][] nums = new int[n + 1][n + 1];
		int[][] diffs = new int[n + 1][n + 1];
		// algo 差分:二维
		// b[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1]
		// 修改区间:[x1,y1]~[x2,y2]
		// b[x1][y1]+=k,b[x1][y2+1]-=k,b[x2+1][y1]-=k,b[x2+1][y2+1]+=k
		// 1、这题差分初始都是0不用计算
		while (m-- > 0) {
			// xy表示左上角,ij表示右下角
			int x1 = scanner.nextInt();
			int y1 = scanner.nextInt();
			int x2 = scanner.nextInt();
			int y2 = scanner.nextInt();
			// 2、修改差分
			diffs[x1][y1] += 1;
			if (y2 + 1 <= n)
				diffs[x1][y2 + 1] -= 1;
			if (x2 + 1 <= n)
				diffs[x2 + 1][y1] -= 1;
			if (y2 + 1 <= n && x2 + 1 <= n)
				diffs[x2 + 1][y2 + 1] += 1;
		}
		// 3、还原数组
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= n; j++) {
				nums[i][j] = diffs[i][j] + nums[i - 1][j] + nums[i][j - 1] - nums[i - 1][j - 1];
				System.out.printf("%d ", nums[i][j]);
			}
			System.out.println();
		}
		scanner.close();
	}
}

哦吼!MLE,只过了20的。对于100%的数据,有n*,*m≤1000,不应该啊?换成一个数组也一样,没解决不是很懂捏/(ㄒoㄒ)/~~。

离散化

P1496 火烧赤壁

这题一看连续区间修改值->差分,然后前缀和还原结果数组。但是注意数据规模

image-20240314100440252

ab对应的是数组下标,直接顶到边界了,但是n的大小并不是很大。所以这题的数据是那种需要开大数组,但是存的有效数据有比较少的,需要使用离散化。

算法——离散化 java实现_java离散化-CSDN博客

八、基础算法——离散化算法_java离散化-CSDN博客

离散化 - Cattle_Horse - 博客园 (cnblogs.com)

算法笔记:离散化的两种实现方式 - 知乎 (zhihu.com)

思想就是把离散的有效值的下标,映射到另一个数组(这个数组不用很大,开到多少个有效值的最大值就行)

然后把原数组存值改为存离散数组的下标,这样找原数组的值就改成找对于离散数组(一般离散数组都要去重+排序),找的话就是二分的找

所以可以看到题解的大佬都是用两个数组来把区间[l,r]值映射到起点数组(存l)和终点数组(存r),这两个映射数组只需要开n的最大值,并且使用int类型(见lr的取值范围)。

image-20240314101954707

将起点和终点按照从小到大的顺序排序,对答案不会产生影响!所以起点数组和终点数组可以分开排序,然后再遍历加上差值。但是注意9-2+11-5还要减去中间重复的部分,所以如果后一个的起点在前一个的终点之前,那就表明有重复的部分(这里注意最后一条线段就不用判断后一个了)

package _2_1;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.Arrays;

public class P1496 {
	private static StreamTokenizer in;

	public static void main(String[] args) throws IOException {
		in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
		int n = nextInt();
		// algo 离散化:把稀疏的下标映射到一个小数组当中
		int[] l = new int[n];
		int[] r = new int[n];
		for (int i = 0; i < n; i++) {
			l[i] = nextInt();
			r[i] = nextInt();
		}
		// 这里如果数组开固定最大值的化,sort会把无效的值也排序,可以指定排序长度
		Arrays.sort(l);
		Arrays.sort(r);
		// algo 排序:Array数组指定范围排序
//		Arrays.sort(l, 0, n);
		int cnt = 0;
		for (int i = 0; i < n; i++) {
			// 左闭右开,所以不用+1
			cnt += r[i] - l[i];
			// 最后区间没有后面的区间了,所以不需要判断是否有重复
			if (i != n - 1) {
				// 如果后面的区间和前面的有重复,就要减去重复的部分
				if (l[i + 1] < r[i]) {
					cnt -= r[i] - l[i + 1];
				}
			}
		}
		System.out.println(cnt);
	}

	public static void fun() throws IOException {
		int n = nextInt();
		// 差分,但是数组开太大不行
		int[] diffs = new int[Integer.MAX_VALUE];
		int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
		while (n-- > 0) {
			// 左闭右开
			int a = nextInt();
			int b = nextInt();
			diffs[a] += 1;
			diffs[b] -= 1;
			min = Math.min(min, a);
			max = Math.max(max, b - 1);
		}
		int cnt = 0;
		for (int i = min; i <= max; i++) {
			diffs[i] = diffs[i] += diffs[i - 1];
			if (diffs[i] > 0) {
				cnt++;
			}
		}
		System.out.println(cnt);
	}

	public static int nextInt() throws IOException {
		in.nextToken();
		return (int) in.nval;
	}
}

P1955 [NOI2015] 程序自动分析

并查集+离散化的题,确实让我更理解离散化了。

要把握把原值改成离散化后数组的下标这一思想的转换(虽然实践起来会有点抽象),然后有几个固定步骤

  • 离散化数组,类型要考虑原值下标的最大范围,大小要考虑有效值数量的范围
  • 离散化数组要排序+去重
  • 把原值改成对应离散化数组的下标(这样对原数组来说,需要的下标范围不久缩短到离散化数组的下标长度了吗,实现了缩短)
  • 以后所有的操作都任然是对原值操作(虽然实际上是对离散化数组下标的操作,但是你要加上这一层转换那就抽象了)
  • 最后要获得真正的原值,就拿现有原值取离散化数组取(不过一般需要离散化的题都不会要求最后获取原值的,所以离散化适合那种重点在于对值的操作,而不是值本身)

有几个RE,懒得改了

package _2_1;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.Arrays;

public class P1955 {
	private static StreamTokenizer in;
	private static int[] f;
	// 离散化数组
	private static long[] nums;
	// 存操作的数组
	private static Op[] ops;
	private static int t;
	private static int n;

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

		t = (int) in.nval;
		while (t-- > 0) {
			boolean flag = true;
			in.nextToken();
			n = (int) in.nval;
			ops = new Op[n];
			nums = new long[n * 2];

			int index = -1;
			int opNum;
			long x1, x2;
			// 1、读入操作
			for (int i = 0; i < n; i++) {
				in.nextToken();
				x1 = (long) in.nval;
				in.nextToken();
				x2 = (long) in.nval;
				in.nextToken();
				opNum = (int) in.nval;
				ops[i] = new Op(x1, x2, opNum == 1 ? true : false);
				nums[++index] = ops[i].x1;
				nums[++index] = ops[i].x2;
			}

			// 2.1、对离散化数组排序、去重
			Arrays.sort(nums);
			// algo 数组去重:这里nums大小会变化
			nums = Arrays.stream(nums).distinct().toArray();
			// 2.2、把原数组的值改为对应离散化数组的下标
			for (int i = 0; i < n; i++) {
				ops[i].x1 = getIndex(ops[i].x1);
				ops[i].x2 = getIndex(ops[i].x2);
			}

			// 3、把操作1放在前面
			Arrays.sort(ops, (p, q) -> {
				// >0表示交换,把false放到后面
				return p.opNum == false ? 1 : -1;
			});

			// 4.1 并查集初始化
			startF();
			// 开始操作
			for (int i = 0; i < n; i++) {
				// 这里就需要离散化了,因为数组下标只能是int,而x1x2都是long范围
				// 注意此时所有的原值都用离散化后的下标表示,下标!!!,不用考虑原值
				if (ops[i].opNum == true) {
					// 4.2、并查集并入,相等的放在同一个集合中(所以要先执行1操作)
					f[find((int) ops[i].x1)] = find((int) ops[i].x2);
				} else {
					// 5、然后开始遍历不相等的约束条件,如果发现x1x2在同一个集合中(即表示两者约束相等),那么就矛盾
					if (find((int) ops[i].x1) == find((int) ops[i].x2)) {
						System.out.println("NO");
						flag = false;
						break;
					}
				}
			}
			if (flag) {
				System.out.println("YES");
			}
		}
	}

//	并查集初始化
	public static void startF() {
		int len = nums.length;
		f = new int[len];
		for (int i = 0; i < len; i++) {
			f[i] = i;
		}
	}

//	获取值对应的离散化数组的下标
	public static int getIndex(long x) {
		int len = nums.length;
		int l = 0, r = len - 1;
		while (l <= r) {
			int m = l + (r - l) / 2;
			if (nums[m] == x) {
				return m;
			} else if (nums[m] > x) {
				r = m - 1;
			} else {
				l = m + 1;
			}
		}
		return -1;
	}

//	并查集找祖先
	public static int find(int x) {
		if (f[x] != x) {
			f[x] = find(f[x]);
		}
		return f[x];
	}
}

// 存输入三元组
class Op {
	long x1;
	long x2;
	// 1=true
	boolean opNum;

	public Op(long x1, long x2, boolean opNum) {
		super();
		this.x1 = x1;
		this.x2 = x2;
		this.opNum = opNum;
	}

}

  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值