设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的长度
定义于字母表∑{a,b,c)上的乘法表如表所示:
依此乘法表,对任一定义于∑{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数组遍历顺序:
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为字符串的长度
给定一个N×N 的方形网格,设其左上角为起点◎,坐标(1,1),X轴为正, Y轴向下为正,每个方格边长为 1 ,如图所示。
一辆汽车从起点◎出发驶向右下角终点▲,其坐标为 (N,N)。
在若干个网格交叉点处,设置了油库,可供汽车在行驶途中加油。汽车在行驶过程中应遵守如下规则:
设计一个算法,求出汽车从起点出发到达终点所付的最小费用。
首先尝试标准动态规划算法:
定义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为方形网络地图宽度