【藍橋杯】歷屆試題 波動數列(動態規劃)

歷屆試題 波動數列

問題描述
觀察這個數列:
1 3 0 2 -1 1 -2 …
這個數列中後一項總是比前一項增加2或者減少3。
棟棟對這種數列很好奇,他想知道長度爲 n 和爲 s 而且後一項總是比前一項增加a或者減少b的整數數列可能有多少種呢?

輸入格式
輸入的第一行包含四個整數 n s a b,含義如前面說述。

輸出格式
輸出一行,包含一個整數,表示滿足條件的方案數。由於這個數很大,請輸出方案數除以100000007的餘數。

樣例輸入
4 10 2 3

樣例輸出
2

樣例說明
這兩個數列分別是2 4 1 3和7 4 1 -2。

數據規模和約定
對於10%的數據,1<=n<=5,0<=s<=5,1<=a,b<=5;
對於30%的數據,1<=n<=30,0<=s<=30,1<=a,b<=30;
對於50%的數據,1<=n<=50,0<=s<=50,1<=a,b<=50;
對於70%的數據,1<=n<=100,0<=s<=500,1<=a, b<=50;
對於100%的數據,1<=n<=1000,-1,000,000,000<=s<=1,000,000,000,1<=a, b<=1,000,000。



—— 分割線之小試牛刀 ——



分析:
讀完這道題之後的第一反應是,可不可以搜索?比如可以假設某個序列的首項爲x,那麼採用搜索算法求解該題的過程大致如下:
採用搜索算法時的遞歸樹
可以看出,搜索算法的時間複雜度在O(2n)級別,而最大的數據範圍:n可以取到1000,超時無疑
因此我們不得不放其這個思路

實際上,有些看起來像是搜索算法的題目,大多都會因爲數據範圍而超時。
此時,通常都會用記憶化搜索或者DP算法來替之解決,本題也正是如此。
對於這個序列而言,我們發現其中每個數的後一項要麼比其大a,要麼比其小b
即:Xi+1 - Xi = set,而set = { a,-b }
於是我們可以把a和b封裝起來,將其視爲一個整體set,則此時的序列如下:
序列
該序列是一個首項爲x,公差爲set的序列
在該序列中,集合set出現的次數爲1+2+…+(n-1)=(n-1)*(1+(n-1))/2=(n-1)*n/2,我們令num=(n-1)*n/2
則可以得到這個序列的總和Sn=n*x+num*set
由於數num表示着數字a和數字b出現的次數,因此若將其分別設爲numa和numb,則有num=numa+numb
那麼序列總和Sn=n*x+numa*a-numb*b=n*x+numa*a-(num-numa)*b
在上述式子中,只有兩個未知數:“首項x”和“數字a出現的次數numa”
由於題目明確地說明了該序列是一個整數序列,而數字a出現的次數顯然也是整數(並且numa∈[0,num])
那麼我們就可以通過枚舉數字a出現的次數,來求出對應的x。如果求出的x是一個整數,那麼就表示找到了一個序列符合題目要求(這個序列的首項爲x,之後的所有項中共有numa個"+a",以及(num-numa)個"-b")。我們可以把這些情況都統計下來,最終輸出總的個數即可

等一下!有個問題!
如果找到了一個numa使得最終算出的x爲整數,那麼這個numa只是指明瞭當前序列中有numa個”+a”,以及(num-numa)個”-b”,但是這numa個”+a”的排列方式可能有很多種(對應”-b”的排列方式也就有多種)!比如在某個情況下算得numa=7,numb=3,則對於numa而言有1+2+4=7,也有3+4=7這兩種組合方式(對應的,對numb就有3=3,1+2=3這兩種組合方式),表現在序列中的情況如下:
情況①:7個"+a"的組合爲4+2+1(對應3個"-b"爲3)
情況①

情況②:7個"+a"的組合爲4+3(對應3個"-b"爲2+1)
情況②
換言之,對於某次求出的符合要求的numa值而言,並不意味着只有這一種情況,因爲其可能含有多種組合。比如上面的7可以由1+2+4組成,還可以用3+4組成;再比如10可以由1+2+3+4組成,還可以用1+4+5和2+3+5組成。現在我們需要解決的問題就是,怎麼得到一個數的組合方案呢?
這裏我們定義一個名詞:子集和。用於表示集合{ 1,2,3,……,n }的所有子集的元素之和的個數。那麼容易得到子集和個數與原集合中元素個數的關係爲:n(n+1)/2=4*5/2=10。比如對於集合{ 1,2,3,4 }而言,其有4個元素,雖然其子集個數爲24=16,但是其子集和個數卻只有10。其子集和的具體情況如下所示:

集合{ 1,2,3,4 }的子集和
公式n(n+1)/2僅僅是給了我們對於某個元素數量爲n的集合,其能產生子集和的個數,但是卻並沒有告訴我們對於其中某一個子集和,其具體的組合方案有多少。而我們在求解本題的時候,是需要這個信息的,因此我們希望能通過打表的方式將上面的這個dp數組求出,從而方便後面得到numa時能直接調用,得出具體的組合方案數。
那要怎麼打表呢?如果細心點你會發現一件事:“從 1,2……,n-1 任意取數,能夠組成numa的方案數”和01揹包問題的題設如出一轍,因此我們可以通過DP來求解上面的dp[numa]數組

到此,整道題的總體思路已經理清,最後總結一下解題過程:

  1. 錄入n,s,a,b的值;
  2. 利用DP得到區間[1, n(n+1)/2]之間的各個子集和的具體組合方案,並將其保存進dp數組中;
  3. 利用等式Sn=n*x+numa*a-(num-numa)*b,通過一層循環來枚舉numa,將滿足要求的numa的dp[numa]值取出並累加統計;
  4. 輸出統計結果


—— 分割線之尋行數墨 ——



接下來針對每個過程,我們分析具體的算法
首先是求dp數組,我們設dp[i][j]表示從集合{ 1,2,3,……,n }中的前i個數進行選擇(假設該集合按從小到大進行了升序排序),使得其和爲j,那麼動態轉移時的選擇有以下兩種:
① 當j<i時,此時的元素都不能選,因爲在上一次已經將情況枚舉完了(最小閉包),故現在不需要再選。即,狀態轉移方程爲:dp[ i ][ j ] = dp[ i-1 ][ j ](問題:那對於dp[i][0]而言,其方程是?)
實際上我們知道,當numa=0時(則numb=num),整個序列就只有一種組合情況,所以在實際進行枚舉時,我們都是直接將所有的dp[i][0]=1,這一點很關鍵!對於上面的動態轉移方程,我們還是舉個例子來說明:
比如對於集合{ 1,2,3 },其子集和表如下:
集合{ 1,2,3 }的子集和
對比集合{ 1,2,3,4 }的子集和表,你會發現,當num<4時,集合{ 1,2,3,4 }的子集和表與集合{ 1,2,3 }的子集和表一致,即dp[4][1]=dp[3][1]=1,dp[4][2]=dp[3][2]=1,dp[4][3]=dp[3][3]=2。這是因爲,當集合由{ 1,2,3 }再加上一個元素4時,前面j∈[1,3]的組合方式不可能涉及到新加進來的這個元素4(4 > 3);同樣地,當集合再由{ 1,2,3,4 }加上一個元素5時,前面j∈[1,4]的組合方式也不可能涉及到新加入的元素5(5 > 4),因此也不會發生任何改變……

② 當j>=i時,此時第j個元素等於上一次選了的再加上現在由於新加了元素而導致增加的
即,狀態轉移方程爲:dp[ i ][ j ] = dp[ i-1 ][ j ] + dp[ i-1 ][ j-i ]
實際上,dp[i-1][j-1]和01揹包中的dp[i][j-w[i]]是一樣的,就是利用前面已經算出的值
比如對於元素4,在集合{ 1,2,3 }的子集和表中,我們得到dp[3][4]=1,即在集合{ 1,2,3 }中,能夠組合出4的方案只有一種{ {1,3} }。現在集合由{ 1,2,3 }新添了一個元素4,那麼此時集合中對於4的組合方案就多出了一個{4},於是現在dp[4][4]=dp[3][4]+dp[3][0]=1+1=2(這裏的dp[3][0]就是利用了前面已經算出的dp[3][0],因爲4-4=0,即當前j=4時,在集合{ 1,2,3 }中多加了一個元素4的前提下,我們只需要關心有多少種組合出0的方案,亦即dp[3][0]的取值即可,這裏再次體現了將dp[i][0]=1的作用);
同樣地,在集和{ 1,2,3 }的子集和表中,得到dp[3][5]=1,即在集合{ 1,2,3 }中,能夠組合出5的方案只有一種{ {2,3} }。現在集合由{ 1,2,3 }新添了一個元素4,那麼此時集合中對於5的組合方案就多出了一個{1,4},於是現在dp[4][5]=dp[3][4]+dp[3][1]=1+1=2(這裏的dp[3][1]就是利用了前面已經算出的dp[3][1],因爲5-4=1,即當前j=5時,在集合{ 1,2,3 }中多加了一個元素4的前提下,我們只需要關心有多少種組合出1的方案,亦即dp[3][1]的取值即可)
……
我們知道num=(n-1)*n/2,因此在n∈[1,1000]的數據範圍下,num的數量級是106,因此我們設置的dp數組的範圍大致爲:M=1000000,N=1000,dp[N][M],顯然我們需要對空間進行優化
優化方案和01揹包一樣,通過利用滾動數組,並且也是一樣的需要採用自右向左更新的方向
於是可以得出這一過程的代碼爲:

const int MOD=100000007;
const int N=1000010;
int dp[N];
void create(int n)
{
	dp[0]=1;
	for(int i=1;i<n;i++)
		for(int j=i*(i+1)/2;j>=i;j--)
			dp[j]=(dp[j]+dp[j-i])%MOD;	//特別注意:這裏也需要取餘 
}

然後,我們就可以利用公式:Sn=n*x+numa*a-(num-numa)*b
通過一層循環來枚舉numa的值(numa∈[0,num],而num=(n-1)*n/2,n∈[1,1000]),也就是說此時枚舉的最大範圍在106級別,在一層循環下是完全可以接受的。如果對於某個枚舉值,通過上述公式計算出的x爲一個整數,那麼我們就可以將其視爲一個合法的numa值,此時我們直接取出對應的dp[numa]值即可,這一過程的代碼如下:

int num=(n-1)*n/2;
int ans=0;
for(int i=0;i<=num;i++){
	long long temp=s-numa*a+(num-numa)*b;
	if(temp%n==0)
		ans=(ans+dp[i])%MOD;
}

這裏還能再優化一下,我們注意到對於temp而言:temp=s-numa*a+(num-numa)*b
在每次枚舉求temp時我們都需要求2次乘法,2次減法,1次加法
實際上,我們對公式Sn=n*x+numa*a-(num-numa)*b進行變換可以得到一個更簡單的式子:
①打開括號,得到: Sn = n*x+numa*a+numa*b-num*b
②方程兩邊同時加num*b,得到:Sn+num*b = n*x+numa*(a+b)
③移項,得到:n*x = (Sn+num*b) - numa*(a+b)
觀察上式,我們發現其中(Sn+num*b)和(a+b)都是不依賴numa的值,是可以提前算出的
於是此時,我們就可以將這一過程的代碼優化爲:

int num=(n-1)*n/2;
long long optnum=a+b;
s += num*b;
int ans=0;
for(int i=0;i<=num;i++){
	long long temp=s-i*optnum;
	if(temp%n==0)
		ans=(ans+dp[i])%MOD;
}

如此進行優化後,在每次枚舉求temp時我們就只需要求1次乘法和1次減法即可



—— 分割線之一蹴而就 ——



下面直接給出求解本題的完整代碼:

#include<iostream>
using namespace std;

const int MOD=100000007;
const int N=1000010;
int dp[N];
long long n,a,b,s;						//特別注意:b,s必須設爲long long型 
void create(int n)
{
	dp[0]=1;
	for(int i=1;i<n;i++)
		for(int j=i*(i+1)/2;j>=i;j--)
			dp[j]=(dp[j]+dp[j-i])%MOD;	//特別注意:這裏也需要取餘 
}

int main()
{
	cin>>n>>s>>a>>b;
	int num=(n-1)*n/2;
	long long optnum=a+b;
	s += num*b;
	create(n);
	int ans=0;
	for(int i=0;i<=num;i++){
		long long temp=s-i*optnum;
		if(temp%n==0)
			ans=(ans+dp[i])%MOD;
	}
	cout<<ans<<endl;
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章