二分法的妙用

大家都知道用二分法在有序序列中查找效率很高,除此之外,二分法的思想 在很多方面有应用,不仅仅限于查找某个数据。
比如,我们对一个单调函数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 >= 99return -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 >= 99return -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) )

posted on 2007-02-17 02:16  woodfish  阅读(1114)  评论(3编辑  收藏  举报

导航