二分法的妙用
大家都知道用二分法在有序序列中查找效率很高,除此之外,二分法的思想 在很多方面有应用,不仅仅限于查找某个数据。
比如,我们对一个单调函数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 之间。
让我们现写出最简单的解法 :
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次循环就可以求出来了,代码如下:
除了这种巧妙的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,知道了上下界,就很好求解了。
附带:
用数学方法来解决(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) )
比如,我们对一个单调函数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;
}
这段代码肯定通过不了,原因如下: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;
}
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;
}
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) )