记一次动态规划的实践

需求如下

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

字母分状态展示

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

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

核心算法来源

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...

相关推荐
朱剑君4 分钟前
贪心算法——分数背包问题
算法·贪心算法
小O的算法实验室11 分钟前
2008年EJOR SCI2区,连续蚁群优化算法ACOR,深度解析+性能实测
算法·智能算法
2301_7944615712 分钟前
力扣-将x减到0的最小操作数
数据结构·算法·leetcode
bing_feilong15 分钟前
树莓派4B搭建Hector SLAM算法, ROS1 & ROS2?
算法·机器人
晨曦夜月25 分钟前
《牛客》数组中出现次数超过一半的数字
算法
aningxiaoxixi39 分钟前
android property 系统
android
白白糖41 分钟前
相同,对称,平衡,右视图(二叉树)
python·算法·二叉树·力扣
speop1 小时前
TASK05【Datawhale 组队学习】系统评估与优化
android·java·学习
星沁城1 小时前
108. 将有序数组转换为二叉搜索树
java·数据结构·leetcode
摩尔线程1 小时前
推测解码算法在 MTT GPU 的应用实践
算法·语言模型·大模型·gpu算力·gpu·摩尔线程