LeetCode 1000. Minimum Cost to Merge Stones【记忆化搜索,动态规划,数组】困难

news2024/11/28 4:09:33

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

There are n piles of stones arranged in a row. The ith pile has stones[i] stones.

A move consists of merging exactly k consecutive piles into one pile, and the cost of this move is equal to the total number of stones in these k piles.

Return the minimum cost to merge all piles of stones into one pile. If it is impossible, return -1.

Example 1:

Input: stones = [3,2,4,1], k = 2
Output: 20
Explanation: We start with [3, 2, 4, 1].
We merge [3, 2] for a cost of 5, and we are left with [5, 4, 1].
We merge [4, 1] for a cost of 5, and we are left with [5, 5].
We merge [5, 5] for a cost of 10, and we are left with [10].
The total cost was 20, and this is the minimum possible.

Example 2:

Input: stones = [3,2,4,1], k = 3
Output: -1
Explanation: After any merge operation, there are 2 piles left, and we can't merge anymore.  So the task is impossible.

Example 3:

Input: stones = [3,5,1,2,6], k = 3
Output: 25
Explanation: We start with [3, 5, 1, 2, 6].
We merge [5, 1, 2] for a cost of 8, and we are left with [3, 8, 6].
We merge [3, 8, 6] for a cost of 17, and we are left with [17].
The total cost was 25, and this is the minimum possible.

Constraints:

  • n == stones.length
  • 1 <= n <= 30
  • 1 <= stones[i] <= 100
  • 2 <= k <= 30

题意:有 N 堆石头排成一排,第 i 堆中有 stones[i] 块石头。每次移动(move)需要将连续的 K 堆石头合并为一堆,而这个移动的成本为这 K 堆石头的总数。找出把所有石头合并成一堆的最低成本。如果不可能,返回 -1 。


相似题目(区间 DP)

  1. 猜数字大小 II
  2. 最长回文子序列
  3. 多边形三角剖分的最低得分
  4. 让字符串成为回文串的最少插入次数
  5. 切棍子的最小成本

解法1 记忆化搜索

不可能合并为一堆的情况是最简单的。从 n n n 堆变成 1 1 1 堆,需要减少 n − 1 n - 1 n1 堆,每次合并会减少 k − 1 k - 1 k1 堆,所以 n − 1 n - 1 n1 必须是 k − 1 k - 1 k1 的倍数

再通过一个例子思考能合并的情况,比如一开始有 7 7 7 堆石头, k = 3 k = 3 k=3 。最后一步会发生什么?很简单,由 3 3 3 堆合并为 1 1 1 堆。注意:合并石头不会改变石头的总数,所以合并这 3 3 3 堆石头的成本等于 3 3 3 堆石头的总数、也等于原来的 7 7 7 堆石头的总数(后面会用到这个结论)。
200
继续思考:这 3 3 3 堆中的第一堆是怎么合并的?有以下几种情况:

  1. 就是 s t o n e s [ 0 ] stones[0] stones[0] ,无需合并
  2. s t o n e s [ 0 : 2 ] stones[0:2] stones[0:2] 进行 1 1 1 次合并得到
  3. s t o n e s [ 0 : 4 ] stones[0:4] stones[0:4] 通过 2 2 2 次合并得到。具体这两次是怎么合并的,还需要继续计算。
  4. ……

对于右边剩余的石头堆,需要计算的问题是:把这些石头堆合并为 2 2 2 堆的最低成本

到这里了,应该能从中总结出一个和原问题相似的子问题了。找到了原问题和子问题的关系,就能通过递归(记忆化搜索)的方式解决。显然,问题是「求出把 s t o n e s [ i : j ] stones[i:j] stones[i:j] 合并为 p p p 堆的最低成本」,定义 d f s ( i , j , p ) dfs(i,j,p) dfs(i,j,p) 表示这个问题的解。

于是对 7 7 7 堆石头有 d f s ( 0 , 6 , 1 ) dfs(0, 6, 1) dfs(0,6,1) ,它等于 d f s ( 0 , 6 , 3 ) + ∑ i = 0 6 s t o n e s [ i ] dfs(0, 6, 3) +\displaystyle \sum^6_{i=0} stones[i] dfs(0,6,3)+i=06stones[i] ,而 d f s ( 0 , 6 , 3 ) dfs(0, 6, 3) dfs(0,6,3) 又等于下面几种情况的最小值:
600
总结如下:
d f s ( i , j , 1 ) = d f s ( i , j , k ) + ∑ q = i j 子数组和可以用前缀和优化 d f s ( i , j , p ) = min ⁡ m = i + ( k − 1 ) x { d f s ( i , m , 1 ) + d f s ( m + 1 , j , p − 1 ) } p ≥ 2 \begin{matrix} dfs(i, j, 1) = dfs(i, j, k) + \sum^j_{q=i} \quad 子数组和可以用前缀和优化 \\ dfs(i, j, p) = \min_{m=i+(k-1)x} \bigg\{ dfs(i, m, 1) + dfs(m + 1, j, p -1) \bigg\} \quad p \ge 2\end{matrix} dfs(i,j,1)=dfs(i,j,k)+q=ij子数组和可以用前缀和优化dfs(i,j,p)=minm=i+(k1)x{dfs(i,m,1)+dfs(m+1,j,p1)}p2

  • 递归边界: d f s ( i , i , 1 ) = 0 dfs(i, i,1) =0 dfs(i,i,1)=0 ,只有一堆石头,不用合并,代价为 0 0 0
  • 递归入口: d f s ( 0 , n − 1 , 1 ) dfs(0, n-1,1) dfs(0,n1,1) 把所有石头堆合并为一堆的最小代价

代码实现时,由于整个递归中有大量重复递归调用(递归入参相同),且递归函数没有副作用(同样的入参无论计算多少次,算出来的结果都是一样的),因此可用记忆化搜索来优化:

  • 如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 d p dp dp 数组(或哈希表)中。
  • 如果一个状态不是第一次遇到,那么直接返回 d p dp dp 中保存的结果。

问:为什么只考虑分出 1 1 1 堆和 p − 1 p-1 p1 堆,而不考虑分出 x x x 堆和 p − x p-x px 堆?
答:无需计算,因为 p − 1 p-1 p1 堆继续递归又可以分出 1 1 1 堆和 p − 2 p-2 p2 堆,和之前分出的 1 1 1 堆组合,就已经能表达出「分出 2 2 2 堆和 p − 2 p−2 p2 堆」的情况了。其他同理。所以只需要考虑分出 1 1 1 堆和 p − 1 p-1 p1 堆。

class Solution {
public:
    int mergeStones(vector<int> &stones, int k) {
        int n = stones.size();
        if ((n - 1) % (k - 1) != 0) return -1; // 不能合并为一堆
        int sum[n + 1]; memset(sum, 0, sizeof(sum));
        for (int i = 0; i < n; ++i) sum[i + 1] = sum[i] + stones[i];
        int dp[n][n][k + 1]; memset(dp, -1, sizeof(dp)); // -1表示没计算过
        // dp[i][j][p]: 把stones[i:j]合并为p堆的最低成本
        function<int(int, int, int)> dfs = [&](int i, int j, int p) -> int {
            int &ans = dp[i][j][p];
            if (ans != -1) return ans;
            if (p == 1) { // 将stones[i:j]中的k堆石头合并为1堆,成本为这K堆石头数的和 
                return ans = i == j ? 0 : dfs(i, j, k) + sum[j + 1] - sum[i]; // 当然,先要将stones[i:j]的石头合并为k堆
            }
            ans = INT_MAX; 
            for (int m = i; m < j; m += (k - 1)) // [m,m+k)
                ans = min(ans, dfs(i, m, 1) + dfs(m + 1, j, p - 1));
            return ans;
        };
        return dfs(0, n - 1, 1);
    }
}; 

复杂度分析:

  • 时间复杂度: O ( n 3 ) O(n^3) O(n3) ,其中 n n n s t o n e s stones stones 的长度。这里状态个数为 n 2 k n^2k n2k 个,单个状态的计算时间为 O ( n k ) O(\dfrac{n}{k}) O(kn) ,因此时间复杂度为 O ( n 3 ) O(n^3) O(n3)
  • 空间复杂度: O ( n 2 k ) O(n^2k) O(n2k)

下面进行优化:把 d f s ( i , j , 1 ) dfs(i, j,1) dfs(i,j,1) 改写成下面的式子, p p p 就取不到 k k k 了,即 1 ≤ p < k 1\le p < k 1p<k
d f s ( i , j , 1 ) = min ⁡ m = i + ( k − 1 ) x ) { d f s ( i , m , 1 ) + d f s ( m + 1 , j , k − 1 ) } + ∑ q = i j s t o n e s [ q ] dfs(i,j,1) = \min_{m=i+(k-1)x)} \bigg \{dfs(i, m, 1) + dfs(m + 1, j, k - 1) \bigg\} + \sum^j_{q=i} stones[q] dfs(i,j,1)=m=i+(k1)x)min{dfs(i,m,1)+dfs(m+1,j,k1)}+q=ijstones[q]

由于枚举 m m m 时,保证 m − i m - i mi k − 1 k - 1 k1 的倍数:

  • 所以对于 d f s ( i , j , 1 ) dfs(i, j, 1) dfs(i,j,1) 来说, j − i j - i ji 必然是 k − 1 k - 1 k1 的倍数。
  • 而对于 d f s ( i , j , p )   ( p ≥ 2 ) dfs(i, j, p)\ (p\ge 2) dfs(i,j,p) (p2) 来说, j − k j - k jk 必然不是 k − 1 k - 1 k1 的倍数(否则可以合并成一堆)。
  • 所以通过判断 j − i j - i ji 是否为 k − 1 k - 1 k1 的倍数,就能知道 p = 1 p = 1 p=1 还是 p ≥ 2 p \ge 2 p2 。所以第三个参数 p p p 其实是多余的

化简后, d f s ( i , j ) dfs(i,j) dfs(i,j) 表示把从 i i i j j j 的石头堆合并到小于 k k k的最小代价:
d f s ( i , j ) = { min ⁡ m = i + ( k − 1 ) x ) { d f s ( i , m ) + d f s ( m + 1 , j ) } + ∑ q = i j s t o n e s [ q ] ( j − i )   m o d   ( k − 1 ) = 0 min ⁡ m = i + ( k − 1 ) x ) { d f s ( i , m ) + d f s ( m + 1 , j ) } ( j − i )   m o d   ( k − 1 ) ≠ 0 dfs(i,j)= \begin{cases} \min_{m=i+(k-1)x)} \bigg \{dfs(i, m) + dfs(m + 1, j) \bigg\} + \sum^j_{q=i} stones[q] \quad& (j-i)\bmod (k-1)=0\\ \min_{m=i+(k-1)x)} \bigg \{dfs(i, m) + dfs(m + 1, j) \bigg\} \quad& (j-i)\bmod (k-1)\ne 0 \end{cases} dfs(i,j)= minm=i+(k1)x){dfs(i,m)+dfs(m+1,j)}+q=ijstones[q]minm=i+(k1)x){dfs(i,m)+dfs(m+1,j)}(ji)mod(k1)=0(ji)mod(k1)=0

  • 递归边界: d f s ( i , i ) = 0 dfs(i,i) =0 dfs(i,i)=0 ,只有一堆石头,无需合并,代价为 0 0 0
  • 递归入口: d f s ( 0 , n − 1 ) dfs(0, n-1) dfs(0,n1) ,把所有石头堆合并成一堆的最小代价
class Solution {
public:
    int mergeStones(vector<int> &stones, int k) {
        int n = stones.size();
        if ((n - 1) % (k - 1)) return -1; // 无法合并成一堆
        int s[n + 1];
        s[0] = 0;
        for (int i = 0; i < n; i++) s[i + 1] = s[i] + stones[i]; // 前缀和
        int dp[n][n];
        memset(dp, -1, sizeof(dp)); // -1 表示还没有计算过
        function<int(int, int)> dfs = [&](int i, int j) -> int {
            if (i == j) return 0; // 只有一堆石头,无需合并
            int &ans = dp[i][j]; // 注意这里是引用,下面会直接修改 dp[i][j]
            if (ans != -1) return ans;
            ans = INT_MAX;
            for (int m = i; m < j; m += k - 1) // 枚举哪些石头堆合并成第一堆
                ans = min(ans, dfs(i, m) + dfs(m + 1, j));
            if ((j - i) % (k - 1) == 0) ans += s[j + 1] - s[i]; // 可以合并成一堆
            return ans;
        };
        return dfs(0, n - 1);
    }
};

解法2 动态规划

把解法1的 d f s dfs dfs 改为 f f f 数组,把递归改为循环即可。需要注意循环的顺序:

  • 由于 i < m + 1 i < m+1 i<m+1 f [ i ] f[i] f[i] 要能从 f [ m + 1 ] f[m + 1] f[m+1] 转移过来,必须先计算出 f [ m + 1 ] f[m+1] f[m+1] ,所以 i i i 要倒序枚举;
  • 由于 j > m j > m j>m f [ i ] [ j ] f[i][j] f[i][j] 要能从 f [ i ] [ m ] f[i][m] f[i][m] 转移过来,必须先计算出 f [ i ] [ m ] f[i][m] f[i][m] ,所以 j j j 要正序枚举。
class Solution {
public:
    int mergeStones(vector<int> &stones, int k) {
        int n = stones.size();
        if ((n - 1) % (k - 1)) return -1; // 无法合并成一堆  
        int s[n + 1];
        s[0] = 0;
        for (int i = 0; i < n; i++) s[i + 1] = s[i] + stones[i]; // 前缀和 
        int f[n][n];
        for (int i = n - 1; i >= 0; --i) {
            f[i][i] = 0;
            for (int j = i + 1; j < n; ++j) {
                f[i][j] = INT_MAX;
                for (int m = i; m < j; m += k - 1)
                    f[i][j] = min(f[i][j], f[i][m] + f[m + 1][j]);
                if ((j - i) % (k - 1) == 0) // 可以合并成一堆
                    f[i][j] += s[j + 1] - s[i];
            }
        }
        return f[0][n - 1];
    }
};

复杂度分析:

  • 时间复杂度: O ( n 3 k ) O(\dfrac{n^3}{k}) O(kn3) ,其中 n n n s t o n e s stones stones 的长度。这里状态个数为 n 2 n^2 n2 个,单个状态的计算时间为 O ( n k ) O(\dfrac{n}{k}) O(kn) ,因此时间复杂度为 O ( n 3 k ) O(\dfrac{n^3}{k}) O(kn3)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/448953.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++类和对象】类和对象(中):析构函数 {析构函数的概念及特性,编译器自动生成的析构函数,构造析构的顺序}

三、析构函数 3.1 概念 通过前面构造函数的学习&#xff0c;我们知道一个对象是怎么来的&#xff0c;那一个对象又是怎么没呢的&#xff1f; 析构函数&#xff1a;与构造函数功能相反&#xff0c;析构函数不是完成对对象本身的销毁&#xff0c;局部对象随函数栈帧的销毁而销毁…

chatGpt自动写文章-chatGpt自动写文章软件

怎么用GPT写文章 使用GPT写文章需要按照以下步骤进行&#xff1a; 确定文章主题&#xff1a;首先需要明确文章的主题&#xff0c;这有助于GPT更好地了解你想要表达的内容&#xff0c;并生成更有针对性的文本。 准备开头和结尾&#xff1a;根据文章主题&#xff0c;准备好文章开…

【参考文献不爆红】Word的多个参考文献连续交叉引用([1] [3]改为[1-3])

文章目录 1. 参考文献格式2. 引入参考文献3. Word的多个参考文献连续交叉引用&#xff08;[1] [3]改为[1-3]&#xff09;3.1引入两个参考文献3.2 引入三个参考文献3.3 知识科普 1. 参考文献格式 参考教程 全选参考文献–>编号的小三角–>自定义编号&#xff0c;修改为[]…

PostMan笔记(三)自动化测试

1. 简介 Postman是一款功能强大的API开发工具&#xff0c;也是一款流行的自动化测试工具。它提供了多种测试功能&#xff0c;包括测试脚本、预请求脚本和测试集合等。 1.1 测试脚本 测试脚本是Postman中用于自动化测试的核心部分。它可以使用JavaScript语言编写&#xff0c;…

使用VScode编写C语言程序 环境安装配置 保姆级教程

Visual Studio Code可通过安装插件来支持C、C#、Python、PHP等语言&#xff0c;使用的工程师越来越多&#xff0c;本文介绍如何使用VS Code进行C语言的编译与调试 目录 一 vsCode配置C/C环境 1. vsCode下载和安装 2. 安装vsCode 二 MinGW编译器下载和配置 1. 下载编译器M…

c++积累8-右值引用、移动语义

1、右值引用 1.1 背景 c98中的引用很常见&#xff0c;就是给变量取个别名&#xff0c;具体可以参考c积累7 在c11中&#xff0c;增加了右值引用的概念&#xff0c;所以c98中的引用都称为左值引用 1.2 定义 右值引用就是给右值取个名字&#xff0c;右值有了名字之后就成了普通…

【达摩院OpenVI】基于流感知的视频目标检测网络LongShortNet

论文&代码 论文链接&#xff1a;[arxiv]代码&应用&#xff1a; 开源代码&#xff1a;[github code]开源应用&#xff1a;[modelscope] 背景介绍 传统视频目标检测&#xff08;Video Object Detection, VOD&#xff09;任务以一段视频作为输入&#xff0c;利用视频的…

项目上线|慕尚集团携手盖雅工场,用数字化推动人效持续提升

过去十年&#xff0c;中国零售业以前所未有的速度被颠覆、被重塑&#xff0c;数字化则是其中重要的推动要素。 随着数字化转型的深入&#xff0c;零售企业的数字化不再局限于布局线上渠道&#xff0c;且更关乎其背后企业核心运营能力的全链路数字化改造。而贯穿于运营全链路的…

mybatis缓存的详细理解和使用

mybatis缓存的简单理解和使用 mybatis缓存数据的介绍 缓存是存在于内存中的临时数据&#xff0c;使用缓存的目的是减少和数据库的数据进行交互的次数&#xff0c;提高执行效率。像很多持久化框架一样&#xff0c;Mybatis也提供了缓存策略&#xff0c;通过缓存策略来减少数据库…

RflySim平台使用篇 | Coptersim系列教程(三)

# 导读 # CopterSim作为RflySim平台核心仿真软件&#xff0c;其主要实现两部分功能&#xff1a;模型和通信&#xff0c;掌握CopterSim使用方法即可轻松运行多旋翼运动动态模型&#xff0c;并连同其他软件构成软/硬件在环仿真。本篇教程将详细介绍coptersim仿真log数据获取。 Co…

webpack plugin源码解析(六) CompressionWebpackPlugin

文章目录 作用涉及 webpack API处理 asset 钩子compilation.hooks.processAssets返回或新建缓存&#xff1a;compilation.getCache返回 asset 文件信息&#xff1a;compilation.getAsset文件名匹配函数&#xff1a;compiler.webpack.ModuleFilenameHelpers.matchObject模版字符…

科研热点|8本期刊被剔除SCIE,4月最新SCIE/SSCI目录已更新 (附下载)~

2023年4月18日&#xff0c;科睿唯安更新了Web of Science核心期刊目录&#xff0c;此次更新后SCIE期刊目录共包含9505本期刊&#xff0c;SSCI期刊目录共包含3557本期刊。此次4月SCIE & SSCI期刊目录更新&#xff0c;与3月更新相比 (警惕&#xff01;多达50本SCI/SSCI被剔除…

Kafka中时间轮分析与Java实现

仿kafka实现java版时间轮_java实现时间轮算法_Hekliu的博客-CSDN博客 https://www.cnblogs.com/softlin/p/7426083.html https://blog.csdn.net/happyjacob/article/details/128518700 一、背景 在Kafka中应用了大量的延迟操作但在Kafka中 并没用使用JDK自带的Timer或是Dela…

m3u8转mp4下载,有URL,IV

1、背景 在线m3u8现在是主流加密方式的视频。 2、下载m3u8视频难点 首先需要连接m3u8文件格式,这个自行百度,其次加密方式确定和key以及iv。如果没有加密直接找一个在线的m3u8转mp4就可以,但是问题就是很多带加密,而且key不是m3m8中key URL返回的数据,市面上软件无法直…

基于matlab评估机场监控雷达上 5G 新无线电 (NR) 信号的干扰

一、前言 随着5G NR系统的频率范围超出LTE中使用的频段&#xff0c;频谱管理变得更加复杂。对扩大5G覆盖范围的需求是由更高的数据速率和更低的延迟的好处推动的。新5G基站的实施反过来又推动了了解这些信号如何影响在相同频段上运行的已安装系统的需求。其中一个系统是空中交通…

类对象

一、类初识 类&#xff1a;表示一种事物所具有的共同特征和行为 对象&#xff1a;一个类的实例 如下图&#xff0c;通过狗这个类进行详解 这是一个Dog类 对象&#xff1a;斗牛犬、小猎犬、牧羊犬 类中的属性&#xff1a;breed(品种)、size(大小)、color(颜色)、age(年龄)、 …

OpenCv基础之绘图及几何变换实例

文章目录 OpenCv基础之绘图及几何变换实例创建背景图线段绘制矩形绘制圆绘制椭圆绘制绘制多边形添加文字 几何变换图像平移图像缩放图像旋转仿射变换透视变化 OpenCv基础之绘图及几何变换实例 绘图在图像处理中&#xff0c;主要是在处理完图像后&#xff0c;将图像中的目标进行…

Python算法设计 - 哈夫曼编码

目录 一、哈夫曼树二、哈夫曼编码三、Python算法实现四、作者Info 一、哈夫曼树 上图是根据“this is an example of a huffman tree”中得到的字母频率来建构的哈夫曼树 二、哈夫曼编码 多年来&#xff0c;哈夫曼编码在统计数据压缩方面是非常先进的&#xff0c;应当指出&am…

C# 类库打包推送到nuget

步骤1&#xff1a;注册nuget 账号&#xff0c;可以使用outlook邮箱进行注册 步骤2&#xff1a;建立 apikey 名字自己起&#xff0c;Glob Pattern 填入“*” 步骤3&#xff1a;把程序打包&#xff0c;打包很简单右键vs2022 打包就好 但是注意*.csproj 文件修改,修改目的是为了…

IGS 产品长文件命名方式简介

文章目录 Part.I IntroductionPart.II 文件命名方式Chap.I 官方说明Chap.II 实例 Reference Part.I Introduction 2022 年 11 月 30 日&#xff08;DOY 331, GPSWD 22380&#xff09;及以后&#xff0c;IGS 的参考框架从 IGS-14 切换为 用 IGS-20&#xff0c;最新的卫星和地…