1. 程式人生 > >深入理解二分查詢(二、二分答案)

深入理解二分查詢(二、二分答案)

二分答案

    如果已知候選答案的範圍[min,max],有時候我們不必通過計算得到答案,只需在此範圍內應用“二分”的過程,逐漸靠近答案(最後,得到答案)!


一、何時可以使用“二分答案”

    不是任何題目都適合使用“二分答案”的,我Sam觀察到一般有以下的一些特徵:


    A. 候選答案必須是離散的 ,且已知答案的範圍是:[最小值min, 最大值max] (連續區間上不能進行二分操作)

        例如,在題目“Kth Largest 第k大的數”中 ==> 答案是閉區間[a[1]b[1], a[n]b[n]]上的正整數
        例如,在題目“Drying 烘乾衣服”中 ==> 烘乾時間t∈[0,maxT], maxT=max{a[i]}


    B. 候選答案在區間[min,max]上某種屬性一類一類的排列 (這樣就能在此屬性上進行二分操作 ),各個類別不交錯混雜

        例如,在題目“Kth Largest 第k大的數”中 ==>

                 (候選答案)第k大的數的值:              a[1]b[1],  ... , a[n]b[n]

                 (屬性分類)>這個乘積的數有多少:      n^2-1     ...      0

        例如,在題目“Drying 烘乾衣服”中 ==>

                 (候選答案)烘乾時間:  t=0,  t=1,  t=2,  t=3,  ...,  t=maxT-1,  t=maxT

                 (屬性分類)能否烘乾:  不能  不能   不能   能     ...    能               能


    C. 容易判斷某個點 是否為答案(即二分過程中,mid指向的點是否為答案)
        例如,在題目“Kth Largest 第k大的數”中 ==> λ∈[ a[1]b[1], a[n]b[n] ]

                 對於某個候選答案,如果“>λ的乘積個數"<k   && “>λ-1的乘積個數”≥k ,則答案為λ

        例如,在題目“Drying 烘乾衣服”中 ==>

                 需要尋找第一個出現的“能”(true),即如果check(mid-1)==false && check(mid)==true ,則答案為mid.

----------------------------------------------------------------------------------------------------------------------------------

二、舉例

例一、Kth Largest       第k大的數

題目來源: http://acm.hrbeu.edu.cn/index.php?act=problem&id=1211 

TimeLimit: 1 Second   MemoryLimit: 32 Megabyte

Description

There are two sequences A and B with N (1<=N<=10000) elements each. All of the elements are positive integers. Given C=A*B, where '*' representing Cartesian product, c = a*b, where c belonging to C, a belonging to A and b belonging to B. Your job is to find the K'th largest element in C, where K begins with 1.

Input

Input file contains multiple test cases. The first line is the number of test cases. There are three lines in each test case. The first line of each case contains two integers N and K, then the second line represents elements in A and the following line represents elements in B. All integers are positive and no more than 10000. K may be more than 10000.

Output

For each case output the K'th largest number.

Sample Input

2
2 1
3 4
5 6
2 3
2 1
4 8

Sample Output

24
8


【題解】:直接二分答案,然後判斷答案的正確性。
               假設當前二分的答案為 t,那麼:
               對於ai <= t的衣服,顯然讓它們自然風乾就可以了。
               對於ai > t的衣服,我們需要知道該衣服最少用多少次烘乾機。
               設該衣服用了x1分鐘風乾,用了x2分鐘烘乾機。
               那麼有 x1 + x2 = t 和 ai <= x1 + x2 * k,聯立兩式可得 x2 >= (ai - t) / (k - 1),即最少使用次數為[(ai - t) / (k - 1)] 的最小上界。
               最後,判斷一下總使用次數是否少於 t 即可。

【題解二】雙重二分。先對兩個序列A,B從大到小排序,然後可以我們進行第一重二分:對要求取的答案二分,即求取的答案在[A[n]*B[n],A[1]*B[1]]之間,取s1=A[n]*B[n],e1=A[1]*B[1],mid=(s1+e1)/2,那麼我們去計算在序列C中大於等於這個mid值的個數是多少,當然不是算出這個序列來,而是進行第二次二分。我們對於兩個序列可以這樣處理,列舉序列A,二分序列B,也就是說對於每個A[i],我們去二分序列B,來計算大於等於mid值的個數。那麼我們可以得到結束條件,當大於等於mid值的個數大於等於k,且大於mid值的個數小於k,那麼答案就是這個mid。那麼時間複雜度就為n*log(n)*log(n^2)了。

程式碼:

Cpp程式碼  收藏程式碼
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3. #include<string.h>  
  4. #include<math.h>  
  5. #include<iostream>  
  6. #include<queue>  
  7. #include<map>  
  8. #include<vector>  
  9. #include<algorithm>  
  10. using namespace std;  
  11. /*測試用例: 
  12. 3 
  13. 4 1 
  14. 1 2 3 4 
  15. 5 6 7 8 
  16. 4 2 
  17. 1 2 3 4 
  18. 5 6 7 8 
  19. 4 3 
  20. 1 2 3 4 
  21. 5 6 7 8 
  22. */  
  23. #define maxn 10010  
  24. int cases;   //用例 #    
  25. int a[maxn],b[maxn];  //a[], b[]  
  26. int n;    //a[]和b[]的長度  
  27. int k;    //對於每個測試用例,找到第 k大的答案   
  28. int ans;  //答案   
  29. int cmp(const void *a,const void *b){  
  30.     return *(int *)b - *(int *)a;  
  31. }  
  32. //對於a[i],統計a[i]*b[1], a[i]*b[2], ... , a[i]*b[n]中 >t的乘積的個數   
  33. int find(int t,int ai){  
  34.     int num=0;  
  35.     int s=1;  
  36.     int e=n;  
  37.     int mid;  
  38.     while(s<=e)    {  
  39.         mid=(s+e)>>1;  
  40.         if(ai*b[mid]>t){   //此時, a[i]*b[1], a[i]*b[2], ... , a[i]*b[mid]都 >t   
  41.             num=mid;  
  42.             s=mid+1;  
  43.         }  
  44.         else  
  45.             e=mid-1;  
  46.     }  
  47.     return num;  
  48. }  
  49. //統計>t的個數   
  50. int count(int t){  
  51.     int i,res=0;  
  52.     for(i=1;i<=n;i++){//列舉A中的每一個數字  
  53.         res+=find(t , a[i]); //對於a[i],二分序列 b[1]~b[n],find >t 的乘積個數  
  54.     }  
  55.     return res;  
  56. }  
  57. /* 
  58.   已知答案取值範圍,二分列舉答案:  
  59.   答案取值範圍是[ s=a[n]b[n],e=a[1]b[1] ],在這個範圍內找到第k大的數 a[i]b[j]  
  60. */  
  61. void solve(int s,int e){  
  62.     int mid=(s+e)>>1,t1,t2;  
  63.     while(s<=e)    {  
  64.         mid=(s+e)>>1;  
  65.         t1=count(mid);//統計大於mid值的個數 [mid+1,∞)   
  66.         t2=count(mid-1);//統計大於等於mid值的個數 [mid,∞)   
  67.         if( t1<k && t2>=k ){//找到答案  
  68.             ans=mid;  
  69.             return;   
  70.         }   
  71.         else if(t2<k)  
  72.             e=mid-1;  
  73.         else  
  74.             s=mid+1;  
  75.     }  
  76. }  
  77. int main(){  
  78.     int i,j;  
  79.     scanf("%d",&cases);  
  80.     while(cases--)    {  
  81.         scanf("%d%d",&n,&k);  
  82.         for (i = 1; i <= n; i++) scanf("%d", &a[i]);  
  83.         for (i = 1; i <= n; i++) scanf("%d", &b[i]);  
  84.         qsort(&a[1], n, sizeof(a[0]), cmp);  //從大到小排列 a[]   
  85.         qsort(&b[1], n, sizeof(b[0]), cmp);  
  86.         ans=0;  
  87.         solve(a[n] * b[n], a[1] * b[1]);  
  88.         printf("%d\n", ans);  
  89.     }  
  90.     system("pause");  
  91.     return 0;  
  92. }  

例二、POJ 3104  Drying  烘乾衣服

程式碼:

Cpp程式碼  收藏程式碼
  1. #include<cmath>  
  2. #include<iostream>  
  3. using namespace std;  
  4. int n;    //衣服件數   
  5. int water[100010];    //水分   
  6. int k;                //烘乾機一次烘掉的水分   
  7. int t=-1;    //最終答案:所有衣服幹完的最少時間  
  8. int maxT=-1;    //所有衣服幹完最長用時   
  9. //用時不超過mid (<=mid)能否烘乾衣服, T=O(n)  
  10. bool check(int mid){   
  11.     /* 
  12.       若某件衣服 i水分water[i]<mid,可以自然風乾;否則需要烘。 
  13.       假設這件衣服自然風乾用時 t1, 烘烤用時 t2. 
  14.           (1) t1+t2 = mid 
  15.           (2) t1+k*t2 >= water[i] 
  16.       ∴ t2 >= (water[i]-mid)/(k-1)  
  17.       只需要判斷使用烘乾機的時間和是否 <= mid 即可  
  18.     */   
  19.     double radTime=0;  //使用烘乾機的總時間   
  20.     for(int i=0;i<n;i++){  
  21.         if(water[i]>mid){  
  22.             radTime += ceil( (double)(water[i]-mid)/(double)(k-1) );  //衣服 a[i]需要烘的次數   
  23.         }   //???1. ceil()函式   
  24.     }   
  25.     if(radTime<=mid)  
  26.         return true;  
  27.     else  
  28.         return false;  
  29. }  
  30. //所有衣服幹完最少時間:[0,maxT], 二分答案 , T=O(log(maxT))O(n)  
  31. void solve(){  
  32.      int l=0;  
  33.      int r=maxT;  
  34.      int mid=(l+r)>>1;  //mid是在“二分答案 ”過程中每次觀察的答案   
  35.      //???2. 在二分查詢中總結這種變形的二分查詢: false, false, false, ..., false, true, true, ..., true  
  36.      //      找第一個出現的 true 的位置   
  37.      while(l<=r){  
  38.          //每次進入迴圈,總能保證最終答案 t∈[l,r]   
  39.          if(check(mid)==true){  
  40.              if(check(mid-1)==false){//再少用1個單位的時間都不能烘乾   
  41.                  t=mid;            //最終答案:最少用時mid   
  42.                  return;  
  43.              }  
  44.              r=mid-1;  
  45.          }else{  
  46.              l=mid+1;  
  47.          }  
  48.          mid=(l+r)>>1;  
  49.      }  
  50. }  
  51. int main(void){  
  52.     scanf("%d",&n); //???3. 看scanf()   
  53.     //while(n>0){  
  54.         //輸入   
  55.         int res=0;  
  56.         for(int i=0;i<n;i++){  
  57.             scanf("%d", &water[i]);  
  58.             maxT=maxT>water[i]?maxT:water[i];  
  59.         }  
  60.         scanf("%d",&k);  
  61.         //計算  & 輸出   
  62.         solve();  
  63.         printf("%d\n",t);  
  64.         //scanf("%d",&n);  
  65.     //}  
  66.     system("pause");  
  67.     return 0;  
  68. }