算法设计与分析第一次作业:求任意排列的字典序值

算法设计与分析第一次作业

问题重述

给定一个正整数 n n n的排列,即 n n n个元素 { 1 , 2 , . . . , n } \{1,2,...,n\} {1,2,...,n}的一个序列,计算出这个排列的字典序值(例如排列 { 1 , 2 , 3 , . . . , n } \{1,2,3,...,n\} {1,2,3,...,n}的字典序值为1,排列 { n , n − 1 , n − 2 , . . . , 1 } \{n,n-1,n-2,...,1\} {n,n1,n2,...,1}的字典序值为 n n n

算法设计

方法1:暴力

设置计数器,通过从字典序值为1的序列开始,不断计算字典序为 2 , 3 , . . , n 2,3,..,n 2,3,..,n的序列,每一次检测是否等于目标序列,在得到目标序列时停止计数并输出计数器的值。

时间复杂度 O ( n 3 ∗ n ! ) O(n^3*n!) O(n3n!)

代码实现(c++)

#include 
using namespace std;
class Solution {
 public:
  void nextPermutation(vector<int>& nums) {  //得到当前排列的下一个排列
    int n = nums.size();
    for (int i = n - 1; i >= 0; i--) {
      for (int j = n - 1; j > i; j--) {
        if (nums[j] > nums[i]) {
          swap(nums[j], nums[i]);
          reverse(nums.begin() + i + 1, nums.end());
          return;
        }
      }
    }
    reverse(nums.begin(), nums.end());
  }
  int Get_Order(vector<int>& nums) {
    vector<int> a;
    int n = nums.size(), cnt = 1;
    for (int i = 1; i <= n; i++) a.push_back(i);
    while (a != nums) {//判断是否到达目标序列
      nextPermutation(a);
      cnt++;//计数器递增
    }
    return cnt;
  }
};
int main() {
  vector<int> nums = {2,5,4,1,3};
  Solution T;
  cout << T.Get_Order(nums) << endl;
  return 0;
}
// output:47

方法2: 找规律

例子
观察序列 { 2 , 5 , 4 , 1 , 3 } \{2,5,4,1,3\} {2,5,4,1,3},计算比其字典序小的序列有多少个

  • 对于第一位数,可选择 1 , 2 1,2 1,2,当选择 1 1 1时,后四位数无限制,即以 1 1 1开头的排列有 1 ∗ 4 ! 1*4! 14!种,当选择 2 2 2时,要求第二位小于 5 5 5,即只有 [ 4 , 3 , 1 ] [4,3,1] [431]满足,有 3 ∗ 3 ! 3*3! 33!
  • 对于前两位数,以 { 2 , 5 } \{2,5\} {2,5}开头时,要求第三位小于4,即只有 [ 1 , 3 ] [1,3] [1,3]满足,有 2 ∗ 2 ! 2*2! 22!
  • 对于前三位数,以 { 2 , 5 , 4 } \{2,5,4\} {2,5,4}开头时,要求第四位小于1,则没有选择
  • 确定前四位数后,所有排列数也就计算完成,答案为上面提到的种类数之和,即 a n s = 1 ∗ 4 ! + 3 ∗ 3 ! + 2 ∗ 2 ! = 46 ans=1*4!+3*3!+2*2!=46 ans=14!+33!+22!=46
  • 这说明在 { 2 , 5 , 4 , 1 , 3 } \{2,5,4,1,3\} {2,5,4,1,3}前有 46 46 46个排列,那么它的字典序值就是 47 47 47
    归纳
    对于正整数n的任意排列 { a 1 , a 2 , a 3 , . . . , a n } \{a_1,a_2,a_3,...,a_n\} {a1,a2,a3,...,an},为方便计算与描述,可以在序列前加一个0,即 { 0 , a 1 , a 2 , a 3 , . . . , a n } \{0,a_1,a_2,a_3,...,a_n\} {0,a1,a2,a3,...,an},这样,需要计算的就是“ a i a_i ai后面比 a i a_i ai小的数的数量",我们假设它等于 c n t i cnt_i cnti,答案就是 ∑ c n t i ∗ ( n − i ) ! \sum cnt_i*(n-i)! cnti(ni)!

时间复杂度 O ( n 2 ) O(n^2) O(n2)

代码实现(c++)

#include 
using namespace std;
class Solution {
 public:
  int Get_Order(vector<int>& nums) {
    int n = nums.size();
    vector<int> fact(n + 1);
    //计算1到n的阶乘
    fact[0] = 1;
    for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i;
    //计算 a_i 后面比其小的数的数量
    int ans = 0;
    for (int i = 0; i < n; i++) {
      int cnt = 0;
      for (int j = i + 1; j < n; j++)
        if (nums[j] < nums[i]) cnt++;
      ans += (cnt * fact[n - i - 1]);
    }
    return ans + 1;  //自身字典序值还需+1
  }
};
int main() {
  vector<int> nums = {2, 5, 4, 1, 3};
  Solution T;
  cout << T.Get_Order(nums) << endl;
  return 0;
}
//output: 47

方法3: 进阶-树状数组优化

我们注意到,在方法2中,主要是计算“ a i a_i ai后面比 a i a_i ai小的数的数量"的过程达到了 O ( n 2 ) O(n^2) O(n2)的时间复杂度,而这一类区间查询问题,能够使用树状数组这一数据结构进行优化,从而使查询的时间复杂度降低到 O ( l o g ( n ) ) O(log(n)) O(log(n))

百科:
树状数组或二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为Fenwick树,最早由Peter M.Fenwick于1994年以A New Data Structure for Cumulative Frequency Tables为题发表在SOFTWARE PRACTICE AND
EXPERIENCE。其初衷是解决数据压缩里的累积频率(Cumulative Frequency)的计算问题,现多用于高效计算数列的前缀和,区间和。[摘自百度百科]
A Fenwick tree or binary indexed tree is a data structure that can efficiently update elements and calculate prefix sums in a table of numbers. [摘自维基百科]

时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

代码实现(c++)

#include 
using namespace std;
class Solution {
 private:
  int n;

 public:
  int lowbit(int x) { return x & (-x); }
  void update(vector<int>& c, int i, int k) {
    while (i < c.size()) {
      c[i] += k;
      i += lowbit(i);
    }
  }
  int getsum(vector<int>& c, int i) {
    int ans = 0;
    while (i > 0) {
      ans += c[i];
      i -= lowbit(i);
    }
    return ans;
  }
  vector<int> countSmaller(vector<int>& A) {//树状数组实现查询比当前数小且在该数后面的数的数量,即cnt_i
    vector<int> c(n, 0);
    vector<int> res(A.size(), 0);
    for (int i = A.size() - 1; i >= 0; i--) {
      res[i] += getsum(c, A[i]);
      update(c, A[i], 1);
    }
    return res;
  }
  int Get_Order(vector<int>& nums) {
    n = nums.size();
    vector<int> fact(n + 1);
    //计算1到n的阶乘
    fact[0] = 1;
    for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i;
    //计算 a_i 后面比其小的数的数量
    vector<int> cnt(n);
    cnt = countSmaller(nums);
    int ans = 0;
    for (int i = 0; i < n; i++) ans += (cnt[i] * fact[n - i - 1]);
    return ans + 1;  //自身字典序值还需+1
  }
};
int main() {
  vector<int> nums = {2, 5, 4, 1, 3};
  Solution T;
  cout << T.Get_Order(nums) << endl;
  return 0;
}

其它方法

在求解“ a i a_i ai后面比 a i a_i ai小的数的数量"的问题上,还可以使用如下方法:

  • 有序数组+ 二分搜索
  • 归并排序
  • 线段树

它们的时间复杂度和方法三一样,此处不再列举。

你可能感兴趣的:(算法课,算法,c++,数据结构)