NC152 数的划分

题意

将整数 分成 份,且每份不能为空,任意两个方案不能相同(不考虑顺序),求方案数,对1e9+7取模。

1. 暴力法(效率很低,数字大了会tle)

我们分析下样例的规律:

n=7, k=3
[1,1,5]
[1,2,4]
[1,3,3]
[2,2,3]

直接使用回溯法,暴力枚举每一位数字,需要注意以下规律:

  • 当前位置的最小数字要>=上一位数字
  • 当前位置的最大数字要<=下一位数字(要留出足够大的数给下一位)

算法基于深度优先搜索,用全局变量记录搜索树的路径和答案数目。

以样例为例,搜索树的过程如下图所示(异常分支简略表示):

图片说明

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param n int 被划分的数
     * @param k int 化成k份
     * @return int
     */
    int k, res;
    const int M = 1e9 + 7;
    int divideNumber(int n, int k2) {
        // 把k赋值到一个全局变量        
        k = k2, res = 0;

        // 从第一层开始搜索, 还剩下可分配的数是n, 本层最小的一个待尝试的数是1
        dfs(1, n, 1);

        return res;
    }

    void dfs(int depth, int rest, int start) {
        // 如果k层都搜完了
        if (depth == k+1) {
            // 恰好没有剩余可分配的数
            if (rest == 0) {
                res++; // 搜到第k+1步,说明找到一个解
                res %= M;
            }
            return;
        }

        // 最小的数是start,最大的数是rest,继续向下深搜
        for (int i = start; i <= rest; i++) {
            // 层数+1, 剩余的数是rest-i,下一层最小待尝试的数是i
            dfs(depth+1, rest-i, i);
        }
    }
};
  • 时间复杂度: , 一共有k层递归,每层递归中,有当前层数减一轮循环遍历。考虑到k极小,所以能过。 2021.8.6 update: k增强到5000, 所以会tle.
  • 空间复杂度: , 一共有k层递归,每层的空间为O(1).

2. 动态规划

观察问题的规律,我们可以发现问题可以拆分成子问题,并且子问题具有独立性,如果把总问题看做f[n][k], 那么每次确定了一个数j,则问题规模缩小为f[n-j][k-1],且二者是独立的. 因此我们可以利用动态规划的性质,解决此题。

dp[i][j]表示把数字i分成j份的方案数。

考虑两种情况:

  • i<j, 无解. 因为每一份至少是1.
  • i==j, 1种方案。
  • i>j. 先把每一份都分配上1,剩下的i-j可以分配给0份,1份,2份。。。。。。j份,故有如下公式:

这个东西有点像前缀和,我们往前写一项:

两式做差得:

实现之, 最后即为答案。

2021.8.6 update: 数据范围提高到5000,1000,题面改为对1e9+7取模,更新ac代码

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param n int 被划分的数
     * @param k int 化成k份
     * @return int
     */

    // n <= 5000, k <= 1000
    static const int N = 5010, K = 1010;
    static const int M = 1e9 + 7;
    int dp[N][K];
    int divideNumber(int n, int k) {
        // 初始化所有的数组值为0
        memset(dp, 0, sizeof(dp));

        // i==j的情况只有一种方案
        for (int i = 0; i <= k; i++) {
            dp[i][i] = 1;
        }

        // i>j的情况
        for (int i = 1; i <= n; i++) 
            // 这里需要同时满足j <= k和 j <= i, 因为j>i就没意义了
            for (int j = 1; j <= min(k, i); j++)
                dp[i][j] = (dp[i-1][j-1] + dp[i-j][j]) % M;

        return dp[n][k];
    }
};
  • 时间复杂度: . 有两重循环。
  • 空间复杂度:. 开了大小的二维数组。