# 区间DP

## 枚举区间

在线性DP的基础上，区间DP能找到**区间的最值**。简单的区间DP复杂度为**O(n²)**，如果把$$f(i, j)$$定义为`[i, j]`区间的答案，那么它取决于与它临近解的值：

$$f\[i, j] = g (f\[i+1, j], f\[i, j-1], f\[i+1, j-1])$$&#x20;

比如最长回文子串的核心代码：

```python
for l in range(1, N+1):
    for i in range(N-l+1):
        j = i+l-1
        if l<=2: f[i][j] = (s[i]==s[j])
        else: f[i][j] = (f[i+1][j-1] and s[i] == s[j])
        if f[i][j] and l>len(res): res = s[i:j+1]
return res
```

## 枚举区间和中点

更复杂的情况，如果**问题的大小随着不同阶段变化而变化**，那很可能用区间DP来枚举这个动态过程。这个时候时间复杂度为**O(n³)**，那么状态转移方程一般可以写成：

$$f(i, j) = \max\_k \[f(j, k) + f(k+1, j) + \text{cost}]$$&#x20;

注意上述两种情况中，都是只有区间的长度在不断变大。所以我们一开始都先枚举区间长度 $$l$$ 来符合DP的自下而上的要求。

**参考模版**

```python
for l in range(N):
    for i in range(N-l):        # 枚举长度为l的[i, j)区间
        j = i+l
        for k in range(i+1, j): # 枚举中点i+1, i+2, ..., j-1
            # do something.
```

用记忆化搜索会更好写些：

```python
@cache
def dfs(i, j):
    if i==j: # do something
    res = 0
    for k in range(i+1, j):
        res = max(res, dfs(i, k)+dfs(k, j)+cost)
    return res
```

## 例题

枚举区间：

* [**预测赢家**](https://leetcode-cn.com/problems/predict-the-winner/)

枚举中点：

* [**切棍子的最小成本**](https://leetcode-cn.com/problems/minimum-cost-to-cut-a-stick/)
* [**戳气球**](https://leetcode-cn.com/problems/burst-balloons/submissions/)
* [**奇怪的打印机**](https://leetcode-cn.com/problems/strange-printer/)
* [**移除盒子**](https://leetcode-cn.com/problems/remove-boxes/)**：**$$f\[i, j, k]$$ = 移除区间 $$n\_i, n\_{i+1}..., n\_j$$ 盒子加上该区间右边有 $$p$$ 个与 $$n\_j$$ 相同的盒子的最大积分

{% tabs %}
{% tab title="预测赢家" %}

```python
给定一个表示分数的非负整数数组。 玩家 1 从数组任意一端拿取一个分数，
随后玩家 2 继续从剩余数组任意一端拿取分数，然后玩家 1 拿，…… 。
每次一个玩家只能拿取一个分数，分数被拿取之后不再可取。
直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。

给定一个表示分数的数组，预测玩家1是否会成为赢家。
你可以假设每个玩家的玩法都会使他的分数最大化。

输入：[1, 5, 2]
输出：False
解释：一开始，玩家1可以从1和2中进行选择。
如果他选择 2（或者 1 ），那么玩家 2 可以从 1（或者 2 ）和 5 中进行选择。
如果玩家 2 选择了 5 ，那么玩家 1 则只剩下 1（或者 2 ）可选。
所以，玩家 1 的最终分数为 1 + 2 = 3，而玩家 2 为 5 。
因此，玩家 1 永远不会成为赢家，返回 False 。
```

```python
N = len(nums)
f = [[0]*(N+1) for _ in range(N+1)]
for l in range(N):
    for i in range(N-l):
        j = i+l
        f[i][j] = max(nums[i]-f[i+1][j],
                      nums[j]-f[i][j-1])
return f[0][N-1] >= 0

# Memoization
@cache
def maxScore(i, j):
    if i==j: return nums[i]
    left = nums[i] - maxScore(i+1, j)
    right = nums[j] - maxScore(i, j-1)
    return max(left, right)
return maxScore(0, len(nums)-1) >= 0
```

{% endtab %}

{% tab title="切棍子的最小成本" %}

```python
有一根长度为 n 个单位的木棍，棍上从 0 到 n 标记了若干位置。
给你一个整数数组 cuts ，其中 cuts[i] 表示你需要将棍子切开的位置。

你可以按顺序完成切割，也可以根据需要更改切割的顺序。

每次切割的成本都是当前要切割的棍子的长度，切棍子的总成本是历次切割成本的总和。
对棍子进行切割将会把一根木棍分成两根较小的木棍（这两根木棍的长度和就是切割前木棍的长度）。

返回切棍子的 最小总成本 。

输入：n = 7, cuts = [1,3,4,5]
输出：16
解释：按 [1, 3, 4, 5] 的顺序切割的情况如下所示：

第一次切割长度为 7 的棍子，成本为 7 。
第二次切割长度为 6 的棍子（即第一次切割得到的第二根棍子），
第三次切割为长度 4 的棍子，最后切割长度为 3 的棍子。总成本为 7 + 6 + 4 + 3 = 20 。
而将切割顺序重新排列为 [3, 5, 1, 4] 后，
总成本 = 16（如示例图中 7 + 4 + 3 + 2 = 16）。
```

```python
cuts = [0] + sorted(cuts) + [n]
N = len(cuts)
f = [[float('inf')]*N for _ in range(N)]
for l in range(1, N):
    for i in range(N-l):
        j = i+l
        if l==1:
            f[i][j] = 0
            continue
        for k in range(i+1, j):
            f[i][j] = min(f[i][j],
                f[i][k]+f[k][j]+cuts[j]-cuts[i])
return f[0][-1]

# Memoization
cuts = [0] + sorted(cuts) + [n]
N = len(cuts)
@cache
def minCost(i, j):
    if j-i==1: return 0
    res = float('inf')
    for k in range(i+1, j):
        left = minCost(i, k)
        right = minCost(k, j)
        res = min(res, left+right+cuts[j]-cuts[i])
    return res

return minCost(0, N-1)
```

{% endtab %}

{% tab title="戳气球" %}

```python
有 n 个气球，编号为0 到 n - 1，每个气球上都标有一个数字，这些数字存在数组 nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球，
你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 
这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。
如果 i - 1或 i + 1 超出了数组的边界，那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。


输入：nums = [3,1,5,8]
输出：167
解释：
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins =  3*1*5    +   3*5*8   +  1*3*8  + 1*8*1 = 167
```

```python
nums = [1] + nums + [1]
n = len(nums)
f = [[0]*n for _ in range(n)]
for l in range(n):
    for i in range(n-l):
        j = i+l
        for k in range(i+1, j):
            f[i][j] = max(f[i][j],
                f[i][k]+f[k][j]+nums[i]*nums[k]*nums[j])
return f[0][-1]

# Memoization
nums = [1] + nums + [1]
@cache
def maxScore(i, j):
    if i>=j-1: return 0
    res = 0
    for k in range(i+1, j):
        left = maxScore(i, k)
        right = maxScore(k, j)
        res = max(res, left+right+nums[i]*nums[j]*nums[k])
    return res

return maxScore(0, len(nums)-1)
```

{% endtab %}

{% tab title="奇怪的打印机" %}

```python
有台奇怪的打印机有以下两个特殊要求：

打印机每次只能打印由 同一个字符 组成的序列。
每次可以在任意起始和结束位置打印新字符，并且会覆盖掉原来已有的字符。
给你一个字符串 s ，你的任务是计算这个打印机打印它需要的最少打印次数。

输入：s = "aaabbb"
输出：2
解释：首先打印 "aaa" 然后打印 "bbb"。
```

```python
N = len(s)
f = [[0]*N for _ in range(N+1)]
for l in range(N):
    for i in range(N-l):
        j = i+l
        f[i][j] = f[i+1][j] + 1
        for k in range(i+1, j+1):
            if s[i]==s[k]:
                f[i][j] = min(f[i][j], f[i][k-1]+f[k+1][j])
return f[0][-1]

# Memoization
@cache
def minCost(i, j):
    if i>j: return 0
    res = minCost(i+1, j) + 1
    for k in range(i+1, j+1):
        left = minCost(i, k-1)
        right = minCost(k+1, j)
        if s[k]==s[i]:
            res = min(res, left+right)
    return res

return minCost(0, len(s)-1)
```

{% endtab %}

{% tab title="移除盒子" %}

```python
给出一些不同颜色的盒子，盒子的颜色由数字表示，即不同的数字表示不同的颜色。

你将经过若干轮操作去去掉盒子，直到所有的盒子都去掉为止。
每一轮你可以移除具有相同颜色的连续 k 个盒子（k >= 1），
这样一轮之后你将得到 k * k 个积分。

当你将所有盒子都去掉之后，求你能获得的最大积分和。

输入：boxes = [1,3,2,2,2,3,4,3,1]
输出：23
解释：
[1, 3, 2, 2, 2, 3, 4, 3, 1] 
----> [1, 3, 3, 4, 3, 1] (3*3=9 分) 
----> [1, 3, 3, 3, 1] (1*1=1 分) 
----> [1, 1] (3*3=9 分) 
----> [] (2*2=4 分)
```

```python
N = len(boxes)
f = [[[0]*(N+1) for _ in range(N+1)] for _ in range(N+1)]
for l in range(N):
    for i in range(N-l):
        j = i+l
        for p in range(N-j):
            if i<j and boxes[j]==boxes[j-1]: # pruning
                f[i][j][p] = f[i][j-1][p+1]
                continue
            f[i][j][p] = f[i][j-1][0]+(p+1)**2
            for k in range(i, j):
                if boxes[k]==boxes[j]:
                    f[i][j][p] = max(f[i][j][p],
                                     f[i][k][p+1]+f[k+1][j-1][0])
return f[0][N-1][0]

# Memoization
@cache
def maxScore(i, j, p):
    if i>j: return 0
    while i<j and boxes[j]==boxes[j-1]:
        j -= 1
        p += 1
    res = maxScore(i, j-1, 0) + (p+1)**2
    for k in range(i, j):
        if boxes[k]==boxes[j]:
            left = maxScore(i, k, p+1)
            right = maxScore(k+1, j-1, 0)
            res = max(res, left+right)
    return res

return maxScore(0, len(boxes)-1, 0)
```

{% endtab %}
{% endtabs %}
