JavaScript: 检查字符串是否可以通过组合数组中的其他字符串重新创建?

9
我正在尝试找出最佳方法来检查特定字符串是否可以通过组合我在数组中拥有的其他字符串来创建。其他字符串可以是任意长度,包括一个字符。此外,其他字符串中的字符可以重新排序。
因此,如果我们正在寻找单词“dodge”,并且我们的字符串数组是['god','house','d','e','cat','c','r','jump'],则会匹配,因为我们可以将'god','d'和'e'中的字母组合起来创建'dodge'。
如果数组中包含“dot”而不是“d”,则我们将没有匹配,因为我们必须使用重新组合的每个单词中的所有字符(我们还必须使用'o'和't')。
我还想知道用于创建指定单词的单词是哪些,因此,如果有匹配,我希望函数返回被重新组合以创建指定单词的单词的数组索引。对于上面的“dodge”示例,它将返回[0,2,3]。

1
我从算法的角度喜欢这个问题!如果这是一些众所周知的问题(看起来像背包问题的修改版),并且存在一些非平凡的解决方案,我会很高兴听到的。 - julx
2
正如我在下面的答案中指出的那样,这个问题似乎等同于子集积问题,而这是NP-完全问题。 - Elian Ebbing
@Elian,我也得到了O(2^n)。我在思考是否有任何方法可以得到类似于(nlogn)或其他的东西。由于它是NP-Complete问题,它永远不会在多项式时间内解决,但也许有一些比指数时间更好的方法... - Stephen Chung
@Stephen Chung,我认为对于最坏情况而言,即词典包含许多重叠单词的情况(例如 ['aaabc'、'aabbc'、'abbcc'、'aabcc' 等)),你不可能做得比 O(2^n) 更好。但我认为你可以优化常见场景,例如你可以创建一个表示单词部分顺序的图表,基于我的 subtract()方法,其中如果您可以从 a 中减去 b,则a < b` 。一旦您拥有了这张图表,您就可以在减法失败时排除整个分支。 - Elian Ebbing
@makeee,我建议你选择@Elain的答案。 - Stephen Chung
@Elian,我一直在考虑构建一系列正则表达式,以查看它是否比自制的更好地支持完全回溯。然而,我无法摆脱这种病态情况下是O(2^n)的事实... - Stephen Chung
6个回答

4

我写了一个解决方案,最坏情况下的时间复杂度为O(2^n),但在大多数情况下它表现得相当好。 我从一个函数开始,将每个字符串映射到一个对象,该对象计算字符串中所有不同字母的数量。 例如:

map("dodge") --> { "d": 2, "e": 1, "g": 1, "o": 1, size: 5 }

如您所见,它还将大小存储在结果中。这是实现方式:

function map(str) {
    var obj = { size: str.length };

    for(var i=0; i<str.length; i++) {   
        var ch = str.charAt(i);
        obj[ch] = (ch in obj) ? obj[ch]+1 : 1;
    }

    return obj;
}

然后我编写了一个函数,用于“减去”两个映射对象。例如:
subtract(map("ab"), map("a")) --> { "b": 1, size: 1 }
subtract(map("dodge"), map("god")) --> { "d": 1, "e": 1, size: 1 }
subtract(map("abc"), map("abc")) --> { size: 0 }
subtract(map("a"), map("b")) --> null

正如您在上一个示例中所看到的,如果无法进行减法运算,则该函数返回null。以下是subtract的实现:

function subtract(a, b) {
    var result = { size: 0 };

    for(var i=97; i<123; i++) { // from a to z (ASCII)
        var ch = String.fromCharCode(i);
        var diff = (a[ch] || 0) - (b[ch] || 0);

        if(diff < 0)
            return null;

        if(diff > 0) {
            result[ch] = diff;
            result.size += diff;
        }
    }
    return result;
}

最后一步是编写一个方法findCombination(word, dict),如果找到任何组合,则返回该组合,否则返回null。例如:
var dict = ['god','house','d','e','cat','c','r','jump'];
findCombination("dodge", dict) --> [0, 2, 3]
findCombination("housecat", dict) --> [1, 4]
findCombination("hose", dict) --> null
findCombination("xyz", dict) --> null

我使用递归方法和回溯技术,尝试从给定的密钥中“减去”单词,直到结果为“空”:

var findCombination = function(word, dict)
{
    var solution = [];
    var mappedDict = [];

    for(var i=0; i<dict.length; i++)
        mappedDict[i] = map(dict[i]);           

    var recursiveFind = function(key,  i)
    {
        if(i == mappedDict.length)
            return false;

        var result = subtract(key, mappedDict[i])

        if(result == null)
            return recursiveFind(key, i+1);

        solution.push(i);

        if(result.size == 0 || recursiveFind(result, i+1))
            return true;

        solution.pop();
        return recursiveFind(key, i+1);
    };

    if(recursiveFind(map(word), 0))
        return solution;

    return null;
};

您可以通过仅在首次调用findCombination()时初始化mappedDict变量,而不是每次调用时都初始化,来优化代码。

减法的想法比我的好!它允许对搜索树进行早期修剪!我的方法进行了更多的比较! - Stephen Chung

1

算法:

  1. 将目标字符串分解为字母并将它们分组,获取每个字母的数量
  2. 形成数组中所有字符串的排列,从一个字符串开始,直到整个数组。如果您的字符串数组很短(即<32),则可以使用自动递增整数和位掩码来生成所有排列。如果您的字符串数组很长(>32),则使用数组来存储每个“位”插槽与字符串长度,并模拟递增整数。
  3. 跳过与目标字符串长度不同的所有排列(这应该消除90%的所有排列)。通过对“1”位的数量x字符串长度(或总和插槽)求和,获取总字母计数;它必须等于目标字符串长度。
  4. 对于每个排列,分解字母并将它们分组,获取每个字母的数量
  5. 按字母遍历目标字符串组,将计数与字符串组进行比较。字母和计数必须完全相同才能成功。如果是这样,则将该排列作为答案返回。
  6. 否则,请继续尝试另一个排列
  7. 在经过所有排列后,返回失败。

JavaScript实现:

// Assume: target=target string, words_array=array of strings

function groupByLetters(map, text) {
    for (var x=0; x < text.length; x++) {
        var ch = text.charAt(x);
        var n = map[ch] || 0;
        map[ch] = n + 1;
    }
}

// Split the target string into letters

var target_map = {};
groupByLetters(target_map, target);

// Create permutation slots

var slots = [];

for (var x=0; x < words_array.length; x++) {
    // Now in order to optimize speed, store the length of each string in the slot
    // Negative = not selected, positive = selected
    slots.push(-words_array[x].length);
}

// Loop through all permutations

while(true) {
    var carry = true;
    var plength = 0;

    for (var x=0; x < slots.length; x++) {
        var slen = slots[x];
        if (carry) {
            if (slen < 0) {     // Bit 0
                carry = false;
                slots[x] = -slen;   // 0->1, no carry
            } else {
                slots[x] = -slen;   // 1->0, continue to carry
            }
        }

        if (slots[x] > 0) plength += slots[x];
    }

    if (carry) {    // We have exhausted the permutations
        return null;
    }

    // Now plength = total number of letters in selected permutation

    if (plength !== target.length) continue;    // Not the same number of letters, skip

    // Build map of all letters in selected permutation

    var pmap = {};
    var permutation = [];

    for (var x=0; x < slots.length; x++) {
        if (slots[x] > 0) {
            groupByLetters(pmap, words_array[x]);
            permutation.push(words_array[x]);
        }
    }

    // Check if the map is the same as the target map

    var match = true;

    for (var letter in target_map) {
        if (!target_map.hasOwnProperty(letter)) continue;
        if (target_map[letter] !== pmap[letter]) {
            match = false;
            break;
        }
    }

    if (match) return permutation;  // Success!
}

注意:我没有尝试运行这个程序。如果我在某处打错了,请告诉我。

抱歉,没有看到那个。让我尝试一个修改后的答案。 - Stephen Chung
@makeee,我已经修改了答案。尝试一下这个算法——它应该能做到你想要的。 - Stephen Chung
Stephen:它可以工作!唯一的问题是当有10个以上的单词时,它往往会导致浏览器崩溃。你有什么想法为什么会这样,或者有什么性能改进的建议吗? - makeee
@makeee,它可能没有崩溃浏览器,只是运行时间很长,因此浏览器超时并认为您的JavaScript已经挂起。运行排列是一个O(2^n)算法,因此它可能是您可以在计算机上运行的最慢的东西。唯一的解决方法是使用任何您能想到的方式积极修剪排列。 - Stephen Chung
@makeee,我已经检查过你的优化。请检查您的逻辑:word_sorted.indexOf(target_sorted)始终为-1,除非word== target,如果有其他字符串包含字符穿插到目标中,则target_sorted.indexOf(word_sorted)可能不匹配(例如target="abbc",words="abc"/"b",您将消除"abc")。您也没有走得够远。您还应该消除长度>目标的单词和包含不存在于目标中的字母的单词(只需循环遍历每个排序字母并在单个字符上执行indexOf即可)。 - Stephen Chung
显示剩余7条评论

1

编辑
这个解决方案应该比朴素的解决方案快得多,因为所有的工作都是由内置的indexOf搜索完成的,它非常快,而且可以在第一个不匹配时退出。

function match(array, word){
    var char_array = word.split(''),
        char, index;
    array = array.join('').split('');
    while(char_array.length > 0){
        char = char_array.shift();
        if ((index = array.indexOf(char)) > -1)
            array.splice(index, 1);
        else
            return false;
    }
    return true;
}

这是一个朴素的解决方案:

function match(array, word){
    var char_array = word.split(''),
        array_elem;
    //loop over array
    for(var i=0, l=array.length; i < l; i++){
        array_elem = array[i].split('');
            //loop over the remaining chars in the word and
            //cross-check with the current array element
        for (var j=0,len =  char_array.length,index; j < len; j++)
            if ((index = array_elem.indexOf(char_array[j])) > -1){
                //char matched, remove it from both arrays
                char_array.splice(j, 1);
                array_elem.splice(index, 1);
            }
        }
    if(char_array.length < 1) return true
        else return false
}

如果速度是一个问题,那么应该有可以进行的优化来加快它。


这是否考虑到了一个规则,即您必须使用每个组合单词中的所有字符来匹配指定的单词? - makeee
是的,朴素的解决方案确实可以,你可以测试一下。但在更好的性能解决方案中,我忽略了它。 - Amjad Masad
太好了!我更新了我的帖子,加入了一个要求,即函数返回一个包含使用的单词的数组索引的数组,例如[4,8,15],以便我知道用于创建指定单词的哪些单词。 - makeee
Amjad:天真的解决方案似乎没有考虑到那个规则。“cod”与数组['god','house','d','e','cat','c','r','jump']匹配。 - makeee

0

在此处查看示例 →

var wordInDict = function ( word, dict ) {
    var dict = dict.slice(0), // copy dict to not mutate original
        wl = word.length,
        dl = dict.length,
        i, j, diw,
        wCount = 0;

    for (i = 0; i < wl; i++) {
        for (j = 0; j < dl; j++) {
            diw = dict[j].indexOf(word[i]); // is letter of word in dict word
            if (diw > -1) {
                wCount++;

                // remove used character from dictionary
                if (diw == dict[j].length-1) {
                    dict[j] = dict[j].slice(0, diw);
                } else if (diw == 0) {
                    dict[j] = dict[j].slice(1);
                } else {
                    dict[j] = dict[j].slice(0,diw) + 
                              dict[j].slice(diw+1,dict[j].length-1);
                }

                // letter found, so move to next letter in target word
                break;
            }
        }
    }

    return wCount == wl;
};

var tW = 'dodge';
var tD = ['god','house','d','e','cat','c','r','jump'];
var tE = ['got','house','d','e','cat','c','r','jump'];
console.log(wordInDict(tW, tD)); // true
console.log(wordInDict(tW, tE)); // false

嘿,我试着使用你的函数,发现“cod”这个单词也返回了 true,但实际上它不应该返回 true,因为组合单词中的所有字母都需要被使用。 - makeee
哇,这很难,因为它必须知道使用数组中首先出现的“house”中的“e”而不是单个“e”...即使您按长度对数组进行排序,我也不知道您是否可以保证结果。 - mVChr

0

我刚意识到这个问题相当于 子集积问题,因此是 NP-完全问题。

假设定义一个大小方法 s(x),它将每个字符串映射为质数并返回这些质数的乘积所得到的整数:

a --> 2, b --> 3, c --> 5, d --> 7, e --> 11, etc.

然后我们得到

s("abc") = 2 * 3 * 5 = 30 = 5 * 2 * 3 = s("cab")

给定一个字典 A,我们现在正在寻找一个子集 A'⊂ A,使得乘积为

p = ∏ { s(a) : a ∈ A' }

对于给定的key,返回s(key)


1
我不确定。首先,只有有限数量的字母,那高素数怎么办?其次,每个字符串的长度与它的值成线性比例关系,而在任何位置制系统中,它都是对数关系。 - julx

0

我能想到两种可能的解决方案

  1. 使用Array.sort()对给定的字符串进行排序,如果排序后的数组匹配,则这些字符串是变位词。

  2. 编写了一段本地代码

    function permutable(input1,input2) { if(typeof input1 === 'string' && typeof input2 === 'string' ) { const array_1 = input1.split('') const array_2 = input2.split('') let count2 = 0; let count1 = 0 let is_permutable = false if(array_1.length === array_2.length) { for(let j =0;j<array_1.length;j++) { count1 = checkrepeatations(array_1[j],array_1) if(array_2.includes(array_1[j])) { count2 = checkrepeatations(array_1[j],array_2) }else { return false; }

         if(count1 === count2) {
             is_permutable = true;
         } else {
           is_permutable = false;
         }
       }
       if(is_permutable) {
         return true;
       } else {
         return false;
       }
     } else {
       return false
     }
    
     }else {
       return false;
     }
    

    }

    function checkrepeatations(word,array_1) { let count = 0; let i =0; // array_1[j] = t and t e s t while(i<array_1.length) { //array_1[i] = t if(word === array_1[i]) { count++; } i++ } return count; }

附言:正在学习编程,如果有关于内存/变量管理的建议,将不胜感激。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接