冒泡排序深入理解

  • 2019 年 10 月 5 日
  • 筆記

冒泡排序深入理解

对于冒泡排序有一个小性质: 每一次都会把序列未排好序的最大数"沉底", 即推到序列尾部

1.P4378 Out of Sorts S

留意着农场之外的长期职业生涯的可能性,奶牛Bessie开始在不同的在线编程网站上学习算法。

她到目前为止最喜欢的算法是“冒泡排序”。这是Bessie的对长度为N的数组A进行排序的奶牛码实现。

sorted = false  while (not sorted):     sorted = true     moo     for i = 0 to N-2:        if A[i+1] < A[i]:           swap A[i], A[i+1]           sorted = false

显然,奶牛码中的“moo”指令的作用只是输出“moo”。奇怪的是,Bessie看上去执着于在她的代码中的不同位置使用这个语句。

给定一个输入数组,请预测Bessie的代码会输出多少次“moo”。

题意即进行多少次冒泡排序

对于一个序列, 我们称之为有序的, 当且仅当对于任意一个位置前面没有比它大的数(可以模拟一下)

比如:6 1 2 3 4 5 进行一次为 1 2 3 4 5 6

那么对于位置i, 冒泡排序进行到i-1时, $a_{i-1}$为前i1个数中最大的一个, 如果它大于$a_i$那么它就会到$a_i$的后面

由此可推知, 每一次位置i前都会将一个比$a_i$大的数推至其后, 直至没有比它大的

那么我们对每位置求一下它前面有几个比它大就好啦(注意要将答案加一)

具体来说先进行离散化, 再树状数组求解即可

代码:

#include<iostream>  #include<cstdio>  #include<cstring>  #include<algorithm>  using namespace std;  const int N = 100500;  int d[N], n;  int read(void) {      int x = 0;      char c = getchar();      while (!isdigit(c)) c = getchar();      while (isdigit(c)){          x = (x << 3) + (x << 1) + c - '0';          c = getchar();      }      return x;  }  struct node{      int val, pos;      bool operator < (const node &i) const{          if (val == i.val) return pos < i.pos;          return val < i.val;      }  }p[N];  inline int low(int x) {      return x & -x;  }  int get(int x) {      int tmp = 0;      for (;x;x -= low(x)) tmp += d[x];      return tmp;  }  void add(int x) {      for (;x <= n; x += low(x)) d[x]++;  }  bool cmp(node i,node j) {      return i.pos < j.pos;  }  int main() {      n = read();      for (int i = 1;i <= n; i++) p[i] = (node){read(),i};      sort(p + 1,p + n + 1);      for (int i = 1;i <= n; i++) p[i].val = i;      sort(p + 1,p + n + 1, cmp);      int ans = 0;      for (int i = 1;i <= n; i++) {          add(p[i].val);          ans = max(ans, i - get(p[i].val));      }      printf ("%dn", ans+1);      return 0;  }

2.P4375 Out of Sorts G

sorted = false  while (not sorted):     sorted = true     moo     for i = 0 to N-2:        if A[i+1] < A[i]:           swap A[i], A[i+1]     for i = N-2 downto 0:        if A[i+1] < A[i]:           swap A[i], A[i+1]     for i = 0 to N-2:        if A[i+1] < A[i]:           sorted = false

给定一个输入数组,请预测Bessie的代码会输出多少次“moo”。

题意:求双向冒泡排序的排序次数

对于一个序列, 我们称之为有序的, 当且仅当对于任意一个位置前面没有比它大的数(可以模拟一下)

我们暂且称它为平衡条件吧

首先将序列离散化

相比较于Out of Sorts S, 本题思路在于不动的位置, 结论为对于位置x, ans = max{ans, 前面有几个数的数值大于x}

为什么呢

在x不满足平衡条件的时候

首先第一波操作的时候,对于前x个位置一定会换出一个大于x的数

因为它不满足平衡条件

第二波操作时, 又会有一个小于等于x的数插回来

因为回来的时候一定会冒泡出一个位置在x后的最小值, 因为x不满足平衡条件, 所以最小值小于等于x, 就又插了回来

有人可能会问为什么Out of Sorts S不能用这个式子嘞, 因为每次换出的一定大于x, 但x+1位置上的数可能换过来, 而它有可能大于x

由此可知, 求每个位置前大于其的数就行啦

代码:

#include<iostream>  #include<cstdio>  #include<cstring>  #include<algorithm>  using namespace std;  const int N = 100500;  int d[N], n;  int read(void) {      int x = 0;      char c = getchar();      while (!isdigit(c)) c = getchar();      while (isdigit(c)){          x = (x << 3) + (x << 1) + c - '0';          c = getchar();      }      return x;  }  struct node{      int val, pos;      bool operator < (const node &i) const{          if (val == i.val) return pos < i.pos;          return val < i.val;      }  }p[N];  inline int low(int x) {      return x & -x;  }  int get(int x) {      int tmp = 0;      for (;x;x -= low(x)) tmp += d[x];      return tmp;  }  void add(int x) {      for (;x <= n; x += low(x)) d[x]++;  }  bool cmp(node i,node j) {      return i.pos < j.pos;  }  int main() {      n = read();      for (int i = 1;i <= n; i++) p[i] = (node){read(),i};      sort(p + 1,p + n + 1);      for (int i = 1;i <= n; i++) p[i].val = i;      sort(p + 1,p + n + 1, cmp);      int ans = 1;      for (int i = 1;i <= n; i++) {          add(p[i].val);          ans = max(ans, i - get(i));      }      printf ("%dn", ans);      return 0;  }  /*  6  2 5 6 3 1 4    */

3.P4372 Out of Sorts P

留意着农场之外的长期职业生涯的可能性,奶牛Bessie开始在不同的在线编程网站上学习算法。她最喜欢的两个算法是“冒泡排序”和“快速排序”,但是不幸的是Bessie轻易地把它们搞混了,最后实现了一个奇怪的混合算法! 如果数组A中A[…i]的最大值不大于A[i+1…]的最小值,我们就称元素i和i+1之间的位置为一个“分隔点”。Bessie还记得快速排序包含对数组的重排,产生了一个分隔点,然后要递归对两侧的A[…i]和A[i+1…]排序。然而,尽管她正确地记下了数组中所有的分隔点都可以在线性时间内被求出,她却忘记快速排序应该怎么重排来快速构造一个分隔点了!在这个可能会被证明是排序算法的历史中最糟糕的算法性失误之下,她做出了一个不幸的决定,使用冒泡排序来完成这个任务。

以下是Bessie最初的对数组AA进行排序的实现的概要。她首先写了一个简单的函数,执行冒泡排序的一轮:

bubble_sort_pass (A) {     for i = 0 to length(A)-2        if A[i] > A[i+1], swap A[i] and A[i+1]  }

她的快速排序(相当快)函数的递归代码是按下面的样子构成的:

quickish_sort (A) {     if length(A) = 1, return     do { // Main loop        work_counter = work_counter + length(A)        bubble_sort_pass(A)     } while (no partition points exist in A)     divide A at all partition points; recursively quickish_sort each piece  }

Bessie好奇于她的代码能够运行得多快。简单起见,她计算出她得主循环的每一轮都消耗线性时间,所以她相应增加一个全局变量work_counter的值,以此来跟踪整个算法总共完成的工作量。

给定一个输入数组,请预测quickish_sort函数接收这个数组之后,变量work_counter的最终值。

这道题用到了一个套路, 就是"横向变纵向"

求每一次冒泡排序的长度, 不如求每一个点被冒泡排序了几次

定义分割点为i与i+1的分割线,不妨假设它就在i上吧

再次定义序列排好序的标准

我们称一个序列是有序的当且仅当所有点(除了n)都是分割点

那么接下来我们要求分割点的出现时间t数组

为什么求:

对于每个点它不用在进行冒泡排序了当且仅当两边都已成为分割点, 也就是两边出现时间的最大值

依据t数组,我们可以求出每个点被排了几次

怎么求(敲重点):

首先离散化

对于一个点x来说, 所有小于它的数却在它后面的, 每一次都会向前走一次

那么它出现的时间就是离它最远的小于它的点冒泡到它前面的时间

即那个点到它的距离, 具体见代码

所以单调队列或指针都可以维护

代码:

#include<iostream>  #include<cstdio>  #include<cstring>  #include<algorithm>  #include<queue>  using namespace std;  const int N = 100500;  int d[N], n;  int read(void) {      int x = 0;      char c = getchar();      while (!isdigit(c)) c = getchar();      while (isdigit(c)){          x = (x << 3) + (x << 1) + c - '0';          c = getchar();      }      return x;  }  struct node{      int val, pos;      bool operator < (const node &i) const{          if (val == i.val) return pos < i.pos;          return val < i.val;      }  }p[N];  bool cmp(node i,node j) {      return i.pos < j.pos;  }  int t[N], k;  int main() {  //  freopen("hs.in","r",stdin);      n = read();      for (int i = 1;i <= n; i++) p[i] = (node){read(),i};      sort(p + 1,p + n + 1);      for (int i = 1;i <= n; i++) p[i].val = i;      sort(p + 1,p + n + 1, cmp);      long long ans = 0;      k = n;      for (int i = n;i >= 1; i--) {          while (p[k].val > i) k--;          t[i] = max(p[k].pos - i, 1);      }      for (int i = 0;i < n; i++) ans += max(t[i], t[i+1]);      printf ("%lldn", ans);      return 0;  }  /*  6  2 5 6 3 1 4    */

4.T99343 奇怪的排序

您有一个正整数序列, 您可以选择任意相邻的两个数$a_i,a_{i+1}$插入另两个数之间,或序列首和尾;
假如序列为: 1 2 4 3 5 6
可以选2 4
插在序列首 2 3 4 3 5 6
插到3后 1 3 2 4 5 6
插到5后 4 3 5 1 2 6
插在6后 1 3 5 6 2 4
现在hs-black需要判断是否进行若干次操作能使序列变得有序(无论正序倒序), 蒟蒻hs-black当然不会啦, 请您帮帮他…..

这道题来源于一位数竞大佬提供的灵感

再次定义一个序列有序

我们称一个序列是有序的,当且仅当它的逆序对数为0或n*(n-1)/2;

引理1: 交换序列中相邻的两个数会改变原序列逆序对个数的奇偶性

引理2: 将序列相邻两个数插入别处不会改变原序列逆序对个数的奇偶性

​ 证明: a~1~…a~i~a~j~…a~q~…a~n~ 不断将a~j~与它右边的数字交换直至正好换到a~q~ 即a~1~…a~j~a~i~…a~n~ 此时共交换了q – j 次

​ 再将a~i~ 向右与相邻数字交换q-1-i次到$a_j$左侧 ,此时共交换2 * (q – j) 次,为偶数次,所以奇偶性不变

那么说明逆序对数与排序好的逆序对数奇偶性不同时不能满足要求

下面证明相同时可以满足要求

以正序为例, 每次将序列最小的数和后面的数插到已排序部分的后面, 如果最小数在最后时就将后2,3个数插在它后面

当未排序列只剩两个数时, 逆序对个数也一定是偶数, 只可能是0

即序列有序, 证毕

具体实现是讨论一下n*(n-1)/2的奇偶性, 并树状数组求出原序列逆序对个数

代码:

#include<cstdio>  #include<cstring>  #include<algorithm>  #include<iostream>  const int M = 500005;  using namespace std;  int n, sum[M];  struct Num{      int val,num;      inline friend bool operator < (Num a,Num b){          return a.val > b.val;      }  }p[M];  inline int lowbit(int x){      return x&-x;  }  void add(int k,int x){      while(k<=n){          sum[k]+=x;          k+=lowbit(k);      }  }  int getsum(int k){      int tmp=0;      while(k>0){          tmp+=sum[k];          k-=lowbit(k);      }      return tmp;  }  long long Ans=0;  char ss[1<<17],*A=ss,*B=ss;  inline char gc()  {if(A==B){B=(A=ss)+fread(ss,1,1<<17,stdin);if(A==B)return EOF;}return*A++;}  template<class T>inline void read(T&x){      cin >> x ;  }  int main(){      int t;      read(t);      while(t--) {          Ans = 0;          memset(sum, 0, sizeof(sum));          read(n);          for(int i=1;i<=n;i++){              read(p[i].val);              p[i].num=i;          }          sort(p+1,p+n+1);          for(int i=1;i<=n;i++){              add(p[i].num,1);              Ans+=getsum(p[i].num-1);          }  //      printf ("%lldn", Ans);          if (n % 4 > 1)              printf("Yesn");          else if (Ans % 2 == 1)              printf("Non");          else              printf("Yesn");      }      return 0;  }