二分
简述
在ACM竞赛中,或者说在程序员的码代码生涯中,二分作为一种基础的、能降低时间复杂度的算法,是我们必须要掌握的,也必须要熟练掌握,下面我们先通过下面这个小问题来了解什么是二分。
问题
我们定义所谓的 Special Number是指满足以下条件的数:
- 该数里面每一个数字都不能重复,譬如123算是S.N.,而122就不算。
- 该数不能有前导0,譬如01不算S.N,而1就是S.N。
多组测试数据(大概20000组),每组测试数据给出你一个n(n <= 10,000,000),现在问你比n小的Special Number有多少个。
怎么做?
看了上一节书的同学,应该很容易想到使用枚举,我们可以枚举1~n-1(至于为什么不是从0开始,是因为……请认真看题~),判断每一个数字是不是S.N。
但是考虑极端情况,在多组测试数据中n都是10,000,000。而给的时间只有1s,所以如果你用之前学过的枚举,毫无疑问就超时了…因为时间复杂度是O(n)。然而我们有一种新的方法来解决这道题,就是二分。
初涉二分
回顾高中学过的知识,如果求一个单调递增或者单调递减的函数与x轴的交点,即 时 的值,
我们知道 , ,那么, 与x轴的交点的横坐标一定落在区间 内,这时我们取x为0和5的中值,即2.5,发现 ,那么 与x轴的交点的横坐标一定落在区间 内,同理,我们取新的x为2.5和5的中值,发现 ,那么 与x轴的交点的横坐标一定落在区间 内......如此往复,我们得到的答案就会越来越逼近交点。
这就是二分算法:通过不断检测左端点和右端点f(x)是否满足题意来减少未知数x的取值范围。这样最后肯定会不断逼近正确答案,直到最终左右端点非常靠近,端点值就是答案。
设计算x次能够得到答案,n是取值范围大小,即右端点-左端点。那么=n,即经过x次二分可以得到n。 所以二分的时间复杂度是log(n),计算机里log(n)就表示log(n),所以时间复杂度是O(log(n))。
回到我们刚才的问题,我们可以先预处理出来1~10,000,000里面的所有Special Number。然后二分这些S.N,直到找到第一个比n小的S.N,就可以知道答案了。代码如下:
#include <stdio.h>
#include <cstring>
#include <algorithm>
using namespace std;
int arr[1000000];
bool check(int x)
{
bool vis[10], flag = true;
///flag作为哨兵变量出现,她用来监视这个数是不是有重复数字
memset(vis, false, sizeof(vis)); ///memset是初始化函数,用于把数组初始化为某个值
while (x) { ///这个while的作用是把给出的数分解出来,然后一个个判断是否出现过..例如:1234分解成1,2,3,4
int tmp = x%10;
x /= 10;
if (vis[tmp]) {
flag = false; ///如果有重复的数字就把哨兵变量标记为false
break;
}
vis[tmp] = true;
}
return flag;
}
int n;
bool ok(int x)
{
return arr[x] < n;
}
int main()
{
int tot = 0;
for (int i = 1; i <= 10000000; ++i) { ///把范围内的符合答案的值都求出来
if (check(i)) {
arr[tot++] = i;
}
}
//printf("_%d\n", tot);
while (~scanf("%d", &n)) {
int r = tot-1, l = 0, mid, res;///把左右边界初始化
if (n <= 1) {
puts("0");
continue;
}
while (l <= r) {
mid = (l+r)/2;
if (arr[mid] < n) {
l = mid+1;
res = mid;
} else {
r = mid-1;
}
}
printf("%d\n", res+1);///因为我们的左边是从0开始的,所以要+1
}
return 0;
}
模版
普通二分模版
int l,r,res; //l, r初始化,问题答案的左边界和右边界的确定 while(l<=r){ int mid=(l+r)/2; if (ok(mid)){ res=mid; r= mid-1; } else l=mid+1; }
但是除去常见的二分题目,还有一种需要大家注意的:浮点数型二分, 因为整数型二分当(l+r)/2的时候是向下取整,所以为了避免出现l=3,r=4,然后mid=(3+4)/2=3的情况(会造成无限循环),所以整数型二分l=mid+1, 而浮点数型就不需要, 看一下模板:
浮点数二分模版
const double eps = 1e-7; double l, r; // l,r初始化,问题答案的左边界和右边界的确定 while (l + eps < r){ double mid = (l + r) / 2.0; if (ok (mid)){ l = mid; // r = mid; } else { r = mid; // l = mid; } }
例题
例题1.hrbust 1530
题意:小M要举行Party,她有n个pie,已知每个pie的半径,问若要分给k个好朋友同等份的pie,每个好朋友最多可以拿多大的pie?每个好朋友得到的同等份的pie只能来自同一个pie。P.S..小M也想要分得一块同等份的pie..(答案精确到小数点后四位)
要求:Time Limit: 1000 MS , Memory Limit: 65535 K
思路:这道题的题意有点难理解,其实可以简化一下,就是有k+1个人,有 n个pie,每个pie的大小都是已知的,问每个人最多能分得多大的pie?例如pie大小为1,2,3。则如果每个人最多分得大小为2的pie,则最多分给两个人。
- 我们可以用二分,二分的是每个人可分得的pie的大小,这样最后就可以算出总共可以分给几个人,以此来逼近答案。
- 初始化的时候l=0,r=最大的pie的大小(π*),为了防止精度问题,我们最后才乘以π,表示最少每个人都没pie吃,最多每个人占据一个最大的pie。
代码:
#include <stdio.h>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
#define eps 1e-8 ///定义精度值10^-8
#define pi acos(-1) ///定义pi值
int n, k;
int arr[10010];
int ok(double x)
{
int ans = 0;
for (int i = 0; i < n; ++i)
ans += (int)arr[i]/x; ///算出每一个pie按照x大小分可以分为几份
///然后累加起来,看一下所有pie按照每份大小为x的时候总共可以得到几份
return ans;
}
int main()
{
int nCase;
double l, r, mid;
scanf("%d", &nCase);
while (nCase--) {
scanf("%d %d", &n, &k);
l = r = 0; ///左边界定义为0,表示最少每个人得到大小为0的pie
///右边界定义为0,是为了到时候得出pie的最大值
for (int i = 0; i < n; ++i) {
scanf("%d", &arr[i]);
arr[i] *= arr[i]; ///理论上我们应该算出每个pie的r*r*π
///但是为了防止计算的时候出现精度误差,我们只计算r*r,最后才乘以π
r = max(r, (double)arr[i]); ///得到最多拿到的pie的大小,那就是最大的那个pie的大小
}
// printf("%.4lf %.4lf\n", l, r);
while (l+eps < r) {
mid = (l+r)/2.0;
if (ok(mid) >= k+1) l = mid;
else r = mid;
}
printf("%.4lf\n", pi*l); ///用结果的r*r*π得到得到的pie的最终大小
}
return 0;
}
例题2.hdu 4282
题意:。告诉我们K的大小,问符合这个等式的X,Y,Z的组合有多少种。
要求:Time Limit: 2000 MS , Memory Limit: 32768 K
错误思路:看到这道题,首先我们应该想到的是枚举,代码如下。但是观察K的范围和时限,枚举显然会超时。
int count=0;
for(long long x=0;x<=k;x++)
for(long long y=x+1;y<=k;y++)
for(long long z=2;z<=31;z++)
if(pow(x*1.0,z*1.0)+pow(y*1.0,z*1.0)+x*y*z==k)
count++;
printf("%d\n",count);
这样做会超时,那我们应该怎么做?这一节我们讲的是二分,大家不妨向二分方向考虑一下。
正确思路:因为幂次的增长速度比较快,我们可以枚举X和Z,这样我们可以把三重循环降低到二重循环,然后二分搜索每一对(X,Z)是否存在对应的Y。对于A的B次方我们可以先打表储存,使用的时候直接调用,这样也可以降低一定的时间复杂度。代码如下:
#include<cstdio>
#include<cstring>
using namespace std;
#define LL long long
LL mat[50001][32]={0};
bool ok(int x,int z,int cnt)
{
int l=x+1,r=50000,mid;
for(;l<=r;)
{
mid=(l+r)>>1;
if(mat[mid][z]==0)
{
r=mid-1;
continue;
}
if(mat[mid][z]+x*mid*z<cnt)
l=mid+1;
else if(mat[mid][z]+x*mid*z>cnt)
r=mid-1;
else
return true;
}
return false;
}
int main()
{
int k;
for(int i=1;i<=50000;++i)
{
mat[i][1]=i;
for(int j=2;j<=31;++j)
{
mat[i][j]=mat[i][j-1]*i;
if(mat[i][j]>2147483648LL)
break;
//这个2147483648LL中的LL一定要加上
}
}
while(scanf("%d",&k)!=EOF)
{
if(k==0)
return 0;
long long summ=0;
int cnt;
for(int x=1;x<=50000&&x<=k;++x)
for(int z=2;z<=31;++z)
{
if(mat[x][z]==0)
break;
cnt=k-mat[x][z];
if(cnt-x*z<=0)
break;
if(ok(x,z,cnt))
summ++;
}
printf("%I64d\n",summ);
//这里的I64d和lld是一个意思
}
return 0;
}