二分法的妙用

大家都知道用二分法在有序序列中查找效率很高,除此之外,二分法的 思想 在很多方面有应用,不仅仅限于查找某个数据。
比如,我们对一个单调函数f(x)求它的零点时,假设我们知道它的零点在一个范围(a,b)中,那么我们就可以用二分法来求f(x)=0的根了。
现在,我们看一个实际的应用。
Topcoder SRM338第2题,(http://www.topcoder.com/stat?c=problem_statement&pm=7386&rd=10662 )

题目抽象成这样一个问题:
给定2个数a,b ,他们的b除以a为q1%,q1为四舍五入的正整数。现在要求一个最小的数m,使(b+m)除以(a+m)的比值q2%,使得q2>q1,如果不存在这样的数,则返回-1。其中a的取值范围在1-1,000,000,000 之间。

让我们现写出最简单的解法 :
int  display( int a,  int b) {
    
return  (b *   100 / a;       //  integer division rounds down
  }

  
int  howManyGames ( int a,  int b) {
    
if  (a == b)  return   - 1 ;
    
int  currentDisplay  =  display(a,b);
    
for  ( int  g = 1 ; ; g ++ )
      
if  (display(a + g,b + g)  >  currentDisplay)
        
return  g;
  }
这段代码肯定通过不了,原因如下:
1.在函数display中b*100有可能溢出,因为b可以达到1,000,000,000 ,所以应该改成(b*100LL)/a
2.这种解法会超时,在最坏的情况下,a = 1,000,000,000, b= 980,000,000 ,将会执行1,000,000,000 此循环。
3.这种解法结果可能错误,无解情况应该是q1>99时,而不是q1==100时,具体证明自己去做。

对于bug2超时,我们用一种hacker方法来巧妙的解决,我们每次让g增加1000,这样增长速度就会增加1000倍,然后对满足条件的g,要求的结果肯定在g-1000到g之间,这样再之多只需要1000次循环就可以求出来了,代码如下:
int  howManyGames ( int  a,  int  b) {
    
int  currentDisplay  =  display(a,b;
    
if  (currentDisplay  >=   99 return   - 1 ;

    
int  g;
    
for  (g = 1 ; ; g += 1000 )
      
if  (display(a + g,b + g)  >  currentDisplay)
        
break ;

    
//  We already crossed the boundary.
    
//  Now we return one step back and find its exact position.
    g -= 1000 ;
    
for  ( ; ; g ++ )
      
if  (display(a + g,b + g)  >  currentDisplay)
        
return  g;
  }

除了这种巧妙的hacker方法,我们要用正常的程序员的方法来解决超时问题!

注意:任何时候你写出的代码类似于上面的那个hacker代码,那么肯定有一种更加快速有效的方法来实现同样的功能:即二分查找(binary search)
用二分法首先要确定所求值的范围,在本题中,很明显所求值肯定大于0,即下界为0,小于2*10^9(上界).但是即使我们不知道上界,我们也可以求出来(只要存在)。重复试下面的值g=2^0,2^1,2^2,2^3,...直到你找到合适的g,使得display(a+g,b+g)>currentdisplay
OK,知道了上下界,就很好求解了。

int  howManyGames ( int  played,  int  won) {
    
int  currentDisplay  =  display(played,won);
    
if  (currentDisplay  >=   99 return   - 1 ;

    
long  minG  =   0 , maxG  =   1 ;
    
while  (display(played + maxG,won + maxG)  ==  currentDisplay)
      maxG 
*=   2 ;

    
while  (maxG  -  minG  >   1 ) {
      
long  midG  =  (maxG  +  minG)  /   2 ;
      
if  (display(played + midG,won + midG)  ==  currentDisplay)
        minG 
=  midG;
      
else
        maxG 
=  midG;
    }

    
return  maxG;
  }


附带:
用数学方法来解决(o(1)的时间复杂度)
设z=currentdisplay,我们必须找到最小的g,使得
floor( 100*(won+g) / (played+g) ) ≥ Z+1.
由于Z是一个整数,我们可以忽略 floor函数,因此,解除g得到
g*(99-Z) ≥ (Z+1)*played - 100*won
可以看到右边的值永远大于0,所以对于Z>=99,g肯定没有解。
当Z<99,时,两边同时除以(99-Z),就可以解除整数g的最小值了
g = ceil( ( (Z+1)*played - 100*won ) / (99-Z) )

你可能感兴趣的:(二分法)