Chapter3 动态规划

Chapter3 动态规划

动态规划的基本思想

动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合用动态规划法求解的问题,经分解得到的子问题往往不是互相独立的。

  • e . g . e.g. e.g.多阶段图最短路问题

下图表示城市之间的交通路网,线段上的数字表示费用,单向通行由 A → E A\to E AE。求 A → E A\to E AE的最省费用。

image-20201005190547579

此图有明显的次序,可以划分为5阶段。故此问题的要求是:在各个阶段选取一个恰当的决策,使由这些决策组成的一个决策序列所决定的一条路线,其总路程最短。

image-20201005190802218

分析:如果 B 1 → E B_1\to E B1E的最短路已知, B 2 → E B_2\to E B2E的最短路已知 ⟹ \Longrightarrow 知道了最短路

所以,动态规划有以下两个特点

  • 原问题的最优解包含了子问题的最优解 ⟶ \longrightarrow 最优子结构
  • 求解 B B B问题时
    • B 1 B_1 B1问题依赖 C 1 , C 2 , C 4 C_1,C_2,C_4 C1,C2,C4的最优解
    • B 2 B_2 B2问题依赖 C 1 , C 3 C_1,C_3 C1,C3的最优解

可以看出, C 1 C_1 C1的解被 B 1 , B 2 B_1,B_2 B1,B2重复使用,子问题的解被多次使用 ⟶ \longrightarrow 子问题重叠 ⟶ \longrightarrow 重复计算

设计问题的步骤

  • 找出最优解的性质,刻画其结构特征
  • 递归的定义最优值
  • 以自底向上的方式计算它
  • 根据计算最优值时得到的信息,构造最优解

定义递归函数注意要点

  • 传的参数,以及它的范围条件
  • 递归的边界条件,通常在记忆化搜索中,以记录的元素存在为边界条件
if(Memorized[i][j]) return Memorized[i][j];
  • 剪枝的条件

矩阵连乘问题

$Question: $ 给定 n n n个矩阵: A 1 , A 2 , … , A n A_1, A_2, …, A_n A1,A2,,An,其中 A i A_i Ai A i + 1 A_{i+1} Ai+1是可乘的。确定一种连乘的顺序,使得矩阵连乘的计算量为最小。

  • 如果直接顺序相乘,矩阵连乘的基本乘法数是

( p × r ) × [ ( p × q ) × q ] (p\times r)\times[(p\times q)\times q] (p×r)×[(p×q)×q]

可以发现,在矩阵很多时,乘法的数目是很庞大的,因此,我们需要最小化乘法的次数来减少计算的时间。

  • 不同计算顺序的差别

可以发现:求多个矩阵的连乘积时,计算的结合顺序是十分重要的。

  • 考虑一种四个矩阵相乘的特殊情况

设矩阵为 A 1 , A 2 , A 3 , A 4 A_1, A_2, A_3, A_4 A1,A2,A3,A4 A 1 A_1 A1的行列为 p 0 , p 1 p_0,p_1 p0,p1 A 2 A_2 A2的行列为 p 1 , p 2 . . . p_1,p_2... p1,p2...以此类推.

这样,四个矩阵的行列值,可以通过一个一维数组存放.

int p[5] = {p0,p1,p2,p3,p4};

根据上表的计算顺序对于乘法次数的影响我们可以发现,乘法次数的大小取决于加的括号的位置,于是,我们可以对 A 1 , A 2 , A 3 , A 4 A_1, A_2, A_3, A_4 A1,A2,A3,A4这四个矩阵的乘法顺序进行划分,有以下三种情况

  1. A 1 , ∣ A 2 , A 3 , A 4 A_1,| A_2, A_3, A_4 A1,A2,A3,A4
  2. A 1 , A 2 , ∣ A 3 , A 4 A_1, A_2, |A_3, A_4 A1,A2,A3,A4
  3. A 1 , A 2 , A 3 , ∣ A 4 A_1, A_2, A_3, |A_4 A1,A2,A3,A4

现在要做的工作就是,如果求出了 A 2 , A 3 , A 4 A_2, A_3, A_4 A2,A3,A4 A 1 , A 2 A_1, A_2 A1,A2 A 3 , A 4 A_3, A_4 A3,A4 A 1 , A 2 , A 3 A_1, A_2, A_3 A1,A2,A3这四个子问题的乘法次数的最小值,再把它们三种情况进行比较,就求得了整个问题的最小值.

如果设 m [ i ] [ j ] m[i][j] m[i][j]是矩阵 Π k = i j A i \Pi_{k=i}^{j} A_i Πk=ijAi的值,那么,上述三个的乘法次数如下

  1. m [ 2 ] [ 4 ] + ( m [ 1 ] [ 1 ] = 0 ) + p 0 p 1 p 4 m[2][4]+(m[1][1]=0)+p_0p_1p_4 m[2][4]+(m[1][1]=0)+p0p1p4
  2. m [ 1 ] [ 2 ] + m [ 3 ] [ 4 ] + p 0 p 2 p 4 m[1][2]+m[3][4]+p_0p_2p_4 m[1][2]+m[3][4]+p0p2p4
  3. m [ 1 ] [ 3 ] + ( m [ 4 ] [ 4 ] = 0 ) + p 0 p 3 p 4 m[1][3]+(m[4][4]=0)+p_0p_3p_4 m[1][3]+(m[4][4]=0)+p0p3p4

通过观察,我们可以归纳出从 A i A_i Ai A j A_j Aj的通用方程

  • 从$A_i 到 到 A_j$的通用方程

    设矩阵 A i A i + 1 . . . A j A_iA_{i+1}...A_j AiAi+1...Aj , k k k是划分的位置

    • m [ i ] [ j ] = m i n ( Π k = i j A i ) = m i n i ≤ k < j ( m [ i ] [ k ] + m [ k + 1 ] [ j ] + p i − 1 p k p j ) ( i < j ) m[i][j]=min(\Pi_{k=i}^{j} A_i)=min_{i\le k<j}(m[i][k]+m[k+1][j]+p_{i-1}p_kp_j) (i<j) m[i][j]=min(Πk=ijAi)=minik<j(m[i][k]+m[k+1][j]+pi1pkpj)(i<j)
    • 0 ( i = j ) 0 (i=j) 0(i=j)

备忘录

使用 m [ i ] [ j ] m[i][j] m[i][j]记录已经求过的子问题,避免了相同子问题的重复计算

递归求解

#include <bits/stdc++.h>

using namespace std;
#define ll long long
#define mod 1000000007
const ll maxn = 2e6 + 7;
ll p[maxn];
ll m[1000][1000];

ll recursiveMatrixChain(ll i, ll j) {
    if (i == j) return  m[i][j]=0;
    if (m[i][j] > 0) return m[i][j];
    ll minVal = recursiveMatrixChain(i, i) + recursiveMatrixChain(i + 1, j) + p[i - 1] * p[i] * p[j];
    m[i][j] = minVal;
    for (long long k = i ; k < j; ++k) {
        ll tmp = recursiveMatrixChain(i, k) + recursiveMatrixChain(k + 1, j) + p[i - 1] * p[k] * p[j];
        if (tmp < minVal) {
            minVal = tmp;
        }
    }
    m[i][j] = minVal;
    return minVal;
}


int main() {
    ll n;
    cin >> n;
    for (long long i = 0; i <= n; ++i) {
        cin >> p[i];
    }
    int ans=recursiveMatrixChain(1, n);//注意范围,因为题目给定的n是矩阵数目-1所以是n
    //如果题目了n个矩阵,那么递归的范围是[1,n-1]
//    RecurMatrixChain(1,n);
//    for (long long i = 0; i <= n; ++i) {
//        for (long long j = 0; j <= n; ++j) {
//            cout << m[i][j] << setw(5) << " ";
//        }
//        cout << '\n';
//    }
    cout<<ans;

    return 0;
}

循环求解

如何避免递归?

image-20201012200630843

可以发现,递归是自顶向下的求解过程,如果我们换一种思路,从树底求解最小子问题(两个矩阵连乘的问题),再用求解好的子问题求解更上层的问题,最终达到求解1-n整个问题答案的目的

时间复杂度为: O ( n 3 ) O(n^3) O(n3)

空间复杂度为: O ( n 2 ) O(n^2) O(n2)

  • Code
#include <bits/stdc++.h>

using namespace std;
#define ll long long
#define mod 1000000007
const ll maxn = 2e6 + 7;
int m[2000][2000];
int p[2000];
int s[2000][2000];
int n;

int matrixChain() {
    for (long long i = 0; i <= n; ++i) {
        //对角线上的元素置0
        m[i][i] = 0;
    }
    for (long long i = n; i >= 1; --i) {
        for (long long j = i + 1; j <= n; ++j) {
            //找出每一个的初值
            m[i][j] = m[i][j] + m[i + 1][j] + p[i - 1] * p[i] * p[j];
            s[i][j] = i;
            for (long long k = i + 2; k < j; ++k) {//i+1已经比过
                //找到最小值
                int tmp = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j];
                if (tmp < m[i][j]) {
                    m[i][j] = tmp;
                    s[i][j] = k;
                }
            }
        }
    }
    return m[1][n];
}

void trace(int i, int j) {
    if (i == j) {
        cout << 'A' << i;
        return;
    }
    cout << "(";
    int k = s[i][j];
    trace(i, k);
    trace(k + 1, j);
    cout << ")";
    return;
}


int main() {

    cin >> n;
    for (long long i = 0; i <= n; ++i) {
        cin >> p[i];
    }
    cout << matrixChain() << endl;
    trace(1, n);
    cout << endl;
    for (long long i = 0; i <= n; ++i) {
        for (long long j = 0; j <= n; ++j) {
            cout << m[i][j] << ' ';
        }
        cout << '\n';
    }
    return 0;
}

路径问题

问题集

  • 数字三角形

https://www.luogu.com.cn/problem/P1216

#include <bits/stdc++.h>

using namespace std;
#define ll long long
#define mod 1000000007
const ll maxn = 2e6 + 7;
int dp[1001][1001];

int main() {
    ll n;
    cin >> n;
    for (long long i = 0; i <= n; ++i) {
        for (long long j = 0; j <= n; ++j) {
            dp[i][j] = -1000000;
        }
    }
    for (long long i = 1; i <= n; ++i) {
        for (long long j = 1; j <= i; ++j) {
            cin >> dp[i][j];
        }
    }
    for (long long i = n - 1; i >= 1; --i) {
        for (long long j = 1; j <= i; ++j) {
            dp[i][j] += max(dp[i + 1][j], dp[i + 1][j + 1]);
        }
    }
//    for (long long i = 0; i <= n; ++i) {
//        for (long long j = 0; j <= n; ++j) {
//            cout << dp[i][j] << ' ';
//        }
//        cout << endl;
//    }
    cout << dp[1][1];


    return 0;
}
  • 滑雪

https://www.luogu.com.cn/problem/P1434

记忆化DFS

#include <bits/stdc++.h>

using namespace std;
#define ll long long
#define mod 1000000007
const ll maxn = 2e6 + 7;
ll dx[4] = {1, -1, 0, 0};
ll dy[4] = {0, 0, 1, -1};
ll a[1001][1001];
ll s[1001][1001];
ll r, c;

ll dfs(ll x, ll y) {
    if (s[x][y]) return s[x][y];
    s[x][y] = 1;
    for (long long i = 0; i < 4; ++i) {
        ll xi = x + dx[i];
        ll yi = y + dy[i];
        if (xi > 0 && yi > 0 && xi <= r && yi <= c && a[x][y] > a[xi][yi]) {
            //这里的边界有效条件除了考虑xi,yi的不能超范围,还要保证已经搜过的不能再搜的边界条件,也就是a[x][y] > a[xi][yi]
            dfs(xi, yi);
            s[x][y] = max(s[x][y], s[xi][yi] + 1);
        }
    }
    return s[x][y];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> r >> c;
    for (long long i = 1; i <= r; ++i) {
        for (long long j = 1; j <= c; ++j) {
            cin >> a[i][j];
        }
    }
    ll ans = -1;
    for (long long i = 1; i <= r; ++i) {
        for (long long j = 1; j <= c; ++j) {
            ans = max(ans, dfs(i, j));
        }
    }
    cout << ans << endl;
//    for (long long i = 0; i <= r; ++i) {
//        for (long long j = 0; j <= c; ++j) {
//            cout << s[i][j] << ' ';
//        }
//        cout << endl;
//    }
    return 0;
}

最长公共子序列(LCS)

Q u e s t i o n Question Question

若给定序列 X = x 1 , x 2 , … , x m X={x_1,x_2,…,x_m} X=x1,x2,,xm,则另一序列 Z = z 1 , z 2 , … , z k Z={z_1,z_2,…,z_k} Z=z1,z2,,zk,是 X X X的子序列是指存在一个严格递增下标序列 i 1 , i 2 , … , i k {i_1,i_2,…,i_k} i1,i2,,ik使得对于所有 j = 1 , 2 , … , k j=1,2,…,k j=1,2,,k有: z j = x i j z_j=x_{ij} zj=xij

例如,序列 Z = B , C , D , B Z={B,C,D,B} Z=BCDB是序列 X = A , B , C , B , D , A , B X={A,B,C,B,D,A,B} X=ABCBDAB的子序列,相应的递增下标序列为 2 , 3 , 5 , 7 {2,3,5,7} 2357

给定 2 2 2个序列 X X X Y Y Y,当另一序列 Z Z Z既是 X X X的子序列又是 Y Y Y的子序列时,称 Z Z Z是序列 X X X Y Y Y的公共子序列。

给定 2 2 2个序列 X = x 1 , x 2 , … , x m X={x_1,x_2,…,x_m} X=x1,x2,,xm Y = y 1 , y 2 , … , y n Y={y_1,y_2,…,y_n} Y=y1,y2,,yn,找出 X X X Y Y Y的最长公共子序列。

  • LCS的结构分析

设序列 X = x 1 , x 2 , … , x m X={x_1,x_2,…,x_m} X=x1,x2,,xm和的 Y = y 1 , y 2 , … , y n Y={y_1,y_2,…,y_n} Y=y1,y2,,yn最长公共子序列为 Z = z 1 , z 2 , … , z k Z={z_1,z_2,…,z_k} Z=z1,z2,,zk ,则
(1)若 x m = y n x_m=y_n xm=yn,则 z k = x m = y n z_k=x_m=y_n zk=xm=yn,且 Z k − 1 Z_{k-1} Zk1 X m − 1 X_{m-1} Xm1 Y n − 1 Y_{n-1} Yn1的最长公共子序列。
(2)若 x m ≠ y n x_m≠y_n xm=yn z k ≠ x m z_k≠x_m zk=xm,则 Z Z Z X m − 1 X_{m-1} Xm1 Y Y Y的最长公共子序列。
(3)若 x m ≠ y n x_m≠y_n xm=yn z k ≠ y n z_k≠y_n zk=yn,则 Z Z Z X X X Y n − 1 Y_{n-1} Yn1的最长公共子序列。

由此可见,2个序列的最长公共子序列包含了这2个序列的前缀的最长公共子序列。因此,最长公共子序列具有最优子结构性质(2,3条件下总有一个最优解)。

由LCS 的最优子结构性质建立子问题最优值的递归关系。用 c [ i ] [ j ] c[i][j] c[i][j]记录序列 X i X_i Xi Y j Y_j Yj的最长公共子序列的长度。其中, X i = x 1 , x 2 , … , x i X_i={x_1,x_2,…,x_i} Xi=x1,x2,,xi Y j = y 1 , y 2 , … , y j Y_j={y_1,y_2,…,y_j} Yj=y1,y2,,yj。当 i = 0 i=0 i=0 j = 0 j=0 j=0时,空序列是 X i X_i Xi Y j Y_j Yj的最长公共子序列。故此时 c [ i ] [ j ] = 0 c[i][j]=0 c[i][j]=0。其它情况下,由最优子结构性质可建立递归关系如下:

image-20201012211637536

可以发现

  • 这是一个二维表
  • 填表顺序从左上角到右下角
  • 时间复杂度和空间复杂度都为 O ( n 2 ) O(n^2) O(n2)

同时LCS还具有子问题的重叠性

image-20201012211937227
  • 算法思路

自左而右自上而下建立表格 m a t r i x matrix matrix[][]。
(1)如果 s t r 1 [ i ] = s t r 2 [ j ] str1[i]=str2[j] str1[i]str2[j]则将左上角元素值加1赋值给 m a t r i x [ i ] [ j ] matrix[i][j] matrix[i][j],如果本身是最左上角元素就为1。
(2)如果 s t r 1 [ i ] str1[i] str1[i]不等于 s t r 2 [ j ] str2[j] str2[j]则该点元素值取 m a t r i x [ i - 1 ] [ j ] matrix[i-1][j] matrix[i1][j] m a t r i x [ i ] [ j - 1 ] matrix[i][j-1] matrix[i][j1]中较大的一个。如果 i = 0 i=0 i0 j = 0 j=0 j0(最左上角)则取 0 0 0

image-20201012212540197
  • Code
#include <bits/stdc++.h>

using namespace std;
#define ll long long
#define mod 1000000007
const ll maxn = 2e6 + 7;
int c[2000][2000];

void LCSLength(char x[], char y[]) {  //调用该函数前,先将c数组置初值为0
    int i, j;
    for (i = 1; i <= strlen(x); i++) //自上而下
        for (j = 1; j <= strlen(y); j++) { //每行自左向右
            if (x[i - 1] == y[j - 1])//下标从0开始
                c[i][j] = c[i - 1][j - 1] + 1;
            else if (c[i - 1][j] >= c[i][j - 1]) {
                c[i][j] = c[i - 1][j];
            } else c[i][j] = c[i][j - 1];
        }
}

void LCS(int i, int j, char x[], char y[]) {
    if (i == 0 || j == 0) {
        return;
    }
    if (x[i - 1] == y[j - 1]) {//下标从0开始
        LCS(i - 1, j - 1, x, y);
        cout << x[i - 1];//下标从0开始
    } else if (c[i - 1][j] >= c[i][j - 1]) {
        LCS(i - 1, j, x, y);
    } else LCS(i, j - 1, x, y);
}


char x[12000];
char y[12000];

int main() {
    cin >> x >> y;
    LCSLength(x, y);
    int lenx = strlen(x);
    int leny = strlen(y);

    cout << c[lenx][leny] << endl;

    for (long long i = 0; i <= max(lenx, leny); ++i) {
        for (long long j = 0; j <= max(lenx, leny); ++j) {
            cout << c[i][j] << ' ';
        }
        cout << endl;
    }

    LCS(lenx, leny, x, y);

    return 0;
}
/*
dabcfbc  eabfcab
 */
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值