ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

算法设计与分析:动态规划

2021-12-05 10:32:27  阅读:119  来源: 互联网

标签:int 复杂度 算法 cost 字符串 动态 规划 dp


目录

3-2 编辑距离

问题描述

设A和B是2个字符串。要用最少的字符操作将字符串A转换为字符串B。这里所说的字符操作包括 (1)删除一个字符; (2)插入一个字符; (3)将一个字符改为另一个字符。 将字符串A变换为字符串B所用的最少字符操作数称为字符串A到 B的编辑距离,记为d(A,B)。对于给定的字符串A和字符串B,计算其编辑距离 d(A,B)。

算法描述

解决两个字符串的动态规划问题,一般都是用两个指针 i, j 分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模。

对于每对字符 s1[i]s2[j],可以有四种操作:

if s1[i-1] == s2[j-1]
    不作操作(skip)
    i, j 同时向前移动
else
    三选一,取最小:
        插入(insert)
        删除(delete)
        替换(replace)

定义 dp 数组:dp[i][j] 存储 s1[0..i-1] 和 s2[0..j-1] 的最小编辑距离

上述四种选择所对应的 dp 递推表达式为:

匹配操作:

\[dp[i][j]=dp[i-1][j-1] \]

插入,删除,替换操作,

\[dp[i][j] = min( dp[i][j - 1] + 1,dp[i - 1][j] + 1,dp[i - 1][j - 1] + 1) \]

关键代码

        int n1 = word1.length();
        int n2 = word2.length();
        int[][] dp = new int[n1+1][n2+1];

        //base case
        for(int i=0;i<=n2;i++){
            dp[0][i]=i;
        }
        for(int i=0;i<=n1;i++){
            dp[i][0]=i;
        }
        //状态转移
        for(int i=1;i<=n1;i++){
            for(int j =1;j<=n2;j++){
                if(word1.charAt(i-1)==word2.charAt(j-1)){
                    dp[i][j]=dp[i-1][j-1];
                }else{
                    dp[i][j]=1+Math.min(
                        dp[i][j-1],
                        Math.min(dp[i-1][j],dp[i-1][j-1])
                    );
                }
            }
        }
 	   System.out.println(dp[n1][n2]);

结果分析

输入: word1 = "fxpimu", word2 = "xwrs"

输出:5

输入: word1 = "horse", word2 = "ros"

输出:3

输入:word1 = "intention", word2 = "execution"

输出:5

dp数组的每一项均只被访问一次,算法时间复杂度为\(O(n1*n2)\),空间复杂度为\(O(n1*n2)\),\(n1,n2\)分别为字符串w1,w2的长度

3-5 乘法表问题

问题描述

定义于字母表∑{a,b,c)上的乘法表如表所示:

  img

依此乘法表,对任一定义于∑{a,b,c}上的字符串,适当加括号表达式后得到一个表达式。

例如,对于字符串x=bbbba,它的一个加括号表达式为(b(bb))(ba)。依乘法表,该表达式的值为a。

试设计一个动态规划算法,对任一定义于∑{a,b,c}上的字符串x=x1x2…xn,计算有多少种不同的加括号方式, 使由x导出的加括号表达式的值为a。

算法描述

设 0, 1 ,2 分别表示常量a,b,c,N为字符串的长度。

定义dp数组:dp[i][j][k],表示字符字串i-j有多少种不同的加括号方式得到k

设字符串的第 i 到 第 j 位乘积为 a 的加括号法有 dp[i][j][0]种,

 字符串的第 i 到 第 j 位乘积为 b 的加括号法有dp[i][j][1] 种,

 字符串的第 i 到 第 j 位乘积为 c 的加括号法有 dp[i][j][2] 种。

则原问题的解是: dp[0][N-1][0] 。

设 k 为 i 到 j 中的某一个字符,则对于 k 来说,根据乘法表有:

dp[i][j][0] += dp[i][k][0] * dp[k + 1][j][2] + dp[i][k][1] * dp[k + 1][j][2] + dp[i][k][2] * dp[k + 1][j][0];
dp[i][j][1] += dp[i][k][0] * dp[k + 1][j][0] + dp[i][k][0] * dp[k + 1][j][1] + dp[i][k][1] * dp[k + 1][j][1];
dp[i][j][2] += dp[i][k][1] * dp[k + 1][j][0] + dp[i][k][2] * dp[k + 1][j][1] + dp[i][k][2] * dp[k + 1][j][2];

dp数组遍历顺序:

img

关键代码

        String str = scan.nextLine();
        int n = str.length();

        //dp[i][j][k]表示字符子串i:j乘积为k的总数(k=a,b,c)
        int[][][] dp = new int[n][n][3];
        for (int i = 0; i < n; i++) {
            dp[i][i][0] = str.charAt(i) == 'a' ? 1 : 0;
            dp[i][i][1] = str.charAt(i) == 'b' ? 1 : 0;
            dp[i][i][2] = str.charAt(i) == 'c' ? 1 : 0;
        }
        for (int r = 2; r <= n; r++) {
            for (int i = 0; i < n - r + 1; i++) {
                //字符子串i:j
                int j = i + r - 1;
                for (int k = i; k < j; k++) {
     dp[i][j][0] += dp[i][k][0]*dp[k + 1][j][2] + dp[i][k][1]*dp[k + 1][j][2] + dp[i][k][2]*dp[k + 1][j][0];
     dp[i][j][1] += dp[i][k][0]*dp[k + 1][j][0] + dp[i][k][0]*dp[k + 1][j][1] + dp[i][k][1]*dp[k + 1][j][1];
     dp[i][j][2] += dp[i][k][1]*dp[k + 1][j][0] + dp[i][k][2]*dp[k + 1][j][1] + dp[i][k][2]*dp[k + 1][j][2];
                }
            }
        }
        System.out.println(dp[0][n - 1][0]);
    }

结果分析

输入:bbbba

输出:6

输入:bbcbabcabc

输出:2093

算法时间复杂度\(O(N^3)\),空间复杂度\(O(N^2)\),N为字符串的长度

3-7 汽车加油行驶问题

问题描述

给定一个N×N 的方形网格,设其左上角为起点◎,坐标(1,1),X轴为正, Y轴向下为正,每个方格边长为 1 ,如图所示。

一辆汽车从起点◎出发驶向右下角终点▲,其坐标为 (N,N)。

在若干个网格交叉点处,设置了油库,可供汽车在行驶途中加油。汽车在行驶过程中应遵守如下规则:

  1. 汽车只能沿网格边行驶,装满油后能行驶 K 条网格边。出发时汽车已装满油,在起点与终点处不设油库。
  2. 汽车经过一条网格边时,若其 X 坐标或 Y 坐标减小,则应付费用 BB ,否则免付费用。
  3. 汽车在行驶过程中遇油库则应加满油并付加油费用 A。
  4. 在需要时可在网格点处增设油库,并付增设油库费用 CC(不含加油费用AA )。
  5. N,K,A,B,C均为正整数, 且满足约束: 2≤N≤100,2≤K≤10。

设计一个算法,求出汽车从起点出发到达终点所付的最小费用。

算法描述

首先尝试标准动态规划算法

定义dp数组:dp[i][j][k]

dp[x][y][0]表示从(1,1)到(x,y)所需最少费用。
dp[x][y][1]表示从汽车行驶到(x,y)还能行驶的网格边数。
最终即求dp[N][N][0]

过程分析:
1.比如现在到了(x,y),上个位置有4种可能:左上右下(考虑越界的情况,有时候小于4),这四个位置都对应一个费用;
2.比如先选择上个位置是a,现在再去考虑当前位置(到油站否,有没有有油),并加上相应的价格;
3.此时得到一种可能下的(x,y)处费用;
4.我们遍历4个位置,寻找到(x,y)处的最小费用即可。

base case:

dp[1][1][0] = 0
dp[1][1][1] = K

递推关系式:

从上个位置到达(x,y),"上个位置"有四种情况。
dp[x][y][0] = min{dp[x+si][y+si][0]},0≤i≤3
dp[x][y][1] = dp[x+si][y+si] - 1
若(x,y)是油库:
dp[x][y][0] += A
dp[x][y][1] = K
若(x,y)是不是油库,但是此时没有油了dp[x][y][1]=0
dp[x][y][0] += A+C
dp[x][y][1] = K
其中si数组表示四个方向的信息:
s={{1,0,B},{0,1,B},{-1,0,0},{0,-1,0}

动态规划的关键代码:

		//cost[i][j][0]从(1,1)走到(i,j)的最小花费
        //cost[i][j][1]走到(i,j)之后的剩余步数
        int[][][] cost = new int[N + 1][N + 1][2];
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= N; j++) {
                cost[i][j][0] = Integer.MAX_VALUE / 4;
                cost[i][j][1] = K;
            }
        }
        cost[1][1][0] = 0;
        int[][] s = {
                {-1, 0, 0},
                {0, -1, 0},
                {1, 0, B},
                {0, 1, B}
        };
        for (int x = 1; x <= N; x++) {
            for (int y = 1; y <= N; y++) {
                if (x == 1 && y == 1) {
                    continue;
                }
                int minCost = Integer.MAX_VALUE / 4;
                int remainStep = 0;
                int tempCost, tempStep;
                for (int i = 0; i < 3; i++) {
                    //越界
                    if (x + s[i][0] < 1 || x + s[i][0] > N || y + s[i][1] < 1 || y + s[i][1] > N) {
                        continue;
                    }
                    //对于回退的情况,tempCost为无穷大,在之后的比较中被忽略
                    tempCost = cost[x + s[i][0]][y + s[i][1]][0] + s[i][2];
                    tempStep = cost[x + s[i][0]][y + s[i][1]][1] - 1;
                    if (a[x][y] == 1) {
                        //遇加油站必须加油
                        tempCost += A;
                        tempStep = K;
                    } else if (tempStep == 0 && (x != N || y != N)) {
                        //走不动了必须建加油站
                        tempCost += C + A;
                        tempStep = K;
                    }
                    if (tempCost < minCost) {
                        minCost = tempCost;
                        remainStep = tempStep;
                    } else if (tempCost == minCost) {
                        if (remainStep < tempStep) {
                            remainStep = tempStep;
                        }
                    }
                    if (cost[x][y][0] > minCost) {
                        cost[x][y][0] = minCost;
                        cost[x][y][1] = remainStep;
                    }
                }
            }
        }
        return cost[N][N][0];

算法时间复杂度\(O(N^2)\),空间复杂度\(O(N^2)\),N为方形网络地图宽度

标准动态规划算法的问题:

虽然该dp算法考虑了上个位置的4种可能,但由于遍历顺序的限制,仔细分析后发现只有左边和上边的位置能为状态转移提供信息,也就是说标准动态规划无法包含回退的情况,所得出的结果是局部最优解,类似于贪心算法的结果。

再考虑SPFA算法

SPFA(Shortest Path Faster Algorithm)算法是求单源最短路径的一种算法,它是Bellman-ford的队列优化,是一种十分高效的最短路算法。实现方法:建立一个队列,初始时队列里只有起始点,在建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。

将方形网格抽象成图结构,将状态抽象成结点,将问题转换成求起点(1,1,K)到(N,N,k)的最短路径。SPFA算法在负权重图也能适用。SPFA算法能让一个结点多次入队,只要该结点的最小花费被更新,就将它入队,重新衡量该结点邻居的最小花费。

cost[x][y][k]表示从起点到(i,j)所剩余步数为k的情况下所花费的最小费用
visited[x][y][k]表示由x,y,z所决定的状态是否已经位于队列中

由a[x][y]的值去处理加油或者建造加油厂的逻辑,对于能更新最小花费的状态就入队

然后处理上一个位置的4种可能,对于能更新最小花费的状态也入队

关键代码

 		int[][] s = {
                {-1, 0, B},
                {0, -1, B},
                {1, 0, 0},
                {0, 1, 0}
        };
		Pair pair1 = new Pair(1, 1, K);
        //cost[i][j][k]表示从起点到(i,j)所剩余步数为k的情况下所花费的最小费用
        queue.offer(pair1);
        cost[1][1][K] = 0;
        visited[1][1][K] = 1;

        while (!queue.isEmpty()) {
            pair1 = queue.poll();
            int x = pair1.x;
            int y = pair1.y;
            int k = pair1.step;
            visited[x][y][k] = 0;

            //需要加油
            if (a[x][y] == 1 && k != K) {
                if (cost[x][y][K] > cost[x][y][k] + A) {
                    cost[x][y][K] = cost[x][y][k] + A;
                    if (visited[x][y][K] == 0) {
                        visited[x][y][K] = 1;
                        queue.offer(new Pair(x, y, K));
                    }
                }
                continue;
            } else {
                //建造加油厂
                if (cost[x][y][K] > cost[x][y][k] + A + C) {
                    cost[x][y][K] = cost[x][y][k] + A + C;
                    if (visited[x][y][K] == 0) {
                        visited[x][y][K] = 1;
                        queue.offer(new Pair(x, y, K));
                    }
                }
            }
            if (k > 0) {
                for (int i = 0; i < 4; i++) {
                    int nx = x + s[i][0];
                    int ny = y + s[i][1];
                    //越界
                    if (nx < 1 || nx > N || ny < 1 || ny > N) {
                        continue;
                    }
                    if (cost[nx][ny][k - 1] > cost[x][y][k] + s[i][2]) {
                        cost[nx][ny][k - 1] = cost[x][y][k] + s[i][2];
                        if (visited[nx][ny][k - 1] == 0) {
                            visited[nx][ny][k - 1] = 1;
                            queue.offer(new Pair(nx, ny, k - 1));
                        }
                    }
                }
            }
        }

结果分析

输入:

9 3 2 3 6
0 0 0 0 1 0 0 0 0
0 0 0 1 0 1 1 0 0
1 0 1 0 0 0 0 1 0
0 0 0 0 0 1 0 0 1
1 0 0 1 0 0 1 0 0
0 1 0 0 0 0 0 1 0
0 0 0 0 1 0 0 0 1
1 0 0 1 0 0 0 1 0
0 1 0 0 0 0 0 0 0

输出:12

输入:

9 2 4 3 6
0 0 0 0 1 0 0 0 0
0 1 0 1 0 0 1 0 0
1 0 0 0 0 0 0 1 0
0 0 1 0 0 0 0 0 1
1 0 0 1 0 1 1 0 0
0 1 0 0 0 0 0 1 0
0 0 0 0 1 0 0 0 1
0 0 0 1 0 0 1 1 0
0 1 0 0 0 0 0 0 0

输出:38

由于SPFA算法本质上依然被认为是Bellman-Ford算法的一个特例,SPFA算法的最差复杂度依然被认为是\(O(VE)\),其中\(E\)为边数,\(V\)为点数。该网格所抽象出的边数为\(4N^2\),所抽象出的点数为\(N^2\),所以算法最差时间复杂度\(O(N^4)\),空间复杂度\(O(N^2)\),N为方形网络地图宽度

标签:int,复杂度,算法,cost,字符串,动态,规划,dp
来源: https://www.cnblogs.com/ziyanblog/p/15644256.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有