当前位置: 首页 > article >正文

算法日记31:leetcode341整数拆分(DFS->记忆化->DP)

一、题目

在这里插入图片描述

二、题解

1、动态规划解题思路:

1、重述问题
2、找到最后一步
3、去掉最后一步,问题变成了什么?
原问题的答案=去掉最后一步的问题+?
4、考虑边界

2、结合题目具体分析:

在这里插入图片描述

  • 假设我们拆出了 5 5 5为第k个数
    在这里插入图片描述

三、DFS实现

//1、拆分n->k(k>=2)个,使得k个数乘积最大
//2、拆出了第k个数x(x<n)
//3、把n-x拆成k-1个数,使得其乘积最大
//答案=把n-5拆成k-1个数的情况下*x1
//4、遍历完全return 1

1、思路解析:

1)枚举拆分某个数 x 1 x1 x1,并且往下递归,每次乘上拆分出的数 x 1 x1 x1

int dfs(int x)
    {
        if(x==0) return 1;  //表示拆分完全
        //if(x<0) return -1e9;
        int res=0;
        for(int i=1;i<x;i++)   //枚举拆分某个数x1
        {
            if(x-i>=0)  //防止越界
            res=max(res,max(i*(x-i),(dfs(x-i)*i)));
        }
        return res;
    }

2)为什么写成 r e s = m a x ( r e s , m a x ( i ∗ ( x − i ) , ( d f s ( x − i ) ∗ i ) ) ) res=max(res,max(i*(x-i),(dfs(x-i)*i))) res=max(res,max(i(xi),(dfs(xi)i))) 而不是 r e s = m a x ( r e s , d f s ( x − i ) ∗ i ) res=max(res,dfs(x-i)*i) res=max(res,dfs(xi)i)

让我们通过一个具体的例子来解释,为什么 res = max(res, max(i * (x - i), (dfs(x - i) * i))) 必须这样写,而如果改成 res = max(res, dfs(x - i) * i) 会出错。

例子:n = 8

我们来分析当 n = 8 时的情况,看看两种写法的区别。

正确的写法:res = max(res, max(i * (x - i), (dfs(x - i) * i)))
  1. 初始调用dfs(8)

    • 我们要将 8 拆分为两个数 ix - i,并递归拆分后选出最大值。
  2. 尝试拆分 i = 1

    • x - i = 8 - 1 = 7
    • 计算 i * (x - i)1 * 7 = 7
    • 计算 dfs(x - i) * idfs(7) * 1,递归调用 dfs(7),假设 dfs(7) = 6(假设值)。
    • 结果是 dfs(7) * 1 = 6 * 1 = 6
    • max(1 * 7, 6 * 1) = max(7, 6) = 7

    所以,res 变成了 7

  3. 尝试拆分 i = 2

    • x - i = 8 - 2 = 6
    • 计算 i * (x - i)2 * 6 = 12
    • 计算 dfs(x - i) * idfs(6) * 2,递归调用 dfs(6),假设 dfs(6) = 5
    • 结果是 dfs(6) * 2 = 5 * 2 = 10
    • max(2 * 6, 5 * 2) = max(12, 10) = 12

    所以,res 变成了 12

  4. 尝试拆分 i = 3

    • x - i = 8 - 3 = 5
    • 计算 i * (x - i)3 * 5 = 15
    • 计算 dfs(x - i) * idfs(5) * 3,递归调用 dfs(5),假设 dfs(5) = 4
    • 结果是 dfs(5) * 3 = 4 * 3 = 12
    • max(3 * 5, 4 * 3) = max(15, 12) = 15

    所以,res 变成了 15

  5. 继续尝试更大的 i,最后的最大值是 15

最终,dfs(8) 返回的最大乘积是 15,通过递归拆分和直接拆分的结合找到了最优解。


错误的写法:res = max(res, dfs(x - i) * i)

如果我们将代码改为 res = max(res, dfs(x - i) * i),会出现问题,因为我们 忽略了直接拆分的乘积,只能通过递归拆分得到的乘积来更新 res

具体问题示例:

  1. 初始调用dfs(8)

    • 我们要将 8 拆分为两个数 ix - i,但是只考虑递归拆分。
  2. 尝试拆分 i = 1

    • x - i = 8 - 1 = 7
    • 计算 dfs(7) * 1,假设 dfs(7) = 6
    • 结果是 dfs(7) * 1 = 6 * 1 = 6

    所以,res 变成了 6

  3. 尝试拆分 i = 2

    • x - i = 8 - 2 = 6
    • 计算 dfs(6) * 2,假设 dfs(6) = 5
    • 结果是 dfs(6) * 2 = 5 * 2 = 10

    所以,res 变成了 10

  4. 尝试拆分 i = 3

    • x - i = 8 - 3 = 5
    • 计算 dfs(5) * 3,假设 dfs(5) = 4
    • 结果是 dfs(5) * 3 = 4 * 3 = 12

    所以,res 变成了 12

  5. 继续尝试更大的 i,最终得到的最大值是 12


关键问题
  • 忽略了直接拆分的乘积res = max(res, dfs(x - i) * i) 只考虑了递归拆分后的结果,而没有考虑当前拆分 i 和剩余部分 x - i 之间的乘积。如果我们直接拆分,像 i * (x - i),有可能得到比递归拆分更大的乘积。

  • 在这个例子中,i = 4 时,i * (x - i) = 4 * 4 = 16 是一个直接拆分的结果,而通过递归得到的 dfs(4) * 4 = 12 明显小于 16。因此,直接拆分出的乘积有时会比递归拆分后的乘积更大。

  • max(i * (x - i), dfs(x - i) * i) 确保了我们在每一步都考虑了直接拆分的乘积和递归拆分后的乘积,从而得出最大乘积。改成 res = max(res, dfs(x - i) * i) 会遗漏直接拆分的潜在最大值,导致结果错误。

2、代码解析:

//1、拆分n->k个,使得k个数乘机最大
//2、拆出了第k个数x(x<n)
//3、把n-x拆成k-1个数,使得其乘积最大
//答案=把n-5拆成k-1个数的情况下*x1
//4、遍历完全return 1
class Solution 
{
public:
    int dfs(int x)
    {
        if(x==0) return 1;  //表示拆分完全
        //if(x<0) return -1e9;
        int res=0;
        for(int i=1;i<x;i++)   //枚举拆分某个数x1
        {
            if(x-i>=0)  //防止越界
            res=max(res,max(i*(x-i),(dfs(x-i)*i)));
        }
        return res;
    }

    int integerBreak(int n) 
    {
        return dfs(n);
    }
};

四、记忆化搜索

1、思路解析

1)在递归(DFS)中,我们发现会出现大量的重复计算,因此我们可以使用记忆化搜索来减少时间复杂度

2)记忆化搜索的数组变量DFS中有关边界的变量基本一致

2、代码实现

//1、拆分n->k个,使得k个数乘机最大
//2、拆出了第k个数x(x<n)
//3、把n-x拆成k-1个数,使得其乘积最大
//答案=把n-5拆成k-1个数的情况下*x1
//4、遍历完全return 1
class Solution 
{
public:
    int mem[60];
    int dfs(int x)
    {
        if(x==0) return 1;  //表示拆分完全
        //if(x<0) return -1e9;
        int res=0;
        for(int i=1;i<x;i++)   //枚举拆分某个数x1
        {
            if(x-i>=0)  //防止越界
            res=max(res,max(i*(x-i),(dfs(x-i)*i)));
        }
        mem[x]=res;
        return mem[x];
    }

    int integerBreak(int n) 
    {
        return dfs(n);
    }
};

五、动态规划

//1、拆分n->k个,使得k个数乘机最大
//2、拆出了第k个数x(x<n)
//3、把n-x拆成k-1个数,使得其乘积最大
//答案=把n-5拆成k-1个数的情况下*x1
//4、遍历完全return 1

1、思路解析:

1)在DFS中,我们发现影响边界的条件有

  • 我们选择了某个数 i i i 作为拆分出的第 k k k 个数
  • k − 1 k-1 k1个数(因为这个数会受第 k k k 个数影响)

2) 遍历每个数字 i i i,从 1 1 1 n n n,计算每个 i i i 拆分后的最大乘积

3)对于每个 i i i,遍历所有可能的拆分点 j j j,尝试拆分为 j j j i − j i - j ij

4)然后选择两种拆分方式(直接拆分与递归拆分)中的最大值,更新 d p [ i ] dp[i] dp[i]

int integerBreak(int n) {
    dp[0] = dp[1] = 1;  // 初始化dp数组,dp[i]表示拆分数字i得到的最大乘积
    for (int i = 1; i <= n; i++) {  // 外层循环,i代表当前要计算的数字
        for (int j = 0; j < i; j++) {  // 内层循环,j代表拆分点
            // 计算将i拆分为j和i-j两部分后,得到的最大乘积
            dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));
        }
    }
    return dp[n];  // 返回dp[n],即拆分n得到的最大乘积
}

2、代码实现:

//1、拆分n->k个,使得k个数乘机最大
//2、拆出了第k个数x(x<n)
//3、把n-x拆成k-1个数,使得其乘积最大
//答案=把n-5拆成k-1个数的情况下*x1
//4、遍历完全return 1
class Solution 
{
public:
    int dp[60];
    // int mem[60];
    // int dfs(int x)
    // {
    //     if(x==0) return 1;  //表示拆分完全
    //     //if(x<0) return -1e9;
    //     int res=0;
    //     for(int i=1;i<x;i++)   //枚举拆分某个数x
    //     {
    //         if(x-i>=0)  //防止越界
    //         res=max(res,max(i*(x-i),(dfs(x-i)*i)));
    //     }
    //     mem[x]=res;
    //     return mem[x];
    // }

    int integerBreak(int n) 
    {
        //return dfs(n);
        dp[0]=dp[1]=1;
        //遍历每个数字 i,从 1 到 n,计算每个 i 拆分后的最大乘积
        for(int i = 1; i <= n; i ++)
        {
            //对于每个 i,遍历所有可能的拆分点 j,尝试拆分为 j 和 i - j,
            //然后选择两种拆分方式(直接拆分与递归拆分)中的最大值,更新 dp[i]。
            for(int j = 0; j < i; j ++) 
            {
                dp[i] = max(dp[i], max(j * (i - j), j * dp[i-j]));
            }
        }
        return dp[n];
    }
};

3、动态转移方程解析:

问题背景:

我们想将一个整数 n 拆分成多个正整数,使得这些数的乘积最大。通过动态规划的方式实现这个问题。

代码分析:

int integerBreak(int n)
 {
    dp[0] = dp[1] = 1;  // 初始化dp数组,dp[i]表示拆分数字i得到的最大乘积
    for (int i = 1; i <= n; i++) 
    {  
    	// 外层循环,i代表当前要计算的数字
        for (int j = 0; j < i; j++) 
        {  
        	// 内层循环,j代表拆分点
            // 计算将i拆分为j和i-j两部分后,得到的最大乘积
            dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));
        }
    }
    return dp[n];  // 返回dp[n],即拆分n得到的最大乘积
}

详细解释:

  1. 初始化部分

    dp[0] = dp[1] = 1;
    
    • dp[i] 表示将整数 i 拆分后得到的最大乘积。
    • 我们初始化 dp[0]dp[1] 都为 1,因为这两者的拆分结果是没有意义的(它们没有拆分的意义)。
  2. 外层循环:for (int i = 1; i <= n; i++)

    • 外层循环遍历所有的整数 i,从 1n
    • 对于每个 i,我们都想找出一个最优的拆分方式,来使得拆分后的乘积最大。
  3. 内层循环:for (int j = 0; j < i; j++)

    • 对于当前的 i,内层循环遍历所有可能的拆分点 j
    • 这里的 j 表示拆分点,意思是我们将 i 拆分成两个部分:ji - j
  4. 核心计算:

    dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));
    

    这行代码的作用是更新 dp[i],即当前数字 i 拆分后的最大乘积。

    • j * (i - j):这个是最直接的拆分方式。假设我们将 i 拆分为 ji - j,然后计算这两个数的乘积。

    • j * dp[i - j]:这个是递归拆分的方式。我们将 i 拆成 ji - j,然后再继续递归计算 i - j 的最大乘积。因此,dp[i - j] 就表示 i - j 的最优拆分乘积。

    通过 max 函数,我们在这两种拆分方式中选出最大值,更新 dp[i]

举例说明

假设 n = 5,我们来看看这段代码是如何工作的:

  1. 初始化:

    • dp[0] = dp[1] = 1
  2. 外层循环开始: (i从 1 到 5)

    • i = 2
      • 内层循环:j = 0,拆分 202,计算:
        • dp[2] = max(dp[2], max(0 * (2 - 0), 0 * dp[2 - 0])) = max(0, 0) = 0
      • j = 1,拆分 211,计算:
        • dp[2] = max(dp[2], max(1 * (2 - 1), 1 * dp[2 - 1])) = max(0, max(1, 1)) = 1
      • dp[2] = 1
    • i = 3
      • 内层循环:j = 0,拆分 303,计算:
        • dp[3] = max(dp[3], max(0 * (3 - 0), 0 * dp[3 - 0])) = max(0, 0) = 0
      • j = 1,拆分 312,计算:
        • dp[3] = max(dp[3], max(1 * (3 - 1), 1 * dp[3 - 1])) = max(0, max(2, 2)) = 2
      • j = 2,拆分 321,计算:
        • dp[3] = max(dp[3], max(2 * (3 - 2), 2 * dp[3 - 2])) = max(2, 2) = 2
      • dp[3] = 2
    • i = 4
      • 内层循环:j = 0,拆分 404,计算:
        • dp[4] = max(dp[4], max(0 * (4 - 0), 0 * dp[4 - 0])) = max(0, 0) = 0
      • j = 1,拆分 413,计算:
        • dp[4] = max(dp[4], max(1 * (4 - 1), 1 * dp[4 - 1])) = max(0, max(3, 3)) = 3
      • j = 2,拆分 422,计算:
        • dp[4] = max(dp[4], max(2 * (4 - 2), 2 * dp[4 - 2])) = max(3, max(4, 4)) = 4
      • dp[4] = 4
    • i = 5
      • 内层循环:j = 0,拆分 505,计算:
        • dp[5] = max(dp[5], max(0 * (5 - 0), 0 * dp[5 - 0])) = max(0, 0) = 0
      • j = 1,拆分 514,计算:
        • dp[5] = max(dp[5], max(1 * (5 - 1), 1 * dp[5 - 1])) = max(0, max(4, 4)) = 4
      • j = 2,拆分 523,计算:
        • dp[5] = max(dp[5], max(2 * (5 - 2), 2 * dp[5 - 2])) = max(4, max(6, 6)) = 6
      • j = 3,拆分 532,计算:
        • dp[5] = max(dp[5], max(3 * (5 - 3), 3 * dp[5 - 3])) = max(6, max(6, 6)) = 6
      • dp[5] = 6

最终,我们得到了 dp[5] = 6,也就是将 5 拆分成 2 + 3 时,乘积最大。


总结:

  • 外层循环:遍历每个数字 i,从 1n,计算每个 i 拆分后的最大乘积。
  • 内层循环:对于每个 i,遍历所有可能的拆分点 j,尝试拆分为 ji - j,然后选择两种拆分方式(直接拆分与递归拆分)中的最大值,更新 dp[i]

这就是如何通过动态规划计算 n 的最大乘积拆分的方式。


http://www.kler.cn/a/565818.html

相关文章:

  • deepseek 和chatgpt的论文降重方法有哪些?
  • 大模型最新面试题系列:训练篇之分布式训练
  • go设计模式
  • 【压力测试】
  • 自然语言处理NLP入门 -- 第五节词向量与嵌入
  • Ollama download DeepSeek Local Install
  • 如意玲珑应用构建指南(一):规范体系与配置文件全解析
  • 二、IDE集成DeepSeek保姆级教学(使用篇)
  • 2-1文件描述符
  • DeepSeek如何快速开发PDF转Word软件
  • DeepSeek 开源周(2025/0224-0228)进度全分析:技术亮点、调用与编程及潜在影响
  • WPF高级 | WPF 3D 图形编程基础:创建立体的用户界面元素
  • Uniapp开发微信小程序插件的一些心得
  • CSS—背景属性与盒子模型(border、padding、margin)
  • ipywidgets深度探索:从交互原理到企业级应用
  • /ɪ/音的字母或字母组合的单词
  • 增强for循环
  • IDEA集成DeepSeek,通过离线安装解决无法安装Proxy AI插件问题
  • 分布式开源协调服务之zookeeper
  • Android实现漂亮的波纹动画