吉林大学软件大二学生 东北师范大学附属中学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;
}