【问题标题】:Find minimum cost to convert to palindrome找到转换为回文的最低成本
【发布时间】:2019-07-24 08:01:01
【问题描述】:

我被这个问题困扰了很长时间,无法找到任何有效的解决方案。任何帮助将不胜感激。

问题:

给定一个包含小写字符的字符串,我们需要找到将其转换为回文的最小成本。我们可以插入新字符并删除现有字符。每个字符都有与其相关的插入和删除成本。

成本是'a' = 1,'b' = 2,'c' = 3,.....,'z' = 26

例如'abc' -> 'c' 成本为 3

我只能想到一种方法,它涉及遍历所有具有指数时间复杂度的子序列。 有什么办法可以优化吗?

【问题讨论】:

  • 换成z的成本是26,换成a的成本是1?!所有替换、插入和删除的成本不都一样吗?那么删除和插入的成本是多少呢?
  • 是的,你是对的。我错误地理解了这个问题。我已经更新了问题。
  • 那么,是作业题吗?
  • 不,我在采访中被问到这个问题。仍在寻找有效的解决方案。
  • 生成的回文必须与原始字符串的长度相同吗?如果没有,您可以删除除最昂贵的字符之外的所有字符。 abcc(费用3)

标签: algorithm recursion dynamic-programming


【解决方案1】:

你可以想象一个递归的解决方案,你解决使第一个和最后一个字符相同的问题,然后解决剩下的字符(不包括第一个和最后一个字符)的问题。

如果字符串的第一个和最后一个字符已经相同,那么考虑在字符串的开头或结尾插入一个字符,或者删除第一个或最后一个字符是没有意义的。那只会增加成本。

当不同时,有几个选项可以让第一个字符与最后一个字符相同:

  1. 在开头插入一个字符:与当前字符串最后一个字符相同的字符。这个字符的成本应该添加到递归对没有最后一个字符的原始字符串的成本中。

  2. 在最后插入一个字符:与当前字符串的第一个字符相同的字符。这个字符的成本应该加到没有第一个字符的原始字符串的递归成本中。

  3. 删除最后的字符:这可能不会立即使第一个和最后一个字符相等,因为可能需要删除/插入更多字符。但是这个选择将是递归调用的工作。该字符的成本应添加到递归将为剩余字符串提供的成本中。

  4. 删除最开始的字符(与选项 3 的推理相同):此字符的成本应添加到递归将为剩余字符串提供的成本中。

请注意,选项 1 和 3 中的递归调用是相同的,并且添加的成本也完全相同。比较选项 2 和 4 时也有类似的观察结果。例如,当输入是 "abcb" 时,我们可以看到在末尾添加 "a" 或从开头删除 "a" 都会产生相同的回文成本。所以实际上我们只需要考虑这 4 个选项中的 2 个。

对这两个选项进行递归调用后,唯一剩下的就是选择两者中最便宜的一个。

当只剩下 1 个字符(或没有)时递归停止:在这种情况下,成本为 0,因为该字符串是回文。该(零)成本甚至相应的回文都可以返回给调用者。

记忆化可以进行一些优化:跟踪每个访问范围的结果。

这是一个 JavaScript 实现,它有一个交互式输入,您可以在其中实时查看相应的计算成本和回文:

function toPalindrome(s, charCost) {
    let visited = [];
    function recur(i, j) {
        let key = i * s.length + j;
        if (visited[key]) return visited[key];  // use memoization
        let cost = 0, palindrome;
        if (i >= j) { // Base case
            palindrome = i > j ? "" : s[i];
        } else if (s[i] === s[j]) {
            // If outermost two characters are equal: take them; no extra cost
            ({ cost, palindrome } = recur(i+1, j-1));
            palindrome = s[i] + palindrome + s[i];
        } else { // Otherwise consider deleting either first or last char
            ({ cost, palindrome } = recur(i, j-1));
            cost += charCost[s[j]];
            let { cost: cost2, palindrome: palindrome2 } = recur(i+1, j);
            cost2 += charCost[s[i]];
            if (cost2 < cost) { // Take best of the two searched branches
                cost = cost2;
                palindrome = palindrome2;
            }
        }
        // Return two informations: cost and palindrome.
        return visited[key] = { cost, palindrome };
    }
    return recur(0, s.length-1);
}

const charCost = [...Array(26).keys()].reduce((acc, i) => 
    (acc[String.fromCharCode(i+97)] = i+1, acc), {});
// I/O handling
(document.oninput = () =>
    output.textContent = JSON.stringify(toPalindrome(input.value, charCost), null, 2)
)();
Input: <input id="input" value="antenna"><br>
<pre id="output"></pre>

返回所有回文

由于在一端删除一个字符与在另一端添加该字符的成本相同,因此可以以相同的最低成本创建多个回文。

这是一个收集所有回文而不是一个回文的代码版本。显然,这会占用一些额外的执行时间和空间:

function toPalindrome(s, charCost) {
    let visited = [];
    function recur(i, j) {
        let key = i * s.length + j;
        if (visited[key]) return visited[key];  // use memoization
        let cost = 0, palindromes;
        if (i >= j) { // Base case
            palindromes = [i > j ? "" : s[i]];
        } else if (s[i] === s[j]) {
            // If outermost two characters are equal: take them; no extra cost
            ({ cost, palindromes } = recur(i+1, j-1));
            palindromes = palindromes.map(pal => s[i] + pal + s[i]);
        } else { // Otherwise consider deleting either first or last char
            ({ cost, palindromes } = recur(i, j-1));
            // add an alternative for every palindrome: using an insertion instead of deletion
            //    at the opposite end of the string
            palindromes = [...palindromes, ...palindromes.map(pal => s[j] + pal + s[j])];
            cost += charCost[s[j]];
            let { cost: cost2, palindromes: palindromes2 } = recur(i+1, j);
            cost2 += charCost[s[i]];
            if (cost2 <= cost) { // Take best of the two searched branches
                if (cost2 < cost) {
                    palindromes = [];
                }
                palindromes = [...palindromes, ...palindromes2, ...palindromes2.map(pal => s[i] + pal + s[i])];
                cost = cost2;
            } 
        }
        // Return two informations: cost and palindrome.
        return visited[key] = { cost, palindromes };
    }
    let result = recur(0, s.length-1);
    result.palindromes = [...new Set(result.palindromes)]; // make unique
    return result;
}

const charCost = [...Array(26).keys()].reduce((acc, i) => 
    (acc[String.fromCharCode(i+97)] = i+1, acc), {});
// I/O handling
(document.oninput = () =>
    output.textContent = JSON.stringify(toPalindrome(input.value, charCost), null, 2)
)();
Input: <input id="input" value="antenna"><br>
<pre id="output"></pre>

【讨论】:

  • 不错的分析。我想知道是否会有一个简单的更改以最低成本捕获所有回文。所以'abcde' =&gt; {cost: 10, palindromes: ['e', 'abcdedcba']}.
  • @ScottSauyet,是的,这是可能的。如果 OP 对此感兴趣,我会添加它。
  • 是的,再仔细看一下代码,怎么做就很清楚了。我希望在我自己尝试这个问题之前没有看到你的答案。这似乎是一个有趣的挑战,但现在我无法想象任何其他方式来看待它!干得好!
  • @trincot 请添加它。这是对问题的一个很好的扩展。
  • 假设输入是 azzzaa,删除最后一个 a 或在开头插入另一个 a 完全有意义。 (关于这种说法:“如果字符串的第一个和最后一个字符已经相同,那么考虑在字符串的开头或结尾插入一个字符,或者删除第一个或最后一个字符是没有意义的.")
【解决方案2】:
enum Action { Initial, Unchanged, Insert, Delete }

int defaultEditCost(char ch) => char.ToLower(ch) - 'a' + 1;

int editDistancePalindrime(string str, Func<char, int> costFn)
{
    // Calculate the levenshtein distance table between `str` and its reverse.
    // str[i-1] is the normal string, and str[n-j] is the reverse.
    int n = str.Length;
    var table = new (Action action, int totalCost, int actionCost)[n + 1, n + 1];
    for (int i = 0; i <= n; i++)
    {
        for (int j = 0; j <= n; j++)
        {
            if (i == 0 && j == 0) table[i, j] = (Action.Initial, 0, 0);
            else if (i == 0)
            {
                var insertCost = costFn(str[n - j]);
                var insertTotalCost = table[i, j - 1].totalCost + insertCost;
                table[i, j] = (Action.Insert, insertTotalCost, insertCost);
            }
            else if (j == 0)
            {
                var deleteCost = costFn(str[i - 1]);
                var deleteTotalCost = table[i - 1, j].totalCost + deleteCost;
                table[i, j] = (Action.Delete, deleteTotalCost, deleteCost);
            }
            else if (str[i - 1] == str[n - j])
            {
                table[i, j] = (Action.Unchanged, table[i - 1, j - 1].totalCost, 0);
            }
            else
            {
                var insertCost = costFn(str[n - j]);
                var deleteCost = costFn(str[i - 1]);
                var insertTotalCost = table[i, j - 1].totalCost + insertCost;
                var deleteTotalCost = table[i - 1, j].totalCost + deleteCost;
                if (insertTotalCost <= deleteTotalCost)
                {
                    table[i, j] = (Action.Insert, insertTotalCost, insertCost);
                }
                else
                {
                    table[i, j] = (Action.Delete, deleteTotalCost, deleteCost);
                }
            }
        }
    }
    // The cost is the sum of actionCost for all inserts or all deletes.
    // (Both have the same value, because of symmetry.)
    int palindromeCost = 0;
    for (int i = n, j = n; i > 0 || j > 0;)
    {
        var (action, totalCost, actionCost) = table[i, j];
        switch (action)
        {
            case Action.Insert:
                palindromeCost += actionCost;
                j--;
                break;
            case Action.Delete:
                i--;
                break;
            case Action.Unchanged:
                i--;
                j--;
                break;
        }
    }
    return palindromeCost;
}
void Main()
{
    editDistancePalindrime("abc", defaultEditCost).Dump();
    // 'abc' -> 'c' or 'abcba' (cost 3)

    editDistancePalindrime("anchitjain", defaultEditCost).Dump();
    // 'anchitjain' -> 'nitin' or 'anchiajtjaihcna' (cost 23)
}

【讨论】:

  • 我将您的解决方案的输出与我的数千个随机输入进行了比较,它们返回的结果相同。不错的解决方案 (+1)
猜你喜欢
  • 2012-04-05
  • 2021-09-06
  • 2020-11-29
  • 2015-05-25
  • 2018-02-08
  • 1970-01-01
  • 2014-08-31
  • 2013-08-01
  • 1970-01-01
相关资源
最近更新 更多