【NOI】9272 偶數個三
題目
鏈接:bajdcc/ACM
描述
在所有的N位數中,有多少個數中有偶數個數字3?結果模12345。(1<=N<=10000)
樣例輸入
2
樣例輸出
73
方法一:窮舉
評價:最簡單又是效率最低的方法。
缺陷:N很大時,用來遍歷的i用long long就放不下了,gg。但是首先,你要耐心等到long long溢出。耗時就不算了,太慢。
#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就行了。
現在是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種可能,乘法原理。
整理下:
- f(1)=8,g(1)=1
- f(n)=g(n-1)+9*f(n-1)
- g(n)=f(n-1)+9*g(n-1)
書寫代碼:
#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)。而公式法本質就是求冪,而求冪也存在依賴關系,且子問題都相同,沒必要分割。窮舉法倒可以保證子任務的獨立性,不過計算量還是很大,當且僅當沒有其他好方法的時候用。
公式法推導很復雜,耗時間,因此,用動態規劃法是絕佳的。
【NOI】9272 偶數個三