連接:bajdcc/ACMios
在全部的N位數中,有多少個數中有偶數個數字3?結果模12345。(1<=N<=10000)
2
73
評價:最簡單又是效率最低的方法。git
缺陷:N很大時,用來遍歷的i用long long就放不下了,gg。可是首先,你要耐心等到long long溢出。耗時就不算了,太慢。github
#include <iostream> using namespace std; #define LL long long #define NUM 3 int main() { LL m,n,i,j,t,count; cin>>n; for(i=0,m=1;i<n;i++) m*=10; // 求N位數上界 for(i=m/10,count=0;i<m;i++) { // 從10..000 ~ 99..999 for (j=0,t=i;t;t/=10) // 取每一位 if (t%10==NUM) j++; // 若是是NUM計數j加一 if (j%2==0) { count++; // 偶數個NUM計數count加一 count%=12345; } } cout<<count; return 0; }
窮舉法有着天生的缺陷:遍歷的i範圍有限,除非用高精度才能避免。多線程
進一步思考,將題目改成「有多少個數中有偶數個4」,結果記爲N4。那麼我想N4應該跟N3是同樣的,對稱性嘛。證實:對應每一個數中有偶數個3的數,我均可以找到相應的數,只要將原數中的3跟4對調下便可,好比133242,調下變144232,歐了。固然了,想到這個結論然並卵,咱們目前只證得N1~N9是相等的,理所應當,假如知道了N1~N9的和,那隻要平均下就能得出結果。然而仍是手足無措,那就用遞推來想一想。函數
假如目前有數6XXXXX,以6開頭的符合條件的數有多少呢?好吧,無視6,得出f(6XXXXX)=f(XXXXX),由於6根本不必算進去嘛,歐了!咱們發現一個重要結論:有些子問題是重複的!因此無腦窮舉法太慢的緣由就是計算了重複的子問題。好吧,如今來找找哪些是重複的子問題。優化
設下函數f(n)和g(n),n是位數,f表示有偶數個3的總數,g表示有奇數個3的總數。從一位數開始,0不算,f(1)=8,g(1)=1,只要看有沒有3就好了。spa
如今是N位數XY,想想,若是Y有奇數個3同時X有奇數個3,那麼f函數歐了;若是Y有偶數個3同時X有偶數個3,那麼f函數歐了。若是Y有奇數個3同時X有偶數個3,那麼g函數歐了;若是Y有偶數個3同時X有奇數個3,那麼g函數歐了。最後,咱們將X定爲最高一位,Y定爲後N-1位,用來遞推,這樣的話X就不能是0,這就決定了f(1)=8而不是9,說到底,0仍是要考慮到,不過是做爲後n-1位了,體如今下面推導式右邊的乘數9上。線程
有點思路了,如今把f和g的推導式寫出來。邊界:f(1)=8,g(1)=1。若是第n位是3,那麼加上g(n-1);若是第n位不是3,那麼加上9*f(n-1),由於不是3的話有9種可能,乘法原理。3d
整理下:code
書寫代碼:
#include <iostream> using namespace std; int g(int n); int f(int n) { return n==1?8:(g(n-1)+9*f(n-1))%12345; } int g(int n) { return n==1?1:(f(n-1)+9*g(n-1))%12345; } int main() { int n; cin>>n; cout<<f(n); return 0; }
運行速度明顯快多了。
方法二仍是須要改進,f和g函數有重複的遞歸調用,固然能夠用記憶化去搞定。這裏既然有了遞推式,狀態轉移方程就呼之欲出了,方法二中已寫出。
#include <iostream> using namespace std; int f[10002][2];//f[][0]=偶數個3,f[][1]=奇數個3 int main() { int n; cin>>n; f[1][0]=8,f[1][1]=1; for (int i=2;i<=n;i++) { f[i][0]=(9*f[i-1][0]+f[i-1][1])%12345; f[i][1]=(f[i-1][0]+9*f[i-1][1])%12345; } cout<<f[n][0]; return 0; }
略。
沒想到吧,這也能用公式作!Fibonacci數列也是有通項公式的,可是要怎麼求呢?(固然參照書上的)
書寫代碼:
#include <iostream> using namespace std; #define MOD 12345 // 快速冪取模 int fast(int a, int N, int mod) { long long r = 1, aa=a; while(N) { //取N的二進制位,是一則乘上相應冪並求餘 if (N & 1) r = (r * aa) % mod; N >>= 1; aa = (aa * aa) % mod; } return (int)r; } // 快速冪取模(2爲底) int fast2(int N, int mod) { static long long a=(1LL<<62)%mod; int s=N%62,t=N/62;// 2^N=2^s*a^t int r = (1LL<<s) % mod; if (t>0) { r *= fast(a,t,mod);// 2^s*a^t % mod r %= mod; } return (int)r; } int main() { int n; cin>>n; //化簡: // an=1/2*{7*2^(3n-3)+9*2^(n-1)*5^(n-1)} // an=2^(n-2)*{9*5^(n-1)+7*2^(2n-2)} int a=fast2(n-2,MOD); int b=a<<1; int ans=a*(9*fast(5,n-1,MOD)+7*((b*b)%MOD)); ans%=MOD; cout<<ans<<endl; return 0; }
能夠看出,爲了優化,代碼顯得不怎麼美觀,若是題目不要求精確值的話,那麼用浮點數以及pow我想應該可讓速度再快一點。
比較而言,其實動態規劃法是最簡潔且高效的。
一個題目,多種方法,其實從本質而言,以計算機的思惟作,天然是DP,以數學家的思惟作,就是推導通項公式。然而,通項公式中有冪,讓計算機作本質上也不高效。
從多線程優化的角度來看,DP法的本質是一層層遞推的計算,後者依賴前者,計算並不獨立,不能分解成小任務,最快就是O(n)。而公式法本質就是求冪,而求冪也存在依賴關係,且子問題都相同,不必分割。窮舉法倒能夠保證子任務的獨立性,不過計算量仍是很大,當且僅當沒有其餘好方法的時候用。
公式法推導很複雜,耗時間,所以,用動態規劃法是絕佳的。