二分算法
一、 sort()排序函数
1、要点:
-
头文件
#include <algorithm> // algorithm是算法的意思
-
sort(num + first, num + last) // last是数组的长度 +first,比如sort(num, num + n)表示从num[0] 排序到num[n - 1], 默认是升序的
-
降序排序
bool cmp(const int &a, const int &b) {//默认写法 return a > b;//降序排序 } sort(num, num + 8, cmp);
-
自定义类型的排序
1、整形的 bool cmp(const int &a, const int &b) { return a > b;//不能写a >= b, 这里严格执行 } 2.类的排序 #include <iostream> #include <algorithm> using namespace std; struct node { int x, y; //直接下面的重定向,就不需要下面的cmp_node函数了,直接sort(t, t + 3); bool operator< (const node &b) const { return this->x > b.x; } }; bool cmp_node(const node &a, const node &b) { if (a.x == b.x) { return a.y > b.y; } return a.x > b.x; } int main() { node t[10]; t[0].x = 1, t[0].y = 2; t[1].x = 3, t[1].y = 5; t[2].x = 3, t[2].y = 1; sort(t, t + 2, cmp_node); } 无法直接排序
2、代码演示
#include <iostream>
using namespace std;
bool cmp(const int &a, const int &b) {
return a > b;//降序
}
int main() {
int num[105] = {99, 3, -1, 7, 2, 54, 5l, 5};//
sotr(num, num + 8);//num[0]~num[7] 升序排序
sort(num + 2, num + 5 cmp);// num[2]~num[4]按照cmp的规则排序,这里是降序
for (int i = 0; i < 8; i++) {
cout << num[i] << " ";
}
cout << endl;
return 0;
}
二、二分算法
对于整数的二分
while (l <= r) {
int mid = (l + r) >> 2;
if (key[mid] == target) return mid;
else if (key[mid] > target) {
r = mid - 1;
} else {
l = mid + 1;
}
}
三、二分的特殊情况
1、情况一:
查找第一个满足情况的,比如 0000011111这一串数字中,查找第一个1,
用法与模板:
while(l != r) {
int mid = (l + r) / 2;
if (num[mid] != target) {
l = mid + 1;
} else {
r = mid;
}
}
分析
- 左边界的移动: l = mid + 1
- 右边界的移动: r = mid
比如下面的,我们在数字 000011中寻找第一个1
首先,mid = 0, 因为二分查是线性排序的,那么mid所指向的这个0和mid前面的所有0就找不符合条件了,所以答案在mid下一个的区间,所以我们是 l = mid + 1;
第二步中mid = 1, 那么这个1有可能是结果,不然就是在这个1的前面,所以的尾指针变为mid , 即 r = mid
例题
某地总共有 M 堆瓜,第 i堆瓜的数量为 Xi。现有 N 组群众现在想要吃瓜,第 i组群众想要吃的瓜的数量为 Yi。现在对于每组想吃瓜的群众,需要在 M堆瓜中查找大于等于需要数量的第一堆瓜,并输出那堆瓜的编号,若所有瓜堆的数量均小于需要数量,则输出 0。
输入
输入共 3 行。
第一行两个整数 M,N。
第二行 M 个整数分别表示 X1,X2…XM。(保证各不相同)
第三行 N 个整数分别表示 Y1,Y2…YN。(保证各不相同)
输出
对于每个 Yi输出一行一个整数为大于等于需要数量的第一堆瓜的编号,若所有瓜堆的数量均小于需要数量,则输出 0。
样例输入
5 5
1 3 26 7 15
27 10 3 4 2
样例输出
0
5
2
4
2
数据规模与约定
时间限制:1 s
内存限制:256 M
100% 的数据保证 1≤M,N≤100,000,1≤Xi,Yi≤1,000,000,0001≤M,N≤100,000,1≤Xi,Yi≤1,000,000,000
**分析:**题目中的意思,观众想吃瓜的数量为a, 那么我们寻找第一个数量大于等于a的瓜堆,没有的话就输出0, 我们可以把前面所有小于a的瓜堆看为0,大于等于的瓜堆看为1,那么就是000011111这样子求第一个1的特殊二分情况了。
代码实现
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
struct node {
int num, cnt;
};
bool cmp(const node &a, const node &b) {//瓜堆按照小到大排序
return a.num < b.num;
}
node wm[100005];
int n, m;
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &wm[i].num);
wm[i].cnt = i;
}
wm[0].num = 2000000000;
wm[0].cnt = 0;//设置哨兵,当没有符合的数据时,这个0号元素,就是符合大于想要吃瓜的数量的最小数量,输出0.
sort(wm, wm + n + 1, cmp);
for (int i = 0; i < m; i++) {
int t;
int l = 0, r = n;
scanf("%d", &t);
while(l != r) {
int mid = (l + r) / 2;
if (wm[mid].num >= t) {
r = mid;
} else {
l = mid + 1;
}
}
printf("%d\n", wm[l].cnt);
}
return 0;
}
OJ-391
题目描述
对于给定的一个长度为 N 的正整数数列 Ai,现要将其分成 M(M≤N)段,并要求每段连续,且每段和的最大值最小。
关于最大值最小:
例如一数列 4 2 4 5 1 要分成 3 段
将其如下分段:
[4 2] [4 5] [1]
第 1 段和为 6,第 2 段和为 9,第 3段和为 1,和最大值为 9。
将其如下分段:
[4] [2 4] [5 1]
第 1 段和为 4,第 2 段和为 6,第 3 段和为 6,和最大值为 6。
并且无论如何分段,最大值不会小于 66。
所以可以得到,要将数列 4 2 4 5 1分成 3 段,每段和的最大值最小为 6。
输入
第一行两个整数 N,MN,M。(1≤M≤N≤100,000)
接下来 NN 行,每行一个数,表示 Ai。(1≤Ai≤100,000,000)
输出
一个正整数,即每段和最大值最小为多少。
样例输入
5 3
4
2
4
5
1
样例输出
6
数据规模与约定
时间限制:1 s
内存限制:256 M
100% 的数据保证 1≤M≤N≤100,000,1≤Ai≤100,000,0001≤M≤N≤100,000,1≤Ai≤100,000,000
#include<iostream>
#include<cstdio>
using namespace std;
long long n, m, num[100005];
long long l, r;
long long func(long long x) {
long long cnt = 0, now = 0;
for (int i = 0; i < n; i++) {
if (now + num[i] == x) {
cnt++;
now = 0;
} else if (now + num[i] > x) {
cnt++;
now = num[i];
} else {
now += num[i];
}
}
if(now) cnt++;
return cnt;
}
long long bs() {
while (l != r) {
long long mid = (l + r) / 2;
long long t = func(mid);
if (t > m) {
l = mid + 1;
} else {
r = mid;
}
}
return r;
}
int main() {
scanf("%lld%lld", &n, &m);
for (int i = 0; i < n; i++) {
scanf("%lld", &num[i]);
l = max(l, num[i]);
r += num[i];
}
printf("%lld\n", bs());
return 0;
}
2、情况二:
查找最后一个满足情况的,比如 111110000这一串数字中,查找最后一个1,
用法与模板:
while(l != r) {
int mid = (l + r + 1) / 2;
if (num[mid] != target) {
l = mid;
} else {
r = mid - 1;
}
}
分析
- 左边界的移动: l = mid
- 右边界的移动: r = mid - 1
比如下面的,我们在数字 1110000中寻找最后一个1
首先,mid = 0, 因为二分查是线性排序的,那么mid所指向的这个0和mid后面的所有0就找不符合条件了,所以答案在mid的钱前面,所以我们是 r = mid - 1;
第二步中mid = 1, 那么这个1有可能是结果,或者答案在它后面,所以的首指针变为mid , 即 l= mid
-
mid = (l + r + 1) / 2
为什么这里是这个呢?q其实是为了避免死循环问题, 比如在10中查找1,
对于mid = (l + r ) / 2 的话, mid 指向的1, 那么 l = mid , 就会发生死循环, 但如果是 mid = (l + r + 1) / 2, 那么mid 指向0, r = mid - 1, 就不会发生死循环了
例题(特殊情况,二分答案)
- 题目描述
某林业局现在 N根原木,长度分别为 Xi,为了便于运输,需要将他们切割成长度相等的 M 根小段原木(只能切割成整数长度,可以有剩余),小段原木的长度越大越好,现求小段原木的最大长度。例如,有 33 根原木长度分别为 6,15,226,15,22,现在需要切成 8 段,那么最大长度为 5。
输入
第一行两个整数 N,M。(1≤N≤100,000,1≤M≤100,000,000)
接下来 N 行,每行一个数,表示原木的长度 Xi。(1≤Xi≤100,000,000)
输出
输出小段原木的最大长度, 保证可以切出 MM 段。
样例输入
3 8
6
15
22
样例输出
5
数据规模与约定
时间限制:1 s
内存限制:256 M
100% 的数据保证 1≤N≤100,000,1≤M≤100,000,000,1≤Xi≤100,000,000
分析
对样例给出的三块木头进行分析:
切成的长度 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
可以切成的段数 | 43 | 21 | 14 | 9 | 8 | 6 | 5 | 3 | 3 | 3 |
我们可以看出,随着可以切成的段数的减小,目标切成的长度是增加的,所以我们可以对所求的答案进行二分,简称—二分答案
- 首先,我们的下边界 l = 1, 上边界 r = max(num[i]),就是最长的木头的长度,因为我们需要切成的木头的长度不可能大于最长的木头的长度
- mid 的含义,mid 作为一个临时的答案,也就是切成的的木头的长度
- 举个例子, 当我们切的长度为4.9, 那么其实我们也可以切成8段的, 当切成5.1 ,就是7, 也就是满足1111100000寻找最后一个1的特殊情况
#include<iostream>
#include<cstdio>
using namespace std;
int n, m, l = 1, r, num[100005];
int func(int x) {//当目标长度是x时,求可以切成的段数是多少
int t = 0;
for (int i = 1; i <= n; i++) {
t += num[i] / x;//每段可以切成的长度相加
}
return t;//总段数
}
int bs() {
while(l != r) {
int mid = ((long long)l + r + 1) / 2;//mid是临时答案,就是需要切成的木头的长度
int t = func(mid);//在mid这个长度下,可以切成多少段
if (t >= m) {
l = mid;
} else {
r = mid -1 ;
}
}
return r;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &num[i]);
r = max(r, num[i]);//右边界是最长的木头的长度,因为我们需要切的木头的长度肯定小于最长的木头
}
printf("%d\n", bs());
return 0;
}
- 例题2暴躁的程序猿
某公司的程序猿每天都很暴躁,因为他们每个人都认为其他程序猿和自己风格不同,无法一同工作,当他们的工位的编号距离太近时,他们可能会发生语言甚至肢体冲突,为了尽量避免这种情况发生,现在公司打算重新安排工位,因为有些关系户的工位是固定的,现在只有一部分工位空了出来,现在有 N 个程序猿需要分配在 M 个工位中,第 i个工位的编号为 Xi,工位编号各不相同,现在要求距离最近的两个程序猿之间的距离最大,求这个最大距离是多少。Xi和 Xj 工位之间距离为|Xi−Xj|。
输入
输入共 M+1 行。
第一行两个整数 M,N。(2≤N≤M≤100,000)(
接下来 M 行,每行一个数,表示剩余的工位的编号。
输出
输出距离最近的两个程序猿之间的最大距离。
样例输入
5 3
1
2
8
4
9
样例输出
3
数据规模与约定
时间限制:1 s
内存限制:256 M
100% 的数据保证 2≤N≤M≤100,000,1≤Xi≤1,000,000,000
分析:
题目的意思就是,比如我们有1 3 5 7 8 这几个位置,需要安排3个人的话, 那么安排这三个人之间的两个人的之间的距离最大为多大
以样例为例子:
座位号 1 2 4 8 9 (排序后的座位号)
如果距离为1,那么每一个作为都可以坐一个人,那么就需要5个人,如果距离为2, 可以安排人的位置分别为1号位、4号位、8号位,因为它们之间的距离都大于等于2; 如果距离为3, 可以安排的位置分别为 1号、 4号、 8号; 如果距离为4,可以安排的位置号分别为1号、 8号
画出表格如下:
安排的距离 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
可以安排的程序员的人数 | 5 | 3 | 3 | 2 | 2 | 2 | 2 | 2 |
可以看出,随着我们的安排的程序猿的人数增加,我们的答案(安排的距离)是递减的,所以我们可以使用二分答案法,再观察一下,是符合11110000的里面找最后一个1的特殊请情况的
#include<iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
int n, m, num[100005];
int func(int x) {
int ans = 1;//可以安排的人数
int last = num[0];//上一次的安排位置
for (int i = 0; i < n; i++) {
if (num[i] - last >= x){//距离够的话,我们安排一个程序员
ans++;
last = num[i];
}
}
return ans;
}
int bs() {
int l = 1, r = num[n - 1] - num[0];//最远的距离是最远两个位置之间的距离
while (l != r) {
int mid = (l + r + 1) / 2;
int ans = func(mid);//在这个距离下,可以安排多少个程序员
if (ans >= m) {
l = mid;
} else {
r = mid - 1;
}
}
return r;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i <n; i++) {
scanf("%d",&num[i]);
}
sort(num, num + n);
printf("%d\n", bs());
return 0;
}
例题三:丢瓶盖
题目描述
小明小时候很贪玩,在他童年时期的某一天,他在地上丢了 A 个瓶盖,为了简化问题,我们可以当作这 A 个瓶盖丢在一条直线上,现在他想从这些瓶盖里找出 B 个,使得距离最近的 2 个距离最大,他想知道,最大可以到多少呢?
输入
第一行两个整数 A,B(2≤B≤A≤100,000)
接下来 A 行,每行一个数,表示瓶盖的位置坐标 Ai。(1≤Ai≤100,000,000)
输出
一个正整数,相邻的两个瓶盖的最大距离。
样例输入
5 3
1
2
3
4
5
样例输出
2
数据规模与约定
时间限制:1 s
内存限制:256 M
100% 的数据保证 2≤B≤A≤100,000,1≤Ai≤100,000,000
#include<iostream>
#include<algorithm>
using namespace std;
int a, b;
int l, r, num[100005];
int func(int x) {
int cnt = 1, last = num[0];
for (int i = 1; i < a; i++) {
if(num[i] - last >= x) {
cnt++;
last = num[i];
}
}
return cnt;
}
int bs() {
l = 1;
while (l != r) {
int mid = (l + r + 1) / 2;
int t = func(mid);
if ( t >= b ) {
l = mid;
} else {
r = mid - 1;
}
}
return l;
}
int main() {
cin >> a >> b;
for (int i = 0; i < a; i++) {
cin >> num[i];
}
sort(num, num + a);
r = num[a - 1] - num[0];
cout << bs() << endl;
return 0;
}
四、对于小数的二分
-
循环条件
while(r - l > 0.00001) { //差值到达一定程度,默认两者相等了 l = mid; r = mid; //对于小数来说,+1的话,区间就会变得非常大,所以这里直接等于mid }
-
如何保留两位小数
- 首先, 如果我们直接printf("%.2f\n", ans); 比如说 1.2365,然后会自己默认进位为1.24,而不是我们想要得到的1.23
- 有两种方法可以解决
- ans = i(int )(ans * 100) / 100.00, 对于(int )(1.2365 * 100) --> 123 --> 123 / 100.00 = 1.230000 – > 再取两位, 就是1.23了
- printf("%.2f\n", ans - 0.005); ans - 0.005 = 1.2314 这样子就不会进位了,如果保留三位,就是ans -0.0005
例题:
题目描述
有 N 条绳子,它们的长度分别为 Li。如果从它们中切割出 K 条长度相同的绳子,这 KK 条绳子每条最长能有多长?答案保留到小数点后 2 位(直接舍掉 2 位后的小数)。
输入
第一行两个整数 N 和 K,接下来 N 行,描述了每条绳子的长度 Li。
输出
切割后每条绳子的最大长度,保证答案大于零。
样例输入
4 11
8.02
7.43
4.57
5.39
样例输出
2.00
数据规模与约定
时间限制:1 s
内存限制:256 M
100% 的数据保证 1≤n≤k≤10,000,0<Li≤100,000
这一题的思路和上面的思路是一样的,使用二分答案法,随着可以切分的木头的长度增加,可以切成的段数是减少的,可以使用二分查找的方法,同时符合111110000这样子的特殊情况。
#include<iostream>
using namespace std;
int n, k;
double num[10005], maxr;
//统计切成长度为x的木头时,总共可以切成的段数
int func(double x) {
int t = 0;
for (int i = 0; i < n; i++) {
t += num[i] / x;
}
return t;
}
double bs() {
double l = 0, r = maxr;
while (r - l > 0.000001) {
double mid = (l + r) / 2;
int t =func(mid);//切成的段数
if ( t >= k ) {//题目需要切成的k段
l = mid;
} else{
r = mid;
}
}
return l;
}
int main() {
cin >> n >> k;
for (int i = 0; i < n; i++) {
cin >> num[i];
maxr = max(maxr, num[i]);
}
double ans = bs();
printf("%.2f\n", ans - 0.005);
return 0;
}
写于2021-07 -11