动态规划
动态规划也是一种分治思想,但与分治算法不同的是,分治算法是把原问题分解为若干子问题,自顶向下求解各子问题,合并子问题的解,从而得到原问题的解。动态规划也是把原问题分解为若干子问题,然后自底向上,先求解最小的子问题,把结果存储在表格中,再求解大的子问题时,直接从表格中查询小的子问题的解,避免重复计算,从而提高算法效率。
与分治法的区别:在分治法中,各个子问题是互不相交的,即相互独立。如果各个子问题有重叠,不是相互独立的,动态规划闪亮登场!
算法要素:
什么问题可以使用动态规划呢?我们首先要分析问题是否具有以下两个性质:
-
最优子结构
**最优子结构性质是指问题的最优解包含其子问题的最优解。**最优子结构是使用动态规划的最基本条件,如果不具有最优子结构性质,就不可以使用动态规划解决。
-
子问题重叠
子问题重叠是指在求解子问题的过程中,有大量的子问题是重复的,那么只需要求解一次,然后把结果存储在表中,以后使用时可以直接查询,不需要再次求解。
子问题重叠不是使用动态规划的必要条件,但问题存在子问题重叠更能够充分彰显动态规划的优势。
解题秘籍:
遇到一个实际问题,如何采用动态规划来解决呢?
- 分析最优解的结构特征。
- 建立最优值的递归式。
- 自底向上计算最优值,并记录。
- 构造最优解。
最长公共子序列
问题分析:
给定两个序列 X={x1,x2,x3,…,x m}和 Y={y1,y2,y3,…,yn},找出 X 和 Y 的一个最长的公共子序列。
例如:X=(A,B,C,B,A,D,B),Y=(B,C,B,A,A,C),那么最长公共子序列是 B,C,B,A。
如何找到最长公共子序列呢?
如果使用暴力搜索方法,需要穷举 X 的所有子序列,检查每个子序列是否也是 Y 的子序列,记录找到的最长公共子序列。X 的子序列有 2m 个,因此暴力求解的方法时间复杂度为指数阶,这是我们避之不及的爆炸性时间复杂度。
那么能不能用动态规划算法呢?
下面分析该问题是否具有最优子结构性质。
-
分析最优解的结构特征
假设已经知道 Zk={z1,z2,z3,…,zk}是 X m={x1,x2,x3,…,x m}和 Yn={y1,y2,y3,…,y n}的最长公共子序列。
这个假设很重要,我们都是这样假设已经知道了最优解。
那么可以分 3 种情况讨论。
-
xm= yn= zk:那么 Zk−1={z1,z2,z3,…,zk−1}是 Xm−1 和 Yn−1 的最长公共子序列:
反证法证明:如果 Zk−1 ={z1,z2,z3,…,zk−1 }不是 X m−1 和 Yn−1 的最长公共子序列,那么它们一定存在一个最长公共子序列。设 M 为 X m−1 和 Yn−1 的最长公共子序列,M 的长度大于Zk−1 的长度,即|M|>|Zk−1|。如果在 X m−1 和 Yn−1 的后面添加一个相同的字符 xm= y n,则 zk=xm=y n,|M+{zk}|>|Zk−1+{zk}|=|Zk|,那么 Zk 不是 X m 和 Yn 的最长公共子序列,这与假设 Zk 是 X m 和 Yn 的最长公共子序列矛盾,问题得证。
-
xm≠yn,xm≠zk:我们可以把 xm 去掉,那么 Zk 是 Xm−1 和 Yn 的最长公共子序列:
反证法证明:如果 Zk 不是 X m−1 和 Y n 的最长公共子序列,那么它们一定存在一个最长公共子序列。设 M 为 X m−1 和 Y n 的最长公共子序列,M 的长度大于 Zk 的长度,即|M|>|Zk|。如果我们在 X m−1 的后面添加一个字符 x m,那么 M 也是 X m 和 Yn 的最长公共子序列,因为|M|>|Zk|,那么 Zk 不是 X m 和 Y n 的最长公共子序列,这与假设 Zk 是 X m 和 Y n 的最长公共子序列矛盾,问题得证。
-
xm≠yn,yn≠zk:我们可以把 y n 去掉,那么 Zk 是 X m 和 Yn−1 的最长公共子序列:
反证法证明:如果 Zk 不是 X m 和 Y n−1 的最长公共子序列,那么它们一定存在一个最长公共子序列。设 M 为 X m 和 Y n−1 的最长公共子序列,M 的长度大于 Zk 的长度,即|M|>|Zk|。如果我们在 Yn−1 的后面添加一个字符 yn,那么 M 也是 Xm 和 Yn 的最长公共子序列,因为|M|>|Zk|,那么 Zk不是 Xm 和 Yn 的最长公共子序列,这与假设 Zk 是 Xm 和 Yn 的最长公共子序列矛盾,问题得证。
-
-
建立最优值的递归式
设 c[i][j]表示 X i 和 Y j 的最长公共子序列长度。
-
xm = yn = zk:那么 c[i][j] = c[i−1][j−1]+1;
-
xm ≠ yn:那么我们只需要求解 X i 和 Y j−1 的最长公共子序列和 X i−1 和 Y j 的最长公共子序列,比较它们的长度哪一个更大,就取哪一个值。即 c[i][j]= max{c[i][j−1],c[i−1][j]}。
-
最长公共子序列长度递归式:
-
-
底向上计算最优值,并记录最优值和最优策略
i=1 时:{x1 }和{y1,y2,y3,…,y n}中的字符一一比较,按递归式求解并记录最长公共子序列长度。
i=2 时:{x2 }和{y1,y2,y3,…,y n}中的字符一一比较,按递归式求解并记录最长公共子序列长度。
…
i=m 时:{x m}和{y1,y2,y3,…,yn}中的字符一一比较,按递归式求解并记录最长公共子序列长度。
-
构造最优解
上面的求解过程只是得到了最长公共子序列长度,并不知道最长公共子序列是什么,那怎么办呢?
例如,现在已经求出 c[m][n]=5,表示 X m 和 Y n 的最长公共子序列长度是 5,那么这个 5是怎么得到的呢?我们可以反向追踪 5 是从哪里来的。根据递推式,有如下情况。
x i = y j 时:c[i][j]= c[i−1][j−1]+1;
x i ≠ y j 时:c[i][j]= max{c[i][j−1], c[i−1][j]};
那么 c[i][j]的来源一共有 3 个:c[i][j]= c[i−1][j−1]+1,c[i][j]= c[i][j−1],c[i][j]= c[i−1][j]。在第 3 步自底向上计算最优值时,用一个辅助数组 b [i][j]记录这 3 个来源:
c[i][j]= c[i−1][j−1]+1,b[i][j]=1;
c[i][j]= c[i][j−1],b[i][j]=2;
c[i][j]= c[i−1][j],b[i][j]=3。
这样就可以根据 b[m][n]反向追踪最长公共子序列,当 b[i][j]=1 时,输出 x i;当 b [i][j]=2时,追踪 c[i][j−1];当 b[i][j]=3 时,追踪 c[i−1][j],直到 i=0 或 j=0 停止。
图解分析:
以字符串 s1 =“ABCADAB”,s2=“BACDBA”为例。
-
初始化
len1=7,len2=6,初始化 c[][]第一行、第一列元素为 0:
-
i=1:s1[0]与 s2[j−1]比较,j=1,2,3,…,len2。即“A”与“BACDBA”分别比较一次。
如果字符相等,c[i][j]取左上角数值加 1,记录最优值来源 b[i][j]=1。
如果字符不等,取左侧和上面数值中的最大值。如果左侧和上面数值相等,默认取左侧数值。如果 c[i][j]的值来源于左侧 b[i][j]=2,来源于上面 b[i][j]=3。
-
j=1:A≠B,左侧=上面,取左侧数值,c[1][1]= 0,最优策略来源 b[1][1]=2:
-
j=2:A=A,则取左上角数值加 1,c[1][2]= c[0][1]+1=2,最优策略来源 b[1][2] =1:
-
j=3:A≠C,左侧≥上面,取左侧数值,c[1][3]= 1,最优策略来源 b[1][3] =2:
-
j=6:A=A,则取左上角数值加 1,c[1][6]=1,最优策略来源 b[1][6]=1:
-
-
i=2:s1[1]与 s2 [j−1]比较,j=1,2,3,…,len2。即“B”与“BACDBA”分别比较一次。
如果字符相等,c[i][j]取左上角数值加 1,记录最优值来源 b[i][j]=1。
如果字符不等,取左侧和上面数值中的最大值。如果左侧和上面数值相等,默认取左侧数值。如果 c[i][j]的值来源于左侧 b[i][j]=2,来源于上面 b[i][j]=3:
-
继续处理 i=2,3,…,len1:s1[i−1]与 s2 [j−1]比较,j=1,2,3,…,len2。处理结果如下:
c[][]右下角的值即为最长公共子序列的长度。
c[7][6]=4,即字符串 s1 =“ABCADAB”,s2 =“BACDBA”的最长公共子序列的长度为 4。
那么最长公共子序列包含哪些字符呢?
-
构造最优解
首先读取 b[7][6]=2,说明来源为 2,向左找 b[7][5];
b[7][5]=1,向左上角找 b[6][4],返回时输出 s[6]=“B”;
b[6][4]=3,向上找 b[5][4];
b[5][4]=1,向左上角找 b[4][3],返回时输出 s[4]=“D”;
b[4][3]=2,向左找 b[4][2];
b[4][2]=1,向左上角找 b[3][1],返回时输出 s[3]=“C”;
b[3][1]=3,向上找 b[2][1];
b[2][1]=1,向左上角找,返回时输出 s[1]=“B”;b[1][0]中列为 0,算法停止,返回,输出最长公共子序列为 BCDB:
即输出追踪到的左上角元素
代码实现:
-
最长公共子序列求解函数
首先计算两个字符串的长度,然后从 i=1 开始,s1 [0]与 s2 中的每一个字符比较。
如果当前字符相同,则公共子序列的长度为 c[i−1][j−1]+1,并记录最优策略来源 b[i][j] = 1。
如果当前字符不相同,则公共子序列的长度为 c[i][j−1]和 c[i−1][j]中的最大值,如果c[i][j−1]≥c[i−1][j],则最优策略来源 b[i][j]=2;如果 c[i][j−1]<c[i−1][j],则最优策略来源b[i][j]=3。直到 i> len1 时,算法结束,这时 c[len1][len2]就是我们要的最长公共序列长度。void LCSL() { int i,j; for(i = 1;i <= len1;i++)//控制s1序列不同的子问题 for(j = 1;j <= len2;j++)//控制s2序列不同的子问题 { if(s1[i-1]==s2[j-1]) { c[i][j] = c[i-1][j-1]+1;//如果当前字符相同,则公共子序列的长度为该字符前的最长公共序列+1 b[i][j] = 1; } else { if(c[i][j-1]>=c[i-1][j]) { c[i][j] = c[i][j-1]; b[i][j] = 2; } else { c[i][j] = c[i-1][j]; b[i][j] = 3; } } } }
-
最优解输出函数
输出最优解仍然使用倒推法。因为我们在求最长公共子序列长度 c[i][j]的过程中,用 b[i][j]记录了 c[i][j]的来源,那么就可以根据 b[i][j]数组倒推最优解。
如果 b[i][j]=1,说明 s1[i−1]=s2[j−1],那么我们就可以递归输出 print(i−1,j−1);然后输出 s1[i−1]。
如果 b[i][j]=2,说明 s1[i−1]≠s2[j−1]且最优解来源于 c[i][j]=c[i][j−1],递归输出 print(i,j−1)。
如果 b[i][j]=3,说明 s1[i−1]≠s2[j−1]且最优解来源于 c[i][j]=c[i−1][j],递归输出 print(i−1,j)。
当 i==0||j==0 时,递归结束。void print(int i, int j)//根据记录下来的信息构造最长公共子序列(从b[i][j]开始递推) { if(i==0 || j==0) return; if(b[i][j]==1) { print(i-1,j-1); cout<<s1[i-1]; } else if(b[i][j]==2) print(i,j-1); else print(i-1,j); }
完整代码:
//program 4-1
#include <iostream>
#include<cstring>
using namespace std;
const int N=1002;
int c[N][N],b[N][N];
char s1[N],s2[N];
int len1,len2;
void LCSL()
{
int i,j;
for(i = 1;i <= len1;i++)//控制s1序列不同的子问题
for(j = 1;j <= len2;j++)//控制s2序列不同的子问题
{
if(s1[i-1]==s2[j-1])
{
c[i][j] = c[i-1][j-1]+1;//如果当前字符相同,则公共子序列的长度为该字符前的最长公共序列+1
b[i][j] = 1;
}
else
{
if(c[i][j-1]>=c[i-1][j])
{
c[i][j] = c[i][j-1];
b[i][j] = 2;
}
else
{
c[i][j] = c[i-1][j];
b[i][j] = 3;
}
}
}
}
void print(int i, int j)//根据记录下来的信息构造最长公共子序列(从b[i][j]开始递推)
{
if(i==0 || j==0) return;
if(b[i][j]==1)
{
print(i-1,j-1);
cout<<s1[i-1];
}
else if(b[i][j]==2)
print(i,j-1);
else
print(i-1,j);
}
int main()
{
int i,j;
cout << "输入字符串s1:"<<endl;
cin >> s1;
cout << "输入字符串s2:"<<endl;
cin >> s2;
len1 = strlen(s1);//计算两个字符串的长度
len2 = strlen(s2);
for(i = 0;i <= len1;i++)
{
c[i][0]=0;//初始化第一列为0
}
for(j = 0;j<= len2;j++)
{
c[0][j]=0;//初始化第一行为0
}
LCSL();
cout << "s1和s2的最长公共子序列长度是:"<<c[len1][len2]<<endl;
cout << "s1和s2的最长公共子序列是:";
print(len1,len2);
cout<<endl;
// /*用于测试
cout<<"c[i][j]数组:"<<endl;
for(i = 0;i <= len1;i++)
{
for(j = 0;j <= len2;j++)
cout <<" "<<c[i][j];
cout<<endl;
}
cout<<"b[i][j]数组:"<<endl;
for(i = 0;i <= len1;i++)
{
for(j = 0;j <= len2;j++)
cout <<" "<<b[i][j];
cout<<endl;
}
// */用于测试
return 0;
}
算法复杂度:
-
时间复杂度:
由于每个数组单元的计算耗费 Ο(1)时间,如果两个字符串的长度分别是 m、n,那么算法时间复杂度为 Ο(m*n)。
-
空间复杂度:
空间复杂度主要为两个二维数组 c[][],b[][],占用的空间为 O(m*n)。
优化拓展:
因为 c[i][j]有 3 种来源:c[i−1][j−1]+1、c[i][j−1]、c[i−1][j]。我们可以利用 c 数组本身来判断来源于哪个值,从而不用 b[][],这样可以节省 O(mn)个空间。但因为 c 数组还是 O(mn)个空间,所有空间复杂度数量级仍然是 O(m*n),只是从常数因子上的改进。
矩阵连乘
问题分析:
矩阵连乘问题就是对于给定 n 个连乘的矩阵,找出一种加括号的方法,使得矩阵连乘的计算量(乘法次数)最小。
-
什么是矩阵可乘?
如果两个矩阵,第 1 个矩阵的列等于第 2 个矩阵的行时,那么这两个矩阵是可乘的。
-
矩阵相乘后的结果是什么?
**多个矩阵相乘的结果矩阵,其行、列分别等于第 1 个矩阵的行、最后 1 个矩阵的列。**而且无论矩阵的计算次序如何都不影响它们的结果矩阵。
-
两个矩阵相乘需要多少次乘法?
Am×n、An×k 相乘执行乘法运算的次数为 m×n×k。
如果穷举所有的加括号方法,那么加括号的所有方案是一个卡特兰数序列,其算法时间复杂度为 2n,是指数阶。因此穷举的办法是很糟的,那么能不能用动态规划呢?
下面分析矩阵连乘问题 AiAi+1…Aj 是否具有最优子结构性质。
-
分析最优解的结构特征
-
假设我们已经知道了在第 k 个位置加括号会得到最优解,那么原问题就变成了两个子问题:(AiAi+1…Ak),(Ak+1Ak+2…Aj):
原问题的最优解是否包含子问题的最优解呢?
-
假设 AiAi+1…Aj 的乘法次数是 c,(AiAi+1…Ak)的乘法次数是 a,(Ak+1Ak+2…Aj)的乘法次数是 b,(AiAi+1…Ak)和(Ak+1Ak+2…Aj)的结果矩阵相乘的乘法次数是 d,那么c=a+b+d,无论两个子问题(AiAi+1…Ak)、(Ak+1Ak+2…Aj)的计算次序如何,都不影响它们结果矩阵,两个结果矩阵相乘的乘法次数 d 不变。
因此我们只需要证明如果 c 是最优的,则 a 和 b 一定是最优的(即原问题的最优解包含子问题的最优解)。
-
-
建立最优值递归式
-
用 m[i][j]表示 AiAi+1 …Aj 矩阵连乘的最优值,那么两个子问题(AiAi+1 …Ak )、(Ak+1Ak+2…Aj)对应的最优值分别是 m[i][k]、m[k+1][j]。剩下的只需要考查(AiAi+1…Ak)和(Ak+1Ak+2…Aj)的结果矩阵相乘的乘法次数了。
-
设矩阵 Am 的行数为 p m,列数为 q m,m=i,i+1, …,j,且矩阵是可乘的,即相邻矩阵前一个矩阵的列等于下一个矩阵的行(q m= p m+1)。(AiAi+1…Ak)的结果是一个 p i×q k矩阵,(Ak+1Ak+2…Aj)的结果是一个 p k+1×qj 矩阵,q k= p k+1,两个结果矩阵相乘的乘法次数是 p i*p k+1 *q j。
-
当 i=j 时,只有一个矩阵,m[i][j]=0;
当 i>j 时,m[i][j]=min{m[i][k]+m[k+1][j]+pi×pk+1×qj}
如果用一维数组 p[]来记录矩阵的行和列,第 i 个矩阵的行数存储在数组的第 i−1 位置,列数存储在数组的第 i 位置,那么 pip k+1qj 对应的数组元素相乘为 p[i−1]p[k] p[j],原递归式变为:
-
-
自底向上计算并记录最优值
先求两个矩阵相乘的最优值,再求 3 个矩阵相乘的最优值,直到 n 个矩阵连乘的最优值。
-
构造最优解
上面得到的最优值只是矩阵连乘的最小的乘法次数,并不知道加括号的次序,需要从记录表中还原加括号次序,构造出最优解,例如 A1(A2A3)。
图解分析:
假设有5个矩阵:
-
初始化
采用一维数组 p[]记录矩阵的行和列,实际上只需要记录每个矩阵的行,再加上最后一个矩阵的列即可。m[i][i]=0,s[i][i]=0,其中 i= 1,2,3,4,5。
最优值数组 m[i][i]=0,最优决策数组 s[i][i]=0,其中 i= 1,2,3,4,5。
-
计算两个矩阵相乘的最优值
规模 r=2。根据递归式:
-
A1 *A2:k=1,m[1][2]=min{ m[1][1]+ m[2][2]+p0p1p2 }=150;s[1][2]=1。
-
A2 *A3:k=2,m[2][3]=min{ m[2][2]+ m[3][3]+p1p2p3 }=400;s[2][3]=2。
-
A3 *A4:k=3,m[3][4]=min{ m[3][3]+ m[4][4]+p2p3p4 }=160;s[3][4]=3。
-
A4 *A5:k=4,m[4][5]=min{ m[4][4]+ m[5][5]+p3p4p5 }=64; s[4][5]=4。
-
-
计算 3 个矩阵相乘的最优值
规模 r=3。根据递归式:
-
计算 4 个矩阵相乘的最优值
-
计算 5 个矩阵相乘的最优值
-
根据最优解
根据最优决策数组 s[][]中的数据来构造最优解,即加括号的位置。
首先读取 s[1][5]=4,表示在 k=4 的位置把矩阵分为两个子问题:(A1A2A3A4)、A5。
再看第一个子问题(A1A2A3A4),读取 s[1][4]=1,表示在 k=1 的位置把矩阵分为两个子问题:A1、(A2A3A4)。
子问题 A1 不用再分解,输出;子问题(A2A3A4),读取 s[2][4]=2,表示在 k=2 的位置把矩阵分为两个子问题:A2、(A3A4)。
子问题 A2 不用再分解,输出;子问题(A3A4 ),读取 s[3][4]=3,表示在 k=3 的位置把矩阵分为两个子问题:A3、A4。这两个子问题都不用再分解,输出
子问题 A5 不用再分解,输出。
最优解为:((A1(A2(A3A4)))A5)。
最优值为:314。
代码实现:
按照算法思想和设计,以下程序将矩阵的行和列存储在一维数组 p[],m[][]数组用于存储分成的各个子问题的最优值,s[][]数组用于存储各个子问题的决策点,然后在一个 for 循环里,将问题分为规模为 r 的子问题,求每个规模子问题的最优解,那么得到的 m[1][n]就是最小的计算量。
-
矩阵连乘求解函数
首先将数组 m[][],s[][]初始化为 0,然后自底向上处理不同规模的子问题,r 为问题的规模,r= 2;r <= n;r++,当 r= 2 时,表示矩阵连乘的规模为 2,即两个矩阵连乘。求解两个矩阵连乘的最优值和最优策略,根据递归式对每一个 k 值,找到最小值用 m[i][j]记录,并用 s[i][j]记录取得最小值的 k 值。
void matrixchain() { int i,j,r,k; memset(m,0,sizeof(m)); memset(s,0,sizeof(s)); for(r = 2; r <= n; r++) //不同规模的子问题 { for(i = 1; i <= n-r+1; i++) { j = i + r - 1; m[i][j] = m[i+1][j] + p[i-1] * p[i] * p[j]; //决策为k=i的乘法次数 s[i][j] = i; //子问题的最优策略是i; for(k = i+1; k < j; k++) //对从i到j的所有决策,求最优值,记录最优策略 { int t = m[i][k] + m[k+1][j] + p[i-1] * p[k] * p[j]; if(t < m[i][j]) { m[i][j] = t; s[i][j] = k; } } } } }
-
最优解输出函数
根据存储表格 s[][]中的数据来构造最优解,即加括号的位置。首先打印一个左括号,然后递归求解子问题 print(i,s[i][j]),print(s[i][j]+1,j),再打印右括号,当 i=j 即只剩下一个矩阵时输出该矩阵即可。
void print(int i,int j) { if( i == j ) { cout <<"A[" << i << "]"; return ; } cout << "("; print(i,s[i][j]); print(s[i][j]+1,j); cout << ")"; }
完整代码:
//program 4-4
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int msize = 100;
int p[msize];
int m[msize][msize];
int s[msize][msize];
int n;
void matrixchain()
{
int i,j,r,k;
memset(m,0,sizeof(m));
memset(s,0,sizeof(s));
for(r = 2; r <= n; r++) //不同规模的子问题
{
for(i = 1; i <= n-r+1; i++)
{
j = i + r - 1;
m[i][j] = m[i+1][j] + p[i-1] * p[i] * p[j]; //决策为k=i的乘法次数
s[i][j] = i; //子问题的最优策略是i;
for(k = i+1; k < j; k++) //对从i到j的所有决策,求最优值,记录最优策略
{
int t = m[i][k] + m[k+1][j] + p[i-1] * p[k] * p[j];
if(t < m[i][j])
{
m[i][j] = t;
s[i][j] = k;
}
}
}
}
}
void print(int i,int j)
{
if( i == j )
{
cout <<"A[" << i << "]";
return ;
}
cout << "(";
print(i,s[i][j]);
print(s[i][j]+1,j);
cout << ")";
}
int main()
{
cout << "请输入矩阵的个数 n:";
cin >> n;
int i ,j;
cout << "请依次输入每个矩阵的行数和最后一个矩阵的列数:";
for (i = 0; i <= n; i++ )
cin >> p[i];
matrixchain();
print(1,n);
cout << endl;
/*用于测试
for (i = 1; i <= n; i++ )
{
for (j = i; j <= n; j++ )
cout << m[i][j]<<" " ;
cout << endl;
}
for (i = 1; i <= n; i++ )
{
for (j = i; j <= n; j++ )
cout << s[i][j]<<" " ;
cout << endl;
}
cout << endl;
*/
cout << "最小计算量的值为 " << m[1][n] << endl;
}
算法复杂度:
-
时间复杂度
由程序可以得出:语句 t= m[i][k] + m[k+1][j] +p[i−1]*p[k]*p[j],它是算法的基本语句,在 3 层 for 循环中嵌套。最坏情况下,该语句的执行次数为 O(n3),print()函数算法的时间主要取决于递归,时间复杂度为 O(n)。故该程序的时间复杂度为 O(n3)。
-
空间复杂度
该程序的输入数据的数组为 p[],辅助变量为 i、j、r、t、k、m[][]、s[][],空间复杂度取决于辅助空间,因此空间复杂度为 O(n2)。