一、矩阵链乘的递归关系
假设有如下五个矩阵链乘,r表示相乘的时候矩阵行或列的大小,(矩阵乘法是前一个矩阵的行乘以后一个矩阵的列嘛,M1列r1行r2,M2列r2行r3。所以他们可以相乘,而且一定是按照这个排好的顺序左右相乘)我们讨论的是怎么个先后乘的问题,可以减少乘法的次数。
从上图我们就可以总结出一个递归方程
C
[
i
,
j
]
=
{
0
,
i
=
j
m
i
n
i
<
k
≤
j
{
C
[
i
,
k
−
1
]
+
C
[
k
,
j
]
+
r
i
r
k
r
j
=
1
}
,
i
<
j
C[i, j]= \begin{cases} 0 &,i=j \\ {min}_{i<k \leq j} \lbrace C[i, k-1]+C[k, j]+r_ir_kr_{j=1} \rbrace &,i<j \end{cases}
C[i,j]={0mini<k≤j{C[i,k−1]+C[k,j]+rirkrj=1},i=j,i<j
递归的伪代码
初始c[][]=-1,没算过的都是存-1. 第i个矩阵的行列值为r[i],r[i+1],j k表示的是第几个矩阵
int fun(r[],j, k)
if j==k return 0; //表示这是单独的一个数组了
else if c[j,k] != -1 return c[j,k]; //动态规划的典型——存储,不要重复计算
minv=MAX_INT //初始化,搞一个超级大的数作为最小值
for i=j+1..k //将当前链乘的矩阵又每种拆分情况都算一次
t=fun(j, i-1)+fun(i, k)+r[j]*r[i]*r[k+1];
//一个重点是i j k的关系;j和k不动(因为你要把这一段链乘拆分嘛),i是每次切一刀的位置(拆分处)
minv=min(t,minv);//每种拆分情况都拿去和上一次比出来的最小 比较
c[j,k]=minv; //经过一系列比较,我们把最小的记录下来
return c[j,k]
二、矩阵链乘迭代及伪代码
通过迭代伪代码可以发现,第一轮计算发生在“切”到j=k的时候,然后第二轮是算的相邻的左右俩矩阵乘法次数。或者换个说法,假设为了算C[1, 3]的情况,得把它切成C[1, 2]与C[2, 3]的情况算出来,这又得算C[1,1]、C[2, 2]、C[3, 3]。
前面的铺垫都是为了表达,我们迭代的顺序是个斜线,哈哈哈。
输入:矩阵个数n,阶r[..n+1],其中r[k],r[k+1]分布为第k个矩阵的行列数
输出: n个矩阵相乘的乘法最小次数,最优顺序的信息数组S[1..n, 1..n]
for i=1 to n C[i, i]=0 //对角线d0置0
for d=1 to n-1 //逐条计算对角线d1~dn.把最长的对角线当作第0条对角线,在上一步我们赋值为0了,由上图我们可以知道d1是哪一条对角线
for i=1 to n-d //当前这条对角线有n-d个待填充的,i是矩阵中他们的行号
j=i+d; //待填充的列号刚好就是对应行i,往后缩进对角线d
C[i, j]=∞, //也是初始化为一个超大的数
for k=i+1 to j //开始行动填表了,他需要的最小值都存了
x= C[i,k-1]+C[k,j]+r[i]*r[k]*r[j+1]
if x<C[i,j]
then C[i, j]=x; S[i, j]=k //除了记录最小的乘法次数还记录了乘法顺序
return C[1,n],S
三、最优剖分思路
设P是一个有n个顶点的凸多边形,P中的弦是P中连接两个非相邻顶点的线段。用P中的(n-3)条弦将P剖分成(n-2)个三角形(如下图所示)。使得(n-3)条弦的长度之和最小的三角形剖分称为最优三角剖分。
具体要求(若在ACM平台上提交程序,必须按此要求)――平台上1701题
输入:输入的第一行是一个正整数m,表示测试例个数,接下来几行是m个测试例的数据,每个测试例的数据由两行组成,第一行含一个正整数n (n<=500),表示凸多边形的顶点个数;第二行含2n个实数x1 , y1 , x2 , y2 , …xn , yn ,按顺时针方向依次给出n个顶点的坐标值(xi, yi) i=1, 2, …, n,整数之间用一个空格隔开。
输出:对于每个测试例输出一行,含一个实数(精确到小数点后三位),表示最优三角剖分的n-3条弦的长度之和。两个测试例的输出数据之间用一个空行隔开,最后一个测试例后无空行。
一张图看懂最优剖分与矩阵链乘的关系(B站一个视频里看到的,这不说只有大佬能想到吧,好奇妙的转换)
边的个数是n,编号是0~n-2,括号的形式指的是某个边相夹的线段。每切一刀还可以看做去递归求解切出来的两个矩形。边标号i对应数组里的点对就应该是下标i和i+1。
简单说一下这里填表的方式,对于更长的边,比如
(
x
0
(
(
x
1
x
2
)
x
3
)
)
(x_0((x_1x_2)x_3))
(x0((x1x2)x3))必定得先算
(
x
1
x
2
)
(x_1x_2)
(x1x2)
*表示就不算了,数字1表示我们第一次填表的地方,黄色是指
(
(
x
1
x
2
)
x
3
)
((x_1x_2)x_3)
((x1x2)x3),他等于min{
(
x
1
x
2
)
,
(
x
2
x
3
)
(x_1x_2), (x_2x_3)
(x1x2),(x2x3)}+distence(
x
1
,
x
3
x_1,x_3
x1,x3)。不要混淆的是,程序中记录值的数组记录的是上图黄色连线的和,而distance函数只是在直接算两点间距离。
四、最优剖分Java实现
import java.util.Arrays;
import java.util.Scanner;
public class Dissection {
// static double[][] arr;
//两个测试数据
// static double[][] arr = {{1,2},{2,1.5},{2,0.5},{1,0},{0,0.5},{0,1.5}};
static double[][] arr = {{723,1220},{463,1074},{370,842},{317,534},{524,192},{992,87},{1378,355},{1683,855},{1301,1131}};
public static void main(String[] args) {
// Scanner in = new Scanner(System.in);
// int times = in.nextInt();
// for(int t=0; t<times; t++) {
// int n = in.nextInt()-2;
//边是从0~n-2,所以把读取点的数目减2
// arr = new double[n][2];
// for(int j=0; j<n; j++) {
// arr[j][0] = in.nextDouble();
// arr[j][1] = in.nextDouble();
// }
int n = 7; //测试数据,边的下标从0~7,九个点
double m[][] = new double[n+1][n+1];// 保存结果
int MAX = Integer.MAX_VALUE;//正无穷
int j;//表示每次填充坐标的列号
for (int time=1; time<=n; time++){
//由图知,斜着填表,七条斜线,算7轮
for (int i=0; i<=n-time; i++){
//二维数组的需要填充的行每次少1
j = i+time;
System.out.println(i+" "+j);
m[i][j] = MAX;
//把可能有值的数组全部初始化为整数最大值
for (int k=i; k<=j-1; k++){
double cur = m[i][k] + m[k + 1][j] + distance(i, j+1);
if (cur < m[i][j]){//当前值有效,填入数组
m[i][j] = cur;
}
}
}
}
for(int k = 0; k<n-1; k++)
System.out.println(Arrays.toString(m[k]));
// System.out.println(s[t]);
// }
// in.close();
}
public static double distance(int i, int j) {
double distance = 0;
double a = Math.pow(arr[i][0]-arr[j][0], 2);
double b = Math.pow(arr[i][1]-arr[j][1], 2);
distance = Math.sqrt(a+b);
return distance;
}
}
不过这份代码测试案例没过,我会尽快调试出来。