赞
踩
【算法2-1】前缀和、差分与离散化 - 题单 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
前缀和:一般用于解决求区间和的问题。
前缀和:用一个数组表示前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]
详细可见题解如下图
一次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; } }
二维前缀和:
递推公式:
b[i][j] = b[i-1][j]+b[i][j-1]-b[i-1][j-1]+a[i][j]
看上图,每个格子表示b[i][j]
,那么求得当前b(从11到ij这一部分的和)可以无非就是上一行的b(表示绿蓝这个部分的和)+左一列的b(表示绿黄这一部分的和)-重叠的左上角b(表示绿色这一部分的和)+当前值(a[i][j])
得到这个递推公式之后,如果要知道以b[x][y]为左上角和b[i][j]为右下角的矩阵的区间和(想象上图11是xy),只需要减去这个有颜色框的上面部分和左面部分,再加上重复剪掉的部分,如下图所示
区间和:[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(); } }
差分:就是当前的值和前一个值得差值。通常差分用于得到多次对连续区间修改值操作后的结果值(区间修改+单点求值,树状数组也可以做)
递推公式:
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; } }
二分+差分
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; } }
二维差分
递推公式:
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]
,就是递推公式移相
第一反应:啊好长,前缀和可以用图像记比较形象,这个有点抽象,但这俩怎么这么像呢?看下面
里面提到一点:原矩阵是差分矩阵的前缀和。我敲确实啊!那来比对一下两者的递推公式
一维前缀和递推公式: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ㄒ)/~~。
这题一看连续区间修改值->差分,然后前缀和还原结果数组。但是注意数据规模
ab对应的是数组下标,直接顶到边界了,但是n的大小并不是很大。所以这题的数据是那种需要开大数组,但是存的有效数据有比较少的,需要使用离散化。
离散化 - Cattle_Horse - 博客园 (cnblogs.com)
算法笔记:离散化的两种实现方式 - 知乎 (zhihu.com)
思想就是把离散的有效值的下标,映射到另一个数组(这个数组不用很大,开到多少个有效值的最大值就行)
然后把原数组存值改为存离散数组的下标,这样找原数组的值就改成找对于离散数组(一般离散数组都要去重+排序),找的话就是二分的找
所以可以看到题解的大佬都是用两个数组来把区间[l,r]值映射到起点数组(存l)和终点数组(存r),这两个映射数组只需要开n的最大值,并且使用int类型(见lr的取值范围)。
将起点和终点按照从小到大的顺序排序,对答案不会产生影响!所以起点数组和终点数组可以分开排序,然后再遍历加上差值。但是注意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; } }
并查集+离散化的题,确实让我更理解离散化了。
要把握把原值改成离散化后数组的下标这一思想的转换(虽然实践起来会有点抽象),然后有几个固定步骤
- 离散化数组,类型要考虑原值下标的最大范围,大小要考虑有效值数量的范围。
- 离散化数组要排序+去重
- 把原值改成对应离散化数组的下标(这样对原数组来说,需要的下标范围不久缩短到离散化数组的下标长度了吗,实现了缩短)
- 以后所有的操作都任然是对原值操作(虽然实际上是对离散化数组下标的操作,但是你要加上这一层转换那就抽象了)
- 最后要获得真正的原值,就拿现有原值取离散化数组取(不过一般需要离散化的题都不会要求最后获取原值的,所以离散化适合那种重点在于对值的操作,而不是值本身)
有几个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; } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。