二分查詢 / 二分答案入門
沒錯!你沒有看錯!
我來寫那個讓我討厭至極的二分了!
二分真的很迷,有的時候必須用二分的地方看不出要用二分,然後就一片TLE
抱怨到這裡,我們來講講二分的原理。
二分二分,顧名思義,就是將查詢的區間分成兩半,找中間的部分,然後判斷查詢左半邊還是右半邊。
很顯然,二分有一個非常重要的條件:查詢元素必須有序
不然就難以判斷它到底往左找還是往右找
同時,要注意二分查詢和二分答案在原理上相同,但二分查詢用於查詢元素,而二分答案更像是列舉演算法的優化,在做題時不要搞錯用哪一種。
那麼,二分到底優秀在哪呢?
我們知道,評價一個演算法,我們可以從時間複雜度和空間複雜度來判斷
二分查詢的空間複雜度:
二分查詢的時間複雜度:
比如我們有105個元素,一般的方法在最壞情況下應查詢105次,如果是二分的話,只需要
所以二分查詢確實優秀了非常多,也是不管在什麼級別的比賽中都是非常重要的一種演算法。
接下來我們來講講二分查詢怎麼寫。
EG 1 二分查詢
輸入一組資料(個數為n)和一個數m,試在這組資料中找出有沒有m
做二分查詢題,在題目沒有明確表示的情況下,我們最好先進行預處理
即給陣列排序
這裡,我果斷上陣了已經餵了5聖盃的STL!
那麼程式碼就很簡單了
sort(a+1,a+n+1 );
接下來開始二分查詢
首先我們要確定查詢的邊界,即這個數可能在第幾到第幾的範圍內出現。
很顯然,這題我們並不能直接圈定一個範圍,那麼我們就無腦直接將範圍設成整個陣列
我們以
即
l=1,r=n;
顯然,左右邊界在查詢過程中會發生改變,所以接下來有一個問題,見下文。
預處理結束,我們就可以開始查找了。
我們之前提到了,分左右之後找中間的部分,也就是最中間的那個。
查詢肯定不能一次就結束啊,所以我們要用迴圈。
設定變數mid為當前查詢的位置。
這邊有兩種寫法:
1. FOR
for(l=1,r=n;l<=r;)
{
mid=l+r >> 1 ;
}
這種寫法用得較少,我個人一般也不這麼寫
2. WHILE
l=1,r=n;
while(l<=r)
{
mid=l+r >> 1;
}
這裡的 >> 1代表二進位制右移一位,即
注:位運算的優先順序低於加減乘除模
接下來我們要進一步判斷是向左找還是向右找。
設定變數flag代表有沒有找到,初始為false。
if(a[mid]==m)//找到了就彈出迴圈
{
flag=true;
break;
}
if(a[mid]>m) r=mid-1;
else l=mid+1;
很好理解吧,當前元素比查詢元素大則我們在查詢元素的右邊,反之亦然。
那麼有同學會注意到,為什麼
假定此時
如果不+1的話會發生什麼呢?
則核心程式碼如下:
sort(a+1,a+n+1);
bool flag=false;
int l=1,r=n;
while(l<=r)
{
int mid=l+r >> 1;
if(a[mid]==m)
{
flag=true;
break;
}
if(a[mid]>m) r=mid-1;
else l=mid+1;
}
接下來我們舉個栗子。
假定陣列為
1 2 4 5 7 8 9 11
我們來找4這個元素
工程開始:
l=1 , r=8 , mid=4 , a[mid]>4 , r=mid-1=3
l=1 , r=3 , mid=2 , a[mid]<4 , l=mid+1=3
l=3 , r=3 , mid=3 , a[mid]=3 , 退出迴圈
工程結束。
真的很短很快有木有?!
那麼二分查詢可以應用在哪裡呢?
LIS 及 LCS 的
接下來就是更喜聞樂見喪心病狂的二分答案
EG 2 二分答案
例題:Luogu P2440 木材加工
原題連結
很多老師應該都會拿這道題當入門題吧。
看到這道題,大多數人的想法應該是暴力列舉切段長度吧。
但轉頭一看,原木的長度——
暴力,不存在的!
於是我們便要在答案可能在的範圍中二分查詢我們的答案了。
對這道題來說,答案的範圍就是從1到 最長的原木長度。
於是,我們設 l=1 , r=maxn
好了,範圍解決了,接下來又面臨一個問題了,怎樣判斷答案可不可行呢?
沒辦法了,只能暴力了。
每根原木掃一遍,算出能切多少根,再加起來和 k 比較一下即可
判斷的問題解決了,怎麼判斷下一步搜尋的範圍呢?
很顯然,如果切得太少,那麼長度太長,r=mid-1
如果切得太多,雖然達到了要求,此時我們可以記錄答案,但長度可能太短,可能有更優解,那麼 l=mid+1
程式碼如下:
#include<cstdio>
#include<algorithm>
using namespace std;
int n,k,a[100005],l,r,mid,ans,sum,maxn;
bool Lets_Cut(int m)
{
sum=0;//設定初值
for(int i=1;i<=n;i++) sum+=a[i]/m;//計算能切多少
if(sum>=k) return 1;//如果切夠了返回true
else return 0;//不夠返回false
}
int main()
{
scanf("%d %d",&n,&k);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),maxn=max(maxn,a[i]);
l=1;
r=maxn;
while(l<=r)
{
mid=l+r >> 1;
if(Lets_Cut(mid))
{
ans=max(ans,mid);
l=mid+1;
}
else r=mid-1;
}//全部同上
printf("%d",ans);
return 0;
}
結果:
程式碼 C++,0.45KB
耗時/記憶體 0ms, 1738KB
總結
二分是OI中非常重要的一種優化的演算法,可以優化非常巨量的時間複雜度,有很大的必要深入研究與練習
原創 By Venus
寫的不好大佬輕噴