如何确定可以从序列中删除子序列的所有可能方式?

Posted

技术标签:

【中文标题】如何确定可以从序列中删除子序列的所有可能方式?【英文标题】:How can I determine all possible ways a subsequence can be removed from a sequence? 【发布时间】:2016-12-27 21:14:14 【问题描述】:

给定两个序列,AB,我如何生成可以从 B 中删除的所有可能方式的列表em>A?

例如,在 javascript 中,如果我有一个函数 removeSubSeq 接受两个数组参数来满足我的要求,它的工作方式如下:

removeSubSeq([1,2,1,3,1,4,4], [1,4,4]) 会返回 [ [2,1,3,1], [1,2,3,1], [1,2,1,3] ],因为最后的 4 会匹配,并且 1 有三个可能匹配的位置

removeSubSeq([8,6,4,4], [6,4,8]) 将返回 [],因为第二个参数实际上不是子序列

removeSubSeq([1,1,2], [1]) 将返回 [ [1,2], [1,2] ],因为有两种方法可以删除 1,即使它会导致重复

【问题讨论】:

使用 LCS 将 JavaScript 代码添加到我的答案中。 我已将 JavaScript 实现添加到我的答案中:***.com/a/39064867/653511 【参考方案1】:

这个问题可以在O(n*m + r)时间解决,其中r是结果的总长度,使用经典的longest common subsequence算法。

制作表格后,就像在***的example 中一样,将其替换为带有对角箭头的单元格列表,该列表也具有与其行对应的值。现在从最后一行中带有对角线的每个单元格向后遍历,累积字符串中的相关索引并复制和拆分累积,以便每个带有对角线箭头的单元格将延续到前一行中带有对角线的所有单元格位于它的左侧(在您构建矩阵时也存储该计数)并且值少一个。当累积到达零单元格时,拼接字符串中累积的索引并将其添加为结果。

(箭头对应目前的LCS是否来自LCS(X[i-1],Y[j]) and/or LCS(X[i],Y[j-1]), or LCS(X[i-1],Y[j-1]),见函数definition。)

例如:

  0  a  g  b  a  b  c  c
0 0  0  0  0  0  0  0  0
a 0 ↖1  1  1 ↖1  1  1  1
b 0  1  1 ↖2  2 ↖2  2  2
c 0  1  1  2  2  2 ↖3 ↖3

JavaScript 代码:

function remove(arr,sub)
  var _arr = [];
  arr.forEach(function(v,i) if (!sub.has(i)) _arr.push(arr[i]); );
  return _arr;


function f(arr,sub)
  var res = [],
      lcs = new Array(sub.length + 1),
      nodes = new Array(sub.length + 1);
     
  for (var i=0; i<sub.length+1;i++)
    nodes[i] = [];
    lcs[i] = [];
   
    for (var j=0; j<(i==0?arr.length+1:1); j++)
      // store lcs and node count on the left
      lcs[i][j] = [0,0];
    
  
 
  for (var i=1; i<sub.length+1;i++) 
    for (var j=1; j<arr.length+1; j++)
      if (sub[i-1] == arr[j-1])
        lcs[i][j] = [1 + lcs[i-1][j-1][0],lcs[i][j-1][1]];
       
        if (lcs[i][j][0] == i)
                  // [arr index, left node count above]
          nodes[i].push([j - 1,lcs[i-1][j-1][1]]);
       
          lcs[i][j][1] += 1;
        
       
       else 
        lcs[i][j] = [Math.max(lcs[i-1][j][0],lcs[i][j-1][0]),lcs[i][j-1][1]];
      
    
  
   
  function enumerate(node,i,accum)
    if (i == 0)
      res.push(remove(arr,new Set(accum)));
      return;
    
    
    for (var j=0; j<node[1]; j++)
      var _accum = accum.slice();
      _accum.push(nodes[i][j][0]);
      
      enumerate(nodes[i][j],i - 1,_accum);
    
  
  
  nodes[sub.length].forEach(function(v,i) 
    enumerate(nodes[sub.length][i],sub.length - 1,[nodes[sub.length][i][0]]); 
  );

  return res;


console.log(JSON.stringify(f([1,2,1,3,1,4,4], [1,4,4])));
console.log(JSON.stringify(f([8,6,4,4], [6,4,8])));
console.log(JSON.stringify(f([1,1,2], [1])));
console.log(JSON.stringify(f(['a','g','b','a','b','c','c'], ['a','b','c'])));

【讨论】:

问题确实归结为 LCS 的一种变体。发布的这段代码不是最易读的,但根据我不科学的微基准测试,它似乎比其他解决方案更快。 @heenenee 感谢您查看!基本上,代码遵循上面的算法描述,因此 LCS 表需要一个额外的字段来表示每个单元格左侧有多少个节点(nodes 是匹配项,也是最长公共子序列的一部分)。根据数据和使用情况,可能有一些优化方法。我也想知道,既然我们知道我们只是在寻找完全匹配,是否可能存在某种数量级的优化。【参考方案2】:

您可以使用递归。通过遍历 A 并按顺序推送元素来构建新的子序列 C。每当您遇到与 B 的头部匹配的元素时,您将递归分为两条路径:一条是从 A 和 B 中删除(即跳过)元素,另一条是忽略它并照常工作。

如果你用尽了所有的 B(意味着你从 A 中“删除”了 B 中的所有元素),那么将 A 的其余部分附加到 C 将产生一个有效的子序列。否则,如果你到达 A 的末尾而不用尽所有 B,则 C 不是有效的子序列,应该被丢弃。

function removeSubSeq(a, b) 
    function* remove(i, j, c) 
        if (j >= b.length) 
            yield c.concat(a.slice(i));
         else if (i >= a.length) 
            return;
         else if (a[i] === b[j]) 
            yield* remove(i + 1, j + 1, c);
            yield* remove(i + 1, j, c.concat(a.slice(i, i + 1)));
         else 
            yield* remove(i + 1, j, c.concat(a.slice(i, i + 1)));
        
    

    if (a.length < b.length) 
        return [];   
    

    return Array.from(remove(0, 0, []));

通过将每个递归分支中Array.concat 的使用替换为简单的push()/pop() 对,可以使内部辅助函数稍微更有效,但这会使控制流更难理解。

function* remove(i, j, c) 
    if (j >= b.length) 
        yield c.concat(a.slice(i));
     else if (i >= a.length) 
        return;
     else 
        if (a[i] === b[j]) 
            yield* remove(i + 1, j + 1, c);
        

        c.push(a[i]);
        yield* remove(i + 1, j, c);
        c.pop();
    

【讨论】:

我喜欢它的优雅,它很好地使用了生成器函数。不错!【参考方案3】:

这个问题可以使用带有回溯的自下而上动态规划方法来解决。

让我们考虑一个递归关系f(i1, i2),它有助于检查序列arr2tail是否可以从序列@987654323的tail中移除@:

f(i1, i2) = true, if(i1 == length(arr1) AND i2 == length(arr2))
f(i1, i2) = f(i1 + 1, i2) OR f(i1 + 1, i2 + 1), if(arr1[i1] == arr2[i2])
f(i1, i2) = f(i1 + 1, i2), if(arr1[i1] != arr2[i2])

solution  = f(0, 0)

我使用术语 tail 来表示 arr1 的子序列,它从索引 i1 开始并跨越到 arr1 的末尾(arr2 也是如此 - arr2 的 em>tail 从索引 i2 开始并跨越到 arr2 的末尾)。

让我们从给定递归关系的自上而下实现开始(但没有记忆,以保持解释简单)。下面是 Java 代码的 sn-p,它打印了删除 arr2 后所有可能的 arr1 子序列:

void remove(int[] arr1, int[] arr2) 
    boolean canBeRemoved = remove(arr1, arr2, 0, 0, new Stack<>());
    System.out.println(canBeRemoved);


boolean remove(int[] arr1, int[] arr2, int i1, int i2, Stack<Integer> stack) 

    if (i1 == arr1.length) 
        if (i2 == arr2.length) 
            // print yet another version of arr1, after removal of arr2
            System.out.println(stack);
            return true;
        
        return false;
    

    boolean canBeRemoved = false;
    if ((i2 < arr2.length) && (arr1[i1] == arr2[i2])) 
        // current item can be removed
        canBeRemoved |= remove(arr1, arr2, i1 + 1, i2 + 1, stack);
    

    stack.push(arr1[i1]);
    canBeRemoved |= remove(arr1, arr2, i1 + 1, i2, stack);
    stack.pop();

    return canBeRemoved;

所提供的 sn-p 代码不使用任何记忆技术,并且对于给定问题的所有实例具有指数运行时复杂度

但是,我们可以看到变量i1只能取区间[0..length(arr1)]的值,变量i2也只能取区间[0..length(arr2)]的值。

因此,可以检查是否可以从具有多项式运行时复杂度的arr1 中删除arr2O(length(arr1) * length(arr2))

另一方面,即使我们发现具有多项式运行时复杂度的 arr2 可以从 arr1 中删除 - 仍然可能有指数数量的可能方法可以从 arr1 中删除 arr2

例如,考虑问题的实例:何时需要从arr1 = [1,1,1,1,1,1,1] 中删除arr2 = [1,1,1]。有7!/(3! * 4!) = 35 的方法可以做到这一点。

尽管如此,以下是具有回溯的自下而上动态规划解决方案,对于给定问题的许多实例,它仍然具有比指数更好的运行时复杂度:

void remove_bottom_up(int[] arr1, int[] arr2) 
    boolean[][] memoized = calculate_memoization_table(arr1, arr2);
    backtrack(arr1, arr2, 0, 0, memoized, new Stack<>());


/**
 * Has a polynomial runtime complexity: O(length(arr1) * length(arr2))
 */
boolean[][] calculate_memoization_table(int[] arr1, int[] arr2) 

    boolean[][] memoized = new boolean[arr1.length + 1][arr2.length + 1];
    memoized[arr1.length][arr2.length] = true;

    for (int i1 = arr1.length - 1; i1 >= 0; i1--) 
        for (int i2 = arr2.length; i2 >= 0; i2--) 

            if ((i2 < arr2.length) && (arr1[i1] == arr2[i2])) 
                memoized[i1][i2] = memoized[i1 + 1][i2 + 1];
            
            memoized[i1][i2] |= memoized[i1 + 1][i2];
        
    
    return memoized;


/**
 * Might have exponential runtime complexity.
 *
 * E.g. consider the instance of the problem, when it is needed to remove
 * arr2 = [1,1,1] from arr1 = [1,1,1,1,1,1,1].
 *
 * There are 7!/(3! * 4!) = 35 ways to do it.
 */
void backtrack(int[] arr1, int[] arr2, int i1, int i2, boolean[][] memoized, Stack<Integer> stack) 

    if (!memoized[i1][i2]) 
        // arr2 can't be removed from arr1
        return;
    

    if (i1 == arr1.length) 
        // at this point, instead of printing the variant of arr1 after removing of arr2
        // we can just collect this variant into some other container
        // e.g. allVariants.add(stack.clone())
        System.out.println(stack);
        return;
    

    if ((i2 < arr2.length) && (arr1[i1] == arr2[i2])) 
        backtrack(arr1, arr2, i1 + 1, i2 + 1, memoized, stack);
    

    stack.push(arr1[i1]);
    backtrack(arr1, arr2, i1 + 1, i2, memoized, stack);
    stack.pop();

所述解决方案的JavaScript实现

function remove_bottom_up(base_arr, removed_arr) 

    // Initialize memoization table
    var memoized = new Array(base_arr.length + 1);
    for (var i = 0; i < base_arr.length + 1; i++) 
        memoized[i] = new Array(removed_arr.length + 1);
    
    memoized[base_arr.length][removed_arr.length] = true;

    // Calculate memoization table 
    for (var i1 = base_arr.length - 1; i1 >= 0; i1--) 
        for (var i2 = removed_arr.length; i2 >= 0; i2--) 
            if ((i2 < removed_arr.length) && (base_arr[i1] == removed_arr[i2])) 
                memoized[i1][i2] = memoized[i1 + 1][i2 + 1];
            
            memoized[i1][i2] |= memoized[i1 + 1][i2];
        
    

    // Collect all variants
    var all_variants = [];
    backtrack(base_arr, removed_arr, 0, 0, memoized, [], all_variants);
    return all_variants;


function backtrack(base_arr, removed_arr, i1, i2, memoized, stack, all_variants) 

    if (!memoized[i1][i2]) 
        // arr2 can't be removed from arr1
        return;
    

    if (i1 == base_arr.length) 
        all_variants.push(stack.slice(0));
        return;
    

    if ((i2 < removed_arr.length) && (base_arr[i1] == removed_arr[i2])) 
        backtrack(base_arr, removed_arr, i1 + 1, i2 + 1, memoized, stack, all_variants);
    

    stack.push(base_arr[i1]);
    backtrack(base_arr, removed_arr, i1 + 1, i2, memoized, stack, all_variants);
    stack.pop();


console.log(JSON.stringify(remove_bottom_up([1, 2, 1, 3, 1, 4, 4], [1, 4, 4])));
console.log(JSON.stringify(remove_bottom_up([8, 6, 4, 4], [6, 4, 8])));
console.log(JSON.stringify(remove_bottom_up([1, 1, 2], [1])));
console.log(JSON.stringify(remove_bottom_up([1, 1, 1, 1, 1, 1, 1], [1, 1, 1])));

【讨论】:

【参考方案4】:

算法:

    递归地构建节点树,从 B 中的第一个元素开始。每个节点的值是与其级别匹配的子序列项的索引,其后代是下一项的索引 -- 所以对于 [1,2,1,3,1,4,4], [1,4,4] 树应该是[ [ 0, [5, [6]], [6] ], [ 2, [5, [6]], [6] ], [ 4, [5, [6]], [6] ]。 遍历这棵树并建立要删除的项目的子序列,即找到树中与子序列一样长的所有路径。这将产生一个类似[ [ 0, 5, 6 ], [ 2, 5, 6 ], [ 4, 5, 6 ] ] 的列表。 对于这样开发的每个列表,附加由被删除索引处的元素产生的列表:[ [ 2, 1, 3, 1 ], [ 1, 2, 3, 1 ], [ 1, 2, 1, 3 ] ]

执行此操作的代码,它匹配您的所有测试用例:


#!/usr/bin/env node

var _findSubSeqs = function(outer, inner, current) 

    var results = [];
    for (var oi = current; oi < outer.length; oi++) 
        if (outer[oi] == inner[0]) 
            var node = 
                value: oi,
                children: _findSubSeqs(outer, inner.slice(1), oi+1)
            ;
            results.push(node);
            
    
    return results;


var findSubSeqs = function(outer, inner) 
    var results = _findSubSeqs(outer, inner, 0);
    return walkTree(results).filter(function(a) return (a.length == inner.length));


var _walkTree = function(node) 
    var results = [];
    if (node.children.length) 
        for (var n = 0; n < node.children.length; n++) 
            var res = _walkTree(node.children[n])
            for (r of res) 
                results.push([node.value].concat(r))
            
        
     else 
        return [[node.value]]
    
    return results


var walkTree = function(nds) 
    var results = [];
    for (var i = 0; i < nds.length; i++) 
        results = results.concat(_walkTree(nds[i]))
    
    return results


var removeSubSeq = function(outer, inner) 
    var res = findSubSeqs(outer, inner);
    var subs = [];
    for (r of res) 
        var s = [];
        var k = 0;
        for (var i = 0; i < outer.length; i++) 
            if (i == r[k]) 
                k++;
             else 
                s.push(outer[i]);
            
        
        subs.push(s);
    
    return subs


console.log(removeSubSeq([1,2,1,3,1,4,4], [1,4,4]))
console.log(removeSubSeq([8,6,4,4], [6,4,8]) )
console.log(removeSubSeq([1,1,2], [1]))

【讨论】:

【参考方案5】:

首先我会使用字符串。更容易操作:

var results = [];

function permute(arr) 
    var cur, memo = [];

    for (var i = 0; i < arr.length; i++) 
        cur = arr.splice(i, 1);
        if (arr.length === 0) 
            results.push(memo.concat(cur));
        
        permute(arr.slice(), memo.concat(cur));
        arr.splice(i, 0, cur[0]);
    
    return results;


function removeSub(arr, sub) 
    strArray = arr.join(' ');
    if(strArray.includes(sub))
        return strArray.replace(sub.join(' ')).split(' ');
    
    return [];


function removeSubSeq(arr, sub) 
    return permute(removeSub(arr, sub));

我没有评论代码,但请毫不犹豫地要求澄清。它没有经过测试,但想法就在其中......

【讨论】:

缺少关闭 ) return permute(removeSub(arr, sub)【参考方案6】:

我的目标是尽可能少地创建和调用函数。这似乎有效。绝对可以清理干净。可以玩的东西...

function removeSubSeq( seq, sub ) 

    var arr,
        sub_v,
        sub_i = 0,
        seq_i,
        sub_len = sub.length,
        sub_lenm1 = sub_len - 1,
        seq_len = seq.length,
        pos = ,
        pos_len = [],
        c_pos,
        map_i = [],
        len,
        r_pos,
        sols = [],
        sol;

    do 

        map_i[ sub_i ] = 0;
        sub_v = sub[ sub_i ];

        if( pos[ sub_v ] ) 

            pos_len[ sub_i ] = pos_len[ sub_i - 1 ];
            continue;
        
        

        arr = pos[ sub_v ] = [];

        c_pos = 0;

        seq_i = seq_len;

        while( seq_i-- ) 

            if( seq[ seq_i ] === sub_v ) 
                
                arr[ c_pos++ ] = seq_i;

            

        

        pos_len[ sub_i ] = arr.length;

     while( ++sub_i < sub_len );

    len = pos[ sub[ 0 ] ].length;

    while( map_i[ 0 ] < len ) 

        sub_i = 0;
        arr = [];

        do 

            r_pos = pos[ sub[ sub_i ] ][ map_i[ sub_i ] ];
            
            if( sub_i && r_pos <= arr[ sub_i - 1] ) break;
            
            arr.push( r_pos );
        
         while( ++sub_i < sub_len );

        if( sub_i === sub_len ) 
            
            sol = seq.slice( 0 );

            while( sub_i-- ) sol.splice( arr[ sub_i ], 1 );

            sols.push( sol );
        
        

        sub_i = sub_lenm1;

        while( ++map_i[ sub_i ] === pos_len[ sub_i ] ) 
            if( sub_i === 0 ) break;
            map_i[ sub_i-- ] = 0;
        

     while( map_i[ 0 ] < len );

    return sols;



console.log(JSON.stringify(removeSubSeq([1,2,1,3,1,4,4], [1,4,4])));
console.log(JSON.stringify(removeSubSeq([8,6,4,4], [6,4,8])));
console.log(JSON.stringify(removeSubSeq([1,1,2], [1])));
console.log(JSON.stringify(removeSubSeq(['a','g','b','a','b','c','c'], ['a','b','c'])));

【讨论】:

以上是关于如何确定可以从序列中删除子序列的所有可能方式?的主要内容,如果未能解决你的问题,请参考以下文章

最长回文子序列

算法题:求一个序列S中所有包含T的子序列(distinct sub sequence)

516. 最长回文子序列

AcWing:135. 最大子序和(前缀和 + 单调队列)

516. 最长回文子序列(Python)

leetcode 16. 最长回文子序列 java