记一次动态规划的实践

需求如下

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

字母分状态展示

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

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

核心算法来源

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

相关推荐
小小unicorn4 分钟前
第二章:算法练习题2
算法
坊钰5 分钟前
【Java 数据结构】合并两个有序链表
java·开发语言·数据结构·学习·链表
抓住鼹鼠不撒手6 分钟前
力扣 429 场周赛-前两题
数据结构·算法·leetcode
神经网络的应用1 小时前
C++程序设计例题——第三章程序控制结构
c++·学习·算法
南宫生1 小时前
力扣-数据结构-3【算法学习day.74】
java·数据结构·学习·算法·leetcode
向宇it1 小时前
【从零开始入门unity游戏开发之——C#篇30】C#常用泛型数据结构类——list<T>列表、`List<T>` 和数组 (`T[]`) 的选择
java·开发语言·数据结构·unity·c#·游戏引擎·list
编程洪同学2 小时前
Spring Boot 中实现自定义注解记录接口日志功能
android·java·spring boot·后端
A懿轩A2 小时前
C/C++ 数据结构与算法【树和二叉树】 树和二叉树,二叉树先中后序遍历详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·二叉树·
-$_$-2 小时前
【LeetCode 面试经典150题】详细题解之滑动窗口篇
算法·leetcode·面试
Channing Lewis2 小时前
算法工程化工程师
算法