3n + 1 問題——引發的緩存思考

3n + 1 問題——引發的緩存思考

問題描述

有這樣一個規律:對任意大於 1 的自然數 n,如果 n 爲奇數,將其變化爲 3n + 1;如果 n 爲偶數,將其變化爲 n/2。經過若干次變化後,一定會使 n 變爲 1。

如果 n = 5:

  • 5 -> 16 -> 8 -> 4 -> 2 -> 1

如果 n = 3:

  • 3 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1

現在,輸入一個整數 n,請計算出總的變化次數。例:n = 5,l = 5;n = 3,l = 7。(l 表示變化次數)

方法 1(實時計算)

最簡單處理方式是遞歸。由於每一次 n/2 或者 3n-1 操作後,變化次數 l 會加 1,可以得出公式模型爲:f(n) = f(n-1) + 1。遞歸函數必須有出口,當 n = 1 時,會進入 1 -> 4 -> 2 -> 1 -> 4 … 這樣的死循環。所以出口條件是 n <= 1。最終得到代碼如下。

size_t solve_3nplus1(size_t n)
{
    if (n <= 1)
        return 0;

    return solve_3nplus1( (n%2 == 0) ? n/2 : 3*n+1 ) + 1;
}

使用:

int main()
{
    cout << solve_3nplus1(3) << endl;
    cout << solve_3nplus1(5) << endl;
}

/* 輸出:*/
7
5

方法 2(結果緩存)

事實上,你會發現 方法1 是一種實時計算。也就是說,第一次 n = 3 的時候,程序會 3 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1,第二次 n = 3 的時候,程序還是會 3 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1,於是這中間多了許多重複計算。

提升程序效率的一種、衆所周知的方式是緩存。常見的緩存處理方式是,在覈心處理邏輯(solve_3nplus1)之上,套一層緩存邏輯(solve_3nplus1_cache)。緩存層負責將計算結果保存下來,每次外部接口請求時,緩存層會先去緩存裏看看,“我有沒有計算過 n = 3 呀”,有就直接將結果返回;沒有,那麼調用 solve_3nplus1 計算出結果。向外部返回結果的同時,拷貝一份結果放在緩存裏。

完整代碼如下:

/* 計算邏輯 */
size_t solve_3nplus1(size_t n)
{
    if (n <= 1)
        return 0;

    return solve_3nplus1( (n%2 == 0) ? n/2 : 3*n+1 ) + 1;
}
/* 緩存邏輯 */
size_t solve_3nplus1_cache(vector<size_t>& v, size_t n)
{
    size_t l = 0;
    
    /* 避免越界訪問 */
    while (n >= v.size()) {
        n = (n%2 == 0) ? n/2 : 3*n+1;
        ++l;
    }

    if (v[n] == 0) {
        cout << "實時計算... n = " << n << endl;
        v[n] = solve_3nplus1(n);
    }
    else
        cout << "緩存中取得... n = " << n << endl;

    return v[n] + l;
}

用簡單的測試用例進行功能測試:

int main()
{
    vector<size_t> v(6);
    cout << solve_3nplus1_cache(v, 3) << endl;
    cout << solve_3nplus1_cache(v, 5) << endl;
    cout << solve_3nplus1_cache(v, 3) << endl;
}

/* 輸出:*/
實時計算... n = 3
7
實時計算... n = 5
5
緩存中取得... n = 3
7

瞧上去功能是實現了,但是哪裏不對勁。得捋捋:

  • 3 -> 1變化中,完整過程是:3 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1。
  • 5 -> 1變化中,完整過程是:5 -> 16 -> 8 -> 4 -> 2 -> 1。

看得出,n = 3 與 n = 5 時有部分是重合的,更準確說,n = 3 包含了 n = 5。結論顯而易見:程序還是在做重複計算。這是因爲,上述代碼僅僅對計算結果緩存,而非計算過程。

方法 3(過程緩存)

3n + 1 問題顯然是符合過程緩存的,因爲過程中產生的數據能夠推導出目標結果(這句話會不會太抽象?總之結合 n = 3、n = 5 就能明瞭許多)。

size_t solve_3nplus1_cache(vector<size_t>& v, size_t n);

size_t solve_3nplus1(vector<size_t>& v, size_t n)
{
    v[1] = 1;  /* 設置出口 */
    return solve_3nplus1_cache(v, n) - 1;  /* 由於v[1]=1, 所以最後需要減1 */
}

size_t solve_3nplus1_cache(vector<size_t>& v, size_t n)
{
    size_t l = 0;
    /* 防越界 */
    while (n >= v.size()) {
        n = (n%2 == 0) ? n/2 : 3*n+1;
        ++l;
    }

    if (v[n] == 0)
        v[n] = solve_3nplus1_cache(v, (n%2 == 0) ? n/2 : 3*n+1) + 1;
    else if (n != 1)  /* 避免 n = 1 的影響 */
        cout << "從緩存中取得... n = " << n << endl;
    return v[n] + l;
}

與 方法2 的不同之處是,方法3 把遞歸過程中計算出來的所有數據都保存下來,而不是隻保留最終結果。

測試代碼:

int main()
{
    vector<size_t> v(20);

    cout << solve_3nplus1(v, 3) << endl;
    cout << solve_3nplus1(v, 5) << endl;
    cout << solve_3nplus1(v, 3) << endl;

    cout << "v[i] = ";
    for (int i=0; i<v.size(); ++i) {
        cout << v[i] << " ";
    }
    cout << endl;
}

/* 輸出:*/
7
從緩存中取得... n = 5
5
從緩存中取得... n = 3
7
v[i] = 0 1 2 8 3 6 0 0 4 0 7 0 0 0 0 0 5 0 0 0
             *   *
  • 注:容器 v 中存儲的次數 l 要比實際值大 1。

方法3 比 方法2 在緩存上做得更極致。但是不可避免的是,方法3 中,緩存層與業務層基本耦合在一起,代碼可閱讀性較差。雖說不是不可以優化,但對程序員的編碼能力要求更高了。

最後

3n + 1 問題可以說是一個很簡單的問題,但在過程中引發的“緩存”思想卻很重要。

—— 其實更像是無聊人的胡思亂想而已!但少閒人如吾一人者耳……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章