【NOI】9272 偶數個三

題目

連接: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

  • 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數列也是有通項公式的,可是要怎麼求呢?(固然參照書上的)

v2-f95a8addc9a192fb3a613e7daa03d402_hd

書寫代碼:

#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)。而公式法本質就是求冪,而求冪也存在依賴關係,且子問題都相同,不必分割。窮舉法倒能夠保證子任務的獨立性,不過計算量仍是很大,當且僅當沒有其餘好方法的時候用。

公式法推導很複雜,耗時間,所以,用動態規劃法是絕佳的。

相關文章
相關標籤/搜索