整数划分问题

整数划分问题

整数划分是数论中的一个概念,它指的是将一个正整数写成一系列正整数之和的方式。在整数划分问题中,这些正整数的顺序不被考虑,即只关注组成部分的数字而不关注它们的排列顺序。

例如,整数4可以有以下五种划分方式:

  1. 4
  2. 3 + 1
  3. 2 + 2
  4. 2 + 1 + 1
  5. 1 + 1 + 1 + 1

可以看出,比如划分2+1+1和1+2+1在整数划分中被认为是相同的,因为它们包含的数字是一样的。

整数划分问题在组合数学中有着重要的地位,与之相关的还有很多有趣的问题和定理,例如,它和分拆函数(partition function)紧密相关,分拆函数用来表示一个正整数的划分数目。整数划分还与费马的大定理、杨辉三角、生成函数等数学领域有所交叉。

研究整数划分不仅仅是纯粹数学的兴趣,它在计算机科学、物理学(比如在统计力学的模型中)等领域也有广泛的应用。


动态规划是解决整数划分问题的一种有效方法,它通过将问题分解成较小的子问题来解决大问题,并存储子问题的解以避免重复计算。在整数划分问题中,我们可以用动态规划自底向上构建一个表格,记录从最小的整数开始到目标整数的所有划分情况。如果学过y总的课就知道这一步其实就是在进行集合划分,或者说状态转移,即我们需要求解的数据,可以由那些情况转移而来。

状态表示:dp[n][k] 表示整数 n 使用不大于 k 的数的划分方式数量。
状态转移方程(集合划分):

dp[n][k] = dp[n][k-1] + dp[n-k][k]

这个方程的含义是:整数 n 的划分可以分为两类:

  1. 不包含任何 k 的划分。
  2. 包含至少一个 k 的划分。

对于第一类,即整数 n 的所有不包含 k 的划分数量,这就相当于 dp[n][k-1]。对于第二类,我们可以先取一个 k,然后计算剩余部分 n-k 的划分数量,这就是 dp[n-k][k]

基于上述思路,动态规划的过程如下:

  1. 初始化:没有初始化的思路就看看转移方程,最初始是什么样子dp[1][1] = dp[1][0] + dp[0][1],然后在思考一下这些代表的含义:dp[1][1]即整数1,使用不大于1的整数,的可能划分方法数量dp[1][0]即整数1,使用不大于0的整数,的可能划分方法数量dp[0][1]即整数0,使用不大于1的整数,的可能划分方法数量。显然dp[1][0]不存在,即为0。dp[0][1]永远为1,即不选这一种情况。同理得出dp[0][i]=1,这就是初始化。

  2. 填表:按照从小到大的顺序填写 dp 表。对于每个 n(从 1 到目标整数)和每个 k(从 1n),根据状态转移方程计算 dp[n][k] 的值。

  3. 结果获取:整数 n 的划分数量等于 dp[n][n],因为这表示使用不大于 n 的所有正整数的划分方式数量。

举例来说,如果我们要计算 4 的划分数量,我们会计算出以下的 dp 表:

n\k | 1  2  3  4
----------------
  1 | 1  1  1  1
  2 | 1  2  2  2
  3 | 1  2  3  3
  4 | 1  3  4  5

从表中可以看出,整数 4 的划分数量是 5,对应于 dp[4][4] 的值。

通过这种自底向上的方式,动态规划不仅能够解决整数划分问题,还能够有效地处理更复杂的变体,例如限制划分中数字的最大值、最小值或者特定的数字集合等。


让我们再来看看状态转移这个方程:dp[n][k] = dp[n][k-1] + dp[n-k][k]。思考一下能不能优化?

交换一下n,k试试:dp[k][n] = dp[k-1][n] + dp[k][n-k]。有没有发现它的第一维,分别是k,k-1,k,在二维表格中,第一维其实就是每一层嘛,也就是说第k层的至多用到了k-1层k层的数据,咱们是不是可以使用滚动数组来优化嘞。

C++代码如下:
#include <iostream>
#include <vector>
using namespace std;

const int MOD = 1000000007;//这个看题目要求,有些划分种数太大会让你取模的

int main() {
    int n;
    cin >> n;
    
    // 创建一个一维数组,因为每个状态dp[i]只依赖于dp[i]和dp[i-j]的值
    vector<int> dp(n + 1, 0);
    
    // 初始化,只有一种方法划分出和为0
    dp[0] = 1;
    
    // 动态规划填表过程
    for (int i = 1; i <= n; ++i) {
        for (int j = i; j <= n; ++j) {
            // 状态转移方程
            dp[j] = (dp[j] + dp[j - i]) % MOD;
        }
    }
    
    // 输出结果
    cout << dp[n] << endl;
    
    return 0;
}
看到这里,相信你会经历这样一个过程:先是觉得状态表示、初始化啥的有点抽象,然后再看到代码又觉得特别简单。但这显然还没有达到我的目的,如果仅是这样也不值得专门写一篇文章了,诸位请往后看。

我若说这其实是一个完全背包问题阁下又该如何应对呢?

假设我有一个背包容量为n,物品体积分别为1,2,3~n。不考虑价值,只考虑放满这个背包的物品拿法。每个物品无限个,可以重复拿。

能来看整数划分问题想必对经典的背包问题应当有所了解(若是不清楚可以移步我的另一篇文章背包问题之完全背包),接着往下看~

状态表示:f[i][j] 表示从前i个物品(数字)中选,总和(数字和)恰好为j的所有选法。
状态转移方程(集合划分):

f[i][j] = f[i-1][j] + dp[i][j-i]

和上面或者说完全背包一样,第f[i][j]这个状态,可以由——第i个数字,不选或者至少选一个,这两种状态转移过来。

C++代码
#include <iostream>
using namespace std;

const int MOD = 1000000007;
const int N=1010;
int n;
int f[N][N];
int main() {
    cin>>n;
    for(int i=1;i<=n;i++) f[i][0]=1;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            f[i][j]=f[i-1][j];
            if(j>=i) f[i][j]+=f[i][j-i];
            f[i][j]%=MOD;
        }
    }
    cout<<f[n][n];
    return 0;
}

初始化:i个数字中选,总和恰好为0的选法,一个不选就行,即存在一种方法。for(int i=1;i<=n;i++) f[i][0]=1;再以二维的视角来理解一下这个初始化,其实就是把这张二维表格的第一列初始化了,为啥嘞?你看状态转移方程,每次只会用到(i,j)上方即(i-1,j)以及左侧的(i,j-i)这两个数据。这也算是从含义计算顺序两个角度来理解初始化叭。

总之,把这个当作完全背包来看待我是万万没想到啊~这也是我为什么特意写一篇文章的原因了。属于是对以前学过的东西活学活用麻了,y总tql。

  • 44
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值