有参加蓝桥杯的同学可以给博主点个关注,博主也在准备蓝桥杯,可以跟着博主的博客一起刷题。
我的AcWing
题目及图片来自蓝桥杯C++ AB组辅导课
二分看似是一个很简单的算法,但我们在写的时候可能会遇到各种各样的问题,尤其是二分的一些边界问题,我们怎么取边界是一个很重要的问题。
思想:
- 确定一个区间[L, R],使得目标值一定在区间中
- 找一个性质,满足:
① 性质具有二段性
② 答案是二段性的分界点
整数二分的模板分为两大类,一种答案是红色区间的右端点,另一种答案是绿色区间的左端点,这是不一样的两个问题。
假设当前区间为LR
,中点为M
。
第一类(ans
是红色区间的右端点):将[L, R]
分为[L, M - 1]
,[M, R]
。
if
M是红色的,说明答案必然在[M, R]
之间;
else
M是绿色的,说明答案必然在[L, M - 1]
之间。
伪代码模板:
while (L < R) {
int M = (L + R + 1) / 2; // 由于整数要下取整,所以要补上1
if (M为红色) L = M;
else R = M - 1;
}
第二类(ans
是绿色区间的左端点):将[L, R]
分为[L, M]
,[M + 1, R]
。
if
M是绿色的,说明答案必然在[L, M]
之间;
else
说明答案必然在[M + 1, R]
之间。
伪代码模板:
while (L < R) {
int M = (L + R) / 2;
if (M为绿色) R = M;
else L = M + 1;
}
我们做题的时候不需要判断这个二分属于第一类还是第二类,我们只需要关注代码里写的是L = M
还是R = M
,注意如果是L = M
一定要注意L + R + 1
,这样就不会出现死循环。
整数二分步骤:
- 找一个区间[L,R],使得答案一定在该区间中
- 找一个判断条件,使得该判断条件具有二段性,并且答案一定是该二段性的分界点。
- 分析中点M在该判断条件下是否成立,如果成立,考虑答案在哪个区间;如果不成立,考虑答案在哪个区间;
- 如果更新方式写的是R(右) = Mid,则不用做任何处理;如果更新方式写的是L(左)= Mid,则需要在计算Mid时加上1。
按照上方步骤写代码就可以完美避开整数二分所有的坑。
实数二分
实数二分相对与整数二分就非常简单了,因为我们的M必然是区间的中点,因为实数是稠密的,我们考虑的时候就不需要考虑中点是属于左边还是右边了,直接将区间[L, R]
划分成[L, M]
,[M, R]
。
整数二分循环停止的条件是当区间里只有一个数,否则继续二分;
实数二分是当我们的区间长度足够小的时候就停止,一般写成while (R - L > 1e-6)
。
if
ans在[M, R] L = M;
else
ans在[L, M] R = M;
伪代码模板:
while (R - L > 1e-6) {
double M = (L + R) / 2;
if (ans在[M, R]) L = M;
else R = M;
}
整数二分
最经典的二分题。
这题是求数的起始位置和终止位置,所以一个数必然有它的左端点和右端点,如果这个数在数组中只有一个,那么它的左端点和右端点是重合的。
数组长度为n,区间范围:[0, n - 1]
。
我们先来找左端点,我们可以用一个什么样的二段性,把这个左端点变成二段性的分界点呢?
假设我们要找的数为x
,那么我们的左端点一定是大于等于x
的第一个位置;
判断条件可以设为:q[mid] >= x
。
最后还需要判断一下,如果q[r] == x
,说明x在数组中存在,输出r就是x的左端点,也就是x的起始位置。
那么右端点怎么二分呢?
先找区间的范围,我们还是可以用[0, n - 1]
,但我们已经找到二分的左端点了,不妨写成[左端点, n - 1]
。
判断条件可以设为:q[mid] <= x
。
import java.util.Scanner;
public class Main {
static int n, m;
static final int N = 100010;
static int[] q = new int[N];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
for (int i = 0; i < n; i++) q[i] = sc.nextInt();
for (int i = 0; i < m; i++) {
int x = sc.nextInt();
int l = 0, r = n - 1; // 确定区间
// 二分求x的左端点
while (l < r) {
int mid = (l + r) / 2;
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
if (q[r] == x) {
System.out.print(r + " ");
// 二分求x的右端点
r = n - 1;
while (l < r) {
int mid = (l + r + 1) / 2; // 因为写的是l = mid,所以需要补上1
if (q[mid] <= x) l = mid;
else r = mid - 1;
}
System.out.println(l + " ");
} else {
System.out.println("-1 -1");
}
}
}
}
实数二分
三次方根函数图像大概如下图:
函数是有单调性的,有单调性一定可以二分,能二分不一定有单调性。
区间就可以取本题的数据范围[−10000, 10000]
。
此题比较简单,直接套模板即可。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
double n = sc.nextDouble();
double l = -10000, r = 10000;
while (r - l > 1e-8) { // 模板是1e-6,但保证我们的精度足够,要比我们输出的小数点多取两位,也就是1e-8
double mid = (l + r) / 2;
if (mid * mid * mid >= n) r = mid;
else l = mid;
}
System.out.print(String.format("%.6f", l)); // 取6位小数
}
}
JavaB组第8题
四个数a b c d,输出字典序最小的表示
蓝桥杯的话,用三层循环暴搜也是可以拿满分的,但y总在AcWing加强了数据,会卡掉 O ( N 3 ) O(N^3) O(N3)的暴力算法,所以本文讲的是y总的二分优化代码(y总Orz)。
先考虑我们的时间复杂度,由于 N ⩽ 5 × 1 0 6 N \leqslant5×10^6 N⩽5×106,每个数都是以平方形式表示的,所以每个数的范围: x x x ⩽ N \leqslant\sqrt{N} ⩽N,大概就是 x ⩽ 2200 x \leqslant 2200 x⩽2200,判断我们最多能枚举几个数,如果枚举三个数的话,就是 220 0 3 2200^3 22003,大概是 8 × 1 0 9 8×10^9 8×109,就会超时,所以我们最多只能枚举两个数。
但本题是有四个数的,其实枚举三次就可以,最后一个数可以通过数学知识推出来。
本来我们应该枚举三重for循环:
for (a...) {
for (b...) {
for (c...){
...
}
}
}
但我们可以用空间换时间,先枚举一半,把这一半的数据存下来,我们先枚举c和d:
for (c = 0; c² <= N; c++) {
for (d = c; c² + d² <= N; d++) { // d = c剪枝优化
把c² + d²存起来
}
}
将数据存起来的时间复杂度是 O ( N ² ) O(N²) O(N²),没超,存完之后我们可以枚举a和b:
for (a = 0; a² <= N; a++) {
for (b = a; b² + a² <= N; b++) {
t = n - a² + b²; // 计算和最终答案还差多少
if t在前面c² + d²出现过,那就说明我们找到了一组解
}
}
我们的时间复杂度大概是:
快速判断某一个数是否在一堆数中出现过,我们可以用哈希或者二分,我们把这一步原先的 O ( N ) O(N) O(N)优化成了 O ( 1 ) O(1) O(1)或者是 O ( l o g n ) O(logn) O(logn)。
但我们还需要找到字典序最小的解,我们枚举完之后,a和b一定是能保证最小的,我们只需要找到c和d同时最小的一个解,也就是说我们在存方案的时候,除了找到t是否出现过,同时还要找到t字典序最小的一个组合,排序的时候要排三个数:c² + d²、c、d
,按照字典序排这三个数的组合即可。
排完之后我们二分找t的时候,只要找到大于等于n - a² + b²
的字典序最小的一个数就可以了。
其实这个题与带分数的转换方法类似, a ² + b ² + c ² + d ² = n a² + b² + c² + d² = n a²+b²+c²+d²=n可以转换成 c ² + d ² = n − a ² − b ² c² + d² = n − a² − b² c²+d²=n−a²−b²(这篇文章讲了带分数)
二分代码
时间复杂度
O ( N 2 l o g N ) O(N^2logN) O(N2logN)
用类存值(AcWing超时,蓝桥杯满分)
这个代码就是y总的Java版代码,但这题数据卡的太死了,超时了,但在蓝桥杯是可以满分的,就是时间有点要爆。
import java.util.Scanner;
import java.util.Arrays;
class Sum implements Comparable<Sum> {
int s, c, d; // s是c² + d²
public Sum(int s, int c, int d) {
this.s = s;
this.c = c;
this.d = d;
}
// 比较器作用:返回字典序最小的值
@Override
public int compareTo(Sum t) {
if (s != t.s) return Integer.compare(s, t.s);
if (c != t.c) return Integer.compare(c, t.c);
return Integer.compare(d, t.d);
}
}
public class Main {
static final int N = 2500010;
static int n, m;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Sum[] sum = new Sum[N];
n = sc.nextInt();
for (int c = 0; c * c <= n; c++) {
for (int d = c; c * c + d * d <= n; d++) {
sum[m++] = new Sum(c * c + d * d, c, d);
}
}
Arrays.sort(sum, 0, m); // 排序前m个数
for (int a = 0; a * a <= n; a++) {
for (int b = a; a * a + b * b <= n; b++) {
int t = n - a * a - b * b;
int l = 0, r = m - 1;
while (l < r) {
int mid = (l + r) / 2;
if (sum[mid].s >= t) r = mid;
else l = mid + 1;
}
if (sum[l].s == t) {
System.out.println(a + " " + b + " " + sum[l].c + " " + sum[l].d);
return ;
}
}
}
}
}
用集合存值(AcWing通过,蓝桥杯满分)
这个是用List
集合存数据,然后在主类里面重写集合排序方法的比较器,AcWing是可以过的。
我认为上一个代码没过的原因是我在Sum类
里重写了比较器,然后在主类又用了Arrays.sort()
方法,导致超时了,应该跟用类存取值或者集合存取值没有关系,如果有大佬知道原因的话,希望可以指明。
import java.util.Scanner;
import java.util.ArrayList;
class Sum {
int s, c, d; // s是c² + d²
public Sum(int s, int c, int d) {
this.s = s;
this.c = c;
this.d = d;
}
}
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
ArrayList<Sum> sum = new ArrayList<>();
int n = sc.nextInt();
for (int c = 0; c * c <= n; c++) {
for (int d = c; c * c + d * d <= n; d++) {
sum.add(new Sum(c * c + d * d, c, d));
}
}
sum.sort((o1,o2)->{
if (o1.s != o2.s) return o1.s - o2.s;
if (o1.c != o2.c) return o1.c - o2.c;
return o1.d - o2.d;
});
for (int a = 0; a * a <= n; a++) {
for (int b = a; a * a + b * b <= n; b++) {
int t = n - a * a - b * b;
int l = 0, r = sum.size() - 1;
while (l < r) {
int mid = (l + r) / 2;
if (sum.get(mid).s >= t) r = mid;
else l = mid + 1;
}
if (sum.get(l).s == t) {
System.out.println(a + " " + b + " " + sum.get(l).c + " " + sum.get(l).d);
return ;
}
}
}
}
}
三层循环暴搜(AcWing超时,蓝桥杯满分)
时间复杂度
O ( N 3 ) O(N^3) O(N3)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
for (int a = 0; a * a <= n; a++) {
for (int b = a; a * a + b * b <= n; b++) {
for (int c = b; a * a + b * b + c * c <= n; c++) {
int t = n - a * a - b * b - c * c;
int d = (int)Math.sqrt(t);
if (d * d == t) {
System.out.print(a + " " + b + " " + c + " " + d);
return ;
}
}
}
}
}
}
看一下三个代码在蓝桥杯系统中的评测结果:
可以看出在蓝桥杯的数据中,暴搜是最快的,为什么我们的优化反而不如暴搜呢?可以多用几个数来做测试,发现a和b的值从来就没有超过10,所以说明a和b循环的次数非常少,所以y总加强了这个题的数据,AcWing中只有第二种用集合存值是可以过的,y总主要是想体现本题的二分思想。
JavaB组第9题
将N块长方形的巧克力均匀分成K块边长最大的正方形巧克力 不可以拼接
例如一块6×5
的巧克力可以切出6
块2×2
的巧克力或者2
块3×3
的巧克力:
我们先想想这个题用二分怎么来做。
边长越大,块数越少,我们看一下函数曲线:
我们要找到一个满足块数大于等于K
的,最大的一个边长,所有小于等于这个点的一定可以切出来K块,这就是我们二分的关键。
二分的判断条件:f(x) >= K
,f(x)返回的是块数,找到满足这个性质最大的x。
if (f(mid) >= K)
,说明所有小于等于mid的值都满足要求,所以更新:l = mid
。
时间复杂度
O ( N l o g H ) O(NlogH) O(NlogH);可能是H或者W。
import java.util.Scanner;
public class Main {
static final int N = 100010;
static int n, k;
static int[] h = new int[N];
static int[] w = new int[N];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
k = sc.nextInt();
for (int i = 0; i < n; i++) {
h[i] = sc.nextInt();
w[i] = sc.nextInt();
}
int l = 1, r = 100000; // 至少能获得一块 1×1 的巧克力,区间为[1, 1e5]
while (l < r) {
int mid = l + r + 1 >> 1; // 右移1位跟/2同理,这里要+1
if (check(mid)) l = mid;
else r = mid - 1;
}
System.out.print(l);
}
private static boolean check(int mid) {
int res = 0;
for (int i = 0; i < n; i++) {
res += (h[i] / mid) * (w[i] / mid); // 图中的公式
if (res >= k) return true;
}
return false;
}
}
有对代码不理解的地方可以在下方评论