题目描述
选修程序设计和算法课程的学生人数为 n,任课老师设置了 m 道练习题目(其中:1 <= m,n <= 100),假定每道题的难度和知识点都是一样的,要求选修本课程的同学利用在线测评系统完成一道题目,同时还要求每道题目至少要被做过一次,问有多少种做题方案?当然也有可能没有一种方案存在。
输入描述
多组输入,每一行输入两个用一个空格分开的整数 n 和 m
输出描述
占一行,对应输入的每组数,输出对应的方案数
样例
4 2
15 12
7
106470
解题思路
粗略描述一下这道屑题
本题的表达比较反人类,它是说把n个不同的物件放入m个相同的篮子,并且要求篮子不能为空。
(关键句是“每道题难度和知识点都是一样的”-_-|||)
从高中正常毕业的学生的想法
(如果你已经通过百度了解“第二类斯特林公式”,请跳过这个屑过程)
利用高中组合数知识解说一下样例:
样例一:
4个物品放入2个篮子,可以3+1分或2+2分,因此我们的answer为:
样例二:
15个物品放入12个篮子有三种分法,
4+1+1+1+1+1+1+1+1+1+1+1;
3+2+1+1+1+1+1+1+1+1+1+1;
2+2+2+1+1+1+1+1+1+1+1+1;
因此answer’为:
(看不懂为什么除以2!或3!的可以退学了 [x)
那么问题来了,实现这样的思考过程十分麻烦,而且计算机执行起来效率也不高,不符合“计算机的思维”。
符合计算机思维的思路
我们知道组合数是可以递推出来的,实际上这道屑题也存在这样的状态转移。
对于n个物品和m个篮子,可以这么考虑答案组成;
1.来自n-1个物品m-1个篮子。
相当于在这种状态下加上一个装有第n个物品的篮子;
2.来自n-1个物品m个篮子。
添加第n个物品,这个物品有m种选择,它可以放在这m个篮子里任意一个中
(思考一下m种方案是否和m个相同的篮子矛盾?为什么?)
由此得出一个简单的递推式:
如果m=1或m=n时,answer=1;
如果n>m时,answer=0;
喜闻乐见的AC代码
这样就能AC了?
#include<stdio.h>
#include<string.h>
#define min(x,y) ((x)<(y)?(x):(y))
int main()
{
int i,j,n,m,k,flag;
int ans[121][101];
while(~scanf("%d %d",&n,&m))
{
memset(ans,0,sizeof(ans));
for(i=1;i<=n;++i)
{
ans[1][1]=1;
for(j=min(i-1,m);j>=1&&n-i>=m-j;--j)
{
for(k=1;k<=120;++k)
ans[k][j]=ans[k][j-1]+j*ans[k][j];
for(k=1;k<=120;++k)
if(ans[k][j]>=10)
{
ans[k+1][j]+=ans[k][j]/10;
ans[k][j]%=10;
}
}
ans[1][i]=1;
}
flag=120;
for(k=120;k>=0;--k)
if(ans[k][m]!=0)
{
flag=k;
break;
}
if(flag==120)flag=1;
for(k=flag;k>=1;--k)
printf("%d",ans[k][m]);
printf("\n");
}
return 0;
}
是不是感觉代码和说好的思路有点出入。
补充一:高精度
——补充一 高精度——
事实证明,我们输入100 50时,数据是很大的,如果不用高精度,妥妥的超出范围。
——高精度的实质就是用数组暴力模拟竖式计算
比如我们计算两个100位数a和b的乘积,可以用字符串读取a和b,然后把用b数组中的最后一位乘以a中的每一位数保存入数组ans的对应位置,并做好进位工作。依次循环,直到b的第一位数乘完数组a中每一个元素。
以下几种高精度参考:
——高精度加法:
#include<stdio.h>
#include<string.h>
int ans[510];//保存答案
char a[510],b[510];//读入数据
int a1[510],b1[510];//转为整型数组保存
int main()
{
int i,flag,l1,l2,j;
while(~scanf("%s %s",a,b))
{
memset(ans,0,sizeof(ans));
memset(a1,0,sizeof(a1));
memset(b1,0,sizeof(b1));
//*************************//
j=0;
for(i=strlen(a)-1;i>=0;--i)
a1[j++]=a[i]-'0';
j=0; //分别把字符串a和字符串b中的每一个元素逆序装入整型数组a1和b1中
for(i=strlen(b)-1;i>=0;--i)
b1[j++]=b[i]-'0';
//*************************//
for(i=0;i<510;++i)
{
ans[i]+=a1[i]+b1[i];
if(ans[i]>=10)
{ //逐位做加法并进位
ans[i+1]+=ans[i]/10;
ans[i]%=10;
}
}
//*************************//
flag=509;
for(i=509;i>=0;--i)
if(ans[i]!=0)
{
flag=i; //找到最高位元素的位置,记录以用于接下来输出答案
break;
}
//************************//
if(flag==509)
{
printf("0\n");
}
else
{
for(i=flag;i>=0;--i)
printf("%d",ans[i]);
printf("\n");
}
}
return 0;
}
——高精度乘法:
#include<stdio.h>
#include<string.h>
char a[101],b[101];//读入数据
long long ans[302];//答案数据
int main()
{
int i,j,la,lb,k,flag;
while(~scanf("%s\n%s",&a,&b))
{
la=strlen(a);
lb=strlen(b);
memset(ans,0,sizeof(ans));
//************************************************//
for(i=la-1;i>=0;--i)
{
for(j=lb-1;j>=0;--j)
ans[la-1-i+lb-j-1]+=(a[i]-'0')*(b[j]-'0');
for(k=la-i-1;k<=la-i-1+lb-1;++k)
if(ans[k]>=10) //倒过来从最小的位数开始乘,加到对应位置上,并且进行一次进位
{ //实际上一次性进位+找到最高位更快,但这么写更符合人类的正常思维
ans[k+1]+=ans[k]/10;
ans[k]%=10;
}
}
//************************************************//
for(i=301;i>=0;--i)
if(ans[i]!=0)
{
flag=i; //找最高位
break;
}
//************************************************//
for(i=flag;i>=0;--i)
printf("%lld",ans[i]);
printf("\n");
}
}
本题中利用高精度计算的代码段:
for(j=min(i-1,m);j>=1&&n-i>=m-j;--j)
{
for(k=1;k<=120;++k)
ans[k][j]=ans[k][j-1]+j*ans[k][j];
for(k=1;k<=120;++k)
if(ans[k][j]>=10)
{
ans[k+1][j]+=ans[k][j]/10;
ans[k][j]%=10;
}
}
补充二:状态压缩
——补充二 状态压缩——
注意本题只有8MB空间限制,开一个100 * 100 * 100的数组保存每一行每一列的高精度数有够呛的。
事实上,我们没必要一定要存储之前的数据,毕竟我们的递推式只涉及到n和n-1这两行,前面的数据完全可以舍弃。
假设现在正在用k递推k+1,由之前的递推式可知状态转移是从(k,m-1)+m*(k,m) 到(k+1,m),我们完全可以只开一维的高精数组(即100 * 100),把每一次计算出来的(k+1,m)取代掉上一行同位置的(k,m),也就是说我们的这个一维高精度数组保存的数据由第k+1行覆盖掉第k行。
如何覆盖?
如果仍旧以人类的思维,从第一个数据开始覆盖是会出错的。
但是从最后一个数据开始覆盖就不会出错:
状态压缩其实还有另外一层意义
从后面开始覆盖,意味着有些数据我们用不着计算了,毕竟答案只是一个点而已。
完整注释代码
以下:
#include<stdio.h>
#include<string.h>
#define min(x,y) ((x)<(y)?(x):(y))
int main()
{
int i,j,n,m,k,flag;
int ans[121][101];//一维高精度数组,只记录某一行的状态
while(~scanf("%d %d",&n,&m))
{
memset(ans,0,sizeof(ans));
for(i=1;i<=n;++i)
{
ans[1][1]=1;//对无法状态转移的数据初始化
for(j=min(i-1,m);j>=1&&n-i>=m-j;--j)//限制计算域
{
//**********************************************//
for(k=1;k<=120;++k)
ans[k][j]=ans[k][j-1]+j*ans[k][j];
for(k=1;k<=120;++k)
if(ans[k][j]>=10) //高精度计算
{
ans[k+1][j]+=ans[k][j]/10;
ans[k][j]%=10;
}
//**********************************************//
}
ans[1][i]=1;//对无法状态转移的数据初始化
}
flag=120;
for(i=120;i>=0;--i)
if(ans[i][m]!=0) //找到最高位
{
flag=i;
break;
}
//**********************************************//
if(flag==120)flag=1;
for(i=flag;i>=1;--i)
printf("%d",ans[i][m]);
printf("\n");
}
return 0;
}
本代码占空间1.51MB,耗时0MS
后记
这个屑题本身不是很难,主要是有一些小优化值得记录一下。
水得题解++;
2018年12月26日