团队训练(五)

团队训练(五)-- 二分查找(1)

前言:先分享一波学二分的心理过程,这几道二分题居然差不多磨了十来天,一开始仅仅是把一些模板记下来,然后现在终于有一丝丝理解,看到很多文章都说,只有10%的程序猿会二分,我也不懂真假,反正我觉得就有那么一点点玄学,既要考虑边界问题,又要考虑左右区间问题,还要考虑返回值等等,据说可以写出2^6种二分,真的懵了,不过可以先掌握自己比较容易理解的写法,再去慢慢消化其他的写法(反正我菜一点就是这样做的,大佬另说)。

1.用户喜好(二分查找边界)

题目

为了不断优化推荐效果,今日头条每天要存储和处理海量数据。假设有这样一种场景:我们对用户按照它们的注册时间先后来标号,对于一类文章,每个用户都有不同的喜好值,我们会想知道某一段时间内注册的用户(标号相连的一批用户)中,有多少用户对这类文章喜好值为k。因为一些特殊的原因,不会出现一个查询的用户区间完全覆盖另一个查询的用户区间(不存在L1<=L2<=R2<=R1)。

输入描述:
输入: 第1行为n代表用户的个数 第2行为n个整数,第i个代表用户标号为i的用户对某类文章的喜好度 第3行为一个正整数q代表查询的组数 第4行到第(3+q)行,每行包含3个整数l,r,k代表一组查询,即标号为l<=i<=r的用户中对这类文章喜好值为k的用户的个数。 数据范围n <= 300000,q<=300000 k是整型

输出描述:
输出:一共q行,每行一个整数代表喜好值为k的用户的个数

输入
5
1 2 3 3 5
3
1 2 1
2 4 5
3 5 3

输出
1
0
2

说明
样例解释:
有5个用户,喜好值为分别为1、2、3、3、5,
第一组询问对于标号[1,2]的用户喜好值为1的用户的个数是1
第二组询问对于标号[2,4]的用户喜好值为5的用户的个数是0
第三组询问对于标号[3,5]的用户喜好值为3的用户的个数是2

题解:这道题是字节跳动和今日头条的校招笔试题,假如要爆搜肯定T,用二分可以到nlogn + qlogn,差不多就这个时间复杂度吧,就是四次二分就搞掂。题意是要我们在一个区间上查找一个有几个数是和我们需要查找的数相同,这时候我们可以先排好顺序,但是要开一个结构数组把原来的下标给记一下,在这里直接拿一个例子来说,如:2 3 1 3 5我们将样例进行排序,得到的序列是1 2 3 3 5 ,
假设我们要在 3 - 5 查找3的出现次数,那我们首先二分查找3所在的区间(排序后)查到是3 - 4均为3,接着我们在3 - 4二分查找这两个元素的原区间是否符合3 - 5,发现只有一个3是在3 - 5区间。假如想STL就是用两个lowerbound 和 upperbound的就行,不过手写二分也挺香的。
不过在这里要注意一下,假如选择循环是(l < r)的查询数组边界的方式是有问题的,因为这时候假如你在1 - 3查询1的个数,你第一次二分找1存在的区间就是(1,1)直接l = r,后面的二分直接没了...
AC代码:
1.手写二分

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 3e5 + 7;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int n, t;
struct node
{
	int val;
	int pos;
}a[maxm];

bool cmp(node b, node c)
{
	if(b.val == c.val)
		return b.pos < c.pos;
	return b.val < c.val;
}

int binary_search1(int l, int r, int target)
{
	while(l <= r)
	{
		int mid = l + r >> 1;
		if(a[mid].val >= target)
			r = mid - 1;
		else
			l = mid + 1; 
	}
	return l;
}

int binary_search2(int l, int r, int target)
{
	while(l <= r)
	{
		int mid = l + r >> 1;
		if(a[mid].val <= target)
			l = mid + 1;
		else
			r = mid - 1;
	}
	return r;
}

int binary_search3(int l, int r, int target)
{
	while(l <= r)
	{
		int mid = l + r >> 1;
		if(a[mid].pos >= target)
			r = mid - 1;
		else
			l = mid + 1;
	}
	return l;
}

int binary_search4(int l, int r, int target)
{
	while(l <= r)
	{
		int mid = l + r >> 1;
		if(a[mid].pos <= target)
			l = mid + 1;
		else
			r = mid - 1;
	}
	return r;
}

int main()
{
	int l, r, k;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
	{
		scanf("%d", &a[i].val);
		a[i].pos = i;
	}
	scanf("%d", &t);
	sort(a + 1, a + n + 1, cmp);
	while(t --)
	{
		int num = 0;
		scanf("%d%d%d", &l, &r, &k);
		int tempL = binary_search1(1, n, k);
		int tempR = binary_search2(1, n, k);
		if(tempR - tempL < 0)
			printf("%d\n", num);
		else
		{
			l = binary_search3(tempL, tempR, l);
			r = binary_search4(tempL, tempR, r);
			num = r - l + 1;
			printf("%d\n", num);
		}
	}
	return 0;
}

/*
5
1 2 2 3 5
1
2 2 2 

6
3 7 5 2 2 1
1 
2 6 3

5
5 3 3 2 1
2
2 5 3
1 2 3

*/

2.STL大法

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 3e5 + 7;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int n, t;
int b[maxm], c[maxm];
struct node
{
    int val;
    int pos;
}a[maxm];
  
bool cmp(node x, node y)
{
    if(x.val == y.val)
        return x.pos < y.pos;
    return x.val < y.val;
}
  
int main()
{
    int l, r, k;
    while(~scanf("%d", &n))
    {
        for(int i = 1; i <= n; i++)
        {
            scanf("%d", &a[i].val);
            a[i].pos = i;
        }
        scanf("%d", &t);
        sort(a + 1, a + n + 1, cmp);
        for(int i = 1; i <= n; i++)
        {
            b[i] = a[i].val;
            c[i] = a[i].pos;
        }
        while(t --)
        {
            scanf("%d%d%d", &l, &r, &k);
            int tempL = lower_bound(b + 1, b + n + 1, k) - b; //区间左闭右开 
            int tempR = upper_bound(b + 1, b + n + 1, k) - b;
            l = lower_bound(c + tempL, c + tempR, l) - c;
            r = upper_bound(c + tempL, c + tempR, r) - c;
            printf("%d\n", r - l);
        }
    }
    return 0;
}

ps:做个笔记,我对第一个手写二分的理解如下:因为我选择了(l <=r )这个条件进行循环,那也就说明当循环结束的时候l != r,不像(l < r)的循环条件,当循环结束时返回l r都一样(除了高精玄学题,下面会有讲),那假如a[mid].val >= target,这时候的条件就包括假如下标为mid的值大于等于的情况,r应该要往左边界靠,这时候就减mid - 1,r = mid可以吗?假如写了这个循环,然后在a[mid].val >= targe时r = mid,会导致无限循环,也就是l r最多只会相等,不会越界,但是我们可以想,假如有一个值与目标值存在的时候,r就会减1,那实际上减了1之后他就不一定相等了,那这时候l再加1,就会找到第一个与目标值相等的数,此外,假如序列中没有目标值,它会返回序列中第一个比目标值target大的位置。比如序列 1 3 5 7,我要查找target = 6,那开始l = 1, r = 4,第一步a[mid = 2] = 3 < 6,那此时l = mid + 1 = 3,接着a[mid = 3] = 5 < 6,l = mid + 1 = 4,此时l与r碰面了,而这时候a[mid = 4] > 6,r就要往左移,这时候结束循环,l = 4,也就是最后一个位置,而a[l] = 7。因为a[mid]比target小,l就会右移,最后也是返回它的位置,它就像一个哨兵一直往前走,而r不断的将区间缩小,同理另外一种情况也是如此。

2.【深基13.例1】查找
题目描述

输入 n(n≤10 ^6) 个不超过 10^9 的单调不减的(就是后面的数字不小于前面的数字)非负整数 a1,a2......,an ,然后进行 m(m≤10^5 ) 次询问。对于每次询问,给出一个整数 q(q≤10 ^9),要求输出这个数字在序列中的编号,如果没有找到的话输出 -1 。

输入格式
第一行 2 个整数 n 和 m,表示数字个数和询问次数。

第二行 n 个整数,表示这些待查询的数字。

第三行 m 个整数,表示询问这些数字的编号,从 1 开始编号。

输出格式
m 个整数表示答案。

输入输出样例
输入
11 3
1 3 3 3 5 7 9 11 13 15 15
1 3 6

输出
1 2 -1

解析:这道题就是找序列中第一个目标值,找不到就返回-1,上面已经做了一波笔记,这里就不废话。
AC代码:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 1e6 + 5;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int a[maxm];
int n, m;

int judge(int l, int r, int k)
{
	while(l <= r)
	{
		int mid = l + r >> 1;
		if(a[mid] >= k)
			r = mid - 1;
		else
			l = mid + 1;
	}
	if(a[l] != k)
		return -1;
	else
		return l;
}

int main()
{
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	while(m --)
	{
		int x, flag = -1;
		scanf("%d", &x);
		flag = judge(1, n, x);
		printf("%d ", flag);
	}
	return 0;
} 

3.小清新切绳子-二分
题目

小清新又双叒叕被WA疯了,原来是被double类型的精度给坑了,但题目那么好,怎么能不安利一波呢=。=
于是他悄悄地修改了下数据,安利给那些可爱的小可爱们(没错( ̄▽ ̄)~*),就是屏幕前的你们。
有N条绳子,它们的长度分别为Li。如果从它们中切割出K条长度相同的绳子,这K条绳子每条最长能有多长?
Input
多组输入!
第一行两个整数N和K,接下来一行N个整数,描述了每条绳子的长度Li ,以空格隔开。
对于100%的数据 1<=Li<10,000,000 , 1<=n,k<=10000
Output
切割后每条绳子的最大长度(一个整数)

Sample Input
4 11
802 743 457 539

Sample Output
200

解析:这道题我用二分写了三种解法,不过我更乐意与大家分享这种我自认为比较好理解的解法吧。实际上这道题就是取一个很大的区间,枚举mid的值,然后再判断是否可行(将每条绳子以最小为mid的切割,切割出来的条数为x,然后再相加是否等于k),在这里,我写的循环条件是(l < r),假如我们发现切出来的条数太多了,也就是mid太小了,这时候l往右边区间靠,反之r往左边区间靠,题目要求是求最长可以多长,那也就是条数太多我们还是可以弥补的也就是还是可行的,所以l = mid,但是假如条数太多r = mid,这时候区间会缩小,最找不到最大值,但是假如条数太少,我们 r = mid - 1,也就是说明它在mid已经是不可行,这时候所以直接减1。然后简单解释一下mid那里是l + r + 1的原因是防止死循环(可找例子观察)。

AC代码:

写法一:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e4 + 7;
const int maxm = 1e8 + 7;
const long long mod = 1e9 + 7;
const long long inf = 0x3f3f3f3f;
int n, k;
int a[maxn];

int judge(int targetL)
{
	int num = 0;
	for(int i = 1; i <= n; i++)
		num += a[i] / targetL;
	if(num >= k) 
		return 1;
	else
		return 0; 
}

int main()
{
	while(~scanf("%d%d", &n, &k))
	{
		for(int i = 1; i <= n; i++)
		scanf("%d", &a[i]); 
		int l = 1, r = maxm;
		while(l < r)
		{
			int mid = l + r + 1 >> 1;
			if(judge(mid))
				l = mid;
			else
				r = mid - 1;
		}
		printf("%d\n", l);
	}	
	return 0;
}

写法二:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e4 + 7;
const int maxm = 1e8 + 7;
const long long mod = 1e9 + 7;
const long long inf = 0x3f3f3f3f;
int n, k;
int a[maxn];

int judge(int targetL)
{
	int num = 0;
	for(int i = 1; i <= n; i++)
		num += a[i] / targetL; //计算将每条绳子分割成枚举的长度后是否满足所需绳子条数
	if(num >= k) //假如分割的条数大于目标条数,说明每条被分割的长度较短 
		return 1;
	else
		return 0; 
}

int main()
{
	while(~scanf("%d%d", &n, &k))
	{
		for(int i = 1; i <= n; i++)
		scanf("%d", &a[i]); 
		int l = 1, r = maxm;
		while(l <= r)
		{
			int mid = l + r >> 1;
			if(judge(mid))
				l = mid + 1;
			else
				r = mid - 1;
		}
		printf("%d\n", r);
	}	
	return 0;
}

写法三:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e4 + 7;
const int maxm = 1e8 + 7;
const long long mod = 1e9 + 7;
const long long inf = 0x3f3f3f3f;
int n, k;
int a[maxn];

int judge(int targetL)
{
	int num = 0;
	for(int i = 1; i <= n; i++)
		num += a[i] / targetL; //计算将每条绳子分割成枚举的长度后是否满足所需绳子条数
	if(num >= k) //假如分割的条数大于目标条数,说明每条被分割的长度较短 
		return 1;
	else
		return 0; 
}

int main()
{
	while(~scanf("%d%d", &n, &k))
	{
		for(int i = 1; i <= n; i++)
		scanf("%d", &a[i]); 
		int l = 1, r = maxm;
		int ans = 0;
		while(l <= r)
		{
			int mid = l + r >> 1;
			if(judge(mid))
			{
				ans = mid; //找到一个不满足的答案先记录下来 
				l = mid + 1;
			}
			else
				r = mid - 1;
		}
		printf("%d\n", ans);
	}	
	return 0;
}

4.小清新的二分查找之旅
题目

小清新又在疯狂懵逼了,遇到了一道题,并且发誓绝对不会告诉别人:在题号899的题目上脸懵逼了好久,于是他决定强化一下题目900,以抒发心中的抑郁之气。所以……
给出一组整数,整数个数为n,n不超过1,000,000,问这组整数中是否有k,总共询问q次。
Input
多组输入。
每组输入输入:
第一行2个整数:n q ,1 <=q , n <= 1,000,000 。
第二行 有 n 个整数,已升序排序。所有数 <= 1 e 9+7.
第三行 有 q 个整数,用于查询。
每行各数据之间有空格隔开。
Output
对每个查询数据分别输出一行
存在输出:
no
不存在输出:
YES

Sample Input
5 2
1 2 3 4 5
2 10

Sample Output
no
YES

解析:纯粹二分查找,这里我没写当a[mid] = target的情况自己return mid,我是将a[mid] > target和等于的情况一起写,也就是当它大于等于的情况下,我可以确定右区间r在小于等于mid的情况下是安全的,假如a[mid] < mid,那这时候我要找等于target的数,那l就必须是mid + 1,而不能是mid,因为我已经知道,你假如l是mid,你的a[l]绝对是不可能等于target,这时可直接排除。这里直接用cin cout会T掉。

AC代码:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 1e6 + 5;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
LL n, q;
LL a[maxm];

int binary_search(int x)
{
	int l = 1, r = n;
	while(l < r)
	{
		int mid = l + r >> 1;
		if(a[mid] >= x)
			r = mid;
		else
			l = mid + 1;
	}
	if(a[r] == x)
		return 1;
	else
		return 0;
}

int main()
{
	scanf("%lld%lld", &n, &q);
	for(int i = 1; i <= n; i++)
		scanf("%lld", &a[i]);
	while(q--)
	{
		LL x, flag = 0;
		scanf("%lld", &x);
		flag = binary_search(x);
		if(flag)
			printf("no\n");
		else
			printf("YES\n");
	}
	return 0;
}


或者可以用一下模板:

int judge(int target, int l, int r, int a[])
{
	while(l <= r)
	{
		int mid = l + r >> 1;
		if(a[mid] == target)
			return mid;
		else if(a[mid] > target)
			r = mid -1;
		else if(a[mid] < target)
			l = mid + 1;
	}
	return -1;
}

5.小清新的函数坐标-二分
题目

一天,小清新用一些奇奇怪怪的工具绘制函数图像,玩得不亦乐乎,期间发现了一个函数:
F(x) = 0.0001 x^5 + 0.003 x^3 + 0.5 x - 3 ,聪明的他一眼see穿了它的单调性,现在,小清新想标注一些点,已经写出了它们的 y 坐标值 ,聪明的你能帮助 可爱善良的小清新把对应的 x 坐标值找出来么。谢谢啦!
保证: -20 < x < 20 。答案精确到小数点后第4位。数据多组输入形式。
Input
(多组输入)每行一个实数 y
Output
每行一个四位小数 x

Sample Input
-356.957952
350.957952

Sample Output
-19.9995
19.9995

解析:这道题是浮点型的二分,只要l与r相差在一个范围内即可,看题目要求的精度吧,没说的话1e-6就足够了,然后当相等时去哪边为mid都无所谓,因为这种题存在一定的误差,不会有什么影响。

AC代码:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const double eps = 1e-6;
const long long inf =0x3f3f3f3f;
const long long mod = 1e9 + 7;
double x, y;
int judge(double x)
{
	double sum =  0.0001 * x * x * x * x * x + 0.003 * x * x * x + 0.5 * x - 3;
	if(sum <= y)
		return 1;
	else
		return 0;
}

int main()
{
	while(~scanf("%lf", &y))
	{
		double l = - 20.0, r = 20.0;
		while(r - l > eps)
		{
			double mid = (l + r) / 2;
			if(judge(mid))
				l = mid;
			else
				r = mid;
		}
		printf("%.4lf\n", l);
	}
	return 0;
} 

6.最佳牛围栏
题目

农夫约翰的农场由 N 块田地组成,每块地里都有一定数量的牛,其数量不会少于1头,也不会超过2000头。

约翰希望用围栏将一部分连续的田地围起来,并使得围起来的区域内每块地包含的牛的数量的平均值达到最大。

围起区域内至少需要包含 F 块地,其中 F 会在输入中给出。

在给定条件下,计算围起区域内每块地包含的牛的数量的平均值可能的最大值是多少。

输入格式
第一行输入整数 N 和 F ,数据间用空格隔开。

接下来 N 行,每行输出一个整数,第i+1行输出的整数代表,第i片区域内包含的牛的数目。

输出格式
输出一个整数,表示平均值的最大值乘以1000再 向下取整 之后得到的结果。

数据范围
1≤N≤100000
1≤F≤N

输入样例:
10 6
6
4
2
10
3
8
5
9
4
1

输出样例:
6500

解析:这道题是紫书上的一道题,解法就是浮点二分 + 前缀和 + 双指针就可以搞定。它的可行性是(a[ 1 ] + a[ 2 ] + …… + a[ n ])/ n >= mid,那变形一下(a[ 1 ] - mid) + (a[ 2 ] - mid)+ …… + (a[ n ] -mid)>= 0就是这个式子,然后我们可以利用双指针加min函数找子段最大和找到s[i]是最小子段,然后s[j]是在不断递增的,所以用s[j] - mins找出最大的子段和即可,最后进行强制转换一下就行。不过这道题卡高精???假如样例不强制转换输出l r均为6.5,但是强制转换后输入r * 1000就是6499,这个还没搞懂...

AC代码:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const double eps = 1e-6;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int n, f;
double a[maxn], s[maxn];
int judge(double avg)
{
	double mins = inf;
	for(int i = 1; i <= n; i++)
		s[i] = s[i - 1] + a[i] - avg; //计算前缀和
	for(int i = 0, j = f; j <= n; i++, j++) //利用双指针找出最大子段和
	{
		mins = min(mins, s[i]);
		if(s[j] - mins >= 0 )
			return 1;
	} 
	return 0;
}

int main()
{
	cin >> n >> f;
	for(int i = 1; i <= n; i++)
		cin >> a[i];
	double l = 0, r = 1e6;
	while(r - l > eps)
	{
		double  mid  = (l + r) / 2;
		if(judge(mid))
			l = mid;
		else
			r = mid;
	}
	cout << (int) (l * 1000) << endl;
	return 0;
} 

猜你喜欢

转载自www.cnblogs.com/K2MnO4/p/12896549.html
今日推荐