记一次动态规划的实践

需求如下

需要实现以下的单词拼写校验功能:

字母分状态展示

  • 等待输入状态
  • 已输入
  • 正确
  • 漏写
  • 多写
  • 错写

简单的描述一下需求就是,需要校验用户输入的单词,和正确的单词之间的差异。

核心算法来源

leetcode.cn/problems/ed...

本次不讨论该题目怎么解决,直接给出ac的代码:

kotlion 复制代码
class Solution {
    fun minDistance(word1: String, word2: String): Int {
        //dp[i][j] 表示 word1[0..i] 和 word2[0..j] 的最小编辑距离
        val dp = Array(word1.length + 1) { IntArray(word2.length + 1) }
        //第一列:word1[0..i] 变成空串的最小编辑距离
        for (i in 1..word1.length) {
            dp[i][0] = i
        }
        //第一行:空串变成 word2[0..j] 的最小编辑距离
        for (j in 1..word2.length) {
            dp[0][j] = j
        }
        for (i in 1..word1.length) {
            for (j in 1..word2.length) {
                if (word1[i - 1] == word2[j - 1]) {
                    //如果 word1[i] == word2[j],则不需要编辑操作
                    dp[i][j] = dp[i - 1][j - 1]
                } else {
                    //如果 word1[i] != word2[j],则 dp[i][j] 可以由以下三种操作得到:
                    //1、如果将 word1[i] 替换成 word2[j],则 dp[i][j] = dp[i-1][j-1] + 1
                    //2、如果在 word1[i] 后面添加一个 word2[j],则 dp[i][j] = dp[i][j-1] + 1
                    //3、如果将 word1[i] 删除,则 dp[i][j] = dp[i-1][j] + 1
                    dp[i][j] = minOf(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
                }
            }
        }
        return dp[word1.length][word2.length]
    }
}

如何将上述的算法应用到需求里面?

需求告诉我们可以对输入的单词做出下面4种动作,从而让输入的单词变为正确的单词。

  • 不操作 [正确的情况]
  • 替换
  • 删除
  • 添加

那我们先定义上述的动作:

kt 复制代码
/**
 * INIT:初始状态
 * NOOP:不操作
 * INSERT:插入
 * DELETE:删除
 * REPLACE:替换
 * */
enum class EditAction {
    INIT, NOOP, INSERT, DELETE, REPLACE
}

我们在回头看一下上述编辑距离的ac代码,发现只能知道最小的编辑距离,并不能知道是执行的那个动作。

那就记录下该动作:

kt 复制代码
data class EditStep(
    val action: EditAction,
    val distance: Int
)

但是还有问题,即使使用了上述的EditStep,我们只能知道最后一步的动作是什么,无法知道整个流程的所以的动作。 我们这里选择链表的方式,通过添加prev的属性来记录上一步是什么,这样我们就能通过遍历链表的方式得知整个流程的动作,修改EditStep:

kt 复制代码
data class EditStep(
    val action: EditAction,
    val distance: Int,
    val prev: EditStep?
)

我们结合上面的分析和编辑距离的ac代码,得到如下的方案:

kt 复制代码
fun minEditDistance(except: CharSequence, actual: CharSequence): List<EditStep>? {
    if (except.isEmpty() && actual.isEmpty()) {
        return null
    }
    val editDp = Array(except.length + 1) {
        Array(actual.length + 1) {
            EditStep(EditAction.INIT, 0, null)
        }
    }
    //第一行: 表示except 与空串的编辑距离
    for (row in 1..actual.length) {
        editDp[0][row] = EditStep(EditAction.DELETE, row, editDp[0][row - 1])
    }
    //第一列: 空串 与 actual 的编辑距离
    for (col in 1..except.length) {
        editDp[col][0] = EditStep(EditAction.INSERT, col, editDp[col - 1][0])
    }

    for (row in 1..except.length) {
        for (col in 1..actual.length) {
            if (Character.toLowerCase(except[row - 1]) == Character.toLowerCase(actual[col - 1])) {
                val noopEditStep = editDp[row - 1][col - 1]
                editDp[row][col] = EditStep(EditAction.NOOP, noopEditStep.distance, noopEditStep)
            } else {
                val replaceEditStep = editDp[row - 1][col - 1]
                val insertEditStep = editDp[row - 1][col]
                val deleteEditStep = editDp[row][col - 1]
                editDp[row][col] = minOfEditStep(replaceEditStep, insertEditStep, deleteEditStep)
            }
        }
    }
    val editSteps = ArrayList<EditStep>()
    var lastStep: EditStep? = editDp[except.length][actual.length]
    while (lastStep != null && lastStep.action != EditAction.INIT) {
        editSteps.add(lastStep)
        lastStep = lastStep.prev
    }
    //这里得到step是后往前,所以我们需要反转一下
    return editSteps.asReversed()
}
kt 复制代码
private fun minOfEditStep(
    replaceEditStep: EditStep,
    insertEditStep: EditStep,
    deleteEditStep: EditStep
): EditStep {
    return if (replaceEditStep.distance <= insertEditStep.distance && replaceEditStep.distance <= deleteEditStep.distance) {
        EditStep(EditAction.REPLACE, replaceEditStep.distance + 1, replaceEditStep)
    } else if (insertEditStep.distance <= replaceEditStep.distance && insertEditStep.distance <= deleteEditStep.distance) {
        EditStep(EditAction.INSERT, insertEditStep.distance + 1, insertEditStep)
    } else {
        EditStep(EditAction.DELETE, deleteEditStep.distance + 1, deleteEditStep)
    }
}

上述方案就是对编辑距离这道题目ac代码的简单修改,各位观众姥爷可以先自己ac一下编辑距离这道题目,这样理解会方便很多。

这里我们先一下简单的测试:

kt 复制代码
val result = minEditDistance("handsome", "hoodsomee")
result?.forEach {
    Log.d("MinEditDistance", "Action: ${it.action}")
}

发现符合我们想要的,最后一步就是通过Action,给文本进行染色:

kt 复制代码
private fun renderMatchResult(editSteps: List<EditStep>?, actual: String): Editable {
    val spannable = SpannableBuilder()
    var cursor = 0
    editSteps?.forEach {
        when (it.action) {
            EditAction.NOOP -> {
                spannable.nextSpannable(actual[cursor++].toString()).setColor(Color.GREEN)
            }
            EditAction.REPLACE -> {
                spannable.nextSpannable(actual[cursor++].toString()).setColor(Color.RED)
            }
            EditAction.INSERT -> {
                spannable.nextSpannable("_").setColor(Color.RED)
            }
            EditAction.DELETE -> {
                spannable.nextSpannable(actual[cursor++].toString()).setColor(Color.GRAY)
            }
            else -> {/**之前过滤了INIT,所以不会有*/}
        }
    }
    return spannable.finish()
}

最后我们就实现了这个需求:

最后

做Android开发也有两年半的时间了,平时工作中很少能用到动态规划去处理问题,难得遇到一次这样的需求,故记录下来和大家分享一下。

文章中用的案例代码: github.com/Mrs-Chang/D...

相关推荐
服装学院的IT男4 小时前
【Android 13源码分析】Activity生命周期之onCreate,onStart,onResume-2
android
hsling松子4 小时前
使用PaddleHub智能生成,献上浓情国庆福
人工智能·算法·机器学习·语言模型·paddlepaddle
Arms2064 小时前
android 全面屏最底部栏沉浸式
android
服装学院的IT男4 小时前
【Android 源码分析】Activity生命周期之onStop-1
android
dengqingrui1234 小时前
【树形DP】AT_dp_p Independent Set 题解
c++·学习·算法·深度优先·图论·dp
C++忠实粉丝4 小时前
前缀和(8)_矩阵区域和
数据结构·c++·线性代数·算法·矩阵
ZZZ_O^O5 小时前
二分查找算法——寻找旋转排序数组中的最小值&点名
数据结构·c++·学习·算法·二叉树
CV-King5 小时前
opencv实战项目(三十):使用傅里叶变换进行图像边缘检测
人工智能·opencv·算法·计算机视觉
代码雕刻家6 小时前
数据结构-3.9.栈在递归中的应用
c语言·数据结构·算法
雨中rain6 小时前
算法 | 位运算(哈希思想)
算法