• 数据结构与算法简记--动态规划实战


    动态规划实战


     新的搜索问题:搜索引擎纠错功能实现?

    • 量化字符串相似度--》编辑距离:将一个字符串转化成另一个字符串,需要的最少编辑操作次数(比如增加一个字符、删除一个字符、替换一个字符)
    • 莱文斯坦距离(Levenshtein distance):允许增加、删除、替换字符这三个编辑操作
    • 最长公共子串长度(Longest common substring length):允许增加、删除字符这两个编辑操作。
    • 举例:两个字符串 mitcmu 和 mtacnu 的莱文斯坦距离是 3,最长公共子串长度是 4。

    编程计算莱文斯坦距离

    • 问题分析:这个问题是求把一个字符串变成另一个字符串,需要的最少编辑次数。整个求解过程,涉及多个决策阶段,我们需要依次考察一个字符串中的每个字符,跟另一个字符串中的字符是否匹配,匹配的话如何处理,不匹配的话又如何处理。所以,这个问题符合多阶段决策最优解模型。
    • 多阶段求最优解,按前面的理论,先套用回溯算法,再画递归树,分析重复子问题,再看是否可列状态转换方程,再填二维状态表,最后翻译为代码实现。
    • 回溯是一个递归处理的过程:
      • 如果 a[i] 与 b[j] 匹配,我们递归考察 a[i+1] 和 b[j+1]。
      • 如果 a[i] 与 b[j] 不匹配,那我们有多种处理方式可选:
        • 可以删除 a[i],然后递归考察 a[i+1] 和 b[j];
        • 可以删除 b[j],然后递归考察 a[i] 和 b[j+1];
        • 可以在 a[i] 前面添加一个跟 b[j] 相同的字符,然后递归考察 a[i] 和 b[j+1];
        • 可以在 b[j] 前面添加一个跟 a[i] 相同的字符,然后递归考察 a[i+1] 和 b[j];
        • 可以将 a[i] 替换成 b[j],或者将 b[j] 替换成 a[i],然后递归考察 a[i+1] 和 b[j+1]。
      • 翻译为代码:
      • private char[] a = "mitcmu".toCharArray();
        private char[] b = "mtacnu".toCharArray();
        private int n = 6;
        private int m = 6;
        private int minDist = Integer.MAX_VALUE; // 存储结果
        // 调用方式 lwstBT(0, 0, 0);
        public lwstBT(int i, int j, int edist) {
          if (i == n || j == m) {
            if (i < n) edist += (n-i);
            if (j < m) edist += (m - j);
            if (edist < minDist) minDist = edist;
            return;
          }
          if (a[i] == b[j]) { // 两个字符匹配
            lwstBT(i+1, j+1, edist);
          } else { // 两个字符不匹配
            lwstBT(i + 1, j, edist + 1); // 删除a[i]或者b[j]前添加一个字符
            lwstBT(i, j + 1, edist + 1); // 删除b[j]或者a[i]前添加一个字符
            lwstBT(i + 1, j + 1, edist + 1); // 将a[i]和b[j]替换为相同字符
          }
        }
      • 画递归树判断重复子问题:
        • 在递归树中,每个节点代表一个状态,状态包含三个变量 (i, j, edist),其中,edist 表示处理到 a[i] 和 b[j] 时,已经执行的编辑操作的次数。 

        • (i, j) 两个变量重复的节点很多,比如 (3, 2) 和 (2, 3)。对于 (i, j) 相同的节点,我们只需要保留 edist 最小的,继续递归处理就可以了,剩下的节点都可以舍弃。
        • 所以,状态就从 (i, j, edist) 变成了 (i, j, min_edist),其中 min_edist 表示处理到 a[i] 和 b[j],已经执行的最少编辑次数。
      • 状态转移方程
        • 本例中,状态 (i, j) 可能从 (i-1, j),(i, j-1),(i-1, j-1) 三个状态中的任意一个转移过来
        • 列出方程
        • 如果:a[i]!=b[j],那么:min_edist(i, j)就等于:
          min(min_edist(i-1,j)+1, min_edist(i,j-1)+1, min_edist(i-1,j-1)+1)
          
          如果:a[i]==b[j],那么:min_edist(i, j)就等于:
          min(min_edist(i-1,j)+1, min_edist(i,j-1)+1,min_edist(i-1,j-1))
          
          其中,min表示求三数中的最小值。     
      • 状态表
      • 翻译为代码实现
        • public int lwstDP(char[] a, int n, char[] b, int m) {
            int[][] minDist = new int[n][m];
            for (int j = 0; j < m; ++j) { // 初始化第0行:a[0..0]与b[0..j]的编辑距离
              if (a[0] == b[j]) minDist[0][j] = j;
              else if (j != 0) minDist[0][j] = minDist[0][j-1]+1;
              else minDist[0][j] = 1;
            }
            for (int i = 0; i < n; ++i) { // 初始化第0列:a[0..i]与b[0..0]的编辑距离
              if (a[i] == b[0]) minDist[i][0] = i;
              else if (i != 0) minDist[i][0] = minDist[i-1][0]+1;
              else minDist[i][0] = 1;
            }
            for (int i = 1; i < n; ++i) { // 按行填表
              for (int j = 1; j < m; ++j) {
                if (a[i] == b[j]) minDist[i][j] = min(
                    minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1]);
                else minDist[i][j] = min(
                    minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1]+1);
              }
            }
            return minDist[n-1][m-1];
          }
          
          private int min(int x, int y, int z) {
            int minv = Integer.MAX_VALUE;
            if (x < minv) minv = x;
            if (y < minv) minv = y;
            if (z < minv) minv = z;
            return minv;
          }

    铮哥技巧

    • 拿到一个问题的时候,我们可以先不思考,计算机会如何实现这个问题,而是单纯考虑“人脑”会如何去解决这个问题
    • 人脑比较倾向于思考具象化的、摸得着看得见的东西,不适合思考过于抽象的问题。所以,我们需要把抽象问题具象化。
    • 那如何具象化呢?我们可以实例化几个测试数据,通过人脑去分析具体实例的解,然后总结规律,再尝试套用学过的算法,看是否能够解决。
    • 除此之外,我还有一个非常有效、但也算不上技巧的东西,我也反复强调过,那就是多练。实际上,等你做多了题目之后,自然就会有感觉,看到问题,立马就能想到能否用动态规划解决,然后直接就可以寻找最优子结构,写出动态规划方程,然后将状态转移方程翻译成代码。

    编程计算最长公共子串长度

    • 同理,先分析:
      • 每个状态还是包括三个变量 (i, j, max_lcs),max_lcs 表示 a[0…i] 和 b[0…j] 的最长公共子串长度。
      • 回溯的处理思路。
        • 从 a[0] 和 b[0] 开始,依次考察两个字符串中的字符是否匹配。
        • 如果 a[i] 与 b[j] 互相匹配,
          • 将最大公共子串长度加一,并且继续考察 a[i+1] 和 b[j+1]。
        • 如果 a[i] 与 b[j] 不匹配,最长公共子串长度不变,这个时候,有两个不同的决策路线:
          • 删除 a[i],或者在 b[j] 前面加上一个字符 a[i],然后继续考察 a[i+1] 和 b[j];
          • 删除 b[j],或者在 a[i] 前面加上一个字符 b[j],然后继续考察 a[i] 和 b[j+1]。
        • 就是说求 a[0…i] 和 b[0…j] 的最长公共长度 max_lcs(i, j),只有可能通过下面三个状态转移过来:
          • (i-1, j-1, max_lcs),其中 max_lcs 表示 a[0…i-1] 和 b[0…j-1] 的最长公共子串长度;
          • (i-1, j, max_lcs),其中 max_lcs 表示 a[0…i-1] 和 b[0…j] 的最长公共子串长度;
          • (i, j-1, max_lcs),其中 max_lcs 表示 a[0…i] 和 b[0…j-1] 的最长公共子串长度。
    • 给出状态转移方程
      • 如果:a[i]==b[j],那么:max_lcs(i, j)就等于:
        max(max_lcs(i-1,j-1)+1, max_lcs(i-1, j), max_lcs(i, j-1));
        
        如果:a[i]!=b[j],那么:max_lcs(i, j)就等于:
        max(max_lcs(i-1,j-1), max_lcs(i-1, j), max_lcs(i, j-1));
        
        其中max表示求三数中的最大值。
    • 代码实现
      • public int lcs(char[] a, int n, char[] b, int m) {
          int[][] maxlcs = new int[n][m];
          for (int j = 0; j < m; ++j) {//初始化第0行:a[0..0]与b[0..j]的maxlcs
            if (a[0] == b[j]) maxlcs[0][j] = 1;
            else if (j != 0) maxlcs[0][j] = maxlcs[0][j-1];
            else maxlcs[0][j] = 0;
          }
          for (int i = 0; i < n; ++i) {//初始化第0列:a[0..i]与b[0..0]的maxlcs
            if (a[i] == b[0]) maxlcs[i][0] = 1;
            else if (i != 0) maxlcs[i][0] = maxlcs[i-1][0];
            else maxlcs[i][0] = 0;
          }
          for (int i = 1; i < n; ++i) { // 填表
            for (int j = 1; j < m; ++j) {
              if (a[i] == b[j]) maxlcs[i][j] = max(
                  maxlcs[i-1][j], maxlcs[i][j-1], maxlcs[i-1][j-1]+1);
              else maxlcs[i][j] = max(
                  maxlcs[i-1][j], maxlcs[i][j-1], maxlcs[i-1][j-1]);
            }
          }
          return maxlcs[n-1][m-1];
        }
        
        private int max(int x, int y, int z) {
          int maxv = Integer.MIN_VALUE;
          if (x > maxv) maxv = x;
          if (y > maxv) maxv = y;
          if (z > maxv) maxv = z;
          return maxv;
        }

    搜索单词纠错分析:

    • 将搜索词分别与词库中的单词求编辑距离,将编辑距离最小的列出,提示给用户。
    • 优化:
      • 问题:单纯利用编辑距离来纠错,效果并不一定好;词库中的数据量可能很大,搜索引擎每天要支持海量的搜索,所以对纠错的性能要求很高
      • 效果优化:
        • 取出编辑距离最小的 TOP 10,再根据其他因素做决策哪个做为纠错单词。
        • 使用多种编辑距离计算方法,取TOP 10,取交集
        • 统计用户习惯信息:在个人常用词库中求编辑距离最小的单词。
      • 性能优化:
        • 多机部署并行计算,归并统计结果。
        • 词库大:分库分表
  • 相关阅读:
    我的公众号:WebHub
    The Qt Resource System
    Node.js Streams:你需要知道的一切
    2019春招前端面试预测题!
    BeamNG.drive物理引擎评鉴
    EcmaScript对象克隆之谜
    死锁与囚徒困境的辩证关系
    停止学习框架
    qt5android
    opencvKmeans图像分割根据颜色
  • 原文地址:https://www.cnblogs.com/wod-Y/p/12152756.html
Copyright © 2020-2023  润新知