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