通過JavaScript看透彩票背后的隨機(jī)算法
雜談
最近大抵是迷上彩票了,幻想著自己若能暴富,也可以帶著家庭"雞犬升天"了,不過事與愿違,我并沒有沖天的氣運(yùn),踏踏實(shí)實(shí)工作才是出路?
買彩票的時(shí)候,我也考慮了很久,到底怎么樣的號(hào)碼可以在1700萬注中脫穎而出,隨機(jī)試過,精心挑選的也試過,找規(guī)律的模式也試過,甚至我還用到了爬蟲去統(tǒng)計(jì)數(shù)據(jù),啼笑人非!
我們默認(rèn)彩票系統(tǒng)是基于統(tǒng)計(jì)學(xué)來實(shí)現(xiàn)一等獎(jiǎng)的開獎(jiǎng),那么歷史以來的一等獎(jiǎng)理所當(dāng)然應(yīng)該是當(dāng)期統(tǒng)計(jì)率最低的一注,所以,最開始的時(shí)候我是這么想的:
獲取歷史以來所有的中獎(jiǎng)彩票號(hào)碼
使用代碼去統(tǒng)計(jì)出所有號(hào)碼的中獎(jiǎng)次數(shù)
按照出現(xiàn)幾率最低的數(shù)字來排序
依次組成某幾注新號(hào)碼
天馬行空,卻也是自己發(fā)財(cái)欲望的一種發(fā)泄渠道罷了,稱之為異想天開也不為過,扯了挺多,哈哈!
上面的思路我已經(jīng)實(shí)踐過了,用了差不多一年的時(shí)間,沒有用!別用!當(dāng)然你也可以試試,如果你中了,恭喜,你才是天選之人!
彩票的規(guī)則
我們這里的彩票規(guī)則統(tǒng)一使用「雙色球」的規(guī)則來說明,其購買的規(guī)則如下:
紅球?yàn)榱?,選項(xiàng)從 1 - 33 中挑選,不可重復(fù)
藍(lán)球?yàn)橐晃?,選項(xiàng)從 1 - 16 中挑選
紅藍(lán)雙色球一共七位組成一注
一等獎(jiǎng)一般中全部購買的注里面挑選一注,這一注可能被多個(gè)人買,也有可能是一個(gè)人買了該注的倍數(shù)。
所以粗略統(tǒng)計(jì),彩票的中獎(jiǎng)幾率計(jì)算公式如下所示:
使用組合數(shù)公式來計(jì)算,從n
個(gè)元素中取k
個(gè)元素的的組合數(shù)公式為:
C(kn)=n!k!(n−k)!C\binom{k}{n}=\frac{n!}{k!(n-k)!}C(nk)=k!(n−k)!n!
根據(jù)公式,我們可以很容易的寫出來一個(gè)簡單的算法:
function factorial(n) { if (n === 0 || n === 1) { return 1 } else { return n * factorial(n - 1) } } function combination(n, k) { return factorial(n) / (factorial(k) * factorial(n - k)) } console.log(combination(33, 6) * combination(16, 1)) // 17721088
所以可以得出的結(jié)論是,雙色球頭獎(jiǎng)的中獎(jiǎng)幾率為: 117721088\frac{1}{17721088}177210881
數(shù)據(jù)量
我們通過上面的算法得知了彩票的總注數(shù)為 17721088
,那么這么多注數(shù)字組成的數(shù)據(jù)到底有多大呢?
簡單計(jì)算下,一注彩票可以用14個(gè)數(shù)字來表示,如 01020304050607
,那么在操作系統(tǒng)中,這串?dāng)?shù)字的大小為 14B
,那么粗略可知的是,如果所有的彩票注數(shù)都在一個(gè)文件中,那么這個(gè)文件的大小為:
const totalSize = 17721088 * 14 / 1024 / 1024 // 236.60205078125MB
很恐怖的數(shù)量,有沒有可能更小?我們研究一下壓縮算法!
01
這個(gè)數(shù)字在內(nèi)存中的占用是兩個(gè)字節(jié),也就是 2B,那如果我們把 01
用小寫 a
代替,那么其容量就可以變成 1B,總體容量可減少一半左右!
這樣子的話,我們上面的一注特別牛的號(hào)碼 01020304050607
就可以表示為 abcdefg
!
這就是壓縮算法最最最基本的原理,壓縮算法有很多種,大體分為有損壓縮和無損壓縮,對(duì)于我們數(shù)據(jù)類的內(nèi)容來講,我們一般都會(huì)選擇無損壓縮!
- 有損壓縮算法:這些算法能夠在壓縮數(shù)據(jù)時(shí)丟棄一些信息,但通常能在不影響實(shí)際使用的前提下實(shí)現(xiàn)更高的壓縮比率,其中最常見的是圖像、音頻和視頻壓縮算法
- 無損壓縮算法:這些算法不會(huì)丟棄任何信息,它們通過查找輸入數(shù)據(jù)中的重復(fù)模式,并使用更短的符號(hào)來表示它們來實(shí)現(xiàn)壓縮。無損壓縮算法常用于文本、代碼、配置文件等類型的數(shù)據(jù)
首先,讓我們先準(zhǔn)備一些測試數(shù)據(jù),我們使用下面這個(gè)簡單的組合數(shù)生成算法來獲取出1000個(gè)組合數(shù):
function generateCombinations(arr, len, maxCount) { let result = [] function generate(current, start) { // 如果已經(jīng)生成的組合數(shù)量達(dá)到了最大數(shù)量,則停止生成 if (result.length === maxCount) { return } // 如果當(dāng)前已經(jīng)生成的組合長度等于指定長度,則表示已經(jīng)生成了一種組合 if (current.length === len) { result.push(current) return } for (let i = start; i < arr.length; i++) { current.push(arr[i]) generate([...current], i + 1) current.pop() } } generate([], 0) return result }
接下來,我們需要生成 1000 注雙色球,紅球是從 1 - 33 中取組合數(shù),藍(lán)球是從 1 - 16 中依次取數(shù)
function getDoubleColorBall(count) { // 紅球數(shù)組:['01', '02' .... '33'] const arrRed = Array.from({ length: 33 }, (_, index) => (index + 1).toString().padStart(2, '0')) const arrRedResult = generateCombinations(arrRed, 6, count) const result = [] let blue = 1 arrRedResult.forEach(line => { result.push(line.join('') + (blue++).toString().padStart(2, '0')) if (blue > 16) { blue = 1 } }) return result }
我們將獲取的彩票內(nèi)容放在文件中以便于下一步操作:
const firstPrize = getDoubleColorBall(1000).join('') fs.writeFileSync('./hello.txt', firstPrize)
這樣子,我們就得到了第一版的文件,這是其文件大?。?/p>
試一下我們初步的壓縮算法,我們將剛剛設(shè)定好的規(guī)則,也就是數(shù)字到字母的替換,用 JavaScript 實(shí)現(xiàn)出來,如下:
function compressHello() { const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFG' const doubleColorBallStr = getDoubleColorBall(1000).join('') let resultStr = '' for (let i = 0; i < doubleColorBallStr.length; i+=2) { const number = doubleColorBallStr[i] + doubleColorBallStr[i+1] resultStr += letters[parseInt(number) - 1] } return resultStr } const firstPrize = compressHello() fs.writeFileSync('./hello-1.txt', firstPrize)
這樣我們就得到了一個(gè)全新的 hello 文件,他的大小如下所示,正好印證了我們的想法!
如果按照這個(gè)算法的方法,我們能將之前的文件壓縮至一半大小,也就是 118.301025390625MB
,但是這就是極限了嗎?不,上面我們講過,這只是最基本的壓縮,接下來,讓我們?cè)囋嚫畹姆椒ǎ?/p>
更精妙的方法
這里我們不對(duì)壓縮算法的原理做過多的解釋,如果諸位感興趣的話,可以自己尋找類似的文章閱讀,鑒于網(wǎng)上的文章質(zhì)量參差不齊,我就不做推薦了!
這里我們需要了解的是,我們正在研究的是一個(gè)彩票系統(tǒng),所以他的數(shù)據(jù)壓縮應(yīng)該具備以下幾個(gè)特征:
具備數(shù)據(jù)不丟失的特性,也就是無損壓縮
壓縮率盡可能小,因?yàn)閭鬏數(shù)奈募赡芊浅4螅缥覀兩厦媾e的例子
便于信息的傳輸,也就是支持HTTP請(qǐng)求
常做前端的同學(xué)應(yīng)該知道,我們?cè)?HTTP 請(qǐng)求頭里面常見的一個(gè)參數(shù) content-encoding: gzip,在項(xiàng)目的優(yōu)化方面,也會(huì)選擇將資源文件轉(zhuǎn)換為 gzip 來進(jìn)行分發(fā)。在日常的使用中,我們也時(shí)常依賴 Webpack,Rollup 等庫,或者通過網(wǎng)絡(luò)服務(wù)器如 nginx 來完成資源壓縮,gzip 不僅可以使得發(fā)送的內(nèi)容大大減少,而且客戶端可以無損解壓訪問源文件。
那么,我們能不能使用 gzip 來完成壓縮呢?答案是可以,Node.js 為我們提供了 zlib 工具庫,提供了相應(yīng)的壓縮函數(shù):
const zlib = require('zlib') const firstPrize = compressHello() fs.writeFileSync('./hello-2.txt.gz', zlib.gzipSync(firstPrize))
得到的結(jié)果是:
我們完成了 14KB -> 3KB 的壓縮過程!是不是很有意思?不過還是那句話,有沒有可能更???當(dāng)然可以!
content-encoding
響應(yīng)頭一般是服務(wù)器針對(duì)返回的資源響應(yīng)編碼格式的設(shè)置信息,常見的值有以下三種:
gzip
所有瀏覽器都支持的通用壓縮格式brotli
比gzip
壓縮性能更好,壓縮率更小的一個(gè)新的壓縮格式,老版本瀏覽器不支持deflate
出于某些原因,使用不是很廣泛,后有基于該算法的zlib
壓縮格式,不過也使用度不高
瀏覽器支持的壓縮格式不只是這些,不過我們列舉出的是較為常用的,我們嘗試使用一下這三種壓縮格式:
const firstPrize = compressHello() fs.writeFileSync('./hello-2.txt.gz', zlib.gzipSync(firstPrize)) fs.writeFileSync('./hello-2.txt.def', zlib.deflateSync(firstPrize)) fs.writeFileSync('./hello-2.txt.br', zlib.brotliCompressSync(firstPrize))
我們可以看到,deflate
和 gzip
的壓縮率不相上下,令人驚喜的是,brotli
的壓縮竟然達(dá)到了驚人的 1KB ! 這不就是我們想要的嗎?
還可能更小嗎?哈哈哈哈,當(dāng)然,如果不考慮HTTP支持,我們完全可以使用如 7-zip
等壓縮率更低的壓縮算法去完成壓縮,然后使用客戶端做手動(dòng)解壓。不過點(diǎn)到為止,更重要的工作我們還沒有做!
在這之前,我們需要先了解一下解壓過程,如果解壓后反而數(shù)據(jù)丟失,那就得不償失了!
// 執(zhí)行解壓操作 const brFile = fs.readFileSync('./hello-2.txt.br') const gzipFile = fs.readFileSync('./hello-2.txt.gz') const deflateFile = fs.readFileSync('./hello-2.txt.def') const brFileStr = zlib.brotliDecompressSync(brFile).toString() const gzipFileStr = zlib.gunzipSync(gzipFile).toString() const deflateFileStr = zlib.inflateSync(deflateFile).toString() console.log(brFileStr) console.log(gzipFileStr) console.log(deflateFileStr) console.log(brFileStr === gzipFileStr, brFileStr === deflateFileStr) // true, true
如上,我們知曉盡管壓縮算法的效果很驚人,但是其解壓后的數(shù)據(jù)依然是無損的!
完整的數(shù)據(jù)
讓我們構(gòu)建出完整的 17721088 注數(shù)據(jù)測試一下完整的壓縮算法的能力如何?這里我們使用 brotli
和 gzip
算法分別進(jìn)行壓縮測試!
首先,應(yīng)該修改我們生成數(shù)據(jù)的函數(shù),如下:
function generateAll() { const arrRed = Array.from({ length: 33 }, (_, index) => (index + 1).toString().padStart(2, '0')) const arrRedResult = generateCombinations(arrRed, 6, Number.MAX_VALUE) const result = [] arrRedResult.forEach(line => { for (let i = 1; i <= 16; i++) { result.push(line.join('') + i.toString().padStart(2, '0')) } }) return result } console.log(generateAll().length) // 17721088
接下來我們要經(jīng)過初步壓縮并將其寫入文件中:
function compressAll() { const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFG' const allStr = generateAll().join('') let resultStr = '' for (let i = 0; i < allStr.length; i += 2) { const number = allStr[i] + allStr[i+1] resultStr += letters[parseInt(number) - 1] } return resultStr } const firstPrize = compressAll() fs.writeFileSync('./all-ball.txt', firstPrize)
正如我們預(yù)料的,經(jīng)過初步壓縮之后,文件大小達(dá)到了大約 118MB,但是其實(shí)際占用 124MB,是屬于計(jì)算機(jī)存儲(chǔ)的范疇,我們現(xiàn)在不在本篇文章中討論,感興趣的同學(xué)可以自己查一查,根據(jù)字節(jié)數(shù)計(jì)算,其大小為:
const totalSize = 124047616 / 1024 / 1024 // 118.30102539 MB
目前來看是符合預(yù)期的,我們來看看兩個(gè)壓縮算法的真本事!
const firstPrize = compressAll() fs.writeFileSync('./all-ball.txt.gz', zlib.gzipSync(firstPrize)) fs.writeFileSync('./all-ball.txt.br', zlib.brotliCompressSync(firstPrize))
其實(shí)是很震驚的一件事情,盡管我對(duì) brotli
的期待足夠高,也不會(huì)想到他能壓縮到僅僅 4M 大小,不過對(duì)于我們來說,這是一件幸事,對(duì)于之后的分發(fā)操作有巨大的優(yōu)勢!
隨機(jī)來兩注
從彩票站購買彩票的時(shí)候,隨機(jī)來兩注的行為是非常常見的,但是當(dāng)你嘗試隨機(jī)號(hào)碼的時(shí)候,會(huì)發(fā)生什么呢?
我們先從彩票數(shù)據(jù)的分發(fā)講起,首先彩票數(shù)據(jù)的分發(fā)安全性和穩(wěn)定性的設(shè)計(jì)肯定是毋庸置疑的,但是這不是我們目前需要考慮的問題,目前我們應(yīng)該解決的是,如果才能更低程度的控制成本!
假設(shè)設(shè)計(jì)這套系統(tǒng)的人是你,如果控制隨機(jī)號(hào)碼的中獎(jiǎng)率?我的答案是,從已有的號(hào)碼池里面進(jìn)行選擇!
如果讓每個(gè)彩票站獲取到其對(duì)應(yīng)的號(hào)碼池,答:數(shù)據(jù)分發(fā)!如果采用數(shù)據(jù)分發(fā)的模式的話,需要考慮的問題如下:
什么時(shí)候進(jìn)行分發(fā)
數(shù)據(jù)回源如何做
如何避免所有數(shù)據(jù)被劫持
數(shù)據(jù)交給彩票站的策略
據(jù)2021年公開信息,彩票站的數(shù)量已經(jīng)達(dá)到20萬家(未查證,無參考價(jià)值),我們假設(shè)目前的彩票站數(shù)量為 30 萬家!
什么時(shí)候進(jìn)行分發(fā)
我們知道的是,彩票的購買截止時(shí)間是在晚上八點(diǎn),開獎(jiǎng)時(shí)間是在晚上的九點(diǎn)十五,在晚上八點(diǎn)之后,我們只能購買到下一期的彩票,那么這個(gè)節(jié)點(diǎn)應(yīng)該從晚上的八點(diǎn)開始,計(jì)劃是這樣子的:
從目前已有的彩票庫里面,按照號(hào)碼出現(xiàn)幾率從高到低排列
挑選出前50萬注分發(fā)給 30 萬彩票站,這個(gè)時(shí)間彩票站的數(shù)據(jù)都是統(tǒng)一的
每個(gè)小時(shí)同步一次數(shù)據(jù),同步的是其他彩票站"特意挑選的數(shù)據(jù)"
50萬注的數(shù)據(jù)量有多大?試試看:
function getFirstSend() { const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFG' const doubleColorBallStr = getDoubleColorBall(500000).join('') let resultStr = '' for (let i = 0; i < doubleColorBallStr.length; i+=2) { const number = doubleColorBallStr[i] + doubleColorBallStr[i+1] resultStr += letters[parseInt(number) - 1] } return resultStr } const firstPrize = getFirstSend() fs.writeFileSync('./first-send.txt.br', zlib.brotliCompressSync(firstPrize))
僅一張圖片的大小,獲取這些數(shù)據(jù)解壓同步到彩票機(jī)時(shí)間不足1s!
解壓示例如下:
function decodeData(brFile) { const result = [] const content = zlib.brotliDecompressSync(brFile) // 按照七位每注的結(jié)構(gòu)拆分 for (let i = 0; i < content.length; i += 7) { result.push(content.slice(i, i + 8)) } return result } const firstSend = fs.readFileSync('./first-send.txt.br') const firstDataList = decodeData(firstSend) console.log(firstDataList.length) // 500000
如何將獲取到的字符形式的彩票轉(zhuǎn)換為數(shù)字,如 abcdefga
轉(zhuǎn)換為 ['01', '02', '03', '04', '05', '06, '01']
:
function letterToCode(letterStr) { const result = [] const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFG' for (let i = 0; i < letterStr.length; i++) { result.push((letters.indexOf(letterStr[i]) + 1).toString().padStart(2, '0')) } return result }
至于分發(fā)?我們可以參考一下市面上已有的一些概念做一下對(duì)比,下面是籠統(tǒng)的一個(gè)網(wǎng)絡(luò)服務(wù)器的TPS預(yù)估值,也就是說彩票服務(wù)器在1秒內(nèi)可以處理的最大請(qǐng)求數(shù):
- 低性能:TPS 在 50 以下,適用于低流量的應(yīng)用場景,例如個(gè)人博客、小型企業(yè)網(wǎng)站等。
- 中性能:TPS 在 50~500 之間,適用于一般的網(wǎng)站和應(yīng)用場景,例如中小型電商網(wǎng)站、社交網(wǎng)絡(luò)等。
- 高性能:TPS 在 500~5000 之間,適用于高流量的網(wǎng)站和應(yīng)用場景,例如大型電商網(wǎng)站、游戲網(wǎng)站等。
- 超高性能:TPS 在 5000 以上,適用于超高流量的網(wǎng)站和應(yīng)用場景,例如互聯(lián)網(wǎng)巨頭的網(wǎng)站、在線游戲等。
按照這種模式的話,50萬彩票站的數(shù)據(jù)同步在100秒內(nèi)就可以完成,當(dāng)然,諸位,這里是單機(jī)模式,如果做一個(gè)彩票服務(wù)的話,單機(jī)肯定是不可能的,想要提高TPS,那就做服務(wù)器集群,如果有100臺(tái)服務(wù)器集群的話,處理這些請(qǐng)求僅僅需要 1 秒!(任性嗎?有錢當(dāng)然可以任性!)(這些數(shù)據(jù)的得出都是基于理論,不提供參考價(jià)值)
數(shù)據(jù)回源如何做
非常簡單!我們需要獲取的數(shù)據(jù)是哪一些呢?沒有經(jīng)過隨機(jī)算法,直接被購買的彩票數(shù)據(jù)!也就是我們經(jīng)常聽到的"守號(hào)"的那些老彩民!
同樣,根據(jù)媒體查詢得知(不做參考),彩票站的客流量是每小時(shí)1至10人,經(jīng)營時(shí)間,早上九點(diǎn)至晚上九點(diǎn),最大客流量預(yù)計(jì)為100人每天!
那么所有彩票站的總體客流量在 100 * 500000 = 50000000,大約為五千萬人次,大約有50%是屬于"守號(hào)"人,這里面可能還需要排除掉彩票站中已知的號(hào)碼,不過在這里我們先不處理,先做全部的預(yù)估,那么
服務(wù)器需要承載的最大TPS為:
// 服務(wù)器集群數(shù)量 const machineCount = 100 // 總訪問量,50%中的號(hào)碼才會(huì)上報(bào) const totalVisit = 50000000 * 0.5 // 25000000 // 總的時(shí)間,因?yàn)槲覀冇?jì)算的是 10個(gè)小時(shí)的時(shí)間,所以應(yīng)該計(jì)算的總秒數(shù)為 36000 秒! const totalSeconds = 10 * 60 * 60 console.log(totalVisit / totalSeconds / machineCount) // 6.944444444444445
TPS僅為7?。∵@還是沒有排除掉已經(jīng)知悉的號(hào)碼的情況,具體的上報(bào)邏輯,參考下圖:
數(shù)據(jù)交給彩票站的策略(避免數(shù)據(jù)被劫持)
所有的彩票數(shù)據(jù)當(dāng)然不能全部都交給彩票站,我們需要對(duì)所有的數(shù)據(jù)做一個(gè)分層,其他彩票站"特意挑選的數(shù)據(jù)"就是我們要分層分發(fā)的數(shù)據(jù)!這樣子也就能解決 "如何避免所有數(shù)據(jù)被劫持" 的問題!
那么我們?nèi)绾螌?duì)數(shù)據(jù)進(jìn)行分層呢?
簡而言之,就是我們將陜西西安彩票站的購票信息同步給山西太原,將上海市購票信息同步給江蘇蘇州!當(dāng)然這里面需要考慮的點(diǎn)非常多,不僅僅是兩地?cái)?shù)據(jù)的交換,邏輯也比較復(fù)雜,通常需要考慮的點(diǎn)是:
- 數(shù)據(jù)同步難度,跨地區(qū)同步對(duì)服務(wù)器壓力巨大,如華南向華北同步
- 數(shù)據(jù)相似程度,兩地的數(shù)據(jù)如果歷史以來相似度區(qū)別很大,反而不能達(dá)到覆蓋的目的,因?yàn)槲覀冏罱K是想要這注號(hào)碼被購買更多次
- 數(shù)據(jù)同步時(shí)差,如新疆等地,鑒于網(wǎng)絡(luò)問題,比其他地要慢很多的情況,這樣就會(huì)漏號(hào),那么就應(yīng)該把這些地方的數(shù)據(jù)同步到更繁華的區(qū)域,如上海市,但是這一點(diǎn)看似是和第一二點(diǎn)相悖的
就說這么多,說的多了其實(shí)我也不懂?;蛘哒f還沒想出來,如果有這方面比較厲害的大佬,可以提供思路!我們先看看隨機(jī)的號(hào)碼結(jié)果如何:
我們來嘗試隨機(jī)獲取你需要的兩注:
function random(count) { let result = [] for (let i = 0; i < count; i++) { const index = Math.floor(Math.random() * firstDataList.length) console.log(firstDataList[index]) result.push(letterToCode(firstDataList[index])) } return result } console.log(random(2))
OK,你覺得可以中獎(jiǎng)嗎?哈哈哈,還是有可能的,繼續(xù)往下看吧!
特意挑的兩注
我是一個(gè)典型的"守號(hào)"人,每天都拿著自己算出來的幾注號(hào)碼,去購買彩票,那么我可以中獎(jiǎng)嗎?(目前沒中)
根據(jù)上面的描述,我們應(yīng)該知道,"守號(hào)"人購買的號(hào)碼需要判斷系統(tǒng)是否存在數(shù)據(jù),如果存在的話,就不會(huì)觸發(fā)上報(bào),如果數(shù)據(jù)不存在,則會(huì)上報(bào)系統(tǒng),由系統(tǒng)將當(dāng)前號(hào)碼分發(fā)給相鄰市或數(shù)據(jù)近似的城市,預(yù)期當(dāng)前號(hào)碼可以被更多的人所購買,一注號(hào)碼如果被購買的越多,其中獎(jiǎng)的概率也就越低!
不過特意挑選是要比隨機(jī)挑選的中獎(jiǎng)概率要大,但是也大不到哪里去。
我要一等獎(jiǎng)
彩票的一等獎(jiǎng)是基于統(tǒng)計(jì)的,即使彩票中心存在空號(hào),也需要考慮空號(hào)所產(chǎn)生的二等獎(jiǎng)至六等獎(jiǎng)的數(shù)量,這是一個(gè)非常龐大的數(shù)據(jù)量,也是需要計(jì)算非常多的時(shí)間的,那么我們?nèi)绾文M呢?
我們?nèi)?0萬注彩票,模擬一下這些彩票被購買的情況,可能會(huì)產(chǎn)生空號(hào),可能會(huì)重復(fù)購買,或者購買多注等,嘗試一下計(jì)算出我們需要付出的總金額!
彩票中中獎(jiǎng)規(guī)則是這樣子的,浮動(dòng)獎(jiǎng)項(xiàng)我們暫時(shí)不考慮,給一等獎(jiǎng)和二等獎(jiǎng)都賦予固定的金額:
6 + 1 一等獎(jiǎng) 獎(jiǎng)金500萬
6 + 0 二等獎(jiǎng) 獎(jiǎng)金30萬
5 + 1 三等獎(jiǎng) 獎(jiǎng)金3000元
5 + 0 或 4 + 1 四等獎(jiǎng) 獎(jiǎng)金200元
4 + 0 或 3 + 1 五等獎(jiǎng) 獎(jiǎng)金10元
2 + 1 或 1 + 1 或 0 + 1 都是 六等獎(jiǎng) 獎(jiǎng)金 5 元
根據(jù)這個(gè)規(guī)則,我們可以先寫出對(duì)獎(jiǎng)的函數(shù):
/** * @param {String[]} target ['01', '02', '03', '04', '05', '06', '07'] * @param {String[]} origin ['01', '02', '03', '04', '05', '06', '07'] * @returns {Number} 返回當(dāng)前彩票的中獎(jiǎng)金額 */ function compareToMoney(target, origin) { let money = 0 let rightMatched = target[6] === origin[6] // 求左邊六位的交集數(shù)量 let leftMatchCount = target.slice(0, 6).filter( c => origin.slice(0,6).includes(c) ).length if (leftMatchCount === 6 && rightMatched) { money += 5000000 } else if (leftMatchCount === 6 && !rightMatched) { money += 300000 } else if (leftMatchCount === 5 && rightMatched) { money += 3000 } else if (leftMatchCount === 5 && !rightMatched) { money += 200 } else if (leftMatchCount === 4 && rightMatched) { money += 200 } else if (leftMatchCount === 4 && !rightMatched) { money += 10 } else if (leftMatchCount === 3 && rightMatched) { money += 10 } else if (leftMatchCount === 2 && rightMatched) { money += 5 } else if (leftMatchCount === 1 && rightMatched) { money += 5 } else if (rightMatched) { money += 5 } return money }
那么,應(yīng)該如何得到利益最大化,步驟應(yīng)該是這樣子:
隨機(jī)生成一組中獎(jiǎng)號(hào)碼
對(duì)于每個(gè)購買的數(shù)字,檢查是否與中獎(jiǎng)號(hào)碼匹配,并計(jì)算它的獎(jiǎng)金金額
對(duì)于所有購買的數(shù)字的獎(jiǎng)金金額進(jìn)行求和
重復(fù)這個(gè)過程,直到找到最優(yōu)的中獎(jiǎng)號(hào)碼
隨機(jī)這個(gè)中獎(jiǎng)號(hào)碼非常重要,他決定著我們計(jì)算出整體數(shù)據(jù)的速度,所以我們按照下面的步驟進(jìn)行獲?。?/p>
將所有的號(hào)碼按照購買數(shù)量進(jìn)行排序(其實(shí)這里真實(shí)的場景應(yīng)該聯(lián)合考慮中獎(jiǎng)號(hào)碼的分布趨勢才更精確)
從空號(hào)開始查詢,依次進(jìn)行計(jì)算
先模擬出我們的購買數(shù)據(jù):
function getRandomCode(count = 500000) { const arrRed = Array.from({ length: 33 }, (_, index) => (index + 1).toString().padStart(2, '0')) // generateCombinations 是我們上面定義過的函數(shù) const arrRedResult = generateCombinations(arrRed, 6, count) const result = [] let blue = 1 arrRedResult.forEach(line => { result.push([...line, (blue++).toString().padStart(2, '0')]) if (blue > 16) { blue = 1 } }) return result } function randomPurchase() { const codes = getRandomCode() const result = [] for (let code of codes) { let count = Math.floor(Math.random() * 50) result.push({ code, count, }) } return result } console.log(randomPurchase())
我們將得到類似于下面的數(shù)據(jù)結(jié)構(gòu),這對(duì)于統(tǒng)計(jì)來說較為方便:
[ { code: [ '01', '02', '03', '04', '05', '10', '05' ], count: 17 }, { code: [ '01', '02', '03', '04', '05', '11', '06' ], count: 4 } ]
接下來,就是很簡單的統(tǒng)計(jì)了,邏輯很簡單,但對(duì)于數(shù)據(jù)量極為龐大的彩票來說,需要的時(shí)間!
// 空號(hào)在前,購買數(shù)量越多越靠后 const purchaseList = randomPurchase().sort((a, b) => a.count - b.count) const bonusPool = [] for (let i = 0; i < purchaseList.length; i++) { // 假設(shè)這就是一等獎(jiǎng),那么就需要計(jì)算其價(jià)值 const firstPrize = purchaseList[0] let totalMoney = 0 for (let j = 0; j < purchaseList.length; j++) { // 與一等獎(jiǎng)進(jìn)行對(duì)比,對(duì)比規(guī)則是參照彩票中獎(jiǎng)規(guī)則 const money = compareToMoney(purchaseList[j].code, firstPrize.code) * purchaseList[j].count totalMoney += money } bonusPool.push({ code: firstPrize.code, totalMoney, }) } const result = bonusPool.sort((a, b) => a.totalMoney - b.totalMoney) // 至于怎么挑,那就隨心所欲了 console.log(result[0].code, result[0].totalMoney)
至于最后的一等獎(jiǎng)怎么挑,那就隨心所欲了,不過上面的算法在我的 M1 芯片計(jì)算需要的時(shí)間也將近10分鐘,如果有更強(qiáng)大的機(jī)器,更厲害的算法,這個(gè)時(shí)長同樣可以縮短,不展開了,累了,就這樣吧!
黃粱一夢
終歸是黃粱一夢,最終還是要回歸生活,好好工作!不過誰知道呢,等會(huì)再買一注如何?
彩票系統(tǒng)純屬臆測,不可能有雷同!
以上就是通過JavaScript看透彩票背后的隨機(jī)算法的詳細(xì)內(nèi)容,更多關(guān)于JavaScript 彩票隨機(jī)算法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript實(shí)現(xiàn)星座查詢功能 附詳細(xì)代碼
最近小編在做一個(gè)項(xiàng)目,其中涉及到一個(gè)模塊關(guān)于星座查詢功能,即在文本框中輸入一個(gè)生日值,點(diǎn)擊按鈕可以得到對(duì)應(yīng)的星座,怎么實(shí)現(xiàn)這個(gè)需求呢?下面小編通過示例代碼給大家介紹下,需要的朋友參考下吧2021-11-11JavaScript中Function與Object的關(guān)系
本文主要介紹了JavaScript中Function與Object的關(guān)系,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05js獲取當(dāng)前頁的URL與window.location.href簡單方法
下面小編就為大家?guī)硪黄猨s獲取當(dāng)前頁的URL與window.location.href簡單方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02主頁面中的兩個(gè)iframe實(shí)現(xiàn)鼠標(biāo)拖動(dòng)改變其大小
iframe實(shí)現(xiàn)鼠標(biāo)拖動(dòng)改變其大小在一個(gè)頁面中的兩個(gè)iframe的情況下,此方法相當(dāng)實(shí)用,感興趣的各位不妨參考下,或許對(duì)你有所幫助2013-04-04