文章目錄
簡介
彈彈車是爲五菱宏光PLUS上市而誕生的,其目的主要在於
- 拉新
- 提高用戶活躍度
- 慶祝宏光plus上市,提高產品知名度
彈彈車遊戲方法
- 彈車。按下力度條,車子彈射出去一段距離,然後停止。
彈車距離影響因素有:
力度大小。
1.1 力度區間爲:(0,0.5] 和 (1,0.5],即中間(0.5)處,力度最大。
1.2 同等條件下,力度越大,彈射距離越大。
是否具備助推劑。
2.1 獲得方式:分享拉新成功則得一瓶氮氣
2.2 同等條件下,有助推器可以彈車更遠的距離。
宏光plus座駕還是舊座駕。
3.1 獲得方式:進行愛車評估
3.2 宏光plus比舊座駕在同等條件下彈射更遠距離。
- 爆旅途獎品。彈車到停止的過程會隨機獲取獎品。
- 累計彈車裏程達到繞地球20圈,則贈與油卡券。
彈彈車涉及到的技術主要有
- 彈射里程計算。
- 路途如何爆出獎品。
- 獎品庫存控制。
彈射里程計算
里程的計算會影響到油卡券的發放,故需要嚴格控制每次彈車的距離,做到
- 油卡券能發放完畢。即用戶在活動結束後有部分人達到目標里程(繞地球20圈);
- 拉新與否、愛車評估與否、彈射力度大小能通過彈車距離來明顯感知。例如,同等條件下,彈射力爲0.1的彈車距離理應小於彈射力爲0.5的彈車距離。
劃分區間
劃分區間可以規範好彈車距離,使之不重疊,即能讓用戶明顯感知條件的不同會影響彈車距離。
彈車距離影響因子:
1. 是否進行愛車評估,設代碼爲plus_car
2. 是否有助推器,設代碼爲props
3. 力度大小,設代碼爲pressure
eg.
plus_car props pressure distance
1 1 1 (0.8,1]
0 1 1 (0.8*0.9,1*0.9]即(0.72,0.9]
...
注:
力度轉成0-1的範圍:[2x , 2(1-x])
正態分佈
雖然劃分區間能夠明確知道每個用戶彈車的距離區間在哪裏,但是其具體值是隨機的。從自然界的一些現象,如
- 某年某地區降雨量
- 某年某高校大學生的身高
- 測量誤差
- 射擊目標的水平或者垂直偏差(跟彈車有點類似)
等等都近似或者服從正態分佈,雖然沒有進行實際進行彈車測試,我們可以做個取巧,即認爲彈車距離就是服從正態分佈(模擬也要近似於常識、自然現象)。
這裏,實現方式採用GB4086.1-83這個規範來實現正態分佈。其原理如下圖所示
Java實現正態分佈
其中k的值,我個人認爲,只要其滿足GB4086.1-83規範給出表格的得出即可。即將u=1.33帶入公式,能得出0.908241值即可。
/**
* 正態分佈,詳細見https://max.book118.com/html/2018/1213/6144221053001235.shtm
* @param u 範圍是0-5
* @return 範圍是[0.5-1]
*/
public static double normalDistribution(double u) {
double y = Math.abs(u);
double y2 = y * y;
double z = Math.exp(-0.5 * y2) * 0.398942280401432678;
double p = 0;
//循環次數(連分式的項數)控制精度
int k = 28;
double s = -1;
double fj = k;
if (y > 3) {
//當y>3時
for (int i = 1; i <= k; i++) {
p = fj / (y + p);
fj = fj - 1.0;
}
p = z / (y + p);
} else {
//當y<3時
for (int i = 1; i <= k; i++) {
p = fj * y2 / (2.0 * fj + 1.0 + s * p);
s = -s;
fj = fj - 1.0;
}
p = 0.5 - z * y / (1 - p);
}
if (u > 0) {
p = 1.0 - p;
}
return p;
}
爆出旅途獎品(開獎、抽獎)
抽獎算法有很多種,但是
- 時間、空間複雜度低(性能好)
- 數據結構恰當(對象建模)
- 算法易於理解
的就比較少了,本活動抽獎算法使用《darts-dice-coins》文章裏介紹的Alias改進算法來實現的。下面介紹其算法的主要思想
拋均勻硬幣
假設硬幣是均勻的,則其結果和概率爲
- 硬幣正面:概率爲1/2。
- 硬幣反面:概率爲1/2。
算法(僞代碼)
Algorithm: Simulating a Fair Die
//可以用(new Random().nextDouble())來生成[0,1)的僞隨機數
1. Generate a uniformly-random value x in the range [0,1).
//n:產生多少種結果,丟硬幣只有正反面2種,故n=2
//⌊⌋是向下取證。
2. Return ⌊xn⌋.
舉個栗子
1. 隨機生成一個[0,1)的數爲0.82
2. 代入算法得⌊xn⌋=⌊0.82 * 2⌋=⌊1.64⌋=1,是正面
// another example
1. 隨機生成一個[0,1)的數爲0.1
2. 代入算法得⌊xn⌋=⌊0.1 * 2⌋=⌊0.2⌋=0,是反面
拋不均勻硬幣
假設硬幣是均勻的,則其結果和概率爲
- 硬幣正面:概率爲1/4。
- 硬幣反面:概率爲3/4。
算法(僞代碼)
Algorithm: Simulating a Biased Coin
1. Generate a uniformly-random value x in the range [0,1).
//pheads就是正面的概率,即pheads=1/4
2. If x<pheads, return "heads."
3. If x≥pheads, return "tails."
Alias改進算法
其實它的算法來源思想就是由
- 拋均勻硬幣
- 拋不均勻硬幣
而來的。要詳細介紹,請移步darts-dice-coins文章。要注意的點是概率總和爲1,因爲這是Alias算法的前提。
Alias算法實現(Java版本)
/******************************************************************************
* File: AliasMethod.java
* Author: Keith Schwarz ([email protected])
*
* An implementation of the alias method implemented using Vose's algorithm.
* The alias method allows for efficient sampling of random values from a
* discrete probability distribution (i.e. rolling a loaded die) in O(1) time
* each after O(n) preprocessing time.
*
* For a complete writeup on the alias method, including the intuition and
* important proofs, please see the article "Darts, Dice, and Coins: Smpling
* from a Discrete Distribution" at
*
* http://www.keithschwarz.com/darts-dice-coins/
*/
import java.util.*;
public final class AliasMethod {
/* The random number generator used to sample from the distribution. */
private final Random random;
/* The probability and alias tables. */
private final int[] alias;
private final double[] probability;
/**
* Constructs a new AliasMethod to sample from a discrete distribution and
* hand back outcomes based on the probability distribution.
* <p>
* Given as input a list of probabilities corresponding to outcomes 0, 1,
* ..., n - 1, this constructor creates the probability and alias tables
* needed to efficiently sample from this distribution.
*
* @param probabilities The list of probabilities.
*/
public AliasMethod(List<Double> probabilities) {
this(probabilities, new Random());
}
/**
* Constructs a new AliasMethod to sample from a discrete distribution and
* hand back outcomes based on the probability distribution.
* <p>
* Given as input a list of probabilities corresponding to outcomes 0, 1,
* ..., n - 1, along with the random number generator that should be used
* as the underlying generator, this constructor creates the probability
* and alias tables needed to efficiently sample from this distribution.
*
* @param probabilities The list of probabilities.
* @param random The random number generator
*/
public AliasMethod(List<Double> probabilities, Random random) {
/* Begin by doing basic structural checks on the inputs. */
if (probabilities == null || random == null)
throw new NullPointerException();
if (probabilities.size() == 0)
throw new IllegalArgumentException("Probability vector must be nonempty.");
/* Allocate space for the probability and alias tables. */
probability = new double[probabilities.size()];
alias = new int[probabilities.size()];
/* Store the underlying generator. */
this.random = random;
/* Compute the average probability and cache it for later use. */
final double average = 1.0 / probabilities.size();
/* Make a copy of the probabilities list, since we will be making
* changes to it.
*/
probabilities = new ArrayList<Double>(probabilities);
/* Create two stacks to act as worklists as we populate the tables. */
Deque<Integer> small = new ArrayDeque<Integer>();
Deque<Integer> large = new ArrayDeque<Integer>();
/* Populate the stacks with the input probabilities. */
for (int i = 0; i < probabilities.size(); ++i) {
/* If the probability is below the average probability, then we add
* it to the small list; otherwise we add it to the large list.
*/
if (probabilities.get(i) >= average)
large.add(i);
else
small.add(i);
}
/* As a note: in the mathematical specification of the algorithm, we
* will always exhaust the small list before the big list. However,
* due to floating point inaccuracies, this is not necessarily true.
* Consequently, this inner loop (which tries to pair small and large
* elements) will have to check that both lists aren't empty.
*/
while (!small.isEmpty() && !large.isEmpty()) {
/* Get the index of the small and the large probabilities. */
int less = small.removeLast();
int more = large.removeLast();
/* These probabilities have not yet been scaled up to be such that
* 1/n is given weight 1.0. We do this here instead.
*/
probability[less] = probabilities.get(less) * probabilities.size();
alias[less] = more;
/* Decrease the probability of the larger one by the appropriate
* amount.
*/
probabilities.set(more,
(probabilities.get(more) + probabilities.get(less)) - average);
/* If the new probability is less than the average, add it into the
* small list; otherwise add it to the large list.
*/
if (probabilities.get(more) >= 1.0 / probabilities.size())
large.add(more);
else
small.add(more);
}
/* At this point, everything is in one list, which means that the
* remaining probabilities should all be 1/n. Based on this, set them
* appropriately. Due to numerical issues, we can't be sure which
* stack will hold the entries, so we empty both.
*/
while (!small.isEmpty())
probability[small.removeLast()] = 1.0;
while (!large.isEmpty())
probability[large.removeLast()] = 1.0;
}
/**
* Samples a value from the underlying distribution.
*
* @return A random value sampled from the underlying distribution.
*/
public int next() {
/* Generate a fair die roll to determine which column to inspect. */
int column = random.nextInt(probability.length);
/* Generate a biased coin toss to determine which option to pick. */
boolean coinToss = random.nextDouble() < probability[column];
/* Based on the outcome, return either the column or its alias. */
return coinToss? column : alias[column];
}
}
獎品庫存控制
庫存是共享資源,多線程下不加以控制會造成多減庫存。控制庫存減去主要利用
- 庫存數據來源唯一,即都由redis控制。
- 利用redis自帶的原子性操作函數來減庫存。
//減庫存
redisTemplate.opsForValue().increment(key, -1)
// 加庫存
redisTemplate.opsForValue().increment(key, 1)
- 庫存從db放入redis緩存中需要加以控制(用分佈式鎖),因爲項目是分佈式的。 參考一章中已經給出具體的實現方案了,這裏不贅餘。
分佈式鎖建議:單實例redis鎖 < redisson < zookepper