Bootstrap

动态规划问题记录

背包问题的分类:

01背包问题:每个物品最多只能用一次

  • 问题描述:有一个容量是 V V V 的背包和 N N N 个物品,第 i i i 个物品的体积是 v i v_i vi,价值是 w i w_i wi每个物品最多只能用一次(可以用一次,也可以不用),问怎样挑选物品才能使得在总体积不超过 V V V 的前提下物品的总价值最大?

完全背包问题:每个物品可以用无限次

多重背包问题:每个物品最多有 s i s_i si

分组背包问题:物品有 N N N 组,每一组里面有若干种,每一组里面最多只能选一种物品

动态规划问题的一般解决思路:(从集合的角度来理解 DP)

DP:(1)状态表示 f ( i , j ) f(i, j) f(i,j)

  • 弄清楚该状态表示的是哪一个集合:

    • f ( i , j ) f(i, j) f(i,j) 表示的是这样的一个集合: 只从前 i i i 个物品中选,且选出的总体积 ≤ j \le j j 的所有选法;
  • 弄清楚该状态表示的是集合的哪一种属性:最大值 or 最小值 or 数量

    • f ( i , j ) f(i, j) f(i,j) 的值表示的是上述所有选法中所选出价值的最大值。

(2)状态计算:集合的划分(不重不漏)(不一定要满足“不重”,但必须要满足“不漏”)

  • 01 背包问题中集合的划分:将 f ( i , j ) f(i, j) f(i,j) 划分为这样两个子集:
    			   f(i, j)	
                不含i  |  含i
          f(i - 1, j) | f(i - 1, j - vi) + wi
          
    -----------------------------------------
    
    => f(i, j) = max{f(i - 1, j), f(i - 1, j - vi) + wi}
    

01 背包问题优化前代码:

#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][N];

int main()
{
    cin >> n >> m;
    
    for (int i = 1; i <= n; i++) scanf("%d%d", &v[i], &w[i]);
    
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= m; j++)
        {
            f[i][j] = f[i - 1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }
    
    cout << f[n][m] << endl;
    
    return 0;
}

01 背包优化成一维:

#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin >> n >> m;
    
    for (int i = 1; i <= n; i++) scanf("%d%d", &v[i], &w[i]);
    
    for (int i = 1; i <= n; i++)
        for (int j = m; j >= v[i]; j--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    
    cout << f[m] << endl;
    
    return 0;
}

完全背包问题:

状态表示 f ( i , j ) f(i, j) f(i,j)

  • 状态表示:

    • f ( i , j ) f(i, j) f(i,j) 表示只考虑前 i i i 个物品,且总体积不大于 j j j 的所有选法;
    • f ( i , j ) f(i, j) f(i,j) 的值表示价值的最大值。
  • 状态计算(集合的划分):按第 i i i 个物品选多少个进行划分:第 i i i 个物品选 0 个,1 个,2 个,…,k 个(注意不能无限选,因为背包体积有限):
    f ( i , j ) = max ⁡ { f ( i − 1 , j ) , f ( i − 1 , j − k ∗ v [ i ] ) + k ∗ w [ i ] } , k = 1 , 2 , . . . , ⌊ V v [ i ] ⌋ f(i, j) = \max\left\{ f(i -1, j), f(i - 1, j - k * v[i]) + k * w[i] \right\}, k = 1, 2, ..., \lfloor \frac{V}{v[i]} \rfloor f(i,j)=max{f(i1,j),f(i1,jkv[i])+kw[i]},k=1,2,...,v[i]V
    f ( i − 1 , j ) f(i - 1, j) f(i1,j) 可以看作是 k = 0 k = 0 k=0 的情形,故上述情形可以合并为:
    f ( i , j ) = max ⁡ { f ( i − 1 , j − k ∗ v [ i ] ) + k ∗ w [ i ] } , k = 0 , 1 , 2 , . . . , ⌊ V v [ i ] ⌋ f(i , j) = \max \left\{f(i - 1, j - k * v[i]) + k * w[i] \right\}, k = 0, 1, 2, ..., \lfloor \frac{V}{v[i]} \rfloor f(i,j)=max{f(i1,jkv[i])+kw[i]},k=0,1,2,...,v[i]V

以上是完全背包问题的朴素做法,其时间复杂度在最坏情况下达到了 O ( N V 2 ) O(NV^2) O(NV2) N , V N, V N,V 分别是物品的个数和背包的容量)。对朴素问题的优化可以从 f ( i , j ) f(i, j) f(i,j) 的表达式入手:

注意到:
f ( i , j ) = max ⁡ { f ( i − 1 , j ) , f ( i − 1 , j − v [ i ] ) + w [ i ] , f ( i − 1 , j − 2 ∗ v [ i ] ) + 2 ∗ w [ i ] , f ( i − 1 , j − 3 ∗ v [ i ] ) + 3 ∗ w [ i ] , . . . } f ( i , j − v [ i ] ) = max ⁡ { f ( i − 1 , j − v [ i ] ) , f ( i − 1 , j − 2 ∗ v [ i ] ) + w [ i ] , f ( i − 1 , j − 3 ∗ v [ i ] ) + 2 ∗ w [ i ] , . . . } f(i, j) = \max \left\{ f(i-1, j), f(i-1, j - v[i]) + w[i], f(i - 1, j - 2*v[i]) + 2*w[i], f(i - 1, j - 3*v[i]) + 3 * w[i], ... \right\} \\ f(i, j-v[i]) = \max \left\{ f(i-1, j - v[i]), f(i - 1, j - 2*v[i]) + w[i], f(i - 1, j - 3*v[i]) + 2 * w[i], ... \right\} f(i,j)=max{f(i1,j),f(i1,jv[i])+w[i],f(i1,j2v[i])+2w[i],f(i1,j3v[i])+3w[i],...}f(i,jv[i])=max{f(i1,jv[i]),f(i1,j2v[i])+w[i],f(i1,j3v[i])+2w[i],...}

f ( i , j ) = max ⁡ { f ( i − 1 , j ) , f ( i , j − v [ i ] ) + w [ i ] } f(i,j) = \max \left \{ f(i - 1, j), f(i, j - v[i]) + w[i] \right\} f(i,j)=max{f(i1,j),f(i,jv[i])+w[i]}
01 背包和完全背包的对比:

01 背包 f ( i , j ) = max ⁡ { f ( i − 1 , j ) , f ( i − 1 , j − v [ i ] ) + w [ i ] } f(i, j) = \max \left \{ f(i - 1, j), f(i - 1, j - v[i]) + w[i] \right\} f(i,j)=max{f(i1,j),f(i1,jv[i])+w[i]}
完全背包 f ( i , j ) = max ⁡ { f ( i − 1 , j ) , f ( i , j − v [ i ] ) + w [ i ] } f(i, j) = \max \left \{ f(i - 1, j), f(i, j - v[i]) + w[i] \right \} f(i,j)=max{f(i1,j),f(i,jv[i])+w[i]}

完全背包问题优化后的代码(滚动数组版):

#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin >> n >> m;
    
    for (int i = 1; i <= n; i++) scanf("%d%d", &v[i], &w[i]);
    
    for (int i = 1; i <= n; i++)
        for (int j = v[i]; j <= m; j++) // 注意:j要从v[i]开始!
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    
    cout << f[m] << endl;
    
    return 0;
}

多重背包问题:

  • 状态表示:所有背包问题的状态都是 f ( i , j ) f(i, j) f(i,j)

    • 集合:所有只从前 i i i 个物品中选,并且总体积不超过 j j j 的选法
    • 属性:集合中每一个选法对应的总价值的最大值
  • 状态计算:根据第 i i i​ 个物品选多少个来划分(枚举第 i i i 个物品选几个)

    • 和完全背包的状态计算一样:
      f ( i , j ) = max ⁡ { f ( i − 1 , j − k ∗ v [ i ] ) + k ∗ w [ i ] } , k = 0 , 1 , 2 , . . . , s [ i ] f(i, j) = \max \{ f(i-1, j - k * v[i]) + k * w[i] \}, k = 0, 1, 2, ..., s[i] f(i,j)=max{f(i1,jkv[i])+kw[i]},k=0,1,2,...,s[i]
      对应到具体的代码即为:

      for (int i = 1; i <= n; i++)
          for (int j = 0; j <= m; j++)
              for (int k = 0; k <= s[i] && k * v[i] <= j; k++)
                  f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
      

      这种朴素做法在最坏情况下的时间复杂度是 O ( N ∗ V ∗ S ) O(N*V*S) O(NVS)

    • 多重背包问题的二进制优化:

      上述循环中的第三维在最坏情况下要从 0 枚举到 s[i],可以将从 0 到 s[i] 之间的数进行二进制划分,例如:

      200可以划分成8组:
      1	2	4	7	16	32	64	73
      每组只有两种选择:选或不选
      于是这一维便转化成了01背包问题,从而这一维的复杂度由O(S)降到了O(logS)。
      

      在实现代码时,要把所有物品按照二进制的方式重新拆分一遍,然后对新拆出来的物品做一遍 01 背包。

      原来的物品总数可以看成是 N ∗ S N * S NS,拆分后的物品总数为 N ∗ log ⁡ S N * \log S NlogS​。

      C++ 在 1s 内能处理的计算量是 1 亿次。

      物品拆分过程的代码如下:

      int cnt = 0;
      for (int i = 1; i <= n; i++)
      {
          int vi, wi, si; // 第i个物品的体积、价值、数量
          scanf("%d%d%d", &vi, &wi, &si);
          int k = 1; // k=2的幂次,初始为1
          while (k <= s)
          {
              cnt++;
              v[cnt] = k * vi;
              w[cnt] = k * wi;
              s -= k;
              k *= 2;
          }
          if (s > 0)
          {
              cnt++;
              v[cnt] = s * vi;
              w[cnt] = s * wi;
          }
      }
      
      n = cnt; // 打包后物品的总数将由N增至N*logS,故要对n重新赋值
      

加了二进制优化 + 滚动数组的多重背包问题的代码如下:

#include <iostream>

using namespace std;

const int N = 11010; // 注:N = 1000, si = 2000时,开11000会WA

int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin >> n >> m;
    
    int cnt = 0;
    for (int i = 1; i <= n; i++)
    {
        int vi, wi, si;
        scanf("%d%d%d", &vi, &wi, &si);
        int k = 1; // k是2的幂次,初始时为1
        while (k <= si)
        {
            v[++cnt] = k * vi;
            w[cnt] = k * wi;
            si -= k;
            k *= 2;
        }
        if (si > 0)
        {
            v[++cnt] = si * vi;
            w[cnt] = si * wi;
        }
    }
    n = cnt;
    
    for (int i = 1; i <= n; i++)
        for (int j = m; j >= v[i]; j--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    
    cout << f[m] << endl;
    
    return 0;
}

分组背包问题:

  • 状态表示: f ( i , j ) f(i, j) f(i,j)
    • 集合:只从前 i i i 组物品中选,且总体积不大于 j j j 的所有选法;
    • 属性:价值的最大值。
  • 状态计算:枚举第 i i i​ 组物品选哪个或不选。

f ( i , j ) = max ⁡ { f ( i − 1 , j ) , f ( i − 1 , j − v [ i , k ] ) + w [ i , k ] } f(i, j) = \max \{ f(i - 1, j), f(i - 1, j - v[i, k]) + w[i, k] \} f(i,j)=max{f(i1,j),f(i1,jv[i,k])+w[i,k]}

​ 式中, v [ i , k ] v[i,k] v[i,k] 表示第 i i i 组中第 k k k 个物品的体积, w [ i , k ] w[i, k] w[i,k] 表示第 i i i 组中第 k k k 个物品的价值。

​ 分组背包问题的代码如下:

#include <iostream>

using namespace std;

const int N = 110;

int n, m;
int v[N][N], w[N][N], s[N];
int f[N];

int main()
{
    cin >> n >> m;
    
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &s[i]);
        for (int j = 0; j < s[i]; j++)
            scanf("%d%d", &v[i][j], &w[i][j]);
    }
    
    for (int i = 1; i <= n; i++)
        for (int j = m; j >= 0; j--)
            for (int k = 0; k < s[i]; k++)
                if (j >= v[i][k])
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
    
    cout << f[m] << endl;
    
    return 0;
}

线性 DP:递推方程有明显的线性顺序(包括一维线性和二维线性)

数字三角形:

  • 状态表示:

    • 状态表示的是哪一个集合:从起点走到 (i, j) 的所有路径。
    • 状态表示的是什么属性:以上所有路径中数字之和的最大值。
  • 状态计算(集合的划分过程):

    • f ( i , j ) = max ⁡ { f ( i − 1 , j − 1 ) , f ( i − 1 , j ) } + a ( i , j ) f(i, j) = \max \{ f(i - 1, j - 1), f(i - 1, j) \} + a(i, j) f(i,j)=max{f(i1,j1),f(i1,j)}+a(i,j)
  • 本题中的 tips:

    • 为了不处理边界,可以先将所有的 f ( i , j ) f(i, j) f(i,j) 设置成 − ∞ -\infty ,将 f ( 1 , 1 ) f(1, 1) f(1,1) 设置成 a ( 1 , 1 ) a(1,1) a(1,1)(本题的下标 i 和 j 均从 1 开始)。

数字三角形的代码如下:

#include <iostream>

using namespace std;

const int N = 510, INF = 1e9;

int n;
int a[N][N], f[N][N];

int main()
{
    cin >> n;
    
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i; j++)
            scanf("%d", &a[i][j]);
    
    // 先将所有的f[i][j]初始化为-INF
    for (int i = 0; i <= n; i++)
        for (int j = 0; j <= i + 1; j++) // 注意要往后多初始化一列
            f[i][j] = -INF;
    
    f[1][1] = a[1][1];
    for (int i = 2; i <= n; i++)
        for (int j = 1; j <= i; j++)
            f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j];
    
    int res = -INF;
    for (int i = 1; i <= n; i++) res = max(res, f[n][i]);
    
    cout << res << endl;
    
    return 0;
}

时间复杂度:

  • 状态数量是 n 2 n^2 n2,状态转移的计算量是 O ( 1 ) O(1) O(1),故时间复杂度是 O ( n 2 ) O(n^2) O(n2)

DP 问题中的下标应该从 0 开始还是从 1 开始?

答:一般而言,如果涉及到了 f ( i − 1 ) f(i -1) f(i1),则最好使 i ≥ 1 i \ge 1 i1,如此 f ( i − 1 ) f(i -1) f(i1) 便是 f ( 0 ) f(0) f(0),不会使下标越界。若没有涉及到 i − 1 i - 1 i1​,则下标从 0 开始即可。

DP 问题的时间复杂度 = 状态数量 × \times ×​ 转移的计算量

DP 问题状态的维度是越多越好吗?

答:不是的,在保证能够求出正确答案的前提下,维度要越少越好,因为状态每多一维,计算量就会增加一维。在遇到具体问题时,一般先考虑一维。如果一维无法解决,再考虑二维。如果二维也无法解决,再考虑三维…,依此类推。

最长上升子序列:

  • 状态表示:

    • 状态表示的是哪一个集合:f(i)表示以第 i 个数结尾的的所有严格递增的子序列所构成的集合
    • 状态表示的是什么属性:所有以 a[i] 结尾的上升子序列中长度的最大值(因此当所有的 f[i] 求出之后,最终的答案就是 f[1]~f[n] 中取 max)。
  • 状态计算:(a[i] 可以由谁上升过来呢?所以需要遍历一遍从 0 到 i - 1 的所有数)

    • f [ i ] = max ⁡ { f [ j ] + 1 } ,   a [ j ] < a [ i ] ,   j = 0 , 1 , 2 , . . . , i − 1 f[i] = \max \{ f[j] + 1 \}, \ a[j] < a[i], \ j = 0, 1, 2, ... , i - 1 f[i]=max{f[j]+1}, a[j]<a[i], j=0,1,2,...,i1

    • 时间复杂度:状态数量是 n n n,转移的计算量(即计算每个状态所需要的时间)是 O ( n ) O(n) O(n)(因为要枚举从 0 到 i - 1 来进行转移),故时间复杂度是 O ( n 2 ) O(n^2) O(n2)

最长上升子序列代码:

#include <iostream>

using namespace std;

const int N = 1010;

int n;
int a[N], f[N];

int main()
{
    cin >> n;
    
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    
    // 状态数组初始化:每个位置处的最长上升子序列的长度至少为1
    for (int i = 0; i < n; i++) f[i] = 1;
    
    for (int i = 1; i < n; i++)
        for (int j = 0; j < i; j++)
            if (a[i] > a[j]) f[i] = max(f[i], f[j] + 1);
    
    // 循环结束后,f[0] ~ f[n - 1]中的最大值即为最终结果
    
    int res = 1;
    for (int i = 0; i < n; i++) res = max(res, f[i]);
 
    cout << res << endl;
    
    return 0;
}

最长上升子序列的优化:

结论:所有不同长度下上升子序列结尾的最小值必然构成一个单调递增序列。

长度是 1 的上升子序列只需要存一个结尾最小的,长度是 2 的上升子序列也只需要存一个结尾最小的,长度是 3 的也是只需要存一个结尾最小的,…,因此可以存储不同长度的最长上升子序列结尾的值最小是多少。随着最长上升子序列长度的增加,结尾的数值应该是严格单调递增的。

a[i] 要接到从右往左第一个小于 a[i] 的数后面,并替换从左到右第一个大于等于 a[i] 的数。

最长上升子序列 + 二分查找优化代码:

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int n;
int a[N], q[N];

int main()
{
    cin >> n;
    
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    
    int len = 0;
    for (int i = 0; i < n; i++)
    {
        int l = 0, r = len;
        while (l < r)
        {
            int mid = l + r + 1 >> 1;
            if (q[mid] < a[i]) l = mid;
            else r = mid - 1;
        } // 如此找到的是最后一个小于a[i]的数的下标
        
        len = max(len, r + 1);
        q[r + 1] = a[i];
    }
    
    cout << len << endl;
    
    return 0;
}

最长公共子序列:

  • 状态表示:

    • 状态表示的是什么集合:f(i, j) 表示A[0,i]和B[0,j]的所有公共子序列所构成的集合;
    • 状态表示的是什么属性:所有公共子序列中长度的最大值
  • 状态计算:(对应集合的划分)

    • 根据 a[i] 和 b[j] 是否包含在最终的最长公共子序列当中,可以分为以下四种情况:

      a[i]和b[j]均不包含:f[i - 1][j - 1]
      a[i]不包含,b[j]包含:情况1
      a[i]包含,b[j]不包含:情况2
      a[i]和b[j]均包含:f[i - 1][j - 1] + 1
      

      特别注意:情况 1 ≠ \ne = f[i - 1][j],情况 2 ≠ \ne = f[i][j - 1],原因是 f[i - 1][j] 不一定是以 b[j] 结尾的,f[i][j - 1] 也不一定是以 a[i] 结尾的。但好消息是:情况 1 ⊆ \subseteq f[i - 1][j],情况 2 ⊆ \subseteq f[i][j - 1],因此在求最大值的意义下,可以用 f[i - 1][j] 来替换情况 1,用 f[i][j - 1] 来替换情况 2。(这种替换可能会导致重复求解某些子状态,但这种重复没有关系,因为最终要求的是最大值。)

      注意 2:上述的 f[i - 1][j - 1] 其实是包含在后三种情况当中的,故最终写代码的时候这种情况可以不写。

最长公共子序列代码:

#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];
int f[N][N];

int main()
{
    cin >> n >> m >> a + 1 >> b + 1;
    
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
        {
            f[i][j] = max(f[i - 1][j], f[i][j - 1]);
            if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
        }
    
    cout << f[n][m] << endl;
    
    return 0;
}

编辑距离:

首先考虑这样一个问题:给定模式串 p 和询问字符串 s,问最少能够经过多少次操作可以将 p 变成 s?

  • 状态表示:

    • 状态表示的是什么集合:f(i, j) 表示将 p[0, i] 变成 s[0, j] 的所有不同的操作方法;
    • 状态表示的是哪种属性:f(i, j) 的值表示所有操作方法中所需操作次数的最小值。
  • 状态计算:

    • // 设模式串是p,要匹配的字符串是s
      f[i][j] = min(f[i - 1][j - 1], // 若p[i] == s[j],则不用做任何操作
      		     f[i - 1][j - 1], // 替换p[i],使之等于s[j]
                   f[i][j - 1],      // p增加一个字符,使之等于s[j]
                   f[i - 1][j]       // 删除p[i],删除之后p[i - 1] = s[j]
                   )
      

编辑距离代码如下:

#include <iostream>
#include <string.h>

using namespace std;

const int N = 1010, M = 15;

int n, m;
char str[N][M];
int f[M][M];

int edit_distance(char a[], char b[])
{
    int la = strlen(a + 1), lb = strlen(b + 1);
    
    for (int i = 0; i <= la; i++) f[i][0] = i;
    for (int i = 0; i <= lb; i++) f[0][i] = i;
    
    for (int i = 1; i <= la; i++)
        for (int j = 1; j <= lb; j++)
        {
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
        }
    
    return f[la][lb];
}

int main()
{
    cin >> n >> m;
    
    for (int i = 0; i < n; i++) scanf("%s", str[i] + 1);
    
    while (m--)
    {
        char s[M];
        int limit;
        scanf("%s%d", s + 1, &limit);
        
        int cnt = 0;
        for (int i = 0; i < n; i++)
            if (edit_distance(str[i], s) <= limit)
                cnt++;
        
        printf("%d\n", cnt);
    }
    
    return 0;
}

石子合并(区间 DP):

  • 状态表示:

    • 状态表示的是什么集合:f(i, j) 表示[i, j] 之间的所有合并方式;
    • 状态表示的是哪种属性:f(i, j) 表示所有合并方式中的最小代价。
  • 状态计算:

    • 考察最后一次合并,该过程必然是将两堆石子合并成一堆,那是将哪两堆合并成一堆呢?因此我们可以以这两堆分界线的位置为标准来进行分类:

      设 k = j - i + 1,则:
      第一类:左边 1 个元素,右边 k - 1 个元素;
      第二类:左边 2 个元素,右边 k - 2 个元素;

      第 k - 1 类:左边 k - 1 个元素,右边 1 个元素。

    • 状态转移方程:
      f ( i , j ) = min ⁡ { f ( i , k ) + f ( k + 1 , j ) + c o s t [ i , j ] } ,   k = i , i + 1 , . . . , j − 1 f(i, j) = \min \{ f(i, k) + f(k + 1, j) + cost[i, j] \}, \ k = i, i +1, ..., j - 1 f(i,j)=min{f(i,k)+f(k+1,j)+cost[i,j]}, k=i,i+1,...,j1
      式中, c o s t [ i , j ] cost[i, j] cost[i,j] 是区间 [ i , j ] [i, j] [i,j]​ 中石子的总重量,可以应用前缀和快速求出这一区间内的总重量。

    • 时间复杂度:状态的数量是 n 2 n^2 n2,状态转移的计算量是 n n n,故总的时间复杂度为 O ( n 3 ) O(n^3) O(n3)​。

    • tips:在写代码的时候,要保证每一个用到的状态都已经提前算过了,因此可以按照区间长度从小到大来枚举。

;