【問題描述】
n 個盒子排成一行(編號爲1..n)。你有A個紅球和B個藍球。球除了顏色沒有任何區別。你可以將球放進盒子。一個盒子可以同時放進兩種球,也可以只放一種,也可以空着。球不必全部放入盒子中。編程計算有多少种放置球的方法。
【輸入格式】
一行,n,A,B,用空格分開。
【輸出格式】
一行,輸出放置方案總數。
【輸入樣例】
2 1 1
【輸出樣例】
9
【樣例解釋】
用一對括號表示一個盒子,R表示紅色,B表示藍色,有如下9種方案:
( ), ( )
(R ), ( )
(B ), ( )
(RB), ( )
(R ), (B )
(B ), (R )
( ), (R )
( ), (B )
( ), (RB)
【數據範圍】
40% 數據滿足:n<=10,A+B<=10
100%的數據滿足:1<=n<=20 ,0<=A<=15 ,0<=B<=15
最後的答案不會超過 -1。
【來源】
NOIP提高組
Vijos1060原題傳送矩陣
重慶一中題庫原題傳送矩陣
【思路梳理】
考試的時候因爲邊界問題糾結了很久=_+,想了一陣居然沒有想通所以交了對拍程序拿了60分走人,現在回想起來還是很虧,給自己在這裏留一個教訓。
言歸正傳,關於這個題首先想到的肯定是回溯算法暴力枚舉,run(i,j,k)=目前在考慮第i個盒子,前i-1個盒子中已經放入了j個紅球,k個藍球,那麼依次枚舉在這個盒子中放入x個紅球,y個藍球(0≤x≤A-j,0≤y≤B-k)的可行方案數:
unsigned long long solve(int i,int j,int k)//考慮第i個盒子,目前已總共放入j個紅球,k個籃球
{
if(i>n) return 1;//所有盒子都考慮完了,是一種可行方案
long long cnt=0;//將各個可能的方案數加在一起求和
for(int x=0;x<=A-i;x++)
for(int y=0;y<=B-j;y++)
cnt+=solve(i+1,i+x,j+y);
return cnt;
}
觀察數據範圍,因爲回溯的時間複雜度是O(AB^N),那麼也就是40分的樣子。方案數考慮高效算法,使用遞推的思想,先列出狀態函數以及狀態轉移方程:
- f(i,j,k)=在前i個盒子中放入不超過j個紅球,不超過k個藍球的方案數。
- 狀態轉移方程:
- 邊界分析:f(0,i,j)=1,不放總會是一種方案,於是可以寫出如下的dp函數,時間複雜度爲O():
void dp_1()
{
//f(i,j,k)=f(i-1,j-m,k)+f(i-1,j,k-n) (0<=m<=j,0<=n<=k)
for(int i=0;i<=A;i++)
for(int j=0;j<=B;j++) d[0][i][j]=1;
for(int i=1;i<=n;i++)
for(int j=A;j>=0;j--)
for(int k=B;k>=0;k--)
{
unsigned long long t=0;
for(int m=0;m<=j;m++)//第i個盒子裝m個紅球
for(int n=0;n<=k;n++)//第i個盒子裝n個藍球
t+=d[i-1][j-m][k-n];
d[i][j][k]=t;
}
}
觀察後可以發現,可以將t+=d[j-m][k-n]改爲t+=d[m][n](區別在於正着加和反着加)
如果你不是一個有志向的OIER,請自動忽略以下內容!
如上函數,五重循環雖然能夠完美解答此題,但是依然不能讓我們滿意:當n達到50,0≤A,B≤30左右時,dp_1是無法在1s內解答的;或者是有多種球,例如說有四種顏色的球、五種顏色的球……顯然我們還需要考慮一些更高效的算法:
這裏所謂的球除了顏色以外沒有任何區別,那麼問題可以分步完成,每一次都考慮一種顏色的球:對於此題的紅藍兩種球,先考慮放紅球再考慮放藍球,那麼最後的總方案數就等於,即在n個盒子中分步放入A個紅球和B個藍球,然後運用乘法原理乘起來。這樣一來就可以三維數組轉二維數組,循環減少爲三重,時間複雜度下降爲:
int limit=max(A,B);
void dp_2()
{
//狀態函數f(i,j)=在前i個盒子中放入不多於j個(紅/藍)球的方案數
//狀態轉移方程f(i,j)=f(i-1,j-m)(0<=m<=j)
for(int i=0;i<=limit;i++) d[0][i]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<=limit;j++)
{
unsigned long long t=0;
for(int m=0;m<=j;m++)//第i個盒子裝m個球
t+=d[i-1][j-m];
//同樣可以使用:t+=d[i-1][m]
d[i][j]=t;
}
cout<<(d[n][A]*d[n][B])<<endl;
}
很好,這一下時間複雜度大大下降,但是仍然沒有達到最優,還可以將三重循環減少爲兩重循環!從dp_2可以看出我們在填第i行第j列時,使用的是第i-1行、從第0列到第j列的數之和。可以由此想到前綴和,將dp_2中的t從第三重循環調整至第二重循環來計算前綴和,筆者不贅述,直接在【Cpp代碼】中給出dp函數和完整程序,時間複雜度爲:
最後提一點,這一個題實質上是斯特林數,的運用,關於斯特林數,詳見百度百科 斯特林數
【Cpp代碼】
#include<cstdio>
#include<iostream>
#define maxn 25
using namespace std;
int n,A,B;
unsigned long long d[maxn][maxn];
void dp_2()
{
int limit=max(A,B);
//狀態函數f(i,j)=在前i個盒子中放入不多於j個(紅/藍)球的方案數
//狀態轉移方程f(i,j)=f(i-1,j-m)(0<=m<=j)
for(int i=0;i<=limit;i++) d[0][i]=1;
for(int i=1;i<=n;i++)
{
unsigned long long t=0;
for(int j=0;j<=limit;j++)//第i個盒子裝j個球
{
t+=d[i-1][j];
d[i][j]=t;
}
}
cout<<(d[n][A]*d[n][B])<<endl;
}
int main()
{
scanf("%d%d%d",&n,&A,&B);
dp();
return 0;
}