第二讲 二分和前缀和

第一节 二分

要点:

  1. 确定一个区间,使得目标值一定在区间中。
  2. 找一个性质,满足:
    1. 性质具有二段性(一段满足、一段不满足)
    2. 答案是二段性的分界点。

整数二分

2二分和前缀和 - 图1

第一类:ans是红色区间的右端点。

将[L,R]分成[L,M-1],[M+1,R]

if M是红色的,说明答案必然在[M,R]

else 说明ans必然在[L,M-1]

2二分和前缀和 - 图2

第二类:ans是绿色区间的左端点。

将[l,R]分成[L,M]和[M+1,R]

if M是绿色,说明ans在[L,M]

else 说明ans在[M+1,R]

2二分和前缀和 - 图3

总结:

2二分和前缀和 - 图4

实数划分

2二分和前缀和 - 图5

将区间[L,R]划分成[L,M],[M,R]

2二分和前缀和 - 图6

例题1:数的范围

https://www.acwing.com/problem/content/791/

给定一个按照升序排列的长度为n的整数数组,以及 q 个查询。 对于每个查询,返回一个元素k的起始位置和终止位置(位置从0开始计数)。 如果数组中不存在该元素,则返回“-1”。

输入格式

第一行包含整数n和q,表示数组长度和询问个数。 第二行包含n个整数(均在1~10000范围内),表示完整数组。 接下来q行,每行包含一个整数k,表示一个询问元素。

输出格式

共q行,每行包含两个整数,表示所求元素的起始位置和终止位置。 如果数组中不存在该元素,则返回“-1”。

数据范围

1≤n≤100000
1≤q≤10000
1≤k≤10000

输入样例:

  1. 6 3
  2. 1 2 2 3 3 4
  3. 3
  4. 4
  5. 5

输出样例:

  1. 3 4
  2. 5 5
  3. -1 -1
  1. /*
  2. 使用二分法,先找左端点
  3. 再找右端点
  4. 1≤n≤100000
  5. 1≤q≤10000
  6. 1≤k≤10000
  7. */
  8. #include <cstdio>
  9. #include <cstring>
  10. #include <iostream>
  11. #include <algorithm>
  12. using namespace std;
  13. int a[100010];
  14. int main() {
  15. int n,q;//数组长度、询问个数
  16. cin >> n >> q;
  17. for(int i = 0; i < n; i++) cin >> a[i];
  18. for(int i = 0; i < q; i++) {
  19. //一个个处理
  20. int x;//待查找的数
  21. cin >> x;
  22. //二分找左端点
  23. int l = 0;
  24. int r = n - 1;
  25. while(l < r) { // 要找到与x相等的最左边的那个
  26. int mid = (l+r)/2; //mid向下取
  27. if(a[mid] >= x) r = mid;//如果中间的数比x大或相等,右端点移过来
  28. else l = mid + 1;//否则左端点移到mid + 1的位置
  29. }
  30. //因为有找不到的可能,所以先判断一下
  31. if(a[l] == x) {
  32. cout << l << " ";//将左端点的下标输出,l和r相等,随便输出哪个
  33. //二分找右端点,l保持不动,r移到n-1,然后收缩
  34. r = n - 1;
  35. while(l < r) {
  36. int mid = (l+r+1)/2; //mid向上取整,所以+1
  37. if(a[mid] <= x) l = mid;
  38. else r = mid - 1;
  39. }
  40. cout << l << endl;//将右端点的下标输出,l和r相等,随便输出哪个
  41. }else {
  42. cout << -1 << -1 << endl;
  43. }
  44. }
  45. return 0;
  46. }

例题2:数的三次方根

https://www.acwing.com/problem/content/792/

给定一个浮点数n,求它的三次方根。

输入格式

共一行,包含一个浮点数n。

输出格式

共一行,包含一个浮点数,表示问题的解。 注意,结果保留6位小数。

数据范围

−10000≤n≤10000

输入样例:

  1. 1000.00

输出样例:

  1. 10.000000
  1. /*
  2. 10000≤n≤10000
  3. */
  4. #include <cstdio>
  5. #include <cstring>
  6. #include <iostream>
  7. #include <algorithm>
  8. using namespace std;
  9. int main() {
  10. double n;
  11. cin >> n;
  12. double l = -10000.0;
  13. double r = 10000.0;
  14. while(r - l >= 1e-8) {//保留6位小数,多取2位保证精度够高
  15. double mid = (l+r)/2;
  16. if(mid*mid*mid >= n) r = mid;//mid太大,缩小mid
  17. else l = mid;//mid太小,放大mid
  18. }
  19. printf("%lf",l);
  20. return 0;
  21. }

作业1:机器人跳跃问题

https://www.acwing.com/problem/content/732/

机器人正在玩一个古老的基于DOS的游戏。 游戏中有N+1座建筑——从0到N编号,从左到右排列。 编号为0的建筑高度为0个单位,编号为 i 的建筑高度为H(i)个单位。 起初,机器人在编号为0的建筑处。 每一步,它跳到下一个(右边)建筑。 假设机器人在第k个建筑,且它现在的能量值是E,下一步它将跳到第k+1个建筑。 如果H(k+1)>E,那么机器人就失去H(k+1)-E的能量值,否则它将得到E-H(k+1)的能量值。 游戏目标是到达第N个建筑,在这个过程中能量值不能为负数个单位。 现在的问题是机器人至少以多少能量值开始游戏,才可以保证成功完成游戏?

输入格式

第一行输入整数N。 第二行是N个空格分隔的整数,H(1),H(2),…,H(N)代表建筑物的高度。

输出格式

输出一个整数,表示所需的最少单位的初始能量值上取整后的结果。

数据范围

1≤N,H(i)≤105

输入样例1:

  1. 5
  2. 3 4 3 2 4

输出样例1:

  1. 4

输入样例2:

  1. 3
  2. 4 4 4

输出样例2:

  1. 4

输入样例3:

  1. 3
  2. 1 6 4

输出样例3:

  1. 3
  1. /*
  2. 注意加到溢出
  3. */
  4. #include <cstdio>
  5. #include <cstring>
  6. #include <iostream>
  7. #include <algorithm>
  8. using namespace std;
  9. int a[100010];
  10. int n;
  11. //输入初始能量值,判断是否全程>=0
  12. bool check(int e){
  13. for(int i = 0; i < n; i++) {
  14. e = 2*e-a[i+1];
  15. if(e >= 1e5)return true;
  16. if(e < 0) return false;
  17. }
  18. return true;
  19. }
  20. int main() {
  21. cin >> n;
  22. a[0] = 0;//第0个高度为0
  23. for(int i = 1; i <= n; i++) cin >> a[i]; //第i个高度为a[i];
  24. int l = 0, r = 1e5;
  25. while(l < r) {
  26. int mid = (l+r)/2;
  27. if(check(mid)) r = mid;
  28. else l = mid + 1;
  29. }
  30. cout << l;
  31. return 0;
  32. }

作业2:四平方和

https://www.acwing.com/problem/content/1223/

四平方和定理,又称为拉格朗日定理: 每个正整数都可以表示为至多 4 个正整数的平方和。 如果把 0 包括进去,就正好可以表示为 4个数的平方和。 比如: 5=02+02+12+22
7=12+12+12+22 对于一个给定的正整数,可能存在多种平方和的表示法。 要求你对 4 个数排序: 0≤a≤b≤c≤d 并对所有的可能表示法按 a,b,c,d为联合主键升序排列,最后输出第一个表示法。

输入格式

输入一个正整数 N。

输出格式

输出4个非负整数,按从小到大排序,中间用空格分开。

数据范围

0

输入样例:

  1. 5

输出样例:

  1. 0 0 1 2
  1. /*
  2. 0<N<5*10^6(5e6)
  3. sqrt(5000000)约等于2236
  4. 思路:枚举cd,存起来,枚举ab,用二分查找判断一下
  5. */
  6. #include <cstdio>
  7. #include <cstring>
  8. #include <iostream>
  9. #include <algorithm>
  10. using namespace std;
  11. const int N = 2500010;
  12. struct TB{
  13. int num,c,d;
  14. };
  15. TB table[N];
  16. int cnt;
  17. bool cmp(TB a,TB b) {
  18. if(a.num != b.num) return a.num < b.num;
  19. if(a.c != b.c) return a.c < b.c;
  20. return a.d < b.d;
  21. }
  22. //在表中找t,返回c和d
  23. bool myfind(int t, int& c, int& d){
  24. int l = 0, r = cnt-1;
  25. while(l < r) {
  26. int mid = l + r >> 1;
  27. if(table[mid].num >= t) r = mid;
  28. else l = mid + 1;
  29. }
  30. if(table[l].num == t) {
  31. c = table[l].c;
  32. d = table[l].d;
  33. return true;
  34. }
  35. return false;
  36. }
  37. int main() {
  38. int n;
  39. cin >> n;
  40. for(int c = 0; c*c <= n; c++) {
  41. for(int d = c; c*c + d*d <= n; d++) {
  42. table[cnt++] = {c*c+d*d,c,d};
  43. }
  44. }
  45. sort(table,table+cnt,cmp);
  46. for(int a = 0; a*a <= n; a++) {
  47. for(int b = a; a*a + b*b <= n; b++) {
  48. int t = n - (a*a + b*b);
  49. int c = 0,d = 0;
  50. if(myfind(t,c,d)) {
  51. printf("%d %d %d %d",a,b,c,d);
  52. return 0;
  53. }
  54. }
  55. }
  56. return 0;
  57. }

作业3:分巧克力

https://www.acwing.com/problem/content/1229/

儿童节那天有 K 位小朋友到小明家做客。 小明拿出了珍藏的巧克力招待小朋友们。 小明一共有 N 块巧克力,其中第 ii 块是 Hi×Wi 的方格组成的长方形。 为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。 切出的巧克力需要满足:

  1. 形状是正方形,边长是整数
  2. 大小相同

例如一块 6×5 的巧克力可以切出 6 块 2×2 的巧克力或者 2 块 3×3 的巧克力。 当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?

输入格式

第一行包含两个整数 N 和 K。 以下 N 行每行包含两个整数 Hi 和 Wi。 输入保证每位小朋友至少能获得一块 1×1 的巧克力。

输出格式

输出切出的正方形巧克力最大可能的边长。

数据范围

1≤N,K≤105
1≤Hi,Wi≤105

输入样例:

  1. 2 10
  2. 6 5
  3. 5 6

输出样例:

  1. 2
  1. /*
  2. 边长越长,分的块数越少
  3. */
  4. #include <cstdio>
  5. #include <cstring>
  6. #include <iostream>
  7. #include <algorithm>
  8. using namespace std;
  9. const int N = 1e5+10;
  10. struct QKL{
  11. int h,w;
  12. };
  13. int n,k;
  14. QKL a[N];
  15. //算变长为x分下来是否满足k个小朋友
  16. bool check(int x) {
  17. int sum = 0;
  18. for(int i = 0; i < n; i++){
  19. sum += (a[i].h/x) * (a[i].w/x);
  20. if(sum >= k) return true;
  21. }
  22. return false;
  23. }
  24. int main() {
  25. cin >> n >> k;//n个巧克力,k个小朋友
  26. for(int i = 0; i < n; i++) {
  27. cin >> a[i].h >> a[i].w;
  28. }
  29. int l = 1, r = 1e5;
  30. while(l < r) {
  31. int mid = l + r + 1 >> 1;
  32. if(check(mid)) l = mid;//块数大于人数,放大变长,缩小块数
  33. else r = mid - 1;
  34. }
  35. cout << l << endl;
  36. return 0;
  37. }

第二节 前缀和

2二分和前缀和 - 图7

一维前缀和:

用一个数组S,让Si = a1 + a2 + … + ai。也就是存着前i个的元素和。

例题1:前缀和

https://www.acwing.com/problem/content/797/

输入一个长度为n的整数序列。 接下来再输入m个询问,每个询问输入一对l, r。 对于每个询问,输出原序列中从第l个数到第r个数的和。

输入格式

第一行包含两个整数n和m。 第二行包含n个整数,表示整数数列。 接下来m行,每行包含两个整数l和r,表示一个询问的区间范围。

输出格式

共m行,每行输出一个询问的结果。

数据范围

1≤l≤r≤n
1≤n,m≤100000
−1000≤数列中元素的值≤1000−1000≤数列中元素的值≤1000

输入样例:

  1. 5 3
  2. 2 1 3 6 4
  3. 1 2
  4. 1 3
  5. 2 4

输出样例:

  1. 3
  2. 6
  3. 10
  1. #include <cstdio>
  2. #include <cstring>
  3. #include <iostream>
  4. #include <algorithm>
  5. using namespace std;
  6. const int N = 100010;
  7. int m,n;
  8. int a[N]; //表示原数组
  9. int s[N]; //表示前缀和数组
  10. int main(){
  11. cin >> n >> m;
  12. for(int i = 1; i <= n; i++) {
  13. cin >> a[i];
  14. s[i] = s[i-1] + a[i];
  15. }
  16. while(m--) {
  17. int l,r;
  18. cin >> l >> r;
  19. cout << s[r] - s[l-1] << endl;
  20. }
  21. return 0;
  22. }

例题2:子矩阵的和

https://www.acwing.com/problem/content/798/

输入一个n行m列的整数矩阵,再输入q个询问,每个询问包含四个整数x1, y1, x2, y2,表示一个子矩阵的左上角坐标和右下角坐标。 对于每个询问输出子矩阵中所有数的和。

输入格式

第一行包含三个整数n,m,q。 接下来n行,每行包含m个整数,表示整数矩阵。 接下来q行,每行包含四个整数x1, y1, x2, y2,表示一组询问。

输出格式

共q行,每行输出一个询问的结果。

数据范围

1≤n,m≤1000
1≤q≤200000
1≤x1≤x2≤n
1≤y1≤y2≤m
−1000≤矩阵内元素的值≤1000−1000≤矩阵内元素的值≤1000

输入样例:

  1. 3 4 3
  2. 1 7 2 4
  3. 3 6 2 8
  4. 2 1 2 3
  5. 1 1 2 2
  6. 2 1 3 4
  7. 1 3 3 4

输出样例:

  1. 17
  2. 27
  3. 21

2二分和前缀和 - 图8

2二分和前缀和 - 图9

  1. #include <cstdio>
  2. #include <cstring>
  3. #include <iostream>
  4. #include <algorithm>
  5. using namespace std;
  6. const int N = 1010;
  7. int n,m,q;
  8. int a[N][N],s[N][N];
  9. int main() {
  10. cin >> n >> m >> q;
  11. for(int i = 1; i <= n; i++){
  12. for(int j = 1; j <=m; j++){
  13. scanf("%d",&a[i][j]);
  14. s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];
  15. }
  16. }
  17. while(q--) {
  18. int x1,y1,x2,y2;
  19. scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
  20. printf("%d\n",s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]);
  21. }
  22. return 0;
  23. }

作业1:激光炸弹

https://www.acwing.com/problem/content/101/

地图上有 N 个目标,用整数Xi,YiXi,Yi表示目标在地图上的位置,每个目标都有一个价值WiWi。 注意:不同目标可能在同一位置。 现在有一种新型的激光炸弹,可以摧毁一个包含 R×R 个位置的正方形内的所有目标。 激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆炸范围,即那个正方形的边必须和x,y轴平行。 求一颗炸弹最多能炸掉地图上总价值为多少的目标。

输入格式

第一行输入正整数 N 和 R ,分别代表地图上的目标数目和正方形的边长,数据用空格隔开。 接下来N行,每行输入一组数据,每组数据包括三个整数Xi,Yi,Wi分别代表目标的x坐标,y坐标和价值,数据用空格隔开。

输出格式

输出一个正整数,代表一颗炸弹最多能炸掉地图上目标的总价值数目。

数据范围

0≤R≤109
00≤Xi,Yi≤5000
0≤Wi≤1000

输入样例:

  1. 2 1
  2. 0 0 1
  3. 1 1 1

输出样例:

  1. 1
  1. #include <cstdio>
  2. #include <cstring>
  3. #include <iostream>
  4. #include <algorithm>
  5. #include <vector>
  6. using namespace std;
  7. const int N = 5010;
  8. int s[N][N];
  9. int mx, my, mval;
  10. int main() {
  11. int n, r;
  12. cin >> n >> r; //n个目标,轰炸边长为r
  13. r = min(5001, r); //r最大取到5001,不然会有数组边界问题。
  14. mx = my = r; //如果所有的目标都小于r,那么最大值为r,不然会有边界问题。
  15. for (int i = 0; i < n; i++) {
  16. int x, y, w;
  17. scanf("%d%d%d", &x, &y, &w);
  18. x++, y++;
  19. s[x][y] += w; //注意这里是+=,不同目标可能在同一位置
  20. mx = max(mx, x);
  21. my = max(my, y);
  22. }
  23. //预处理前缀和
  24. for (int x = 1; x <= mx; x++) {
  25. for (int y = 1; y <= my; y++) {
  26. s[x][y] += s[x - 1][y] + s[x][y - 1] - s[x - 1][y - 1];
  27. }
  28. }
  29. for (int x1 = 1; x1 <= mx; x1++) {
  30. for (int y1 = 1; y1 <= my; y1++) {
  31. int x2 = x1 + (r - 1);
  32. int y2 = y1 + (r - 1);
  33. if (x2 > mx || y2 > my)continue;
  34. int val = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
  35. mval = max(mval, val);
  36. }
  37. }
  38. cout << mval << endl;
  39. return 0;
  40. }

作业2:K倍区间

https://www.acwing.com/problem/content/1232/

给定一个长度为 N 的数列,A1,A2,…AN,如果其中一段连续的子序列 Ai,Ai+1,…Aj 之和是 K 的倍数,我们就称这个区间 [i,j] 是K 倍区间。 你能求出数列中总共有多少个 K 倍区间吗?

输入格式

第一行包含两个整数 N 和 K。 以下 N 行每行包含一个整数 Ai。

输出格式

输出一个整数,代表 K 倍区间的数目。

数据范围

1≤N,K≤100000
1≤Ai≤100000

输入样例:

  1. 5 2
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5

输出样例:

  1. 6
  1. #include <cstdio>
  2. #include <cstring>
  3. #include <iostream>
  4. #include <algorithm>
  5. using namespace std;
  6. typedef long long LL;
  7. const int N = 100010;
  8. LL a[N], cnt[N];
  9. int main() {
  10. int n, k;
  11. LL ans = 0;
  12. cin >> n >> k; // 数列长度为n,k倍区间
  13. //输入序列并预处理前缀和
  14. for (int i = 1; i <= n; i++) {
  15. scanf("%d", &a[i]);
  16. a[i] += a[i - 1];
  17. }
  18. cnt[0] = 1;
  19. for (int i = 1; i <= n; i++) {
  20. ans += cnt[a[i] % k];
  21. cnt[a[i] % k]++;
  22. }
  23. //输出答案
  24. cout << ans << endl;
  25. return 0;
  26. }