区间动态规划例题:最小合并次数使数组成为回文数组问题
题目描述
给定一个长度为 n 的整数数组 arr,你可以对数组进行合并操作:每次操作可以选择数组中相邻的两个元素,将它们合并为一个元素,新元素的值为这两个元素的和。合并操作会减少数组的长度。你的目标是:通过若干次合并操作,使得最终数组成为一个回文数组(即正序和倒序相同)。问:最少需要多少次合并操作?如果无法通过合并操作使数组成为回文数组,则返回 -1。
示例 1
输入:arr = [1, 2, 3, 1]
输出:1
解释:合并索引 1 和 2 的元素(2+3=5),得到数组 [1, 5, 1],这是一个回文数组。只进行 1 次合并。
示例 2
输入:arr = [1, 2, 3, 4]
输出:-1
解释:无论如何合并,都无法得到回文数组。
示例 3
输入:arr = [2, 3, 5, 1, 1]
输出:2
解释:一种方案:先合并索引 0 和 1(2+3=5),得到 [5, 5, 1, 1];再合并索引 2 和 3(1+1=2),得到 [5, 5, 2],这不是回文。实际上,另一种方案:合并索引 1 和 2(3+5=8),得到 [2, 8, 1, 1];再合并索引 2 和 3(1+1=2),得到 [2, 8, 2],是回文。最少 2 次。
数据范围
1 <= n <= 500-1000 <= arr[i] <= 1000
解题过程
步骤 1:问题分析
题目要求将数组通过合并相邻元素,最终变成一个回文数组,且希望合并次数最少。
我们可以从区间动态规划的角度来思考:
- 定义子问题:考虑原数组的某个子区间
[l, r](闭区间),我们能否通过合并操作,将这个子区间变成一个单一的值(即合并成一个数)?如果可能,这个值是多少? - 进一步,对于整个数组,我们希望最终回文数组的每个“块”是由原数组的若干连续元素合并而成。最终回文数组的左右两端的“块”必须值相等,然后我们逐渐向中间匹配。
- 因此,大问题可以分解为:从数组两端向中间,不断匹配相等的“块”,每个块是原数组的一个连续子区间合并得到的一个值。
关键点:最终的回文数组,可能是奇数长度(中间一个块)或偶数长度(中间两个块匹配)。我们需要找到一种划分,使得从外到内匹配的相邻块两两相等,并且划分的块数尽可能少(对应合并次数最少,因为总合并次数 = 原数组长度 - 最终块数)。
定义:
- 设原数组长度为
n。 - 一次合并操作会减少一个元素,所以如果最终数组有
k个块,则合并次数 =n - k。 - 要使合并次数最少,等价于使最终块数
k最多(但块越多,匹配越难,可能无法形成回文)。
步骤 2:区间 DP 状态定义
设 dp[l][r] 表示:将子数组 arr[l...r] 通过合并操作,变成一个回文数组所需的最少合并次数。
- 如果不可能变成回文数组,则
dp[l][r] = INF(一个很大的数,表示不可能)。 - 最终答案就是
dp[0][n-1],如果是 INF 则返回 -1。
状态转移思考:
对于一个区间 [l, r],我们考虑它的最终形态:
- 如果
l == r,已经是单个元素,是回文,不需要合并,dp[l][r] = 0。 - 如果
l < r,我们有两种可能:- 情况 A:区间最终合并成一个数(即整个区间合并成一个块)。这需要区间内所有元素的和能够通过合并得到,并且这个块本身是回文(单个元素是回文)。这种情况下的合并次数是
r - l(因为要从长度r-l+1合并到 1,需要(r-l+1) - 1 = r-l次合并)。但注意:并不是所有区间都能合并成一个数,只有当我们能够通过合并相邻元素,使得最终左右两半“匹配”合并时才行。实际上,这种情况可以视为区间最终变成单一值的特殊情况,我们稍后讨论。 - 情况 B:区间最终由多个块组成,且是回文。那么回文的最外层两个块必须相等,并且分别由区间左端的一段和右端的一段合并得到。我们设左端块由子区间
[l, i]合并得到,右端块由子区间[j, r]合并得到,且这两个块的值相等。那么剩下的中间部分[i+1, j-1]也必须自身是回文。这样就有:
但这样很复杂,因为dp[l][r] = min( dp[l][i] + dp[j][r] + (i-l) + (r-j) + dp[i+1][j-1]? )dp[l][i]表示将[l,i]合并成回文的最少次数,而不是合并成一个值的次数。 - 我们换一种思路:先预处理出每个区间合并成一个数是否可能,以及这个数的值。
- 情况 A:区间最终合并成一个数(即整个区间合并成一个块)。这需要区间内所有元素的和能够通过合并得到,并且这个块本身是回文(单个元素是回文)。这种情况下的合并次数是
步骤 3:预处理区间和与合并可能性
定义 sum[l][r] 为区间 [l, r] 的元素和(容易用前缀和 O(1) 得到)。
定义 canMergeToOne[l][r] 表示区间 [l, r] 能否合并成一个数,以及这个数是多少。但这里“合并成一个数”是指通过相邻合并,最终得到一个数。实际上,因为合并是相邻相加,所以最终得到的数必然是区间和,但前提是我们能通过某种合并顺序,最终合并成一个数。
关键观察:对于任意区间,我们总是可以合并成一个数,因为相邻元素可以两两合并,最终合并成一个。所以 canMergeToOne[l][r] 总是 true,且这个数就是区间和 sum[l][r]。
但是,我们关心的是:在形成回文的过程中,我们需要匹配左右两端的块的值。所以,我们可以定义 val[l][r] 表示区间 [l, r] 合并成一个数时的值(即区间和)。这个值用于判断左右块是否相等。
步骤 4:改进 DP 状态定义
由于最终回文数组的每个块都是原数组的某个连续子区间合并成一个数得到的,我们可以这样定义状态:
设 dp[l][r] 表示:将子数组 [l, r] 合并成一个回文数组所需的最少合并次数,且这个回文数组的每个块都是原数组的某个连续子区间合并成一个数得到的。
注意:这里 [l, r] 合并成回文数组后,可能由多个块组成,也可能只有一个块。
转移方程:
-
如果区间只有一个元素(
l == r),已经是回文,dp[l][r] = 0。 -
如果区间可以整个合并成一个数(即作为一个块),并且这个块是回文(总是),那么此时合并次数 =
r - l(即区间长度减1次合并)。这是一种可能:dp[l][r] = r - l。 -
否则,我们需要考虑将区间分成左右两半,使最外层的两个块相等,然后中间部分自成回文。具体来说,我们枚举左块结束位置
i(l <= i < r)和右块起始位置j(i < j <= r),使得左块[l, i]合并成一个数等于右块[j, r]合并成一个数。然后中间部分[i+1, j-1]必须自身是回文(即dp[i+1][j-1]有解)。那么总合并次数为:左块合并次数 = (i - l) // 将 [l, i] 合并成一个数 右块合并次数 = (r - j) // 将 [j, r] 合并成一个数 中间部分合并次数 = dp[i+1][j-1] 总次数 = (i - l) + (r - j) + dp[i+1][j-1]我们需要对所有可行的
i, j取最小值。 -
另外,如果区间可以合并成一个数,但作为单个块的回文,其合并次数是
r-l,这已经包含在情况2中。
边界条件:
- 当
l > r时,区间为空,我们认为它是回文,且合并次数为 0。在代码中,当i+1 > j-1时,中间部分为空,dp[i+1][j-1]视为 0。
初始化:
- 所有
dp[l][r]初始化为 INF(一个很大的数,比如n+1或更大)。 - 对于
l == r,dp[l][r] = 0。
答案:
dp[0][n-1],如果为 INF 则返回 -1,否则返回该值。
步骤 5:算法细节
我们需要快速知道区间 [l, i] 和 [j, r] 合并成一个数的值是否相等,即判断 sum[l][i] == sum[j][r]。
预处理前缀和数组 prefix,使得 sum[l][r] = prefix[r+1] - prefix[l]。
时间复杂度:
- 状态数 O(n²)。
- 转移需要枚举
i和j,最坏 O(n²) 次,总复杂度 O(n⁴),对于 n=500 来说太大(500⁴=6.25×10¹⁰ 不可接受)。 - 需要优化。
步骤 6:优化转移
我们可以优化枚举:
对于每个区间 [l, r],我们只枚举左块的结束位置 i,然后根据左块的值 sum[l][i],我们需要找到右块 [j, r] 使得 sum[j][r] 等于这个值。
我们可以用哈希表记录每个右端点 r,以 r 结尾的区间和的值对应的左端点。但这里我们也可以从右向左枚举 j,但这样仍然是 O(n³)。
实际上,我们可以用 DP 的常见技巧:当外层两个块相等时,中间部分必须已经计算好。我们可以从小区间向大区间递推。
另一种思路:定义 pair[l][r] 表示区间 [l, r] 能否合并成一个数(总是能,值为 sum),但我们只在外层匹配时使用。
考虑到 n=500,O(n³) 是可行的(500³=1.25×10⁸,稍大但可接受,需注意常数)。我们可以实现 O(n³) 的算法。
O(n³) 算法:
- 计算前缀和数组。
- 初始化
dp[l][r] = INF,dp[i][i] = 0。 - 按区间长度
len从 2 到 n 枚举区间。 - 对每个区间
[l, r]:- 先考虑整个区间合并成一个块:
dp[l][r] = min(dp[l][r], r-l)。 - 然后枚举分割点
i从l到r-1,表示左块为[l, i],右块为某个[j, r]使得sum[l][i] == sum[j][r]。为了找到这样的j,我们可以枚举j从i+1到r,检查sum[l][i] == sum[j][r]。如果相等,则中间部分为[i+1, j-1],其 dp 值已知(因为长度更小)。则:if sum[l][i] == sum[j][r]: if i+1 <= j-1: dp[l][r] = min(dp[l][r], (i-l) + (r-j) + dp[i+1][j-1]) else: dp[l][r] = min(dp[l][r], (i-l) + (r-j)) // 中间为空
- 先考虑整个区间合并成一个块:
- 最终答案为
dp[0][n-1]或 -1。
复杂度:枚举区间 O(n²),对每个区间枚举 i 和 j 最坏 O(n²),总 O(n⁴)?不对,我们枚举 i 和 j 是嵌套的,所以是 O(n³)。因为区间长度固定时,枚举 l, i, j,但 l 固定时,i 和 j 的枚举是 O(n²),总 O(n³)。实际上,对于每个区间 [l, r],我们枚举 i 从 l 到 r-1,对每个 i 枚举 j 从 i+1 到 r,所以是 O((r-l)²),对所有区间求和,总复杂度约为 O(n³)。
n=500 时,500³=1.25e8,在优化良好的情况下(如用前缀和 O(1) 比较区间和)可以在规定时间(如 2 秒)内通过。
步骤 7:示例推演
以 arr = [1, 2, 3, 1] 为例:
- n=4,前缀和 prefix=[0,1,3,6,7]。
- 初始化 dp[i][i]=0。
- len=2:
- [0,1]: sum=3,整个合并 dp=1。枚举 i=0,左块[0,0]和=1,找 j 使 [j,1] 和=1,不存在。dp[0][1]=1。
- [1,2]: sum=5,dp=1,左块[1,1]和=2,找 j 使 [j,2] 和=2,不存在。dp=1。
- [2,3]: sum=4,dp=1,左块[2,2]和=3,找 j 使 [j,3] 和=3,j=3 时 [3,3] 和=1 不相等,无。dp=1。
- len=3:
- [0,2]: sum=6,整个合并 dp=2。枚举 i=0,左块[0,0]和=1,找 j 使 [j,2] 和=1,无。i=1,左块[0,1]和=3,找 j 使 [j,2] 和=3,j=2 时 [2,2] 和=3 相等,中间[2,1]为空,所以 dp= (i-l)+(r-j) = (1-0)+(2-2)=1。所以 dp[0][2]=min(2,1)=1。
- [1,3]: sum=6,整个合并 dp=2。枚举 i=1,左块[1,1]和=2,找 j 使 [j,3] 和=2,无。i=2,左块[1,2]和=5,找 j 使 [j,3] 和=5,无。dp=2。
- len=4: [0,3]: sum=7,整个合并 dp=3。枚举 i=0,左块[0,0]和=1,找 j 使 [j,3] 和=1,j=3 时 [3,3] 和=1 相等,中间[1,2] 的 dp[1][2]=1,所以总次数 = (0)+(3-3)+1=1。所以 dp[0][3]=min(3,1)=1。
最终答案为 1。
步骤 8:实现注意
- 用 INF 表示不可能,如
n+1。 - 注意中间区间为空时,dp 值为 0。
- 如果最终 dp[0][n-1] >= INF,返回 -1。
步骤 9:代码框架(伪代码)
function minMergesToPalindrome(arr):
n = arr.length
prefix = [0] * (n+1)
for i in range(n):
prefix[i+1] = prefix[i] + arr[i]
function sum(l, r): return prefix[r+1] - prefix[l]
dp = array of size n x n, filled with INF
for i in range(n):
dp[i][i] = 0
for length from 2 to n:
for l from 0 to n-length:
r = l + length - 1
dp[l][r] = r - l // 整个区间合并成一个块
for i from l to r-1:
left_sum = sum(l, i)
for j from i+1 to r:
if sum(j, r) == left_sum:
mid_cost = dp[i+1][j-1] if i+1 <= j-1 else 0
total = (i - l) + (r - j) + mid_cost
dp[l][r] = min(dp[l][r], total)
return dp[0][n-1] if dp[0][n-1] < INF else -1
步骤 10:复杂度与优化
- 时间复杂度 O(n³),空间 O(n²)。
- 对于 n=500,O(1.25e8) 在 C++ 中可过,Python 可能需优化(如用字典记录右块和对应左端点)。
- 进一步优化:可以预处理每个右端点 r,记录区间和到左端点的映射,但实现较复杂。本题 O(n³) 是标准解法。
最终答案:通过上述 DP 计算得到最少合并次数,若不可行则为 -1。
总结:该问题通过区间 DP,将大区间回文拆分成外层两相等块和中间回文部分,枚举分界点求解。关键是定义 dp[l][r] 为区间变成回文的最少合并次数,并注意区间和相等的判断。