在排列中查找已排序的子序列

时间:2009-12-01 06:37:25

标签: algorithm

给定一个数组A,其中包含1,2,...,n的排列。数组A[i..j]的子块A 如果A[i..j]中出现的所有数字都是连续数字(可能不是有序的话),则称为有效阻止。

给定数组A= [ 7 3 4 1 2 6 5 8] 有效块为[3 4], [1,2], [6,5], [3 4 1 2], [3 4 1 2 6 5], [7 3 4 1 2 6 5], [7 3 4 1 2 6 5 8]

给出一个O(n log n)算法来计算有效块的数量。

4 个答案:

答案 0 :(得分:17)

这是最坏情况的 O(n log n)分而治之算法。给定排列的非空子列表,将其划分为左半部分,中间元素和右半部分。递归计算左半部分包含的块数和右半部分包含的块数。现在在O(n)时间内,计算包含中间元素的块数,如下所示。

观察到两个有效块的交集为空或有效块,并且整个置换是有效块。总之,这些事实意味着存在闭包:包含指定(非连续)子序列的唯一最小有效块。将左扩展定义为中间元素的闭包和不在右半部分的元素。将右扩展定义为中间元素的闭包和不在左半部分的元素。左扩展(分别是右扩展)相对于子列表关系是完全有序的。它们可以通过简单的工作列表算法按线性时间顺序计算。

现在观察两个重叠有效块的并集本身就是一个有效块。我声称包含中间元素的每个有效块都可以写为由最左边的元素生成的左扩展的并集与由最右边的元素生成的右扩展。要计算此表单的联合,请按递增顺序遍历左侧扩展。保持指向最右边扩展的指针,其最右边的元素不在左扩展的最右边,并且最左边的元素留在左扩展的最左边。由于单调性,这些指针只能移动到更大的扩展,因此总工作是线性的。它们绑定在符合条件的合作伙伴的上方和下方,用于当前的左侧扩展。

C ++实现:

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <queue>
#include <stdexcept>
#include <vector>

namespace {
typedef std::vector<int> IntVector;

struct Interval {
  int left;
  int right;
};

Interval MakeInterval(int left, int right) {
  Interval i = {left, right};
  return i;
}

typedef std::vector<Interval> IntervalVector;

enum Direction {
  kLeft,
  kRight,
};

// Finds the valid intervals obtained by starting with [pi[mid],
// pi[mid]] and repeatedly extending in direction dir
//
// O(right_boundary - left_boundary)
void FindExtensions(const IntVector& pi, const IntVector& pi_inv,
                    int left_boundary, int right_boundary,
                    Direction dir, IntervalVector* extensions) {
  int mid = left_boundary + (right_boundary - left_boundary) / 2;
  int left = mid;
  int right = mid;
  int lower = pi[mid];
  int upper = pi[mid];
  std::queue<int> worklist;
  while (true) {
    if (worklist.empty()) {
      extensions->push_back(MakeInterval(left, right));
      if (dir == kLeft) {
        if (left == left_boundary) break;
        --left;
        worklist.push(left);
      } else {
        if (right == right_boundary) break;
        ++right;
        worklist.push(right);
      }
    } else {
      int i = worklist.front();
      worklist.pop();
      if (i < left) {
        if (i < left_boundary) break;
        for (int j = left - 1; j >= i; --j) worklist.push(j);
        left = i;
      } else if (right < i) {
        if (right_boundary < i) break;
        for (int j = right + 1; j <= i; ++j) worklist.push(j);
        right = i;
      }
      int x = pi[i];
      if (x < lower) {
        for (int y = lower - 1; y > x; --y) worklist.push(pi_inv[y]);
        lower = x;
      } else if (upper < x) {
        for (int y = upper + 1; y < x; ++y) worklist.push(pi_inv[y]);
        upper = x;
      }
    }
  }
}

int CountValidRecursive(const IntVector& pi, const IntVector& pi_inv,
                        int left, int right) {
  if (right < left) return 0;
  int mid = left + (right - left) / 2;
  int count = CountValidRecursive(pi, pi_inv, left, mid - 1) +
      CountValidRecursive(pi, pi_inv, mid + 1, right);
  IntervalVector left_exts;
  FindExtensions(pi, pi_inv, left, right, kLeft, &left_exts);
  IntervalVector right_exts;
  FindExtensions(pi, pi_inv, left, right, kRight, &right_exts);
  typedef IntervalVector::const_iterator IVci;
  IVci first_good = right_exts.begin();
  IVci first_bad = right_exts.begin();
  for (IVci ext = left_exts.begin(); ext != left_exts.end(); ++ext) {
    while (first_good != right_exts.end() && first_good->right < ext->right) {
      ++first_good;
    }
    if (first_good == right_exts.end()) break;
    while (first_bad != right_exts.end() && ext->left <= first_bad->left) {
      ++first_bad;
    }
    count += std::distance(first_good, first_bad);
  }
  return count;
}

// Counts the number of intervals in pi that consist of consecutive
// integers
//
// O(n log n)
int CountValid(const IntVector& pi) {
  int n = pi.size();
  IntVector pi_inv(n, -1);
  // Validate and invert pi
  for (int i = 0; i < n; ++i) {
    if (pi[i] < 0 || pi[i] >= n || pi_inv[pi[i]] != -1) {
      throw std::runtime_error("Invalid permutation of {0, ..., n - 1}");
    }
    pi_inv[pi[i]] = i;
  }
  return CountValidRecursive(pi, pi_inv, 0, n - 1);
}

// For testing purposes
int SlowCountValid(const IntVector& pi) {
  int count = 0;
  int n = pi.size();
  for (int left = 0; left < n; ++left) {
    for (int right = left; right < n; ++right) {
      int lower = pi[left];
      int upper = pi[left];
      for (int i = left + 1; i <= right; ++i) {
        if (pi[i] < lower) {
          lower = pi[i];
        } else if (pi[i] > upper) {
          upper = pi[i];
        }
      }
      if (upper - lower == right - left) ++count;
    }
  }
  return count;
}
}  // namespace

int main() {
  int n = 10;
  IntVector pi(n);
  for (int i = 0; i < n; ++i) pi[i] = i;
  do {
    if (SlowCountValid(pi) != CountValid(pi)) {
      fprintf(stderr, "Bad permutation:");
      for (IntVector::const_iterator x = pi.begin(); x != pi.end(); ++x) {
        fprintf(stderr, " %d", *x);
      }
      fputc('\n', stderr);
    }
  } while (std::next_permutation(pi.begin(), pi.end()));
}

答案 1 :(得分:1)

这不是一个完整的解决方案,而是更多思考的起点。

诀窍似乎在于集合始终是1,2,...,n,从中可以清楚地看出整个集合始终是第一个明显有效的块。如果你从完整的设置开始并按下工作,它似乎更容易掌握。

具有最有效块的集合为M = [1 2 3 4 5 . . . n],因为每个子集[i..j]都保证是有效块。 M是一个很好的测试用例,因为您可以轻松确定有效块的实际数量:((n - 1) / 2) * n

答案 2 :(得分:1)

想象一下,你有一个函数,给定一个n个整数的列表可以告诉你它们是否是一个有效的块。

想象一下你修改了这个函数,所以它返回了一个有效的子块数的计数,所以给定[1 3 2 4]会找到[1 3 2 4],[1 3 2],[3 2 4] ,[3 2]。

要进行修改,你只需遍历所有可能的子块,将它们传递给原始函数,直到你只有2个数字:

1,3,2,4 is valid
1,3,2 is valid
1,3 is NOT valid
3,2,4 is valid
3,2 is valid
There were 4 valid sub blocks

第一个功能是:

def isValid(li):
    return (max(li)-min(li) == len(li)-1) 

也就是说,假设所有值都不同,最大值减去最小值应该使数组的长度减1.对于[1 3 2 4],最大= 4,最小= 1,最大 - 最小= 3, length-1 = 3,完成工作。

主要检查功能:

def countValidSubs(li):
    count = 0
    length = len(li)
    for start in range(0,length-2):
        for newlen in range(length-start,1,-1):
            newli = li[start:start+newlen]
            if isValid(newli):
                print(','.join(`i` for i in newli)+" is valid")
                count += 1
            else:
                print(','.join(`i` for i in newli)+" is NOT valid")
    return count

然后打电话给:

countValidSubs([1, 3, 2, 4, 5, 7, 9, 8, 6])

(顺便说一下,答案是14)

作为家庭作业答案的唯一问题是它是O(n 2 / 2)

答案 3 :(得分:0)

我认为你不需要算法。这就是你需要的。求和(i = 0到i = n-1)(n-i)*(i + 1)!