看了小白书二分,之后被那些神奇的判断条件看的有点蒙,然后在很长一段时间对二分有点恐惧,上次总结赛,碰到基础二分也没敢开。趁寒假末尾将二分这个基础算法总结一下,将一些题型套路,陈列下,可能有点长。。。
本文重点对各题型的判断条件的讲解
第0部分:
题型陈列,和一些例题:
1.假定一个解并判断是否可行 :POJ 1064 POJ 3122 Pie (这两个均为实数二分)
2.最大化最小值 POJ2456 POJ3258 POJ3273 POJ 3104 POJ3045
3.最大化平均值 POJ2976 POJ3111.
4.查找第K大值POJ3579 POJ3685
5.最小化第K大值 POJ 2010 POJ3662
6.略带小技巧的简单数组查找: HDU - 2141 UVA - 1152
7.枚举方程解 HDU - 2199 SDNU1416
8.三分+其他 POJ 1759 POJ3484
第一部分
模板简介:
我经常使用的模板:
1.整数定义域:
int l = 1, r = n, ans;
while(l <= r)
{
int mid = (l + r) / 2;
if(check(mid) >= k) ans = mid, r = mid - 1;
else l = mid + 1;
}
2.实数域二分
double binary(double l, double r)
{
while(r - l <= dlt)
{
double mid = (l + r) / 2;
if(calc(mid)) r = mid;
else l = mid;
}
return l;
}
for(int i=0;i<100;i++)
{
double mid=(l+r)/2;
if(calc(mid)) r=mid;else l=mid;
}
在实数域上的二分简单,重要的是确定好所需的精度dlt,每次根据在mid 上的判定选择r=mid 或 l=mid分支之一即可,一般需要保留k位小数是,则取dlt=10^-(k+2)
有时精度不容易确定或者表示,就干脆采用循环固定次数的二分方法,也是一种相当不错的策略。这种方法得到的结果的精度通常比设置dlt更高。
2.拆抄的另一种实数域上的模板
整数域上的二分,分三步 (其中mid最好是>>1 而不是/2, 因为>>1 是向下取整,而/2是向0取整,在负数时很有用)
(1)通过分析具体问题,确定左右半段哪一个是可行区间,以及mid归属那一半段。
(2)根据分析结果,选择“r=mid, l=mid+1, mid=(l+r)>>1” 和 “l=mid, r=mid-1, mid=(l+r+1)>>1”两个配套形式之一。
(3)二分终止条件是l==r, 该值就是答案所在位置。
(一定要注意区间的选择,区间[0,n] 和 [1,n+1] 这两个不能够乱用 ,0和n+1 都是越界下标,只有在特定情境下若没有找到才会等于越界下标)
例子:
1.在单调递增序列a中查找>=x的数中的最小的一个:
while(l<r)
{
int mid=(l+r)>>1;
if(a[mid]>=x) r=mid; else l=mid+1;
}
return a[l];
2. 在单调递增序列a中查找<=x的数中的最大的一个:
while(l<r)
{
int mid=(r+l+1)>>1;
if(a[mid]<=x) l=mid; else r=mid-1;
}
return a[l];
第二部分:
分题型解析:
1.假定一个解并判断是否可行:
题型讲解:最基础的二分搜索, 在一定的区间内利用二分思想来判断,找到的值,是否符合题意,并不断进行优化,缩小。
可作为二分的入门,理解。
(1)POJ 1064
题意切割出K条长度相等的绳子,这K条绳子最长有多长
唯一需要注意的是精度。
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n, k;
double a[10010];
bool check(double mid)
{
int sum = 0;
for(int i = 0; i < n; i++)
{
sum = sum + (int)(a[i] / mid);
}
return sum < k;
}
int main()
{
while(scanf("%d%d", &n, &k) != EOF)
{
double r = 0, l = 0;
for(int i = 0; i < n; i++)
{
scanf("%lf", &a[i]);
r = max(r, a[i]);
}
for(int i = 0; i < 100; i++)
{
double mid = (r + l) / 2;
if(check(mid)) r = mid;
else l = mid;
}
printf("%.2lf\n", floor(l * 100) / 100);
}
return 0;
}
(2)POJ 3122 Pie
这题和上题一样的思路和题型
#include<bits/stdc++.h>
using namespace std;
const int maxn = 10010;
const double PI = acos(-1.0); // 派的值
int t, n, f, r;
double a[maxn], sum;
bool check(double mid)
{
int ans = 0;
for(int i = 0; i < n; i++)
{
ans += (int)(a[i] / mid); // 这里需要注意下,虽然我也没搞太懂懂为啥不加强制转化不行但
//确实挺恶心的这错误
}
return ans >= (f + 1);
}
int main()
{
scanf("%d", &t);
while(t--)
{
sum = 0.0;
scanf("%d%d", &n, &f);
for(int i = 0; i < n; i++)
{
scanf("%d", &r);
a[i] = PI * double(r * r);
sum += a[i];
}
double l = 0.0, r = sum;
while(r - l >= 1e-7)
{
double mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
printf("%.4f\n", r);
}
return 0;
}
2.最大化最小值
思想:贪心法确定位置,使需要确定的值达到最优化。
在这里讲两个题,基础的POJ2456 牛舍安放题,和进阶版POJ3104 烘干衣服。
(1)POJ2456
题意:有N间牛舍,M条牛。给定牛舍的位置,安放牛使得牛相隔的尽可能的远。
题目分析:将牛舍的位置进行排序,然后在间隔下初始算一间,那就是在选M-1间牛舍。
判断条件讲解:因为要牛距离最大那初始处有一条牛,才能尽可能做到牛之间相隔的距离最大,所以我们只需要在选M - 1个牛舍,我们可以枚举牛舍间的距离,来判定能不能在这么样的距离下,安放下牛。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 100100;
const int INF = 0x3f3f3f3f;
int N, C, a[maxn];
bool check(int mid)
{
int sum = 0, last = 0, cnt;
for(int i = 1; i < C; i++)
{
cnt = last + 1;
while(cnt < N && a[cnt] - a[last] < mid)
{
cnt++;
}
if(cnt == N)
return false;
last = cnt;
}
return true;
}
int main()
{
while(scanf("%d%d", &N, &C) != EOF)
{
for(int i = 0; i < N; i++)
{
scanf("%d", &a[i]);
}
sort(a, a + N);
int l = 0, r = INF, ans;
while(l <= r)
{
int mid = (l + r) / 2;
if(check(mid)) ans = mid, l = mid + 1;
else r = mid - 1;
}
printf("%d\n", ans);
}
return 0;
}
(2)POJ3104
题意:冬天烘干衣服,在炉子里每分水分减k,在外面自然风干每分减1;
题意分析:
1.我们可以枚举会花费多少分钟m来烘干所有衣服。
2.然后减去在m分钟内会自然风干的水分,看其还需要多少次加热器风干。
3.需要注意的是,在加热器干时就不会再自然风干所以需要判定的是(k - 1)。
4.还需注意的是当k == 1时需特判。
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 100010;
long long n, a[maxn], k;
bool check(long long mid)
{
long long sum = 0;
for(int i = 0; i < n; i++)
{
// a[i] -= mid;
if(a[i] > mid)
{
sum += (a[i] - mid) / (k - 1);
if((a[i] - mid) % (k - 1) != 0)
sum += 1;
}
}
return sum <= mid;
}
int main()
{
while(scanf("%lld", &n) != EOF)
{
long long l = 0, r = 0, ans;
for(int i = 0; i < n; i++)
{
scanf("%lld", &a[i]);
r = max(r, a[i]);
}
scanf("%d", &k);
if(k==1)//k=1对下面的情况不易判断,所以在此特判
{
printf("%lld\n",r);
continue;
}
while(l <= r)
{
long long mid = (l + r) >> 1;
if(check(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
}
printf("%lld\n", ans);
}
return 0;
}
3.最大化平均值
思想:
1.单位质量的价值:
2.我们需要二分查找的就是筛选后达到的单位质量的价值x
所以判断条件:
变形后
所以我们就可以将的值排序后贪心选取前k个不为0的数。
这种题型都类似模板题,这里就简单讲解一个:
POJ3111
题意:有n个珠宝,求出k个必须留下的珠宝。
题意分析见上方思想。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 100010;
int n, k;
double v[maxn], w[maxn];
bool used[maxn];
struct nod
{
double num;
int it;
};
bool cmp(nod a, nod b)
{
return a.num > b.num;
}
bool check(double x)
{
nod y[maxn];
memset(used, false, sizeof(used));
for(int i = 1; i <= n; i++)
{
y[i].num = v[i] - x * w[i];
y[i].it = i;
}
sort(y + 1, y + n + 1, cmp);
double sum = 0;
for(int i = 1; i <= k; i++)
{
sum += y[i].num;
used[y[i].it] = true;
}
return sum >= 0;
}
int main()
{
while(scanf("%d%d", &n, &k) != EOF)
{
for(int i = 1; i <= n; i++)
{
scanf("%lf%lf", &v[i], &w[i]);
}
double l = 0, r = 1e8, ans;
while(r - l >= 1e-7)
{
double mid = (l + r) / 2;
if(check(mid)) ans = mid, l = mid;
else r = mid;
}
int flag = 0;
for(int i = 1; i <= n; i++)
{
if(used[i])
{
if(flag)
printf(" ");
flag = 1;
printf("%d", i);
}
}
printf("\n");
// printf("%lf",ans);
}
return 0;
}
4.查找第K大值
思想:一般为2次二分,第一次查找符合题意的值,第二次来二分查找符合题意的第K大的值。
例题 POJ - 3579
题目解析链接见我另一篇博客。
5.最小化第K大的值
未完待续。