洛谷P1883 【模板】三分 | 函数

原题:P1883 【模板】三分 | 函数

题面:

P1883 【模板】三分 | 函数

题目描述

给定 n n n 个二次函数 f 1 ( x ) , f 2 ( x ) , … , f n ( x ) f_1(x),f_2(x),\dots,f_n(x) f1(x),f2(x),,fn(x)(均形如 a x 2 + b x + c ax^2+bx+c ax2+bx+c),设 F ( x ) = max ⁡ { f 1 ( x ) , f 2 ( x ) , . . . , f n ( x ) } F(x)=\max\{f_1(x),f_2(x),...,f_n(x)\} F(x)=max{f1(x),f2(x),...,fn(x)},求 F ( x ) F(x) F(x) 在区间 [ 0 , 1000 ] [0,1000] [0,1000] 上的最小值。

输入格式

输入第一行为正整数 T T T,表示有 T T T 组数据。

每组数据第一行一个正整数 n n n,接着 n n n 行,每行 3 3 3 个整数 a , b , c a,b,c a,b,c,用来表示每个二次函数的 3 3 3 个系数,注意二次函数有可能退化成一次。

输出格式

每组数据输出一行,表示 F ( x ) F(x) F(x) 的在区间 [ 0 , 1000 ] [0,1000] [0,1000] 上的最小值。答案精确到小数点后四位,四舍五入。

输入输出样例 #1

输入 #1

2
1
2 0 0
2
2 0 0
2 -4 2

输出 #1

0.0000
0.5000

说明/提示

对于 50 % 50\% 50% 的数据, n ≤ 100 n\le 100 n100

对于 100 % 100\% 100% 的数据, T < 10 T<10 T<10   n ≤ 1 0 4 \ n\le 10^4  n104 0 ≤ a ≤ 100 0\le a\le 100 0a100 ∣ b ∣ ≤ 5 × 1 0 3 |b| \le 5\times 10^3 b5×103 ∣ c ∣ ≤ 5 × 1 0 3 |c| \le 5\times 10^3 c5×103

S o l u t i o n Solution Solution

首先为了解决这个问题,我们需要引入一个定义:单峰函数。

定义

设有一定义在实数集合 D D D 上的实值函数 f ( x ) f(x) f(x) ,若 ∃ x 0 ∈ D \exist x_0 \in D x0D ,使得 ∀ x 1 , x 2 ∈ D \forall x_1,x_2 \in D x1,x2D ,若 x 1 < x 2 < x 0 x_1<x_2<x_0 x1<x2<x0 ,有

f ( x 1 ) ≤ f ( x 2 ) f(x_1) \le f(x_2) f(x1)f(x2)

x 0 > x 1 > x 2 x_0>x_1>x_2 x0>x1>x2 ,有

f ( x 1 ) ≥ f ( x 2 ) f(x_1) \ge f(x_2) f(x1)f(x2)

则称 f ( x ) f(x) f(x) 为定义在 D D D 上的单峰函数, x 0 x_0 x0 称为 f ( x ) f(x) f(x) D D D 上的峰值点。

三分法

有的时候,我们需要求解一个单峰函数的峰值点,则此时我们引入三分法来解决这个问题。

什么是三分法?回顾二分法的定义,我们将整个区间分成两个部分,然后通过中值的性质来不断缩小区间,进而迫近得到答案或近似解。

那么三分法也是一样的道理,我们通过将整个区间分成三个部分,具体的,设有当前区间 [ l , r ] [l,r] [l,r] m 1 = l + r − l 3 , m 2 = r − r − l 3 m_1=l+ \dfrac {r-l}{3},m_2=r-\dfrac {r-l}{3} m1=l+3rl,m2=r3rl ,分别计算 f ( m 1 ) , f ( m 2 ) f(m_1),f(m_2) f(m1),f(m2)

若有 f ( m 1 ) < f ( m 2 ) f(m_1) < f(m_2) f(m1)<f(m2) ,则回顾单峰函数的定义,其峰值点必然在 [ m 1 , r ] [m_1,r] [m1,r] 中,于是将区间缩小为 [ m 1 , r ] [m_1,r] [m1,r]

同样的,若有 f ( m 1 ) > f ( m 2 ) f(m_1)>f(m_2) f(m1)>f(m2) ,其峰值点必然在 [ l , m 2 ] [l,m_2] [l,m2] 中,于是将区间缩小为 [ l , m 2 ] [l,m_2] [l,m2]

综上,我们通过这样反复缩小,最终可以得到一个较小的范围,于是可以通过暴力枚举或直接估计误差去近似解得到答案。

应用

那么讲了那么多,本题怎么使用三分法求解呢?你可能会说,这要求的不是最小值吗,单峰函数不是最大值吗,但其实是一样的,本题这个函数是单谷函数,将单峰函数的定义稍作变形,即由极大值点变为极小值点即可。

我们来证明本题的函数为单谷函数,使用数学归纳法。

首先本题函数可表示为:

∑ i = 1 n m a x ( f i ( x ) ) \sum_{i=1}^nmax(f_i(x)) i=1nmax(fi(x))

对于 n = 1 n=1 n=1 的情况:

a ≠ 0 a \not= 0 a=0 ,则给出函数的形式是一个二次函数,显然其有极小值点 x 0 = − b a x_0= - \dfrac {b}{a} x0=ab ,在任意区间 D D D ,其有且仅有一个极小值。

a = 0 a=0 a=0 则为一个一次函数,其在 R \mathbb {R} R 上单调递增或单调递减,显然其在任意区间 D D D 上也仅有一个极小值。

故此时 ∑ i = 1 n m a x ( f i ( x ) ) = f 1 ( x ) \sum_{i=1}^{n}max(f_i(x))=f_1(x) i=1nmax(fi(x))=f1(x) ,其为单谷函数,即 n = 1 n=1 n=1 时命题成立。

下证对于 ∑ i = 1 k m a x ( f i ( x ) ) \sum_{i=1}^{k}max(f_i(x)) i=1kmax(fi(x)) 为单谷函数成立时, ∑ i = 1 k + 1 m a x ( f i ( x ) ) \sum_{i=1}^{k+1}max(f_i(x)) i=1k+1max(fi(x)) 也为单谷函数。

F ( x ) = ∑ i = 1 k m a x ( f i ( x ) ) F(x)=\sum_{i=1}^{k}max(f_i(x)) F(x)=i=1kmax(fi(x)) ,则 ∑ i = 1 k + 1 m a x ( f i ( x ) ) = m a x ( F ( x ) , f k + 1 ( x ) ) \sum_{i=1}^{k+1}max(f_i(x))=max(F(x),f_{k+1}(x)) i=1k+1max(fi(x))=max(F(x),fk+1(x))

即证明对于两个单谷函数 f ( x ) , g ( x ) f(x),g(x) f(x),g(x) m a x ( f ( x ) , g ( x ) ) max(f(x),g(x)) max(f(x),g(x)) 也为单谷函数。

我们先证明一个引理:设 f , g f,g f,g 在闭区间 D D D 上均为单谷函数,取任意极小值点 x 1 ( 对 f ) x_1(对f) x1(f) x 2 ( 对 g ) x_2(对g) x2(g) ,且假设 x 1 ≤ x 2 x_1 \le x_2 x1x2 ,令

A = { x ∈ D : f ( x ) ≤ g ( x ) } , B = { x ∈ D : f ( x ) ≥ g ( x ) } A=\{x \in D:f(x) \le g(x)\}, B=\{x \in D:f(x) \ge g(x)\} A={xD:f(x)g(x)},B={xD:f(x)g(x)}

A , B A,B A,B 都是闭区间(可能退化为点),并且 D = A ∪ B D=A \cup B D=AB ,从而 H ( x ) = m a x ( f ( x ) , g ( x ) ) H(x)=max(f(x),g(x)) H(x)=max(f(x),g(x)) D D D 左端由某一函数主导,右端由某一函数主导,中间至多切换一次主导者,故 H ( x ) H(x) H(x) 为单谷函数。

下面我们来证明这个引理。

证明:

先说明为什么 A A A 必为区间:

由于 x 1 ≤ x 2 x_1 \le x_2 x1x2 ,将区间 D D D 分为三段:

·左段 L = D ∩ ( − ∞ , x 1 ] L=D \cap (-\infty ,x_1] L=D(,x1]

·中段 M = D ∩ [ x 1 , x 2 ] M=D \cap [x_1,x_2] M=D[x1,x2]

·右段 R = D ∩ [ x 2 , + ∞ ) R=D \cap [x_2,+\infty) R=D[x2,+)

在每段上, f , g f,g f,g 的单调性分别为:

·在 L : L: L: f f f 单调不增;同时因为 $x \le x_1 \le x_2 $ ,也有 g g g L L L 上单调不增。

·在 R : R: R: f f f R R R 上单调不减(因为 x ≥ x 1 x \ge x_1 xx1) ,且 g g g R R R 上单调不减(因为 x ≥ x 2 x \ge x_2 xx2)。

·在中段 M : M: M: f f f M M M 的左半段仍可能单调不增直到 x 1 x_1 x1 ,到 x 1 x_1 x1 f f f 变为单调不减; g g g M M M 的右半在 x 2 x_2 x2 前仍单调不增,在 x 2 x_2 x2 后单调不减。总之在 M M M 上,每个函数最多在其极小值点位置改变单调性一次。

断言1

在左段 L L L 因为 f , g f,g f,g 都单调不增,如果存在两个点 u < w ∈ L u<w \in L u<wL 使得 f ( u ) ≤ g ( u ) f(u) \le g(u) f(u)g(u) f ( w ) ≤ g ( w ) f(w) \le g(w) f(w)g(w) ,则对于任意 v v v 满足 u ≤ v ≤ w u \le v \le w uvw ,有

f ( w ) ≤ f ( v ) ≤ f ( u ) , g ( w ) ≤ g ( v ) ≤ g ( u ) f(w) \le f(v) \le f(u), \qquad g(w) \le g(v) \le g(u) f(w)f(v)f(u),g(w)g(v)g(u)

所以从 f ( u ) ≤ g ( u ) f(u) \le g(u) f(u)g(u) f ( w ) ≤ g ( w ) f(w) \le g(w) f(w)g(w) 可得

f ( v ) ≤ m a x { f ( u ) , f ( w ) } ≤ m i n { g ( u ) , g ( w ) } ≤ g ( v ) , f(v) \le max\{f(u),f(w)\} \le min\{g(u),g(w)\} \le g(v), f(v)max{f(u),f(w)}min{g(u),g(w)}g(v),

因此 f ( v ) ≤ g ( v ) f(v) \le g(v) f(v)g(v) ,所以在左段内,若在两个端点都属于 A A A 中间任意点也属于 A A A 。因此 A ∩ L A \cap L AL 是区间。

同理如果在左段两个端点均满足 f ( x ) ≥ g ( x ) f(x) \ge g(x) f(x)g(x) ,则中间也必然满足 f ( x ) ≥ g ( x ) f(x) \ge g(x) f(x)g(x) 。所以在左段 A A A B B B 各自是区间。

断言2

在右段 R R R ,两函数都单调不减,由同理可得 A ∩ R , B ∩ R A \cap R , B \cap R AR,BR 也都是区间。

断言3

在中段 M = [ x 1 , x 2 ] M=[x_1,x_2] M=[x1,x2] ,每个函数至多在对应极小值点处改变单调性。因为 x 1 ≤ x 2 x_1 \le x_2 x1x2 ,中段 M M M 的单调性结构是“先两者都可能非增,接着 f f f 变非减,最后 g g g 变非减”。这限制了 f − g f-g fg 的符号变化次数: 最多发生一次从负到正再回到负的复杂模式被排除 ,从而 A ∩ M A \cap M AM 也是区间。

断言 4

结合前面的断言1-3, A A A L , M , R L,M,R L,M,R 各段内都是区间,且三段相连,因此 A A A 在整个 D D D 上为区间。同理 B B B 也是区间。因为 D = A ∪ B D=A \cup B D=AB ,两个区间的并覆盖 D D D 且交集非空,主导函数至多发生一次切换。

从而引理得证。

归纳结论:

则显然有 F k + 1 ( x ) = m a x ( F k ( x ) , f k + 1 ( x ) ) F_{k+1}(x)=max(F_k(x),f_{k+1}(x)) Fk+1(x)=max(Fk(x),fk+1(x)) 仍为单谷函数,所以对任意 n n n 命题成立。

因此我们可以用三分法来解决本题。

C o d i n g Coding Coding

#include <iostream>
#include <cstring>
#include <iomanip>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;

#define ll long long
#define ull unsigned long long
#define debug(x) cout << #x << "=" << x << "\n";

int t;
int n;
const int maxn = 1e4 + 10;
const double eps = 1e-8;
struct func
{
    double a, b, c;
    double min_val, max_val;
} f[maxn];

inline double calc(double a, double b, double c, double x)
{
    return a * x * x + b * x + c;
}

double maxx(double a, double b)
{
    return (a > b ? a : b);
}

double calc_point_max(double x)
{
    double res = -1e9;
    for (int i = 1; i <= n; i++)
        res = maxx(res, calc(f[i].a, f[i].b, f[i].c, x));

    return res;
}

void solve()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> f[i].a >> f[i].b >> f[i].c;

    double l = 0, r = 1000;
    while (r - l >= eps)
    {
        double len = r - l;
        double m1 = l + len / 3, m2 = r - len / 3;
        double res1 = calc_point_max(m1), res2 = calc_point_max(m2);

        if (res1 >= res2)
            l = m1;
        else
            r = m2;
    }

    double ans = calc_point_max((l + r) / 2);
    cout << fixed << setprecision(4) << ans << "\n";
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> t;
    while (t--)
        solve();

    return 0;
}

关于时间复杂度:

每次三分都把区间长度缩小为原来的 2 3 \dfrac {2}{3} 32

假设初始区间长度为 L 0 = r − l L_0=r-l L0=rl

迭代 k k k 次后,区间长度变成:

L k = L 0 × ( 2 3 ) k L_k=L_0 \times (\dfrac {2}{3})^k Lk=L0×(32)k

我们希望最终区间长度 ≤ \le 误差上限 ϵ \epsilon ϵ.

于是有:

L 0 × ( 2 3 ) k ≤ ϵ L_0 \times (\dfrac {2}{3})^k \le \epsilon L0×(32)kϵ

取对数:

k ≥ l o g ( ϵ L 0 ) l o g ( 2 3 ) k \ge \dfrac {log(\frac {\epsilon}{L_0})}{log(\frac {2}{3})} klog(32)log(L0ϵ)

通过数学计算可以推导出在绝大多数情况下,我们使用 100 100 100 次迭代就已经绝对安全。

这里由于我是第一次写所以使用了 e p s eps eps 来估计误差,一开始估计大了直接满江红 ,直接循环 100 100 100 次就好,够快也足够精确。

<think>我们正在处理 P1177 题目,这是一道排序题。题目要求对给定的数组进行快速排序(从小到大)并输出排序后的结果。 根据引用[1]和引用[2]中提供的代码风格,我们可以使用递归的快速排序算法,但需要注意题目数据量,递归深度可能过大,或者最坏情况时间复杂度为O(n^2)可能导致超时。 因此,我们可以考虑使用非递归的快速排序,或者使用归并排序(题目要求任何排序算法,但通常快速排序在平均情况下效率高,但最坏情况需要避免)。然而,题目要求快速排序,所以我们需要实现快速排序。 但是,注意题目中可能包含大量重复元素或者有序数组,这会导致快速排序效率降低。我们可以采用以下优化: 1. 随机选择基准元素(pivot)以避免最坏情况。 2. 使用三数取中法选择基准元素。 3. 对于小数组,使用插入排序。 然而,题目要求的是快速排序,所以我们主要实现快速排序,并加入随机化。 另外,引用[4]提到类型提示,但本题解中不需要使用。 由于题目要求使用快速排序,我们使用递归实现,并加入随机化选择基准。 步骤: 1. 如果数组长度小于等于1,直接返回。 2. 随机选择一个元素作为基准(这里选择随机位置,避免有序数组导致性能下降)。 3. 将数组分为三部分:小于基准、等于基准、大于基准。这样处理重复元素效率更高(三路划分)。 4. 递归地对小于基准和大于基准的部分进行排序。 但是,题目要求输出快速排序后的结果,所以我们按照上述思路编写代码。 然而,题目要求使用Python,且注意输入输出格式: 输入:第一行一个整数n,第二行n个整数(用空格隔开) 输出:一行n个整数(用空格隔开) 由于题目数据量可能较大(n<=100000),使用递归快速排序需要注意递归深度,我们可以使用尾递归优化或者使用迭代方式,但这里我们使用三路划分的递归快速排序,并递归处理较小的子数组,可以避免最坏情况。 但是,为了避免递归深度过大,我们可以使用迭代法(栈)来实现快速排序。不过,题目没有明确要求递归实现,所以我们也可以使用递归,但需要随机化来避免最坏情况。 考虑到引用[2]中提到C++的输入方式,而Python需要整个读取,我们使用sys.stdin来加快输入速度。 具体步骤: 1. 读取n和数组 2. 实现快速排序函数(三路划分,随机基准) 3. 排序并输出 但是,我们也可以使用Python内置的排序函数,但题目要求快速排序,所以必须自己实现。 参考引用[3]中提到的二分查找,但本题是排序,所以不涉及。 因此,我们按照以下代码实现: 注意:由于递归深度可能达到O(n),在n=100000时,递归深度可能超过Python默认的递归深度(通常1000),所以我们需要设置递归深度或者使用非递归方法。 为了避免递归深度问题,我们使用迭代法(非递归)实现快速排序。 迭代法快速排序思路: 1. 使用一个栈来存储需要排序的子数组的起始和结束索引。 2. 当栈非空时,弹出一个子数组进行划分。 3. 划分后,将需要排序的子数组的索引范围压入栈。 划分方法:随机选择基准,然后将数组划分为小于基准、等于基准、大于基准三部分?或者传统的划分(两个部分:小于基准和大于基准)。这里我们使用传统的划分(两个部分)加上随机基准。 迭代快速排序步骤: 初始化:将整个数组的起始索引0和结束索引n-1压入栈。 循环直到栈空: 弹出栈顶元素,得到left, right。 如果left>=right,跳过。 否则,进行划分,得到基准的位置pivot_index。 将左子数组(left, pivot_index-1)和右子数组(pivot_index+1, right)压入栈(注意先压入较小的子数组,或者任意顺序,但为了避免栈深度过大,可以压入较大的子数组先?这里我们按常规,先压入右子数组再压入左子数组?实际上,我们通常先处理左子数组,但栈是后进先出,所以先压入右子数组再压入左子数组,这样先处理左子数组。但为了减少栈深度,我们选择先压入较大的子数组,这样栈中元素最多为O(log n)。所以我们比较两个子数组的大小,将较大的子数组先压入栈,再压入较小的子数组。 但是,我们也可以不比较,直接压入两个子数组,因为栈深度在平均情况下是O(log n),最坏情况(有序数组)下通过随机化避免,所以栈深度不会太大。 划分函数(partition): 随机选择一个基准索引,与最后一个元素交换(或者第一个元素,这里我们选择与最后一个元素交换,然后使用双指针法)。 设置两个指针:i从left-1开始,j从left开始。 遍历j从left到right-1: 如果当前元素小于基准,则i++,并交换arr[i]和arr[j]。 最后将基准(当前在right位置)与i+1位置的元素交换,返回i+1。 但是,这种划分方法(Lomuto划分)在重复元素较多时效率较低,且只能将数组分为两部分。 另一种划分方法:Hoare划分,使用两个指针从两端向中间扫描,但同样在重复元素多时效率不高。因此,我们考虑使用三路划分的迭代快速排序?但迭代的三路划分实现复杂,通常三路划分用于递归。 由于题目要求快速排序,我们使用传统的划分方法(Lomuto)即可,但为了避免重复元素较多的情况,我们可以使用三路划分的迭代版本?但迭代版本的三路划分通常不常见。 因此,我们选择使用递归的三路划分快速排序,并设置递归深度限制?或者使用迭代方法实现两路划分。 权衡后,我们使用迭代的两路划分快速排序(随机基准)来避免递归深度问题。 步骤: 1. 导入random模块,用于随机选择基准。 2. 划分函数:partition(arr, left, right) random_index = random.randint(left, right) arr[random_index], arr[right] = arr[right], arr[random_index] # 将随机选择的基准放到最右边 pivot = arr[right] i = left - 1 for j in range(left, right): if arr[j] < pivot: i += 1 arr[i], arr[j] = arr[j], arr[i] arr[i+1], arr[right] = arr[right], arr[i+1] return i+1 3. 迭代排序: stack = [] stack.append((0, n-1)) while stack: left, right = stack.pop() if left >= right: continue pivot_index = partition(arr, left, right) # 压入两个子数组:注意先压入右子数组再压入左子数组,这样先处理左子数组(因为栈是后进先出) # 先压入左子数组:left, pivot_index-1 # 再压入右子数组:pivot_index+1, right # 这样处理顺序是:先处理右子数组,再处理左子数组(因为栈是后进先出)。但为了平衡,我们也可以先压右再压左,这样先处理左(因为左后被压入,所以先弹出)。 # 我们按照:先压入右子数组,再压入左子数组,这样先处理左子数组(左子数组后进先出)。 stack.append((pivot_index+1, right)) stack.append((left, pivot_index-1)) 但是,这样压栈顺序会导致先处理左子数组(left, pivot_index-1)再处理右子数组(pivot_index+1, right)?实际上,弹出顺序是:先弹出最后压入的,也就是左子数组(left, pivot_index-1)先被处理,然后右子数组(pivot_index+1, right)再被处理。这样没有问题。 4. 注意:当数组已经有序时,随机化基准可以避免最坏情况,使得时间复杂度为O(n log n)(平均情况)。 但是,Lomuto划分在重复元素较多时,仍然会将数组划分为两个不平衡的部分(因为重复元素会被放到基准的右边)。因此,我们可以考虑使用Hoare划分,或者使用三路划分的迭代版本。 然而,迭代的三路划分实现较为复杂,我们这里使用两路划分,因为题目数据范围是整数,可能有重复元素,但两路划分在随机基准下平均性能还是可以接受的。 另外,我们可以使用Hoare划分,它比Lomuto划分效率更高,因为交换次数更少。Hoare划分: 随机选择基准索引,与left交换(或者不交换,直接取arr[left]作为基准,但随机化需要随机选择并与left交换)。 设置两个指针i=left, j=right pivot = arr[left] # 基准值 while True: while i<j and arr[j]>=pivot: j-- while i<j and arr[i]<=pivot: i++ if i>=j: break swap(arr[i], arr[j]) 最后,将基准值(arr[left])与arr[i]交换(注意此时i和j相遇,位置为i,但arr[i]可能小于等于基准值?) 实际上,Hoare划分后,基准值不一定在i位置,而是将数组划分为小于等于基准值和大于等于基准值的两部分,但基准值的位置不确定。因此,我们返回i(或j)作为划分点?实际上,我们返回的是最后相遇的位置,然后递归处理[left, i]和[i+1, right]?或者[left, i-1]和[i, right]?需要根据划分逻辑确定。 由于Hoare划分的边界处理较为复杂,我们这里使用Lomuto划分,虽然交换次数较多,但易于实现。 考虑到题目要求,我们使用Lomuto划分的迭代快速排序。 但是,Lomuto划分在重复元素较多时,会导致划分不平衡(所有重复元素都大于等于基准,会被分到右边,导致左边为空,右边很大)。为了避免这种情况,我们可以使用三路划分的迭代方法,但迭代方法的三路划分较为复杂。 因此,我们改为使用三路划分的递归快速排序,并设置递归深度限制?但递归深度可能太大。 或者,我们使用迭代的三路划分?实现起来代码较长。 权衡后,我们使用两路划分的迭代快速排序,并希望随机基准能够避免最坏情况。 另外,当子数组较小时,我们可以使用插入排序来优化。这里我们暂时不实现。 所以,我们按照以下步骤编写代码: 1. 读取输入:第一行n,第二行n个整数 2. 初始化数组arr 3. 实现partition函数(Lomuto划分,随机选择基准) 4. 使用栈实现迭代快速排序 5. 输出数组(空格分隔) 注意:如果数组已经有序,随机基准可以避免最坏情况O(n^2),但最坏时间复杂度仍然是O(n^2)(虽然概率很低),但平均为O(n log n)。题目数据n<=100000,最坏情况可能超时,所以我们需要进一步优化。 优化:三数取中法(选择左、中、右三个数的中值作为基准)来避免极端情况。或者使用三路划分的迭代版本?三路划分可以将数组分为小于、等于、大于三部分,这样重复元素就不用再排序了。 我们决定使用三路划分的迭代快速排序,但迭代的三路划分通常使用双轴(如DualPivotQuickSort)?这里我们使用单轴的三路划分迭代。 三路划分(一次划分将数组分为三部分)的迭代快速排序: 划分函数partition_3way(arr, left, right)返回两个索引(lt, gt): arr[left...lt-1] < pivot arr[lt...gt] = pivot arr[gt+1...right] > pivot 然后我们只需要对小于和大于的部分进行排序。 迭代时,我们压栈(left, lt-1)和(gt+1, right)。 划分步骤: random_index = random.randint(left, right) pivot = arr[random_index] lt = left # 指向小于pivot的最后一个元素的下一个位置 gt = right # 指向大于pivot的第一个元素的前一个位置 i = left while i <= gt: if arr[i] < pivot: arr[i], arr[lt] = arr[lt], arr[i] lt += 1 i += 1 elif arr[i] > pivot: arr[i], arr[gt] = arr[gt], arr[i] gt -= 1 else: i += 1 这样,划分后,[left, lt-1]是小于pivot,[lt, gt]是等于pivot,[gt+1, right]是大于pivot。 然后,我们将两个子数组(left, lt-1)和(gt+1, right)压入栈。 三路划分在重复元素多时,可以避免重复元素的重复排序,提高效率。 因此,我们使用三路划分的迭代快速排序。 代码步骤: import random def quick_sort_iterative(arr): n = len(arr) stack = [] stack.append((0, n-1)) while stack: left, right = stack.pop() if left >= right: continue # 随机选择基准 random_index = random.randint(left, right) pivot = arr[random_index] # 三路划分 lt = left gt = right i = left # 注意:在循环过程中,i从left到gt,而gt会变化 while i <= gt: if arr[i] < pivot: arr[i], arr[lt] = arr[lt], arr[i] lt += 1 i += 1 elif arr[i] > pivot: arr[i], arr[gt] = arr[gt], arr[i] gt -= 1 else: i += 1 # 现在,数组分为三部分: [left, lt-1] < pivot, [lt, gt] == pivot, [gt+1, right] > pivot # 将两个子数组压栈:先压右子数组(gt+1, right),再压左子数组(left, lt-1) stack.append((gt+1, right)) stack.append((left, lt-1)) 注意:压栈顺序不影响最终排序结果,因为每个子数组都是独立排序的。 但是,我们需要注意,当子数组长度很小时,可以改用插入排序(例如长度<=10),这里我们省略。 最后,输出数组。 由于题目要求从小到大排序,我们使用上述排序后,数组就是有序的。 输入输出示例: 输入: 5 4 2 4 5 1 输出: 1 2 4 4 5 我们使用上述三路划分迭代快速排序。 但是,注意:三路划分中,我们随机选择了一个基准,然后进行划分。在交换元素时,我们使用双指针(lt, gt)和i。 现在,我们编写完整代码。 注意:使用sys.stdin来加快输入速度,因为n可能达到100000。 代码: import sys import random n = int(sys.stdin.readline().strip()) arr = list(map(int, sys.stdin.readline().split())) # 如果n为0,则输出空行 if n == 0: print("") else: # 使用迭代的三路划分快速排序 stack = [(0, n-1)] while stack: left, right = stack.pop() if left >= right: continue # 随机选择基准 random_index = random.randint(left, right) pivot = arr[random_index] lt = left gt = right i = left # 注意:循环条件 i<=gt while i <= gt: if arr[i] < pivot: arr[i], arr[lt] = arr[lt], arr[i] lt += 1 i += 1 elif arr[i] > pivot: arr[i], arr[gt] = arr[gt], arr[i] gt -= 1 else: i += 1 # 压入子数组:先压右子数组(gt+1, right),再压左子数组(left, lt-1) stack.append((gt+1, right)) stack.append((left, lt-1)) # 输出 print(" ".join(map(str, arr))) 但是,这个代码在压栈时,如果子数组长度大于0,则压入。我们使用栈来模拟递归。 测试:输入[4,2,4,5,1] 初始:arr=[4,2,4,5,1], left=0, right=4 随机选择基准,假设选择index=0(值为4) 划分过程: i=0, arr[0]=4 -> 等于,i=1 i=1, arr[1]=2 -> 小于:交换arr[0]和arr[1] -> [2,4,4,5,1];然后lt=1, i=2 i=2, arr[2]=4 -> 等于,i=3 i=3, arr[3]=5 -> 大于:交换arr[3]和arr[4] -> [2,4,4,1,5];gt=3, i=3(不变,因为交换后arr[3]=1) 然后重新判断i=3:arr[3]=1 -> 小于:交换arr[1]和arr[3] -> [2,1,4,4,5];lt=2, i=4 此时i=4,而gt=3,所以i>gt,循环结束。 划分后:小于部分:arr[0:1] = [2,1] -> 注意,我们交换后,小于部分应该是[0,lt-1]即[0,1]:即2和1,但是2和1并没有排序好,所以需要继续排序。 等于部分:arr[2:3] = [4,4] -> 实际上等于部分是从lt到gt:lt=2, gt=3 -> [4,4] 大于部分:arr[4]=5 然后压入两个子数组: (gt+1, right) -> (4,4) 和 (left, lt-1) -> (0,1) 先处理(0,1):弹出(0,1) 随机选择基准,假设选择0(值为2) 划分:lt=0, gt=1, i=0 i=0: arr[0]=2 -> 等于?但是基准是2,所以等于,i=1 i=1: arr[1]=1 -> 小于:交换arr[0]和arr[1] -> [1,2];然后lt=1, i=2 -> 结束 划分后:小于部分:[0,0] -> [1];等于部分:[1,1] -> [2] 压入(0,0)和(2,1) -> 注意(2,1)是无效的(left>right),所以只处理(0,0)?实际上(0,0)不需要处理,因为长度1。 排序后,这部分变为[1,2] 然后处理(4,4):长度为1,跳过。 最后数组为[1,2,4,4,5] 所以,代码正确。 但是,当数据量很大时,递归深度(栈深度)为O(log n),可以接受。 提交P1177,看是否通过。 注意:P1177题目名称是【模板】快速排序,所以需要自己实现快速排序。 由于三路划分快速排序在重复元素多时效率高,且随机基准避免最坏情况,应该可以AC。 但是,我们也可以使用Python的递归深度设置,但迭代方法没有递归深度问题。 因此,我们使用上述迭代的三路划分快速排序。 完整代码:</think>### P1177 【模板】快速排序 - Python 解法 #### 题目分析 P1177 要求实现快速排序算法,对给定的 $n$ 个整数进行升序排序。关键点: 1. **数据范围**:$n \leq 10^5$,需考虑时间效率 2. **排序要求**:严格 $O(n \log n)$ 平均时间复杂度 3. **特殊场景**:需处理重复元素和有序数组的退化情况 #### 优化策略 1. **随机化基准**:避免有序数组导致 $O(n^2)$ 退化 2. **三路划分**:高效处理重复元素(小于/等于/大于基准) 3. **迭代代替递归**:避免 Python 递归深度限制 4. **小数组插入排序**:提升局部效率(可选) #### Python 代码实现 ```python import sys import random def quick_sort(arr): """迭代式三路划分快速排序""" stack = [(0, len(arr) - 1)] while stack: low, high = stack.pop() if low >= high: continue # 随机选择基准并交换到末尾 rand_idx = random.randint(low, high) arr[high], arr[rand_idx] = arr[rand_idx], arr[high] pivot = arr[high] # 三路划分:lt(小于基准), eq(等于基准), gt(大于基准) lt = low eq = low gt = high while eq <= gt: if arr[eq] < pivot: arr[lt], arr[eq] = arr[eq], arr[lt] lt += 1 eq += 1 elif arr[eq] == pivot: eq += 1 else: arr[eq], arr[gt] = arr[gt], arr[eq] gt -= 1 # 压栈子数组(先处理大区间以平衡栈深度) if (high - gt) > (lt - low): stack.append((gt + 1, high)) stack.append((low, lt - 1)) else: stack.append((low, lt - 1)) stack.append((gt + 1, high)) def main(): data = sys.stdin.read().split() if not data: return n = int(data[0]) arr = list(map(int, data[1:1+n])) quick_sort(arr) print(" ".join(map(str, arr))) if __name__ == "__main__": main() ``` #### 算法特点 1. **时间复杂度**: - 平均 $O(n \log n)$ - 最坏 $O(n^2)$(概率极低,由随机化保证) 2. **空间复杂度**:$O(\log n)$(栈空间) 3. **稳定性**:非稳定排序(交换破坏顺序) 4. **优势**: - 随机化避免退化 - 三路划分高效处理重复值 - 迭代实现无递归深度限制 #### 性能对比 | 方法 | 平均时间 | 最坏时间 | 重复元素处理 | |---------------|----------|----------|--------------| | 朴素快速排序 | $O(n \log n)$ | $O(n^2)$ || | 随机化+三路划分 | $O(n \log n)$ | $O(n^2)$ || | 归并排序 | $O(n \log n)$ | $O(n \log n)$ | 稳定 | > **提示**:若需进一步优化,可添加插入排序(当子数组长度 < 15 时),减少小数组的划分开销。 ### 相关问题 1. 快速排序的最坏时间复杂度如何触发?如何避免? 2. 三路划分算法在处理大量重复元素时为何更高效? 3. 迭代实现快速排序相比递归有哪些优势和劣势? 4. 快速排序在什么场景下会退化成 $O(n^2)$?如何通过概率分析证明随机化优化的有效性? 5. 比较快速排序与归并排序在空间复杂度、稳定性和实际性能上的差异。 [^1]: P1177 官方题目说明 [^2]: 《算法导论》第 7 章 - 快速排序的理论分析 [^3]: 三路划分算法在 Dutch National Flag 问题中的应用 [^4]: Python 迭代实现避免递归深度的工程实践
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值