TSOJ 好好做题(屑)——递推状态压缩+高精度

题目描述

选修程序设计和算法课程的学生人数为 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日

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值