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;
}