记一次动态规划的实践

需求如下

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

字母分状态展示

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

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

核心算法来源

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

相关推荐
bingw011418 分钟前
华为机试HJ62 查找输入整数二进制中1的个数
数据结构·算法·华为
苏言の狗21 分钟前
小R的二叉树探险 | 模拟
c语言·数据结构·算法·宽度优先
jianqimingtian40 分钟前
如何使用 Matlab 制作 GrabCAD 体素打印切片
数据结构·数据库
gkdpjj1 小时前
C++优选算法十四 优先级队列(堆)
开发语言·数据结构·c++·算法
几窗花鸢1 小时前
力扣面试经典 150(上)
数据结构·c++·算法·leetcode
lu_rong_qq2 小时前
决策树 DecisionTreeClassifier() 模型参数介绍
算法·决策树·机器学习
Heisenberg~2 小时前
详解八大排序(五)------(计数排序,时间复杂度)
c语言·数据结构·排序算法
找藉口是失败者的习惯3 小时前
Jetpack Compose 如何布局解析
android·xml·ui
Estar.Lee8 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
LNTON羚通8 小时前
摄像机视频分析软件下载LiteAIServer视频智能分析平台玩手机打电话检测算法技术的实现
算法·目标检测·音视频·监控·视频监控