1. 程式人生 > 其它 >AcWing 338. 計數問題

AcWing 338. 計數問題

題目傳送門

1、為什麼不能用暴力解?

資料範圍太大了,\(0<a,b<100000000\)
兩個數字\(a-b\)之間,最多就有\(1^8\)個數字,每個\(10^8\)數,需要遍歷每一位,就是一個數字需要遍歷8次最多。
那就一次的時間複雜度最高就是:$10^8 * 8 $,而且有多組測試資料,不出意外會TLE。

2、按小學數奧問題分析

如計算 78501 中 "5" 出現的次數。 [答案:41502]
我們可以列舉“5”出現的位置,
1、如當“5”位於倒數第1位時,寫成 xxxx5,由於5大於1(當前數位1小於目標值5),前面只能取0~7849(共7850個);

2、如當“5”位於倒數第2位時,寫成xxx5x,由於5大於0(當前數位0小於目標值5),前面只能取0~784(共785個),後面無限制為10; 總計:7850個

3、如當“5”位於倒數第3位時,寫成xx5xx,由於5等於5(當前數位5等於目標值5),前面取0~77乘以後面無限制的100,共7800個;加上,前面取78,後面取“01”;因為還有"00",所以還需要加上一個1 ,總計:7802個。

4、如當“5”位於倒數第4位時,寫成 x5xxx,由於5小於8(當前數位8大於目標值5),前面可取0~7(共8個),後面無限制的1000. 總計:8000個

5、如當“5”位於倒數第5位時,寫成 5xxxx,由於5小於7(當前數位7大於目標值5),後面無限制的10000. 總計:10000個
總之,列舉x出現的位置,按x與n在該位上的大小關係,分為大於、小於、等於三類討論。

加在一起: 7850+7850+7802+8000+10000=41502

ll count_x(ll n, ll x) {
    ll res = 0, tmp = 1;
    ll tmp_n = n;                                                           //n的副本,因為下面是以n為運算的基礎,一路除下去的,如果我們想要得到原始的n,就找不到了,這裡給它照了一個像
    while (n) {                                                             //從後向前一位一位的討論
        //程式碼中註釋掉的兩行,用於跟蹤除錯程式的分步執行情況
        //int t = res;
        if (n % 10 < x) res += n / 10 * tmp;                                //小於  n/10代表當前位數前面的數字值,比如785,tmp是指需要乘以10的幾次方
        else if (n % 10 == x) res += n / 10 * tmp + (tmp_n % tmp + 1);      //等於  等於最麻煩,需要再次分情況討論
        else res += (n / 10 + 1) * tmp;                                     //大於  這個加1,是因為是0~77,共78個
        n /= 10;                                                            //進入前一位
        tmp *= 10;                                                          //計算需要乘幾個10的變數
        //cout << res - t << endl;
    }
    return res;
}

3、對於0這個東東比較討厭,需要單獨討論一下

(1)因為0不能當首位
就是上面討論中提到的什麼07,07849 啊,都不能從0開始,那樣的話,前面就成了存在前導0了。

(2)在每一位判斷時,不存在比0小的數字。
只需要分成兩類,不可能存在當前位小於0的情況,只討論等於和大於。

ll count_0(ll n) {
    ll res = 0, tmp = 1;
    ll tmp_n = n;
    while (n) {
        //程式碼中註釋掉的兩行,用於跟蹤除錯程式的分步執行情況
        //int t = res;
        if (n % 10 == 0) res += (n / 10 - 1) * tmp + (tmp_n % tmp + 1);    //等於,剔除了大於0的情況
        else  res += (n / 10) * tmp;                                       //大於,剔除了大於0的情況

        n /= 10;                                                           //進入前一位
        tmp *= 10;                                                         //計算需要乘幾個10的變數
        //cout << res - t << endl;
    }
    return res;
}

4、寫一個最笨的計算辦法,與用數學方法計算出來的結果進行對比,驗證一下思路的正確性

//數位分離之原始版本[笨蛋版本,慢,但絕對準確,用於演算法對比判斷正確性]
ll stupidCount(int n, int x) {
    ll res = 0;
    for (int i = 1; i <= n; ++i) {
        int tmp = i;
        while (tmp) {
            int t = tmp % 10;
            if (t == x) res++;
            tmp /= 10;
        }
    }
    return res;
}

驗證辦法:
int n = 78501;
printf("%d ", stupidCount(n, 5));
printf("%d ", stupidCount(n, 0));

5、要的是區間段,我們算的是從1到n,怎麼辦呢?

現在的題目要求我們求出\([a,b]\)之間 \([0-9]\)出現的次數,這不太好求,如果我們有一個函式,可以計算出\([1-n]\)之間\(x\)出現的次數,就可以使用類似於字首和的思想,計算出來,比如:
\(f[a,b]=count(b,x)-count(a-1,x)\)

6、測試對拍的程式

 int n = 78501;
printf("%lld ", count_0(n));
for (int i = 1; i <= 9; i++) printf("%lld ", count_x(n, i));

printf("\n");

for (int i = 0; i <= 9; i++)  printf("%lld ", stupidCount(n, i));

//printf("%lld ", count_x(n,5));
//printf("%d ", stupidCount(n, 5));

7、完整C++ 程式碼

#include <iostream>

using namespace std;
typedef long long ll;

ll count_x(ll n, ll x) {
    ll res = 0, tmp = 1;
    ll tmp_n = n;                                                           //n的副本,因為下面是以n為運算的基礎,一路除下去的,如果我們想要得到原始的n,就找不到了,這裡給它照了一個像
    while (n) {                                                             //從後向前一位一位的討論
        //程式碼中註釋掉的兩行,用於跟蹤除錯程式的分步執行情況
        //int t = res;
        if (n % 10 < x) res += n / 10 * tmp;                                //小於  n/10代表當前位數前面的數字值,比如785,tmp是指需要乘以10的幾次方
        else if (n % 10 == x) res += n / 10 * tmp + (tmp_n % tmp + 1);      //等於  等於最麻煩,需要再次分情況討論
        else res += (n / 10 + 1) * tmp;                                     //大於  這個加1,是因為是0~77,共78個
        n /= 10;                                                            //進入前一位
        tmp *= 10;                                                          //計算需要乘幾個10的變數
        //cout << res - t << endl;
    }
    return res;
}

/**
對於0,稍作修改,
此時只需分成兩類,因為不存在當前為小於0的情況,不過每次的最高為要排除全0的情況。
*/
ll count_0(ll n) {
    ll res = 0, tmp = 1;
    ll tmp_n = n;
    while (n) {
        //程式碼中註釋掉的兩行,用於跟蹤除錯程式的分步執行情況
        //int t = res;
        if (n % 10 == 0) res += (n / 10 - 1) * tmp + (tmp_n % tmp + 1);    //等於,剔除了大於0的情況
        else  res += (n / 10) * tmp;                                       //大於,剔除了大於0的情況

        n /= 10;                                                           //進入前一位
        tmp *= 10;                                                         //計算需要乘幾個10的變數
        //cout << res - t << endl;
    }
    return res;
}


int main() {
    int a, b;
    while (cin >> a >> b, a) {
        if (a > b) swap(a, b);
        
        cout << count_0(b) - count_0(a - 1) << ' ';
        
        for (int i = 1; i <= 9; i++)
            cout << count_x(b, i) - count_x(a - 1, i) << ' ';
        cout << endl;
    }
    return 0;
}