一.题目——复杂的整数划分问题
总时间限制:
200ms
内存限制:
65536kB
描述
将正整数n 表示成一系列正整数之和,n=n1+n2+…+nk, 其中n1>=n2>=…>=nk>=1 ,k>=1 。
正整数n 的这种表示称为正整数n 的划分。
输入
标准的输入包含若干组测试数据。每组测试数据是一行输入数据,包括两个整数N 和 K。
(0 < N <= 50, 0 < K <= N)
输出
对于每组测试数据,输出以下三行数据:
第一行: N划分成K个正整数之和的划分数目
第二行: N划分成若干个不同正整数之和的划分数目
第三行: N划分成若干个奇正整数之和的划分数目
样例输入
5 2
样例输出
2 3 3
提示
第一行: 4+1, 3+2,
第二行: 5,4+1,3+2
第三行: 5,1+1+3, 1+1+1+1+1+1
二.DFS——Time Limit Exceeded
写出来了DFS,结果发现超时。
#include<bits/stdc++.h>
using namespace std;
int N,K;
int a[100];
int sum=0;
void divide1(int n,int k)
{
if(n==0&&k==K)
{
sum+=1;
return ;
}
if(n<=0||k>=K)
{
return ;
}
for(int i=n;i>=1;i--)
{
divide1(n-i,k+1);
}
}
void divide2(int n,int k)
{
if(n==0&&k==K)
{
sum+=1;
return ;
}
if(n<=0||k>=K)
{
return ;
}
for(int i=n;i>=0;i--)
{
divide2(n-i,k+1);
}
}
void divide3(int n,int k)
{
if(n==0)
{
sum+=1;
return ;
}
if(n<=0)
{
return ;
}
for(int i=N;i>=0;i--)
{
if(i%2==1)
divide3(n-i,k+1);
}
}
int main()
{
cin>>N>>K;
divide1(N,0);
cout<<(sum+1)/2<<endl;
sum=0;
divide2(N,0);
cout<<(sum+1)/2<<endl;
sum=0;
divide3(N,0);
cout<<(sum+1)/2<<endl;
return 0;
}
三.动态规划——Accepted
网上学习了大佬的代码
原文链接如下:
复杂整数划分问题(动态规划)_复杂的整数划分问题-CSDN博客
1. N划分成K个正整数之和的划分数目
注意此处的限定,N必须划分成K个正整数,不允许有0的存在,所以j<=i。从另一个角度讲,dp[i][j]划分出的数里最大的必须是j,于是:
(1)j=i,dp[i][j]=1,因为划分的最大数必须是i,就一种情况;
(2)j<i,dp[i][j]=dp[i-1][j-1]+dp[i-j][j],这里是按照划分中最小数是否为1推出的,当最小数为1时,首先从i中取出1,占用1部分,然后把i-1分为j-1部分,dp[i-1][j-1]。当最小数比1大时,先给每一个部分都放一个1进去,花费j*1,再将i-j分到这j个部分里,dp[i-j][j]。
2. N划分成若干个不同正整数之和的划分数目
将N分成若干部分,但是每一部分值都不同,考虑dp[i][j],此处,理解为数字i的划分里最大值为j,因为要考虑不同的数,这样理解显然更容易解题。
(1)j=i,dp[i][j]=1+dp[i][j-1],当最大数为i时有 1 种情况,这里并不强制最大数必须为i,所以当最大数不为i时有dp[i][j-1]种可能;
(2)j<i,dp[i][j]=dp[i-j][j-1]+dp[i][j-1],最大数为j时,先从i中取出j,再对i-j进行划分,由于不允许划分重复,最大数变为j-1,dp[i-j][j-1]。最大数不为j时,那么最大为j-1,dp[i][j-1];
(3)j>i,dp[i][j]=dp[i][i],这里应该很容易理解,最大数不可能超过它自身。
3.N划分成若干个奇正整数之和的划分数目
这个问题我认为是随着理解角度的不同,解题复杂性就差的多了,如果将dp[i][j]理解为整数i划分成j个奇数,那解起来就麻烦了,我也查阅了很多资料,这种方法是最容易找到的,我不再叙述;此处最好的理解应该是整数i的划分里最大的数是j。
首先,我们要求将N划分为多个奇数,对于dp[i][j],j为偶数表示划分里最大的数为偶数,那么它实质上只能取得的最大数为j-1(奇数),所有j%2=0,dp[i][j]=dp[i][j-1],之后讨论j为奇数的情况。
(1)j=i,dp[i][j]=1+dp[i][j-1],当最大数为i时有 1 种情况,这里并不强制最大数必须为i,所以当最大数不为i时有dp[i][j-1]种可能;
(2)j<i,dp[i][j]=dp[i-j][j]+dp[i][j-1],最大数为j时,先从i中取出j,再对i-j进行划分,允许划分重复,最大数依然为j,dp[i-j][j]。最大数不为j时,那么最大为j-1,dp[i][j-1];
(3)j>i,dp[i][j]=dp[i][i],这里同上。
#include <iostream>
using namespace std;
#include <cstring>
int main()
{
int N=0, K=0;//数N
int dp1[51][51]; int dp2[51][51]; int dp3[51][51];
while (cin >> N >> K)
{
//N分成K个正数,要注意这里不允许出现0,所以j<=i,i最多分成j个非0数
//dp[i][j],可以理解为该划分中一定会出现 j
memset(dp1, 0, sizeof(dp1));
dp1[0][0] = 1;
for (int i = 1; i <= N;++i)
{
//dp1[i][1] = 1;
for (int j = 1; j <= i; ++j)//这里的j不大于i,
{
if (i == j) //一定出现i,就一种
dp1[i][j] = 1 ;
else //划分中出现1 + 划分中都比1大
dp1[i][j] = dp1[i - 1][j - 1] + dp1[i - j][j];
}
}
cout << dp1[N][K] << endl;
//N分成若干不相同的整数
memset(dp2, 0, sizeof(dp2));
dp2[0][0] = 1;
for (int i = 1; i <= N; ++i)
{
for (int j = 1; j <=N; ++j)
{
//dp2[0][j] = 1;
if (j == i)
dp2[i][j] = dp2[i][j - 1] + 1;
else if (j > i)
dp2[i][j] = dp2[i][i];
else //划分中出现了j则下一次最大到j-1;
//划分中不出现j,下一次同样不允许出现j
dp2[i][j] = dp2[i - j][j - 1] + dp2[i][j - 1];
}
}
cout << dp2[N][N] << endl;
//N分成若干个奇数
memset(dp3, 0, sizeof(dp3));
dp3[0][0] = 1;
for (int i = 1; i <= N; i++)
{
//dp3[i][1] = i % 2 ? 0 : 1;
//dp3[i][0] = 0;
for (int j = 1; j <= N; j++)
{
if (j % 2 == 0)//此处理解为划分中含有j,如果j为偶数,显然直接到j-1
dp3[i][j] = dp3[i][j - 1];
else //以下三个分支中都将dp3[i][j]理解为划分中含有j
{
if (i < j)
dp3[i][j] = dp3[i][i];
else if (i == j)
dp3[i][j] = dp3[i][j - 1] + 1;
else
dp3[i][j] = dp3[i - j][j] + dp3[i][j - 1];
}
}
}
cout << dp3[N][N] << endl;
}
return 0;
}
四.DFS与动归解决这道问题的复杂度比较
1.第一段代码:递归方法
函数 divide1(int n, int k):
递归地尝试将 n 分成 K 个正整数。
在每一层递归中,从 n 遍历到 1,调用下一层递归。
时间复杂度为 O(K * N^K)。因为每层递归有最多 N 种选择,总共有 K 层递归。
函数 divide2(int n, int k):
递归地尝试将 n 分成若干不同的正整数。
在每一层递归中,从 n 遍历到 0,调用下一层递归。
时间复杂度与 divide1 类似,也是 O(K * N^K)。
函数 divide3(int n, int k):
递归地尝试将 n 分成若干个奇数正整数。
在每一层递归中,从 N 遍历到 0,只选择奇数,调用下一层递归。
时间复杂度仍然是 O(K * N^K)。
总结
由于这些函数的递归深度和每层递归中的循环次数都与 N 和 K 成指数关系,递归方法的时间复杂度非常高,尤其对于较大的 N 和 K 值来说,容易造成时间超时。
2.第二段代码:动态规划方法
第一类划分(dp1 数组):
使用二维数组 dp1[i][j] 存储将 i 分成 j 个正整数之和的划分数目。
两层循环,外层循环遍历 i 从 1 到 N,内层循环遍历 j 从 1 到 i。
时间复杂度为 O(N^2)。
第二类划分(dp2 数组):
使用二维数组 dp2[i][j] 存储将 i 分成若干个不同的正整数之和的划分数目。
两层循环,外层循环遍历 i 从 1 到 N,内层循环遍历 j 从 1 到 N。
时间复杂度为 O(N^2)。
第三类划分(dp3 数组):
使用二维数组 dp3[i][j] 存储将 i 分成若干个奇数正整数之和的划分数目。
两层循环,外层循环遍历 i 从 1 到 N,内层循环遍历 j 从 1 到 N,但仅处理奇数。
时间复杂度为 O(N^2)。
总结
动态规划方法使用了二维数组来存储中间结果,每个子问题的解只需要常数时间进行组合,避免了递归方法中的大量重复计算,因此其时间复杂度显著降低。