情况一:
将一个数拆分为2的k次方的和的拆分方式和,例如,4可以拆分为1 +1+1+1, 1+1+2, 2+2, 4,一共有四种拆法。
我们定义数组dp[i]为 数 i的拆分方式总数。通过分析,对于一个奇数来说,他的拆分总数和比它小一位的偶数是相同的,即dp[i]=dp[i-1];对于偶数,我们分为两种情况,当偶数的拆分里没有1时,那么显然dp[i]=dp[i/2],因为每一位拆分的项数都可以除以二,当偶数的拆分里有1时,则有dp[i]=dp[i-1],因为必然有1故减去1不影响总数。根据加法原理,可以得到递推式:
i为奇数,dp[i]=dp[i-1]; i为偶数,dp[i]=dp[i/2]+dp[i-1];
这题用递推的方法巧妙地实现了数与数之间的联系。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
int dp[1000];
int main()
{
int n;
cin>>n;
dp[1]=1;
for(int i=2;i<=n;i++)
{
if(i%2==0)dp[i]=dp[i/2]+dp[i-1];
else dp[i]=dp[i-1];
}
cout<<dp[n];
}
例题链接洛谷1734最大约数和
题目的描述里,需要找到和不超过S的若干个数,使其约数和最大。对于小于S的每一个数而言,其约数和是固定的,就像有固定的价值。这样,就转化为了一个01背包问题。背包的容量大小为S,将小于S的每一个数的值看做消耗,得到的价值为其约数。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
int T[1005];
int dp[1005];
int main()
{
int n;
cin>>n;
for(int i=2;i<=n;i++)
{
T[i]=1;
for(int j=2;j*j<=i;j++)
{
if(i%j==0)
{
T[i]=T[i]+j;
if(j*j!=i)T[i]=T[i]+i/j;
}
}
}
T[1]=0;
for(int a=1;a<=n;a++)
{
for(int b=n;b>=a;b--)
{
dp[b]=max(dp[b],dp[b-a]+T[a]);
}
}
cout<<dp[n];
}
注意1的约数和为0。
情况三:
例题链接
对于每一个数而言,他的素数和表达式里,如果单独地看其中的某一个素数,就会成为另一个数的素数表达式再加上一个素数。这样,对于一个数 n,我们可以枚举所有不超过它的素数 prime,假如k<n且k+prime=n,则n的种数要加上k的种数。
为了使答案正确,我们先逐个枚举素数,然后更新 k+素数 的值。这样就模拟了类似于 2+2+2+3,2+2+5…这样的拆分过程,避免了重复计数。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
int check[1005],prime[1005];
int tot=0;
long long ans[1005];
void get(int n)
{
for(int i=2;i<=n;i++)
{
if(!check[i])
{
prime[tot]=i;
tot++;
}
for(int j=0;j<tot&&prime[j]*i<=n;j++)
{
check[prime[j]*i]=1;
if(i%prime[j]==0)break;
}
}
}
int main()
{
int n;
cin>>n;
get(n);
ans[0]=1;//初始化很重要,相当于让素数本身代表的数加1
for(int i=0;i<tot&&prime[i]<=n;i++)
{
for(int j=0;j<=n;j++)
{
if(ans[j]&&(j+prime[i]<=n))ans[prime[i]+j]+=ans[j];
}
}
cout<<ans[n];
}
再用动态规划的角度去看待,则是一个类似于完全背包的问题,因为物品即素数是任取的且无限多的,背包大小为当前数,得到的价值就为可以新增的方式数。转移方程为:
dp[i] = dp[i] + dp[ i-prime[j] ] prime[j]为小于 i 的可能的素数。贴一下关键代码:
for(int i=0;i<tot;i++)
{
for(int j=prime[i];j<=n;j++)
{
dp[j]+=dp[j-a[i]]; //更小的背包已更新完,再更新当前背包
}
}
情况四:
例题链接
该题是情况三的一种拓展。对于一个数,要将其分为不多于四个的平方数的和,例如25=12+22+22+42,求有多少种不同的分法。
该题的理解过程和情况三类似,对于任意一个数,可以有状态转移方程 dp[k] = dp[k] + dp[k-i*i](和素数的拆分基本一致,相当于找更小规模的数),需要注意的是限制条件,所以改用二次数组记录 dp[h][k],k用于记录已有的项数。状态转移方程为:
dp[h+i* i][k]=dp[h+i*i][k]+dp[h][k-1]
注意要把项数一到四的情况全部枚举更新。初始化dp[0][o]=1。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
int dp[32770][5];
int main()
{
int n;
cin>>n;
int maxn=32768;
dp[0][0]=1;
for(int i=1;i*i<=maxn;i++)
{
int h=i*i;
for(int j=0;j<=maxn;j++)
{
if(dp[j]&&j+h<=maxn)
{
for(int k=1;k<=4;k++)
{
dp[j+h][k]+=dp[j][k-1];
}
}
}
}
while(n--)
{
int num;
cin>>num;
int ans=dp[num][1]+dp[num][2]+dp[num][3]+dp[num][4];
cout<<ans<<endl;
}
}