题目描述
把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法。
输入
第一行是测试数据的数目t(0≤t≤20)。以下每行均包含二个整数M和N,以空格分开。1≤M,N≤400。
输出
对输入的每组数据M和N,用一行输出相应的K。
样例输入
1
7 3
样例输出
8
算法分析
样例分析
苹果和盘子都是不加区分的,当有 7个苹果3个盘子时,总有8种不同的放置方案
//一个盘子
7
//两个盘子
6+1
2+5
3+4
//三个盘子
1+1+5
1+2+4
1+3+3
2+2+3
讨论初始项
设
F
(
i
,
j
)
F(i,j)
F(i,j)为将
i
i
i个苹果放到
j
j
j个盘子的方案数.
当只有一个苹果或只有一个盘子时,只有一种方案数
F(i,j)=1 (i=1 or j=1 )
F(i,j)=1 (i=0 )//只有0个苹果时,方案数也为1,具体看下面情况2的分析
分类讨论可能出现的情况
情况1:苹果比盘子少
情况2:苹果大于等于盘子的数量
设
F
(
i
,
j
)
F(i,j)
F(i,j)为将
i
i
i个苹果放到
j
j
j个盘子里的方案总数
对于情况1:假如求3个苹果放入5个盘子的方案数,不管怎么放,至少会剩下2个空盘子,因为苹果和盘子是不加以区分的,剩下的2个空盘子也是不加以区分的,上述的问题就变成了求将3个苹果放个3个盘子的方案数,所以可得:
F(i,j)=F(i,i) (i<j)
讨论情况2:苹果大于等于盘子的数量
根据样例分析我们可以得知,在求三个盘子的方案的时候,其中又包含了两个盘子的方案数,所以可以通过某种方式,找出“嵌套”在其中的子问题(问题类型相同但规划更小)。
如
3+4和1+2+4
2+5和1+1+5
对于情况2—拆分问题
在拆分问题时,我们要保证拆分后的各个子问题之间是互斥(递推中),并且各个子问题的方案总和刚好是当前问题方案数,如果各个子问题之间有相交的部分,在计算方案数量时则需要把相交的部分的方案数减去。
当前问题可以拆分成两个子问题:
子问题1:把当前所有盘子的填满
子问题2:还剩一个盘子没有填满
设 F ( i , j ) F(i,j) F(i,j)为将 i i i个苹果放到 j j j个盘子的方案数,
对于子问题1
所有的盘子都不为空,每个盘子至少放一个苹果,相当于把 ( i − j ) (i-j) (i−j)个盘子放到 j j j个盘子的方案数量,即 F ( i − j , j ) F(i-j,j) F(i−j,j)。
用比如求7个苹果放3个盘子的且盘子不为空方案数,
第一步:每个盘子都放一个苹果,方案数量为1(已满足每个盘子不为空的条件)
第二步:将剩下的4个苹果放入3个盘子里面,方案数为a(这时盘子可以为空)
根据乘法原理:方案数为1*a=a,
求7个苹果放3个盘子的且盘子不为空方案数相当于把4个苹果放3个盘子的方案数。
当 i i i为0时,也是要保存初始项的,例 F ( 2 , 2 ) = F ( 0 , 2 ) + F ( 2 , 1 ) = 1 + 1 + 2 F(2,2)=F(0,2)+F(2,1)=1+1+2 F(2,2)=F(0,2)+F(2,1)=1+1+2.
对于子问题2
有一个盘子为空,问题变为求将i个苹果放到(j-1)个盘子里面的方案数,即 F ( i , j − 1 ) F(i,j-1) F(i,j−1)。
显然,子问题1和子问题2是互斥的,并且子问题1加上子问题2的方案数为原问题的总方案数。递推公式为:
F(i,j)=F(i-j,j)+F(i,j-1) (i>=j)
注: F ( i , j − 1 ) F(i,j-1) F(i,j−1)表示空一个盘子的方案数,如果继续 F ( i , j − 1 ) F(i,j-1) F(i,j−1)这个问题拆分,拆分后的子问题包含了空两个盘子的方案数,也就是 F ( i , j − 1 ) F(i,j-1) F(i,j−1)包含了 F ( i , j − 2 ) F(i,j-2) F(i,j−2)。
汇总
F(i,j)=F(i,i) i<j
F(i,j)=1 i=1 or j=1 or i=0
F(i,j)=F(i,i) i<j
F(i,j)=F(i-j,j)+F(i,j-1) i≥j
代码实现
代码1:递归实现(33分)
#include <iostream>
using namespace std;
long long fun(int n,int m){
if(n==1 || n==0||m==1) return 1;
else if(n<m) return fun(n,n);
else return fun(n-m,m)+fun(n,m-1);
}
int main(){
int n,m,t;
cin>>t;
while(t--){
cin>>n>>m;
cout<<fun(n,m)<<endl;
}
return 0;
}
代码2:递归实现+优化(100分)
上面的代码1有很多子问题重复求解,我们可以用一个二维数组保存子问题的解,如果当前的子问题已经解求过了,则直接返回,这种方法称为“记忆化搜索”。
需要注意的是,每次计算数组的值都需要重置为0,以便下一次使用,初始化可以使用 memset(f,0,sizeof(f));
语句。
#include <iostream>
using namespace std;
const int MAXN=401;
long long f[MAXN][MAXN]={0};
long long fun(int n,int m){
if(n==1 || n==0||m==1) return 1;
if(f[n][m]>0) return f[n][m];//如果当前子问题已经有解,直接返回
else if(n<m) return f[n][m]=fun(n,n);
else return f[n][m]=fun(n-m,m)+fun(n,m-1);
}
int main(){
int n,m,t;
cin>>t;
while(t--){
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
f[i][j]=0;
cin>>n>>m;
cout<<fun(n,m)<<endl;
}
return 0;
}
代码3:递推(100分)
使用迭代(递推)的方式求解,苹果数作为外层循环(从0开始循环),盘子数作为内层循环,这样就能保证每次求解时其依赖的子问题已经有解了。
#include <iostream>
#include <cstring>
using namespace std;
const int MAXN=401;
long long f[MAXN][MAXN]={0};
int main(){
int n,m,t;
cin>>t;
while(t--){
memset(f,0,sizeof(f));//将f数值的值全部初始化为0;
cin>>n>>m;
for(int i=0;i<=n;i++)
for(int j=1;j<=m;j++)
if(i==1||i==0||j==1) f[i][j]=1;
else if(i<j) f[i][j]=f[i][i];
else f[i][j]=f[i-j][j]+f[i][j-1];
cout<<f[n][m]<<endl;
}
return 0;
}