目录
1. 题目描述
2. 算法设计基本思想
2.1 满足条件的划分情况
为了使得 有最小值,则两个集合的元素个数应该尽量接近,在此前提下,为了使得 有最大值,则当且仅当一边的所有元素都小于等于另一边的所有元素满足题意.
对于第一个条件,若 A 的元素个数为偶数 n,则划分所得两边集合的元素个数分别为 即可;若 A 的元素个数为奇数 n,则划分所得两边集合的元素个数分别为⌊ ⌋, ⌊ ⌋ - 1,其中 ⌊ ⌋, 表示下取整符号,譬如,n = 5,则两边集合元素个数分别为 2,3.
与统计学中的顺序统计量类似地,我们定义 A(n) 表示将 A 排好序后的排在第 n 位的元素,也即 A 中第 n 小的元素.
对于第二个条件,若 A 的元素个数为偶数 n,可以把 A(1),A(2) ... A()划分在一个集合中,把 A( + 1), A( + 2),... A(n)划分在另一个集合中即可;若 A 的元素个数为奇数 n,则需考虑 A (⌊⌋ + 1) 应该归为哪一方:
记 M1 为A(1) 到 A(⌊⌋)的求和值,M2 为A (⌊⌋ + 2) 到 A(n)的求和值,k = A (⌊⌋ + 1)
1. 若将 k 归于 M1,则S1 = k + M1,S2 = M2, = ,进一步地,另 x = (M2 - M1),有 =
2. 若将 k 归于 M2,则S1 = M1,S2 = k + M2, = ,进一步地,另 x = (M2 - M1),有 =
由于 k > 0, x > 0,则 < ,故将 k 划分至 M2最为合适.
2.2 划分算法的实现
在2.1的证明中,我们已经知道需要无论 A 的元素个数为奇数还是偶数,均需要找到第⌊⌋ + 1小的元素,并将比它小元素归在一个划分中,将它以及比它大的元素归在另一个划分中.
显然,一个比较直观的解法,是对所给集合A从头到尾进行全序列的快速排序,再在有序序列中查找排位在⌊⌋ + 1的元素进行划分。如此,平均时间复杂度将与快速排序相同,即O().
但由于我们仅需统计比 A (⌊⌋ + 1) 小元素的元素和 S1,大于等于 A (⌊⌋ + 1) 的元素和 S2,无需对这两个划分内部进行排序,因此在快速排序递归划分的过程中,若某次枢纽元素恰为 A (⌊⌋ + 1),则可以停止排序,此时A (⌊⌋ + 1) 前面的元素均比它小(虽然可能是无序状态), A (⌊⌋ + 1) 后面的元素均比它大(虽然也可能是无序状态),但不影响我们求和;同时,由于我们的目标转为找到 A (⌊⌋ + 1) 作为枢纽的划分,因此每次划分只需递归进行元素个数大于等于⌊⌋ + 1的子段即可,因为我们可以保证A (⌊⌋ + 1)一定在该子段当中.
3. 算法的C++代码实现
划分 (Partition) 部分的算法,可直接照搬一般快速排序中的划分,这里选取序列尾元素作为划分枢纽 (pivot),函数返回枢纽最终的下标:
int Partition(int a[], int low, int high){
int i = low, j = high; //待划分序列的左、右下标范围
int pivot = a[high]; //选取序列尾元素作为划分枢纽
while(1){
while(a[i] < pivot) i++;
while(j > i && a[j] > pivot) j--;
if(i >= j)
break;
else
swap(a[i++],a[j--]);
}
swap(a[i], a[high]);
return i;
}
由于本题只需递归划分一个子段,故在此采用迭代的方式实现A (⌊⌋ + 1)的查询:
void Solution(int a[], int n){
int l = 1; //原始序列首元素下标
int r = n; //原始序列尾元素下标
int i = 0; //记录当前划分的枢纽
bool suc = false; //是否成功找到中间值目标
while(!suc){
i = Partition(a, l, r);
//成功找到
if(i == n / 2 + 1)
suc = true;
//失败,则待找枢纽在元素个数超过 n / 2 + 1的一方
else if(i < n / 2 + 1)
l = i + 1;
else
r = i - 1;
}
int S1 = 0, S2 = 0; //记录求和结果
cout << "S1: ";
for(int t = 1; t < i; t++){
cout << a[t] << " ";
S1 += a[t];
}
cout << endl << "S2: ";
for(int t = i; t <= n; t++){
cout << a[t] << " ";
S2 += a[t];
}
//输出结果
cout << endl << "|S1 - S2| = " << abs(S1 - S2);
}
主函数,注意元素下标从1开始:
int main(){
int n;
cin >> n;
int a[n+1];
for(int i = 1; i <= n; i++)
cin >> a[i];
Solution(a, n);
}
代码运行:
输入:
5
5 1 3 4 2
输出:
S1: 2 1
S2: 5 4 3
|S1 - S2| = 9
输入:
10
5 1 9 8 2 6 3 4 7 0
输出:
S1: 0 5 1 9 2
S2: 3 4 6 7 8
|S1 - S2| = 11
4. 算法时间、空间复杂度分析
4.1 空间复杂度分析
本题采用与快速排序类似的算法,无需增加额外空间,因此空间复杂度为 O(1).
4.2 时间复杂度
4.2.1 精确递推式
记 C(n)为问题规模为 n 的平均比较次数,
其中,n - 1表示当前轮划分的比较次数,每个元素都需与枢纽元素比较,共n - 1次.
其中, 表示本轮枢纽落在1 ~ 时,所有可能子段(即pivot以右的子段)的查找长度之和.
其中, 表示本轮枢纽落在 + 2时,所有可能子段(即pivot以左的子段)的查找长度之和.
其中,0表示本轮枢纽恰好落在 + 1,则无需继续查找.
前面的系数 是对所有可能情况的平均.
4.2.2 直观理解
上述递推公式的求解较为困难,但我们可以参照快速排序平均性能的分析对本题加以理解:
根据《算法导论7.2 快速排序的性能》中的分析,我们可以直观地认为平均情况下,最优划分与最差划分交替出现,
可见,紧跟最差划分后的最优划分使得剩余子段重新回归平均划分的情况,即我们可以认为:最优划分能够吸收最差划分,故直观上认为,平均情况下的划分结果是接近最优划分的.
本题中,最优划分情况下,一次划分便可得到结果,故比较次数只有 n-1,即时间复杂度为O(n),最差划分情况下,每次划分仅排除一个元素(如第一次划分排除最大元素,第二次划分排除最小元素,第三次划分排除次大元素,以此类推),则至多需要 O()的时间复杂度才能找到 A (⌊⌋ + 1),相当于对序列做了排序。由于划分时平均情况接近最优情况,故本题平均时间复杂度为O(n).
🚩 欢迎读者指正!