正确答案是55965365465060!
在某一篇博客上说这一题的答案是1这显然是理解错了题意!
先看原题:
题意分析:一开始写这一道题的时候以为题意是让把2019分解为两个不相等的素数之和,但是我仔细研读题意后发现正确题意是把2019分解为很多个不相等是素数之和例如可以分解为:3 + 5 + 2011等等。
这一题使用了记忆化搜索,记忆化搜索是DP的一类,不了解的同学先简单的来看一下记忆化搜索的概念:
我也是前几天才学习了记忆化搜索,刚开始学习的时候在想为什么把记忆化搜索归为DP而不是DFS,直到今天在复习质数拆分这一题时才突然悟了:记忆化搜索是利用递归来实现状态转移,并避免重复运算。
来看一下这一题的代码,我会在后面解释代码中的一些不好理解的地方:
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 3000;
//#####当在第 i 个位置 使得总和为 j 的选法的总数
LL f[N][N];//注释1
bool check[N];
vector<int> prime;
void make_prime()//注释2
{
for(int i = 2; i <= 2019; i++)
{
if(!check[i]) prime.push_back(i);
for(int j = 0; j < prime.size() && i * prime[j] <= 2019; j++)
{
check[i * prime[j]] = true;
if(!(i % prime[j])) break;
}
}
}
//##利用递归来实现状态转移
LL DP(int pos,int sum)//求出在位置为pos,总和为sum时对应的总选法
{
if(f[pos][sum] != -1) return f[pos][sum];//记忆化搜索:将已经计算的 结果保存下来,避免重复运算
if(sum == 2019) return 1;
if(pos >= prime.size() || sum > 2019) return 0;//剪枝
LL ans = 0;
//注释3
ans += DP(pos + 1,sum);//不要这个位置上的素数
ans += DP(pos + 1,sum + prime[pos]);//要当前这个位置上的素数
f[pos][sum] = ans;//#基于递归的状态转移
return ans;
}
int main()
{
make_prime();//筛
memset(f,-1,sizeof f);
LL ans = DP(0,0);
cout << ans << endl;
return 0;
}
DP问题需要注意的最重要的两点无非就是:状态表示和状态转移(计算)。
注释1:这一题的状态表示f[i][j]可以理解为:对于素数数组中的位置i,目前总和为j的选法的总数(这里可能不太好理解建议多敲几遍代码,结合代码来理解)
注释2:线性筛,给大家推荐一篇博客线性筛算法
注释3:这里主要解释一下 ans 为什么同时需要加上两个函数的返回值:我们先明确一下DP函数的作用:计算出在某个素数数组的位置pos上当总和为sum时对应的总选法 ,举个例子:我们先截取一段素数数组:
2 3 5 7 11 13 17 19 23 29
在某种情况下递归到达了素数数组的第5个位置即到达了素数13,现在面临着两个问题:
1.不让13参与构成组成2019的一部分:
ans += DP(pos + 1,sum);//不要这个位置上的素数
2.让13参与构成组成2019的一部分
ans += DP(pos + 1,sum + prime[pos]);//要当前这个位置上的素数
其实每当到达递归的一个位置时都要面临一个问题:是否选择prime[pos],而对于每一种选择都可能出现多种或0种构成2019的方式,比如,有可能在本层选择13的情况下无论后面怎么选都凑不出2019(我是说如果),这时DP(pos + 1,sum + prime[pos])的值就为 0
至此,这篇文章就先结束吧,如果有解释不清楚的地方或不恰当的地方就留言在评论区吧,我会进一步完善文章的。