1. 程式人生 > >【NOI】9272 偶數個三

【NOI】9272 偶數個三

pla 數字 原理 OS 動態 sin 多少 xxxx 遍歷

題目

鏈接: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 偶數個三