01背包 质数和分解

该博客围绕求解自然数N可写成多少种本质不同的质数和表达式展开。作者起初认为是简单的01背包问题,实际操作中遇到去重和效率问题。通过不断调整DP数组,最终引入新的加和数组,利用01背包思想得出核心递推公式,完成题目求解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

吉林大学软件大二学生 东北师范大学附属中学OJ jinxi20111 2019.05.23

题目描述

任何大于 1 的自然数 N,都可以写成若干个大于等于2且小于等于 N 的质数之和表达式(包括只有一个数构成的和表达式的情况),并且可能有不止一种质数和的形式。例如9 的质数和表达式就有四种本质不同的形式:9 = 2+5+2 = 2+3+2+2 = 3+3+3 = 2+7 。
这里所谓两个本质相同的表达式是指可以通过交换其中一个表达式中参加和运算的各个数的位置而直接得到另一个表达式。
试编程求解自然数 N 可以写成多少种本质不同的质数和表达式。

输入

读入一个自然数 N , 2≤N≤2000。

输出

输出自然数 N 的本质不同的质数和表达式的数目。

样例输入

2

样例输出

1

提示

对于40%的数据 N<=200

对于100%的数据 N<=2000


分析过程

首先,我要承认我的错误。我本来以为这是一道非常简单的,换汤不换药的01背包。但是真的做起来的确是有一点棘手的。

那么棘手的点有哪些,容我一一分析。


按照惯例,看到一道题,我习惯直奔数据范围

2000以内,素数,心里有谱了,这道题的空间代价不是问题,所以要使用一定的空间换时间。


然后速打素数表,代码一遍无错误是最佳状态(比如我)

代码如下

#include<iostream>
using namespace std;
int main()
{
	int i,j,k,n,num[2000],top;
	bool flag;
	cin>>n;
	num[1]=2;
	top=1;
	for(i=3;i<=n;i++)
	{
		flag=true;
		for(j=1;j<=top;j++)
		{
			if (i%num[j]==0) flag=false;
		}
		if (flag)
		{
			top++;
			num[top]=i;
		}
	}
	for(i=1;i<=top;i++)
	{
		cout<<num[i]<<endl;
	}
	cout<<'*'<<top;
	return 0;
}

这里的top表示了有多少素数,是我后加进去的,具体作用容后再表。

打完素数表,开始写DP数组。

最开始的DP数组,我觉得一维数组就可以表示。这里的DP[x],表示的是质数和为x的数列数量。

当然,这种写法很快就被我否定了。

否定方式是2+2+3,和2+3+2的去重,这种dp数组没有办法找到这个数列的重复情况,由于长度也不相同,也没有办法用排列组合的数学方法进行去重。

这里有点跳跃,看不懂可以自己模拟一下。


那么就来到了第二种dp数组。我将2,2,3这个数列排序,那我就可以得知,每次我加在数列后面的数,不能小于最后一个数,否则就会造成重复,如我在2+2+3+5后面再+2,那么,一定会和2+2+2+3后面+5两者相同。所以,我规定,每次加在后面的数一定要大于等于前面的数。

那在DP数组中,怎么分清哪个数结尾的呢?引入新的维度就可以了。一维变二维,这时候,DP[x][y],表示的,变成了以X结尾的,和为y的质数和数列有多少。

这个数组很不错,能够完全表示我的意愿。但很快,我就发现了这个数组很弱。

因为状态转移的过程,我需要预留出当前对应质数的空间,如DP[5][12],需要dp[2][7],dp[3][7],dp[5][7],dp[7][7],甚至还需要dp[2][2],dp[3][2],dp[5][2],dp[7][2]。而这种加和的过程将在后面不停地重复,效率很低。

效率有多低呢?

我大概算了一下时间,1000*(2000+1)*2000/2

emmm,炸了,甚至炸得让我怀疑DP能不能做。

于是我去北苑一公寓三楼西北角的洗手间的第三个坑蹲了一会……

哦对!空间换时间!

我把加和提前算好,存到一个数组当中,用的时候直接调用就OK了

于是,我引入了一个数组,用来表示dp[1到x][n]的和。

用着用着,,我惊讶的发现,原来的DP数组根本没什么用了,用这个新的加和数组就能做题了。

那么新的加和数组代表什么呢?

DP[x][y]表示,使用的质数小于等于x,和为y的数列数量。

这时候规则就特别简单了。

1.如果当前的y< x所对应的质数,即num[x],那么dp[i][j]=dp[i-1][j]
2.如果y=num[x],那么dp[i][j]=dp[i-1][j]+1,加得是这个素数单独成和的情况
3.y>num[x],dp[i][j]=dp[i-1][j]+dp[i][j-num[i]]
核心递推公式,相当于之前算出来的数列和,加上结尾为num[i]的新生成的数列。这些数列先预留出一个num[i]的空间,然后加上num[i]。这里就是01背包的思想了。

如果你有疑问,可以用下面的代码,打印出dp数组,然后手写确定一下这些数都是哪里来的。

我建议用20打一个数组,算到10左右应该就能懂了。

	cout<<"   ";
	for(i=1;i<=n;i++)
	{
		printf("%2d ",i);
	}
	cout<<endl;
	for(i=1;i<=top;i++)
	{
		printf("%2d ",num[i]);
		for(j=1;j<=n;j++)
		{
			printf("%2d ",a[i][j]);
		}
		cout<<endl;
	}

初始化的问题,我是对第一行,也就是2这一行进行了手动生成。因为2只能构成2,2+2,2+2+2,就把J等于偶数的置为1,奇数置为0就ok了,剩下的自己去生成。

运行时,如果报错可能跟电脑和编译软件有关系。在自己的废物苹果本的C-Free上运行,我调小了a数组。(伏笔,这里,2000以下的素数一共有303个,就是前面的打印素数表算出来的top。)

OJ上在调回去就OK了。

这道题就完成了,下面是AC代码

吉林大学软件大二学生 东北师范大学附属中学OJ jinxi20111 2019.05.23

//运行错误的话请调小a数组,调至201,1001即可。
#include<iostream>
using namespace std;
int main()
{
	int i,j,k,n,num[2001],top,a[304][2001];
	bool flag;
	cin>>n;
	num[1]=2;
	top=1;
	for(i=3;i<=n;i++)
	{
		flag=true;
		for(j=1;j<=top;j++)
		{
			if (i%num[j]==0) flag=false;
		}
		if (flag)
		{
			top++;
			num[top]=i;
		}
	}
	for(i=2;i<=n;i++)
	{
		if (i%2==0) a[1][i]=1;
	} 
	for(i=2;i<=top;i++)
	{
		for(j=1;j<num[i];j++)
		{
			a[i][j]=a[i-1][j];
		}
		a[i][num[i]]=a[i-1][num[i]]+1;
		for(j=num[i]+1;j<=n;j++)
		{
			a[i][j]=a[i-1][j]+a[i][j-num[i]];
		}
	}

	cout<<a[top][n];
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值