C语言矩阵连乘 (动态规划)详解

C语言矩阵连乘 (动态规划)详解

算法原理

矩阵乘法不满足交换律和结合律,因此计算矩阵连乘的顺序会影响计算时间。即使只有6个矩阵相乘,也有可能有超过百万种计算次序。因此需要通过算法来优化时间复杂度。动态规划是一种可用于求解最优化问题的算法,它将原问题分解为子问题求解,并将每个子问题的最优解存储在表格中,以便在较大的子问题中简化计算。

设矩阵 $A_{1 \times 5}$、$A_{5 \times 10}$、$A_{10 \times 3}$ 和 $A_{3 \times 12}$ 依次相乘,分别用 $A_1$、$A_2$、$A_3$ 和 $A_4$ 表示。根据动态规划的思想,将矩阵连乘问题分解为子问题,即:

$M(1,4)=min{M(1,k)+M(k+1,4)+p_{0}p_{1}p_{4}}$

其中 $p_0$、$p_1$ 和 $p_4$ 分别表示 $A_1$ 的列数、$A_2$ 的列数和 $A_4$ 的行数,$M(1,4)$ 表示将矩阵 $A_1$ 到 $A_4$ 连乘所需要的最少次数,$k$ 表示从 $A_1$ 到 $A_4$ 中的任意一矩阵,即矩阵乘法的括号位置。

算法实现

步骤一:分配空间

由于要计算从 $A_1$ 到 $A_n$ 的所有矩阵连乘次序的最小值,因此需要分配一个 $n \times n$ 的 int 类型数组存储最小次数。

int m[MAXSIZE][MAXSIZE];

同样地,还需要分配一个 $n \times n$ 的 int 类型数组存储最优解对应的括号位置。

int s[MAXSIZE][MAXSIZE];

步骤二:计算最小次数和最优解括号位置

根据矩阵连乘算法原理,可以用递归方法暴力枚举所有可能次序,并计算出最小次数和最优解括号位置,时间复杂度为$O(2^n)$,其中 $n$ 表示矩阵个数。

为了提高速度,使用动态规划算法。将矩阵乘法的次序由2个矩阵相乘逐渐扩大,直到求解所有 $n$ 个矩阵相乘。假设现在要求解从 $A_i$ 到 $A_j$ 连乘所需的最小次数,设 $k$ 为括号位置,则可以得到递推式:

$$ M(i,j) = \min{M(i,k) + M(k+1,j) + p_{i-1}p_kp_j} $$

其中 $p_i$ 表示第$i$个矩阵的行数和第$i+1$个矩阵的列数,$M(i,j)$ 表示将矩阵 $A_i$ 到 $A_j$ 连乘所需要的最少次数。为了实现上表达式中的$M(i,k)$和$M(k+1,j)$,需要额外存储一个 $n \times n$ 的 int 类型数组存储中间过程。

我们只需要枚举括号的位置 $k$,得到最小的连乘次数 $M(i,j)$。同时,将括号位置 $k$ 存储到 $s(i,j)$ 中。动态规划问题的最终结果为 $M(1,n)$,此时需要用 $s$ 数组中存储的括号位置信息来生成最优解。

void matrixChainOrder() {
    for (int i = 1; i <= n; i++) {
        m[i][i] = 0;
    }
    for (int l = 2; l <= n; l++) {
        for (int i = 1; i <= n - l + 1; i++) {
            int j = i + l - 1;
            m[i][j] = INT_MAX;
            for (int k = i; k < j; k++) {
                int q = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j];
                if (q < m[i][j]) {
                    m[i][j] = q;
                    s[i][j] = k;
                }
            }
        }
    }
}

步骤三:输出最优解

根据存储的括号位置信息 $s$,可以生成最优解的字符串,即括号序列。下面给出一个示例输出。

假设有矩阵 $A_1$、$A_2$、$A_3$ 和 $A_4$,形状分别为 $1 \times 5$、$5 \times 10$、$10 \times 3$ 和 $3 \times 12$。将这四个矩阵连乘所需要的最少次数为 620。最优解为 $((A_1(A_2A_3))A_4)$。

代码如下:

void printOptimalParens(int i, int j) {
    if (i == j) {
        printf("A%d", i);
    } else {
        printf("(");
        printOptimalParens(i, s[i][j]);
        printOptimalParens(s[i][j] + 1, j);
        printf(")");
    }
}

示例说明

假设有四个矩阵相乘,形状分别为 $A_{1 \times 5}$、$A_{5 \times 10}$、$A_{10 \times 3}$ 和 $A_{3 \times 12}$。代码实现如下:

#include <stdio.h>
#include <limits.h>

#define MAXSIZE 100

int n = 4;
int p[MAXSIZE + 1] = { 1, 5, 10, 3, 12 };
int m[MAXSIZE][MAXSIZE];
int s[MAXSIZE][MAXSIZE];

void matrixChainOrder() {
    for (int i = 1; i <= n; i++) {
        m[i][i] = 0;
    }
    for (int l = 2; l <= n; l++) {
        for (int i = 1; i <= n - l + 1; i++) {
            int j = i + l - 1;
            m[i][j] = INT_MAX;
            for (int k = i; k < j; k++) {
                int q = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j];
                if (q < m[i][j]) {
                    m[i][j] = q;
                    s[i][j] = k;
                }
            }
        }
    }
}

void printOptimalParens(int i, int j) {
    if (i == j) {
        printf("A%d", i);
    } else {
        printf("(");
        printOptimalParens(i, s[i][j]);
        printOptimalParens(s[i][j] + 1, j);
        printf(")");
    }
}

int main() {
    matrixChainOrder();
    printf("最少次数:%d\n", m[1][n]);
    printf("最优解:%s\n", "");
    printOptimalParens(1, n);
    printf("\n");
    return 0;
}

运行结果如下:

最少次数:620
最优解:((A1(A2A3))A4)

在这个示例中,我们将 $A_1$ 到 $A_4$ 连乘,记为 $((A_1A_2)A_3)A_4$ 和 $A_1(A_2(A_3A_4))$,其中前者需要计算 $90+500+1500=2090$ 次,后者需要计算 $5+180+4320=4510$ 次。由此可见,采用动态规划算法可以得到最优解,而暴力枚举算法时间复杂度太高,无法得到最优解。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:C语言矩阵连乘 (动态规划)详解 - Python技术站

(1)
上一篇 2023年5月24日
下一篇 2023年5月24日

相关文章

  • C++ Clock类模拟实现闹钟运行

    C++中的Clock类可以用于时钟和计时器的计算和管理。模拟实现一个闹钟可以借助Clock类的一些方法和属性,具体步骤如下: 1. 定义Clock类 首先需要定义一个Clock类,至少需要有开始计时、停止计时、获取当前时间等方法。 class Clock { public: void start(); // 开始计时 void stop(); // 停止计时…

    C 2023年5月23日
    00
  • C++如何实现二叉树链表

    C++可以通过定义结构体来表示二叉树链表节点,结构体中包含左右子节点指针和数据域。通过指针来实现二叉树的构建和遍历。 以下是具体的实现步骤: 1. 定义结构体 首先我们需要定义一个结构体来表示二叉树链表节点,结构体定义如下: struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNod…

    C 2023年5月23日
    00
  • C语言用指针传递数据

    C语言中,通过指针传递数据是常见的编程方式,它可以使变量在多个函数中共享,同时也可以避免函数返回值造成的资源浪费等问题。 一、指针的基础语法 指针是存储其他变量地址的变量,可以通过 * 运算符获取该地址存储的值。指针的定义方式如下: int *p; // 定义一个指向 int 类型变量的指针 通过 & 运算符可以获取变量的地址,如: int a = …

    C 2023年5月9日
    00
  • 一个基于C#开发的Excel转Json工具使用教程

    下面将会给出一份“一个基于C#开发的Excel转Json工具使用教程”的完整攻略。 一、背景 在数据处理中,Excel表格是非常常见的一种数据表现形式。而Json格式则是Web开发中常用的数据格式。因此,将Excel表格转换为Json格式也是一个非常实用的需求。本文将介绍如何使用一个基于C#开发的工具将Excel表格转换为Json格式。 二、准备工作 在使用…

    C 2023年5月23日
    00
  • 详解C++11 线程休眠函数

    详解C++11 线程休眠函数 在C++11中,新增了一个<chrono>头文件,其中包含了许多与时间相关的类和函数。其中,std::this_thread::sleep_for是一个非常实用的函数,它可以让当前线程休眠一段时间。 函数原型 namespace std { namespace chrono { template<class R…

    C 2023年5月22日
    00
  • C++ Boost CircularBuffer算法超详细精讲

    C++ Boost CircularBuffer算法超详细精讲 算法简介 CircularBuffer 算法是一个环形缓冲区的实现,允许在队列的尾部添加元素并从队列的头部删除元素。当缓冲区达到最大容量时,最旧的元素将会被替换。 该算法是 C++ Boost 库的一部分,也可以单独使用。 环形缓冲区的实现 头文件 首先,我们需要引入头文件 <boost/…

    C 2023年5月22日
    00
  • C语言自动生成enum值和名字映射代码

    以下是详细讲解“C语言自动生成enum值和名字映射代码”的完整攻略: 背景 在C语言中,枚举类型(enum)是一个非常常用的数据类型。在实际的编程过程中,我们常常需要将枚举类型的变量转换成其对应的字符串表示或者将字符串表示转换成枚举类型的变量。手动编写这样的代码往往非常繁琐且容易出错,因此我们需要一种自动生成这样代码的工具。 工具 在这里,我们推荐使用开源工…

    C 2023年5月24日
    00
  • C 程序 按升序排列数字

    下面我将为你详细讲解如何使用 C 语言编写一个程序,实现对一组数字按升序排列的功能。在这个过程中,我将提供两条示例说明,帮助你更好地理解。 一、题目描述 编写一个 C 语言程序,实现对一组数值按升序排列的功能。程序输入一个整数数组,长度不超过 100,输出数组按升序排列后的结果。 二、实现思路 我们可以使用 C 语言中的冒泡排序算法来实现对一组数字的升序排列…

    C 2023年5月9日
    00
合作推广
合作推广
分享本页
返回顶部