冒泡排序深入理解

时间:2024-01-25 13:51:58

冒泡排序深入理解

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

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 ("%d\n", 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 ("%d\n", 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 ("%lld\n", ans);
	return 0;
}
/*
6
2 5 6 3 1 4

*/