【问题描述】
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分的样子。方案数考虑高效算法,使用递推的思想,先列出状态函数以及状态转移方程:
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])<
很好,这一下时间复杂度大大下降,但是仍然没有达到最优,还可以将三重循环减少为两重循环!从dp_2可以看出我们在填第i行第j列时,使用的是第i-1行、从第0列到第j列的数之和。可以由此想到前缀和,将dp_2中的t从第三重循环调整至第二重循环来计算前缀和,笔者不赘述,直接在【Cpp代码】中给出dp函数和完整程序,时间复杂度为:
最后提一点,这一个题实质上是斯特林数,的运用,关于斯特林数,详见百度百科 斯特林数
【Cpp代码】
#include
#include
#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])<int main()
{
scanf("%d%d%d",&n,&A,&B);
dp();
return 0;
}