二分查找
經典問題:在一個嚴格遞增的序列中找出給定的數x
二分查找的前提條件是給定的序列要是有序的。這樣的話算法的一開始令[left,right]爲整個序列的下標區間,然後每次測試當前[left,right]的中間位置mid=(left+right)/ 2,判斷A[mid]與欲查詢的元素x的大小:若A[mid]=x,則查找成功。若A[mid] > x ,則說明x在mid位置的左邊,因此向左子區間[left,mid-1]查找。若A[mid]<x,則說明x在mid位置的右邊,向右子區間[mid+1,right]操作。循環結束的判斷條件是left<=right。
//A[]是嚴格遞增的序列,
int binarySearch(int A[],int left, int right,int x){
int mid;
while(left<=right){
mid = left +(right-left) /2; //防止溢出
if(A[mid]==x) return mid;
else if(A[mid] > x){
right = mid-1;
}else{
left = mid+1;
}
}
return -1;
}
注意二分的界限爲[0,n-1]
二分的擴展
如果給定的遞增序列A中的元素可能重複,如何對給定的x,求出序列中的第一個大於等於x的元素位置L以及第一個大於x的元素的位置R,這樣元素x在序列中存在的區間就是一個左開右閉的區間[L,R)。注意:如果序列中不存在x,則返回的應該是假設x存在,它應該在的位置。
首先對於求第一個大於等於x的位置L。和上面的查找代碼有相似之處。
int lower_bound(int A[],int left, int right, int x){
int mid;
while(left < right){
mid = (left+right)/2;
if(A[mid]>=x){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
- 這裏的循環結束條件爲 left < right 。因爲不需要判斷x是否存在。當left==right時可以唯一確定x的位置
- 循環結束的條件爲left==right 因此最後既可以返回left,也可以返回right
- 因爲考慮到x可能比A序列中的所有的元素都要大,因此二分的界限應該爲[0,n]
類似的,第一個大於x的位置R的求法:
int upper_bound(int A[],int left, int right, int x){
int mid;
while(left < right){
mid = (left+right)/2;
if(A[mid]>x){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
在C++中,algorithm庫中有用於計算lower_bound和upper_bound的函數,
下面給出一個測試例子:
#include<cstdio>
#include<algorithm>
using namespace std;
int main(){
int a[]={1,2,2,3,4,5,6};//注意序列一定要是嚴格單調遞增的
int lower = lower_bound(a,a+7,2)-a;
printf("%d\n",lower); //結果是1(第一個2的下標)
int upper = upper_bound(a,a+7,2)-a;
printf("%d\n",upper);//結果是3(3的下標)
}
幾種經典的二分應用題目
假定一個解判斷是否可行
書上給出了一個切繩子的例子:給出了若干段繩子的數目和各自的長度,要求從中切取K條相同長度的繩子,求繩子的最長長度。
這道題就是二分法的一個實際應用。可以這樣來想:先給出一個足夠大的長度,然後判斷以該長度截取繩子能否得到K條相同長度的繩子數。若滿足,那麼令繩長的最小值等於mid,從而來尋找最值,若不滿足,則令繩長的最大值等於mid,從而縮小範圍。要注意邊界條件的確定,書上給出了一種解決方案是循環100次,這樣肯定是符合精度的要求的。:
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const double INF = 1e10;
const int maxn = 10000;
int n; //繩子數
double len[maxn];//每一段的繩長
int num;//需要截的繩子數目
bool judge(double x){
int ans = 0;
for(int i=0; i<n; i++){
ans+=(int)len[i]/x;
}
return ans >= num;
}
int main(){
scanf("%d%d",&n,&num);
for(int i=0; i<n; i++){
scanf("%lf",&len[i]);
}
double lb =0,hb=INF;
for(int i=0; i<100; i++){
double mid = (lb+hb)/2;
if(judge(mid))lb=mid;
else hb=mid;
}
printf("%.2f",lb);
return 0;
}
可以看到題目中一個關鍵的式子就是:Judge(x) =(int(Li/x)的和是否是大於num)的,以這個爲條件實現折半查找。因此這類型的題目關鍵就是寫出判斷式子。
最大化最小值
像最大化最小值或者最小化最大值問題,通常可以用二分搜索法可以很好的解決(自我感覺這樣的題目好繞)。還是結合一下實際例子來看看:書中的題目可以抽象爲這樣的例子:
例如:將3個東西放到這5個點上,要求兩兩之間要儘可能遠,在這種情況下求兩點之間的最短距離。
二分法有一種枚舉的意味,當然這種枚舉是高效的。也就是說,我們可以先拋出一個解,然後通過判斷這個解的合理性來縮小解的範圍,直到求到最優解。可以得到下面的表達式:
C(d)=安排放的位置使得相鄰的東西的距離不小於d。
於是就可以這樣來思考:
- 將物品按照位置從小到大的順序排放;
- 若第i個物品放在xi,則第i+1個物品要放在xi+d<=xk的最小的xk中。
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int INF = 1e9;
const int maxn = 10000;
int n,m;//位置數目和地點數目
int pos[maxn]; //位置
bool Cal(int x){
int last = 0;
for(int i=1;i<m; i++){
int temp = last +1;
while(temp < n && pos[temp] < pos[last]+x){
temp ++;
}
if(temp == n) return false;
last = temp;
}
return true;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0; i<n; i++){
scanf("%d",&pos[i]);
}
sort(pos,pos+n);
int lb=0,lt=INF;
while(lt-lb>1){
int mid = (lb+lt)/2;
if(Cal(mid))lb=mid;
else lt = mid;
}
printf("%d",lb);
}
最大化平均值問題
題目的意思就是給了n件物品的各自的價值和重量,要求從中選擇K件物品,使得這K件物品的單位價值最大。
這個題容易犯的錯誤就是簡單的認爲將單位價值按照從大到小的順序排列,取前k件物品的總價值除以總的重量就是答案。這樣做是有問題的。給出一個簡單的證明:
若按照上面的單位價值越大的思路,得出的結論就是:
劃一下簡:
由上面的推論可知左邊的式子的第一項和第三項是大於0的,但是第二項是小於0的,因此不能直接得出上面的式子是恆大於0的。猜想是有問題的,這道題用二分的思路可以解決:設可選擇的單位重量價值不小於x,則就是求
於是可以得到:
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const double INF = 1e9;
const int maxn = 10000;
int n,k;//物品數和 選擇數
int v[maxn],w[maxn];
double y[maxn]; //vi-wi*x
bool Cal(double x){
for(int i=0; i<n; i++){
y[i] = v[i]-w[i]*x;
}
sort(y,y+n);
double sum = 0;
for(int i=0; i<k; i++){
sum+=y[n-i-1];
}
return sum >=0;
}
int main(){
scanf("%d%d",&n,&k);
for(int i=0; i<n; i++){
scanf("%d%d",&w[i],&v[i]);
}
double lb=0,ub=INF;
for(int i=0; i<100; i++){
double mid = (lb+ub)/2;
if(Cal(mid))lb = mid;
else ub = mid;
}
printf("%.2f",lb);
}