陳鍾誠

Version 1.0

暴力法 Brute Force

在演算法的理論領域,暴力法幾忽視很少被提到的一個,但是在實務上,很多時候我們會採用暴力法來解題!

舉例而言、有時我們會採用暴力法來破解某個帳號的密碼。

假如該密碼的長度不是很長,而且都是英文大小寫或數字,那麼就有機會在 $(26*2+10)^n = 62^n$ 的計算次數內破解該密碼。

對於長度為 4 的密碼,可以在 $62^4=14776336$ 次計算內將密碼暴力破解。

怎樣暴力呢? 就是把方程式裡每個變數,都從《最小到最大》算一遍,然後看看是否有符合解答的結果!

舉例而言,假如我們要求解

$x^2-4x+1=0$

而且假如我們知道《解答》在 之間, 那麼我們可以從 -100 到 +100,每隔 0.01 計算一次,如果有非常接近 0 的結果,那對應的 x 值就是解答了。以下是用暴力法求解 $x^2-4x+1=0$ 的程式。

檔案: bruteForce.js

function f(x) {
  return x*x-4*x+1;
}

for (var x=-100; x<=100; x+=0.001) {
  if (Math.abs(f(x)) < 0.001)
    console.log("x=", x, " f(x)=", f(x));
}

該暴力法程式的執行結果如下:

$ node bruteForce.js
x= 0.268000000113438  f(x)= -0.00017600039294918268
x= 3.7320000001131377  f(x)= -0.00017599960809100423

不考慮速度的話,這種方法其實非常強大,你只要將 f(x) 寫成副程式,就可以列出任何的 f(x)=0 的解答。

像是要求方程式 $\frac{sin(x^2+2x)}{x^3} = 0$ 時,只要把函數 f(x) 換成 $f(x)=\frac{sin(x^2+2x)}{x^3}$,還是可以列出相當符合條件的答案!

檔案: bruteForce2.js

function f(x) {
  return sin(x*x+2*x)/x*x*x;
}

for (var x=-100; x<=100; x+=0.001) {
  if (Math.abs(f(x)) < 0.001)
    console.log("x=", x, " f(x)=", f(x));
}

上述程式的執行結果如下,只是找到了太多 f(x) 非常接近 0 的解,得要再過濾一下,讓每個區域只傳回一個最接近零的解。

D:\Dropbox\gitbook\rlab\code\solveEquation>node bruteForce2.js
x= -8.15999999988748  f(x)= -0.0009591341662344855
x= -1.999999999886453  f(x)= 4.5418779845626724e-10
x= -0.021999999886562205  f(x)= 0.0009570498717614835
x= -0.020999999886562204  f(x)= 0.0008724877870570477
x= -0.019999999886562203  f(x)= 0.000791793010175386
x= -0.018999999886562202  f(x)= 0.0007149721474338136
x= -0.0179999998865622  f(x)= 0.0006420317778407609
x= -0.0169999998865622  f(x)= 0.0005729784528665162
x= -0.0159999998865622  f(x)= 0.0005078186962133998
x= -0.014999999886562199  f(x)= 0.00044655900358536667
x= -0.013999999886562198  f(x)= 0.00038920584245704287
x= -0.012999999886562197  f(x)= 0.00033576565184219716
x= -0.011999999886562196  f(x)= 0.0002862448420616496
x= -0.010999999886562195  f(x)= 0.0002406497945106185
x= -0.009999999886562194  f(x)= 0.00019898686142551006
x= -0.008999999886562193  f(x)= 0.00016126236565015064
x= -0.007999999886562192  f(x)= 0.00012748260040146528
x= -0.006999999886562192  f(x)= 0.00009765382903460421
x= -0.005999999886562192  f(x)= 0.00007178228480751984
x= -0.004999999886562192  f(x)= 0.000049874170644996144
x= -0.003999999886562192  f(x)= 0.00003193565890213333
x= -0.0029999998865621923  f(x)= 0.00001797289112728971
x= -0.0019999998865621923  f(x)= 0.000007991977824483308
x= -0.0009999998865621923  f(x)= 0.0000019989982152556445
x= 1.134378077582987e-10  f(x)= 2.5736272459477198e-20
x= 0.0010000001134378078  f(x)= 0.000002000999118756898
x= 0.002000000113437808  f(x)= 0.000008007979511478681
x= 0.003000000113437808  f(x)= 0.00001802689287776661
x= 0.004000000113437808  f(x)= 0.00003206365843608212
x= 0.005000000113437808  f(x)= 0.00005012416268243532
x= 0.006000000113437808  f(x)= 0.00007221425914855319
x= 0.007000000113437808  f(x)= 0.00009833976815952997
x= 0.008000000113437808  f(x)= 0.00012850647659096262
x= 0.009000000113437809  f(x)= 0.00016272013762557382
x= 0.01000000011343781  f(x)= 0.00020098647050932508
x= 0.01100000011343781  f(x)= 0.00024331116030702323
x= 0.012000000113437811  f(x)= 0.00028969985765742233
x= 0.013000000113437812  f(x)= 0.0003401581785278241
x= 0.014000000113437813  f(x)= 0.0003946917039681801
x= 0.015000000113437814  f(x)= 0.0004533059798646973
x= 0.016000000113437815  f(x)= 0.0005160065166929512
x= 0.017000000113437816  f(x)= 0.0005827987892705086
x= 0.018000000113437817  f(x)= 0.0006536882365090636
x= 0.019000000113437818  f(x)= 0.0007286802611660879
x= 0.02000000011343782  f(x)= 0.0008077802295960021
x= 0.02100000011343782  f(x)= 0.0008909934715008663
x= 0.02200000011343782  f(x)= 0.0009783252796805963
x= 1.0350000001134347  f(x)= 0.0003805209790674367
x= 4.112000000113146  f(x)= -0.0008109997278331277
x= 4.96300000011343  f(x)= 0.0007453837110089035
x= 6.16000000011383  f(x)= 0.0007240722293772872

雖然這個方法很好用,但是有個重大的缺點!

這個重大的缺點就是:「暴力法的速度比較慢」!

當變數很多,或是範圍很大時,會非常的慢!

像是求解 $x^2+y^2-z = 0$ 這樣三個變數的方程式,就會需要執行 $\frac{(100-(-100))}{0.001}^3$ 次函數計算,也就是八千兆次,如果有六個變數,就需要算 ( 八千兆 * 八千兆 ) 次。

所以, 通常很少人用《暴力法》解決問題!

我們可以想出更好的方法,來求解方程式的根!

求解方程式的方法還有很多,像是《二分搜尋法、爬山演算法、迭代法》等等。

雜湊現金 – 區塊鏈挖礦

範例程式: https://github.com/ccccourse/se/blob/master/algorithm/08-bruteForce/digitalcach/mining.js

const crypto = require('crypto');

let record = {
  nonce: 0,
  data: 'john => mary : $2.7; george => john : $1.3',
}

function hash (text) {
  return crypto.createHmac('sha256', '').update(text).digest('hex')
}

function mining(record) {
  for (var nonce=0; nonce<1000000000000; nonce++) {
    record.nonce = nonce
    let h = hash(JSON.stringify(record))
    if (h.startsWith('00000')) return { nonce: nonce, hash: h }
  }
}

console.log(mining(record))

執行結果:

$ node mining
{ nonce: 419709,
  hash: '000000b464c7f27bf31e475758e614f189742772fd4f137424de8df9a56d9b5b' }
$ node mining
{ nonce: 419709,
  hash: '000000b464c7f27bf31e475758e614f189742772fd4f137424de8df9a56d9b5b' }

隨機版挖礦

const crypto = require('crypto');

let record = {
  nonce: 0,
  data: 'john => mary : $2.7; george => john : $1.3',
}

function hash (text) {
  return crypto.createHmac('sha256', '').update(text).digest('hex')
}

function mining(record) {
  // for (var nonce=0; nonce<1000000000000; nonce++) {
  while (true) {
    let nonce = Math.floor(Math.random()*99999999)
    record.nonce = nonce
    let h = hash(JSON.stringify(record))
    if (h.startsWith('00000')) return { nonce: nonce, hash: h }
  }
}

console.log(mining(record))

執行結果

csienqu-teacher:digitalcach csienqu$ node miningRandom.js
{ nonce: 93322139,
  hash:
   '00000a397ac30bf8bdd2231fc488186d01795544ac6c6c61c9ddd929ef6a2f78' }
csienqu-teacher:digitalcach csienqu$ node miningRandom.js
{ nonce: 71260011,
  hash:
   '000001ea8ffbff5bbc02bdc30dfbad19972aad6f55c577522b585d2cee21195a' }
csienqu-teacher:digitalcach csienqu$ node miningRandom.js
{ nonce: 75892054,
  hash:
   '00000af97625476c91b59a48120939e64a749f1da8d2823077805c9b153e028e' }
csienqu-teacher:digitalcach csienqu$ node miningRandom.js
{ nonce: 41468301,
  hash:
   '00000e962faa1d35f1d2020881ea78e411ed085158fa3509d0bc8f3cb978ba8e' }
csienqu-teacher:digitalcach csienqu$ node miningRandom.js
{ nonce: 53203594,
  hash:
   '0000046402ca0f25adfa6747d89fa62a4d04c026727951ae264c80ecb664d966' }

《布林邏輯滿足問題》(SAT)

除了破解密碼之外,我們也會用《暴力法》解決很多《沒有聰明解法》的問題。舉例而言、在《計算理論》領域,我們知道《布林邏輯滿足問題》(SAT) 是一種 NP-Complete 問題,這代表我們沒有《快速的方法》可以解決 SAT 問題,這時我們就可以考慮用《暴力法》來解決 SAT 問題,雖然可能會花很久的時間,甚至在《布林變數很多的時候》算超久都不會有答案,但是因為沒有《快速方法》可以解決,所以只好採用這樣的方式。

現在、就讓我們介紹一下《布林邏輯滿足問題》(SAT) ,然後用《暴力法》來解決它吧!

一個布林邏輯式,是包含 AND, OR, NOT 的運算式,寫程式的人應該都很熟悉。

舉例而言, (x && !y) 是個布林邏輯式,當我們給定 x=1, y=0 的時候,可以滿足該算式,因為 (1 && !0) == (1&&1) == 1 。

我們可以用 node.js 來計算布林邏輯式:

$ node
> x=1
1
> y=0
0
> x&&y
0
> x&&!y
true
> x||y
1
> z=1
1
> (x&&y)||(x&&!z)
false
> (x&&!y)||(x&&z)
true
>

在上面的例子中,我們使用 0, 1 代替 JavaScript 當中的 false, true,這樣寫會比較好懂,只是有時計算結果會是 false 或 true,而不是 0 或 1而已!

以下是一個求解 SAT 問題的程式:

function satisfy(exp, vars, values) { // 測試 exp 在指令 vars[0..i]=values[0..i] 時,是否能被滿足。
  if (values.length === vars.length) {
    let assign = {}
    for (var i in vars) {
      assign[vars[i]] = values[i]
    }
    with (assign) {
      let result = eval(exp)
      console.log('%j => %d', assign, result)
      if (result) return values
    }
    return
  }
  let v0 = values.slice(0)
  let v1 = values.slice(0)
  v0.push(0)
  v1.push(1)
  return satisfy(exp, vars, v0) || satisfy(exp, vars, v1)
}

function SAT(exp, vars) {
  console.log('exp=', exp)
  let values = satisfy(exp, vars, [])
  return values
}

console.log(SAT('(x||y)&&(!x||!z)&&(x)&&(y)', ['x', 'y', 'z']))
console.log(SAT('(x)&&(!x)&&(!y)&&(!z)', ['x', 'y', 'z']))

讓我們執行看看!

$ node sat.js
exp= (x||y)&&(!x||!z)&&(x)&&(y)
{"x":0,"y":0,"z":0} => 0
{"x":0,"y":0,"z":1} => 0
{"x":0,"y":1,"z":0} => 0
{"x":0,"y":1,"z":1} => 0
{"x":1,"y":0,"z":0} => 0
{"x":1,"y":0,"z":1} => 0
{"x":1,"y":1,"z":0} => 1
[ 1, 1, 0 ]
exp= (x)&&(!x)&&(!y)&&(!z)
{"x":0,"y":0,"z":0} => 0
{"x":0,"y":0,"z":1} => 0
{"x":0,"y":1,"z":0} => 0
{"x":0,"y":1,"z":1} => 0
{"x":1,"y":0,"z":0} => 0
{"x":1,"y":0,"z":1} => 0
{"x":1,"y":1,"z":0} => 0
{"x":1,"y":1,"z":1} => 0
undefined

您可以看到第一個式子 (x||y)&&(!x||!z)&&(x)&&(y) 在 {x:1, y:1, z:0} 的時候被滿足了,因此結果印出 [1,1,0]。

但是第二個式子 (x)&&(!x)&&(!y)&&(!z) 完全無法被滿足,因此 (x,y,z) 的八種組合都傳回 0,於是最後就失敗傳回 undefined 了。

其實暴力法不只可以用在《布林算式》或者《猜密碼問題》上,也可以用來解決一些《數值問題》,舉例而言 假如我們想知道函數 f(x,y,z) 在 -10 < x,y,z < 10 區間的最小值,也可以密密麻麻的從 (x,y,z) = (-10,-10,-10) 開始列舉,每隔 0.1 列舉一次,這樣只要該函數在 0.1 的範圍內不會變化太大,就應該可以找到《差不多最好的解答》了!

f(-10, -10, -10)=...
f(-10, -10, -9.9)=...
...
f(-10, -10,  9.9)=...
f(-10, -10,  10)=...
...
f( 10,  10, -10)=...
f( 10,  10, -9.9)=...
...
f( 10,  10,  9.9)=...
f( 10,  10,  10)=...

結語

以上就是暴力法的原理,透過這樣《完全暴力列舉》的方式,雖然可能需要很久,但卻是很有系統的解決方式。

當然、速度永遠是個問題,有些問題用暴力法計算兩千年也算不出來,此時就不應該用暴力法,而應該尋找一些比較聰明的方式了!

思考

  • 使用『蒙地卡羅+暴力法』合併後會有什麼優缺點呢?