[动态规划] 整数划分 (完全背包解法 + 状态初始化的个人看法)

本文详细探讨了整数划分问题,它与完全背包问题的关系,并提供了完全背包的解题思路。通过状态表示和状态计算,解析了初始化问题的关键,包括f[i,0]和f[0,0]的含义。最后,介绍了如何通过优化减少时间复杂度,包括从二维状态到一维状态的转换。
摘要由CSDN通过智能技术生成


前言

整数划分问题可以看作是完全背包的一种变体形式,即:从1~n中任取数字,和恰好为n的方法数,其中每个数字都可以取任意次。而区别在于,这里求的是方法数,而经典的完全背包问题求解最值,两者在状态的初始化上有所差异(这也是本文的重点)


一、题目

  一个正整数 n 可以表示成若干个正整数之和,形如:n = n1 + n2 + …+ nk,其中 n1 ≥ n2 ≥ … ≥ nk, k ≥ 1。我们将这样的一种表示称为正整数 n 的一种划分。现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。
【输入格式】
  共一行,包含一个整数 n。
【输出格式】
  共一行,包含一个整数,表示总划分数量。由于答案可能很大,输出结果请对 109+7 取模。
【数据范围】1 ≤ n ≤ 1000
【输入样例】5
【输出样例】7

二、完全背包思路

把1,2,3, … n分别看做n个物体的体积,这n个物体均无使用次数限制,求恰好能装满总体积为n的背包的总方案数

1、状态表示 f[i, j]

f[i, j] 表示用前 i 个数字组成和恰好为 j 的方法数。最终答案f[n, n]

2、状态计算

f[i,j]可以看成由 0 ~ k 个 i 组成的方法数的集合(k * i <= j)
即: f[i, j] = f[i - 1, j]       //不选i, 全用 1 ~ i - 1表示 j
    + f[i - 1, j - i]     //选1个i, 剩下 j - i 全用 1 ~ i - 1表示
    + f[i - 1, j - 2 * i]   //选2个i, 剩下 j - 2 * i 全用 1 ~ i - 1表示
     ……
    + f[i - 1, j - k * i]   //选k个i, 剩下 j - k * i 全用 1 ~ i - 1表示

例 : f[2,5]表示用前2个数表示5
    f[2, 5] = f[1, 5] + f[1, 3] + f[1, 1]
 分别对应方法为:
    f[1, 5] : 1+1+1+1+1 //不选2,方法数1
    f[1, 3] : 2+1+1+1 //选1个2, 剩下全用1表示,方法数1
    f[1, 1] : 2+2+1 //选2个2, 剩下全用1表示,方法数1
 总方法数:f[2, 5] = 3

三、朴素完全背包解法

1、初始化问题

最开始状态计算理清楚之后就直接敲代码了,结果一直报0
关键代码如下

for(int i = 1; i <= n; i ++) //枚举数字 1 ~ n (物品)
    for(int  j = 1; j <= n; j ++) //枚举和 1 ~ n(体积)
        for(int k = 0; k * i <= j; k++) //状态表计算
            f[i][j] += f[i-1][j - k * i];

想了一下应该是初始化问题,于是想当然的加上了如下代码

for(int i = 1; i <= n; i ++){
    f[i][1] = 1; //用前i个数表示1方法数为1
    f[1][i] = 1; //用1表示i的方法数为1
}

for(int i = 2; i <= n; i ++)
    for(int  j = 2; j <= n; j ++)
        for(int k = 0; k * i <= j; k++)
            f[i][j] += f[i-1][j - k * i];

  结果依旧有问题,那么问题就来了。我去浏览了网上这道题的一些完全背包做法的题解,抛开优化过的代码不谈,单纯用二维状态的解法中, 很多是直接将f[i, 0]全部置为1,更有甚者将f[0, 0]置为1,j 从0开始枚举依旧也能AC。
  这我才意识到,除了f[i, 1] 、 f[1, i] ,还有 j 可能为0的情况, f[i, 0] 表示什么呢?用前i个数字表示0的方法数?挠头

  首先在一般的背包问题中,状态f[i, j]表示的是用前 i 件物品组成体积为 j 时的最大价值,这里求的是最值,很明显当体积 j 为0时,最大价值一定都是0。而本体求的是方法数,这就要好好说一说了。

  首先给出我改完代码后的AC代码(关键部分),初始化比较麻烦,但是个人认为比较好理解。

for(int i = 0; i <= n; i ++)    
	f[i][0] = 1; //初始化f[i][0] = 1
for(int i = 1; i <= n; i ++){
    f[i][1] = 1; //用前i个数表示1方法数为1
    f[1][i] = 1; //用1表示i的方法数为1
}
for(int i = 2; i <= n; i ++)
    for(int  j = 2; j <= n; j ++)
        for(int k = 0; k * i <= j; k++)
            f[i][j] = (f[i][j] + f[i-1][j - k * i]) % mod; //mod = 1e9+7

所以f[i, 0]到底有什么含义呢?
  前i个数组成0, 换句话说其实就是 : 前 i 个数一个都不选的方法数, 应当为1
  比如f[2, 4] (用前两个数表示4) = f[1, 4] + f[1, 2] + f[1, 0]
      f[1, 4] 表示不选2, 全部用1表示4, 即 1+1+1+1
      f[1, 2] 表示选1个2, 剩下的2用1表示, 即 2+1+1
      f[1, 0] 表示选2个2, 也就是一个1也不选, 即 2+2
  这里f[1, 0]表示的就是一个1都不选的状态, 此时方法数为1

  那f[0, i]需要初始化成什么呢?很明显,用0组成除0外的任何数字,方法数都应该是0,f[i, j]开的是全局,所以不用管

2、初始化的简化过程

  上述初始化过程是简单易懂的,回顾网上的代码,为什么只初始化f[i, 0] = 1,甚至仅初始化f[0, 0] = 1就可以呢。
  通过状态计算我们可以发现,即使不初始化f[i, 1]和f[1, i],也能在计算过程中得到它们的值。
  比如计算f[3, 1], 根据状态转移方程可以得到:
    f[3, 1] = f[2, 1] = f[1, 1] = f[0, 1] + f[0, 0] = f[0, 0]
  由此可以看出任意f[i, 1]都可以由f[0, 0]得到

  再比如计算f[1, 3]
    f[1, 3] = f[0, 3] + f[0, 2] + f[0, 1] + f[0, 0] = f[0, 0]
  由此可以看出任意f[1, 0]都可以由f[0, 0]得到

  再比如计算f[3, 0]
    f[3, 0] = f[2, 0] = f[1, 0] = f[0, 0]
  由此可以看出任意f[1, 0]都可以由f[0, 0]得到
  综上所述,只要初始化f[0, 0] = 1,所有的初始化问题就都能解决(当然枚举的时候 j 自然是得从0开始枚举了)

简化后的AC代码如下

#include <iostream>
#include <cstdio>
using namespace std;

const int N = 1005, mod = 1e9+7;
int f[N][N], n;

int main()
{
    scanf("%d", &n);
    
    f[0][0] = 1;
    for(int i = 1; i <= n; i ++)
        for(int  j = 0; j <= n; j ++)
            for(int k = 0; k * i <= j; k++)
                f[i][j] = (f[i][j] + f[i-1][j - k * i]) % mod;
    
    printf("%d",f[n][n]);
    return 0;
}

四、完全背包优化

上述解法由于3个循环的存在,时间复杂度较大
  f[i,j] = f[i - 1, j] + f[i - 1, j - i] + f[i - 1, j - 2i] + f[i - 1, j - 3i] + …… + f[i - 1, j - ki]
  f[i, j - i] =    f[i - 1, j - v] + f[i - 1, j - 2i] + f[i - 1, j - 3i] + ……, + f[i - 1, j - ki]
由上式可以发现,f[i, j]实际上可以化简成两个状态,即 f[i, j] = f[i - 1, j] + f[i, j - i]
故可以写成类似01背包的写法:

#include <iostream>
#include <cstdio>
using namespace std;

const int N = 1005, mod = 1e9+7;
int f[N][N], n;

int main()
{
    scanf("%d", &n);
    
    f[0][0] = 1;
    for(int i = 1; i <= n; i ++)
        for(int  j = 0; j <= n; j ++){
            f[i][j] = f[i-1][j]; 
            if(j >= i)  f[i][j] = (f[i-1][j] + f[i][j-i]) % mod;
        }
        
    printf("%d",f[n][n]);
    return 0;
}

进一步改写成1维状态

#include <iostream>
#include <cstdio>
using namespace std;

const int N = 1005, mod = 1e9+7;
int f[N], n;

int main()
{
    scanf("%d", &n);
    
    f[0] = 1;
    for(int i = 1; i <= n; i ++)
        for(int  j = i; j <= n; j ++)
        	f[j] = (f[j] + f[j - i]) % mod;
        
    printf("%d",f[n]);
    return 0;
}

总结

到此困扰了我一个中午的初始化问题终于解决了,这里关于完全背包的优化方式不作为重点阐述了~

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值