记一次动态规划的实践

需求如下

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

字母分状态展示

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

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

核心算法来源

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

相关推荐
hanruiding几秒前
mysql之主从切换
android·mysql·adb
小竹子147 分钟前
L3-1 夺宝大赛
数据结构·算法·图论
蜀黍@猿8 分钟前
Android调试工具之ADB
android·adb
清霜之辰14 分钟前
安卓Compose中accompanist库使用详解
android
LDG_AGI15 分钟前
【深度学习】多元物料融合算法(一):量纲对齐常见方法
人工智能·深度学习·算法·机器学习·均值算法·哈希算法·启发式算法
EDPJ18 分钟前
(2025|ICLR|厦大&华为,LoSA,基于表示互信息的动态层级稀疏率,基于重构误差的秩分配)LLM 的动态低秩稀疏自适应
深度学习·算法·语言模型
KangkangLoveNLP21 分钟前
从Swish到SwiGLU:激活函数的进化与革命,qwen2.5应用的激活函数
人工智能·深度学习·神经网络·算法·机器学习·自然语言处理·cnn
奔跑的废柴22 分钟前
LeetCode 513. 找树左下角的值 java题解
java·算法·leetcode
Awesome Baron25 分钟前
LeetCode hot 100 每日一题(7)--3. 无重复字符的最长子串
算法·leetcode·职场和发展
SsummerC30 分钟前
【leetcode100】组合总和
数据结构·python·算法·leetcode