Skip to content

算法入门:复杂度、排序与二分查找

1. 时间与空间复杂度

在算法竞赛中,时间复杂度和空间复杂度是评估算法性能的关键指标,通常更侧重于时间复杂度。

1.1. 概述

  • 时间复杂度: 描述算法的执行效率(运行时间)。越低越好。
  • 空间复杂度: 描述算法占用的内存量。在算法竞赛中,时间复杂度通常是主要挑战,空间限制一般较宽松,但仍需注意。

1.2. 时间复杂度

1.2.1. 定义与表示法

时间复杂度描述了算法执行时间随输入数据规模 N 变化的趋势。我们使用大 O 表示法 O(f(N)) 来表示。

常见的时间复杂度函数(高中数学函数):

  • O(1)O(1) (常数时间)
  • O(logN)O(\log N) (对数时间;底数通常不重要,所以 O(log2N)O(\log_2 N)O(lnN)O(\ln N) 都简化为 O(logN)O(\log N))
  • O(N)O(\sqrt{N}) (平方根时间)
  • O(N)O(N) (线性时间)
  • O(NlogN)O(N \log N) (线性对数时间)
  • O(N2)O(N^2) (平方时间)
  • O(N3)O(N^3) (立方时间)
  • O(2N)O(2^N) (指数时间)
  • O(N!)O(N!) (阶乘时间)

函数性质:

  • 在算法竞赛中,数据规模 N 通常是非负的(N1N \ge 1),所以我们只考虑函数在第一象限的行为。
  • 在第一象限,上面列出的函数都是递增的,意味着执行时间随 N 的增长而增长。
  • 核心原则:忽略常数因子。 例如,O(2N) 和 O(5N) 都被视为 O(N)。这是因为大 O 表示法代表增长趋势,常数因子不影响渐近行为。
  • O(1) 的特殊性: 当操作次数是常数时(例如 A+B),我们用 O(1) 来表示。

1.2.2. 时间复杂度分析示例

寻找数组中的最小值

  • 问题: 给定一个长度为 N 的数组 A,找到最小值。

  • 逻辑: 遍历数组一次,维护当前的最小值。

  • 数据范围: N200,000N \le 200,000

  • 伪代码结构:

    const int MAXN = 200000 + 5; // 常见做法,分配稍大的数组以防越界
    int a[MAXN]; // 或使用 vector<int> a;
    int n;
    // cin >> n; // 读入 N
    // 循环读入 a[1]...a[N] (算法竞赛推荐1-based索引)
    
    int min_val = 2e9; // 初始化为正无穷 (2 * 10^9)
    for (int i = 1; i <= n; ++i) { // 循环 N 次
        // min_val = min(min_val, a[i]); // 每次操作是常数时间
        if (a[i] < min_val) {
            min_val = a[i];
        }
    }
    // cout << min_val;
  • 复杂度分析: 循环执行 N 次。循环内部的操作(比较、赋值、增量)都是常数时间。总操作数约为 C×NC \times N。根据忽略常数的原则,时间复杂度为 O(N)O(N)

最大公约数 (GCD) - 欧几里得算法

  • 问题: 找到两个正整数 X 和 Y 的最大公约数。

  • 算法原理: 基于 GCD(X, Y) = GCD(Y, X \pmod Y),以及 GCD(X,0)=XGCD(X, 0) = X

  • 示例代码 (递归):

    int gcd(int x, int y) {
        if (y == 0) {
            return x;
        }
        return gcd(y, x % y); // 递归调用
    }
  • 复杂度分析: 在每一步递归中,Y 都在减小,并且保证至少会减半(与斐波那契数列的逆向相关)。因此,该算法的时间复杂度为 O(logN)O(\log N),其中 N 是 X 和 Y 中较大的那个数。这里的对数底数不重要,因为 logaN=logbNlogba\log_a N = \frac{\log_b N}{\log_b a},意味着不同底数仅相差一个常数因子。

一个特殊的嵌套 For 循环

  • 问题: 计算以下嵌套循环的执行次数。

  • 代码示例:

    int n; // 假设 n 已经输入
    long long c = 0; // 计数器
    for (int i = 1; i <= n; ++i) {
        for (int j = i; j <= n; j += i) {
            c++; // 每次执行时增加计数器
        }
    }
    // cout << c; // 输出最终计数
  • 复杂度分析:

    • 外层循环变量 i 从 1 到 N。
    • 内层循环变量 ji 开始,每次增加 i,直到超过 N
    • i=1 时,j 取 1, 2, ..., N,执行 N/1 = N 次。
    • i=2 时,j 取 2, 4, ..., N' (不超过N),执行 N/2 次。
    • i=3 时,j 取 3, 6, ..., N'',执行 N/3 次。
    • ...
    • i=N 时,j 取 N,执行 N/N = 1 次。
    • 总执行次数为 N/1 + N/2 + N/3 + \dots + N/N = N \times (1 + 1/2 + 1/3 + \dots + 1/N)。
    • 括号中的部分是调和级数,其和约等于 lnN\ln N (自然对数)。
    • 因此,总执行次数约为 NlnNN \ln N。时间复杂度为 O(NlogN)O(N \log N)

1.3. 空间复杂度

空间复杂度衡量算法在执行过程中占用的临时存储空间。

  • 单个变量: O(1)O(1) (常数空间)。例如 int x;
  • 长度为 N 的数组: O(N)O(N) (线性空间)。例如 int arr[N];
  • N 行 M 列的二维数组: O(N×M)O(N \times M)

通常,算法竞赛中的空间限制是足够的,除非题目明确要求空间优化。

2. 冒泡排序 (Bubble Sort)

冒泡排序是最基础、最直观的排序算法之一。虽然它的效率不高,但理解冒泡排序有助于掌握排序算法的基本思想。

核心思想

冒泡排序的工作原理是:

  1. 重复遍历要排序的数组
  2. 比较相邻元素,如果它们的顺序错误就交换它们
  3. 重复这个过程,直到没有需要交换的元素,说明数组已经有序

较大的元素会像"气泡"一样逐渐"浮"到数组的末尾,这也是"冒泡排序"名称的由来。

代码实现

基础版本:

cpp
// 对数组 a[1...n] 进行冒泡排序
void bubble_sort(int a[], int n) {
    // 外层循环:需要 n-1 轮
    for (int i = 1; i < n; i++) {
        // 内层循环:每轮比较相邻元素
        for (int j = 1; j <= n - i; j++) {
            // 如果前面的元素大于后面的,交换它们
            if (a[j] > a[j + 1]) {
                swap(a[j], a[j + 1]);
            }
        }
    }
}

优化版本(提前终止):

cpp
// 优化版冒泡排序
void bubble_sort_optimized(int a[], int n) {
    for (int i = 1; i < n; i++) {
        bool swapped = false; // 标记本轮是否发生交换
        
        for (int j = 1; j <= n - i; j++) {
            if (a[j] > a[j + 1]) {
                swap(a[j], a[j + 1]);
                swapped = true;
            }
        }
        
        // 如果本轮没有发生交换,说明已经有序,提前退出
        if (!swapped) break;
    }
}

算法步骤详解

以数组 [5, 3, 8, 4, 2] 为例,展示冒泡排序的执行过程:

第 1 轮:(从左到右比较相邻元素)

  • 比较 5 和 3:5 > 3,交换 → [3, 5, 8, 4, 2]
  • 比较 5 和 8:5 < 8,不交换 → [3, 5, 8, 4, 2]
  • 比较 8 和 4:8 > 4,交换 → [3, 5, 4, 8, 2]
  • 比较 8 和 2:8 > 2,交换 → [3, 5, 4, 2, 8] ✓(最大值 8 就位)

第 2 轮:

  • 比较 3 和 5:3 < 5,不交换 → [3, 5, 4, 2, 8]
  • 比较 5 和 4:5 > 4,交换 → [3, 4, 5, 2, 8]
  • 比较 5 和 2:5 > 2,交换 → [3, 4, 2, 5, 8] ✓(次大值 5 就位)

第 3 轮:

  • 比较 3 和 4:3 < 4,不交换 → [3, 4, 2, 5, 8]
  • 比较 4 和 2:4 > 2,交换 → [3, 2, 4, 5, 8]

第 4 轮:

  • 比较 3 和 2:3 > 2,交换 → [2, 3, 4, 5, 8] ✓(排序完成)

复杂度分析

  • 时间复杂度:
    • 平均:O(n2)O(n^2) - 需要约 n2/2n^2/2 次比较
    • 最坏:O(n2)O(n^2) - 数组完全逆序时
    • 最好:O(n)O(n) - 数组已有序(优化版本)
  • 空间复杂度: O(1)O(1) - 只需要常数级的额外空间
  • 稳定性: 稳定 - 相等元素的相对位置不会改变

为什么不推荐冒泡排序?

虽然冒泡排序简单易懂,但在实际应用中几乎不使用,原因如下:

  1. 时间复杂度太高 - O(n2)O(n^2) 的复杂度在大数据量时效率极低
  2. 实际性能差 - 即使是同为 O(n2)O(n^2) 的算法,冒泡排序的常数因子也较大
  3. 有更好的选择 - 快速排序、归并排序等 O(nlogn)O(n \log n) 算法效率更高

但是: 理解冒泡排序有助于理解排序的基本思想和算法分析方法,在学习阶段仍有重要价值。

3. 快速排序 (Quick Sort)

快速排序是一种基于"分治"(Divide and Conquer)思想的高效排序算法。

核心思想

  1. 分(Partition):
    • 从数组中选取一个元素作为“基准值”(Pivot)。
    • 重新排列数组,将所有小于基准值的元素移动到基准值的左侧,所有大于基准值的元素移动到基准值的右侧。
  2. 治(Conquer):
    • 递归地对基准值左侧的子数组和右侧的子数组进行快速排序。
  3. 合(Combine):
    • 由于是原地排序,当子数组排序完成后,整个数组即完成排序,无需合并。

代码模板

c++
// a[] 是全局数组
void quick_sort(int l, int r) {
    if (l >= r) return; // 递归出口:区间内没有元素或只有一个元素

    // 1. 选取基准值 和 初始化指针
    int x = a[l + r >> 1], i = l - 1, j = r + 1;// a[l + r >> 1]是位运算,二进制值删去最后一位,相当于处以2,位运算符优先级比加减小,所以 l+r 不需要加括号

    // 2. 核心分区循环
    while (i < j) {
        // 3. 移动左指针
        while (a[++i] < x);
        // 4. 移动右指针
        while (a[--j] > x);
        
        // 5. 交换
        if (i < j) swap(a[i], a[j]);
    }

    // 6. 递归处理子区间
    quick_sort(l, j);
    quick_sort(j + 1, r);
}

步骤详解

  1. 基准值与指针 (第 5 行)
    • if (l >= r) return;:这是递归的终止条件。如果左边界 l 已经等于或超过了右边界 r,说明这个区间最多只有一个元素,自然是有序的,直接返回。
    • int x = a[l + r >> 1]:选取区间的中间位置的元素作为基准值 xl + r >> 1(l + r) / 2 的位运算写法,效率更高。
    • i = l - 1, j = r + 1:这是此模板的精髓所在ij 指针分别从待排序区间的外侧开始。i 从左边界-1 开始,j 从右边界+1 开始。
  2. 分区循环 (第 8 行)
    • while (i < j):当左指针 i 仍然在右指针 j 的左侧时,继续分区。当 i >= j 时,分区结束。
  3. 移动左指针 (第 10 行)
    • while (a[++i] < x);先将 i 右移一位(++i,然后比较 a[i]x。只要 a[i] 小于基准值 x,就继续右移。
    • 循环结束时,i 会停在第一个大于等于 x 的元素的位置上。
  4. 移动右指针 (第 12 行)
    • while (a[--j] > x);先将 j 左移一位(--j,然后比较 a[j]x。只要 a[j] 大于基准值 x,就继续左移。
    • 循环结束时,j 会停在第一个小于等于 x 的元素的位置上。
  5. 交换 (第 15 行)
    • if (i < j) swap(a[i], a[j]);
    • 此时,a[i] 是左侧找到的 "不该在左侧" 的元素(x\ge x),a[j] 是右侧找到的 "不该在右侧" 的元素(x\le x)。
    • 如果 i < j,说明 i 还在 j 的左边,两者尚未相遇,所以我们交换它们,将它们放到正确的分区。
    • 如果 i >= j,说明两个指针已经相遇或交错,分区过程结束,跳出 while (i < j) 循环。
  6. 递归 (第 19-20 行)
    • quick_sort(l, j);
    • quick_sort(j + 1, r);
    • 为什么是 jj+1 这是此模板的第二个精髓
    • while (i < j) 循环结束时,j 左侧(包括 j)的所有元素都小于等于基准值 xj 右侧的所有元素都大于等于基准值 x
    • 因此,我们将数组分为 [l, j][j + 1, r] 两个子区间,分别进行递归排序。
    • 思考: 为什么不用 i 来划分?因为当循环结束时,ij 可能重合(i == j),也可能交错(i == j + 1)。但无论哪种情况,j 始终是左侧分区的“分界点”。使用 [l, j][j+1, r] 来划分区间,可以完美覆盖所有情况,不多也不少。

复杂度

  • 时间复杂度:
    • 平均:O(nlogn)O(n \log n)
    • 最坏:O(n2)O(n^2) (当数组已经有序或接近有序,且基准值总选到最大/最小值时)
  • 空间复杂度: O(logn)O(\log n) (递归栈的深度)
  • 稳定性: 不稳定

4. 归并排序 (Merge Sort)

归并排序是另一种基于"分治"思想的排序算法,它以稳定、高效著称。

核心思想

  1. 分(Divide): 不断将当前数组对半切分,直到每个子数组只剩一个元素(天然有序)。
  2. 治(Conquer): 这一步在归并排序中体現在“合”的阶段。
  3. 合(Merge): 将两个已经有序的子数组,合并成一个新的、更大的有序数组。

代码模板解析

// a[] 和 N 是全局的
void merge_sort(int l, int r) {
    if (l >= r) return; // 递归出口

    // 1. 临时数组,用于合并
    int temp[N]; 
    
    // 2. 分:计算中点并递归
    int mid = l + r >> 1;
    merge_sort(l, mid);
    merge_sort(mid + 1, r);

    // 3. 合:合并两个有序子数组 [l, mid] 和 [mid+1, r]
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r) {
        if (a[i] < a[j]) temp[k++] = a[i++];
        else temp[k++] = a[j++];
    }

    // 4. 处理剩余元素
    while (i <= mid) temp[k++] = a[i++];
    while (j <= r) temp[k++] = a[j++];

    // 5. 将排好序的 temp 数组复制回原数组 a
    for (int i = l, j = 0; i <= r; i++, j++) a[i] = temp[j];
}

步骤详解

  1. 临时数组 (第 5 行)
    • int temp[N];:归并排序的核心代价在于需要一个额外的 O(n)O(n) 空间。temp 数组用于在“合并”阶段临时存放排好序的元素。
    • 注意: 你给的模板在递归函数内部声明了 temp。这意味着每次递归调用都会声明一个 temp 数组,这在空间上是低效的(虽然功能上没错)。更优化的写法是将 temp 作为全局变量或在 merge_sort 之外声明,通过参数传入。但我们这里只解析你给的模板。
  2. 分 (第 8-10 行)
    • int mid = l + r >> 1;:计算中点。
    • merge_sort(l, mid), merge_sort(mid + 1, r);:递归地对左右两个子区间进行排序。当这两行代码执行完毕后,我们可以保证 a[l...mid]a[mid+1...r] 两个子数组各自内部已经是有序的了。
  3. 合 (第 13-17 行)
    • k = 0, i = l, j = mid + 1;i 是左半边有序数组的指针,j 是右半边有序数组的指针,ktemp 数组的指针。
    • while (i <= mid && j <= r):当两个子数组都还有元素时,进行比较。
    • if (a[i] < a[j])...:将 a[i]a[j] 中较小的那个元素放入 temp 数组,并移动相应的指针。
  4. 处理剩余 (第 20-21 行)
    • 上面的 while 循环结束后,ij 中必定有一个指针已经越界(即它所指向的子数组已经全部存入 temp)。
    • 这两个 while 循环的作用是,将另一个未越界的子数组中剩余的元素(它们本身已经有序且都大于已存入 temp 的元素)全部复制到 temp 数组的末尾。
  5. 复制回原数组 (第 24 行)
    • for (int i = l, j = 0; i <= r; i++, j++) a[i] = temp[j];
    • 这是至关重要的一步。此时 temp 数组的 [0...k-1](即 temp[0...r-l])中存放的是 a[l...r] 区间合并排序后的结果。
    • 这个循环将 temp 中的元素按顺序复制回原数组 a[l...r] 位置。注意 il 开始,j0 开始。

复杂度

  • 时间复杂度: O(nlogn)O(n \log n) (无论最好最坏,都非常稳定)
  • 空间复杂度: O(n)O(n) (需要 temp 数组)
  • 稳定性: 稳定(在 a[i] < a[j] 的判断中,如果不加等号,可以保证相等元素的相对顺序不变)

(〃∀〃)

恭喜!你刚刚学完了两种最核心的 O(nlogn)O(n \log n) 排序算法。

然而,在 99% 的实际开发和算法竞赛中...

忘掉它们(bushi),然后无脑用 C++ 的 std::sort 就完事了!

它更快(经过高度优化)、更安全(规避了最坏情况)、更省心(sort(a, a+n); 一行搞定)。

(严肃脸:当然,归并排序的"稳定"特性和"分治"思想本身还是需要牢记的!面试要考!)

二分查找(或称折半查找)是一种在有序数组中查找某一特定元素的搜索算法。它的核心思想是不断地将搜索区间减半。

下面有两个模板,它们分别用于解决两种最常见的二分问题:

  1. 模板1: 在一个区间内找到满足某个条件的最小(最左侧)的值。
  2. 模板2: 在一个区间内找到满足某个条件的最大(最右侧)的值。

模板 1:查找左边界

// 检查 q[mid] 是否 "大于等于 x"
// 区间 [l, r] 被划分为 [l, mid] 和 [mid+1, r]
// check 函数是判断要找的值是否符合条件
while (l < r) {
    int mid = l + r >> 1; // (l+r)/2,向下取整
    if (check(q[mid])) r = mid; // check为true,答案可能在[l, mid]
    else l = mid + 1; // check为false,答案一定在[mid+1, r]
}
// 循环结束时 l == r,即为答案

用途: 查找第一个满足 check()true 的位置。

  • 例如:在一个升序数组中,找到第一个 x\ge x 的数。

流程解析:

  1. int mid = l + r >> 1;mid 向下取整。
  2. if (check(q[mid])) r = mid;
    • 如果 q[mid] 满足条件(例如 q[mid] >= x),说明 mid 有可能是我们要找的那个 "最左侧" 的答案,或者答案在 mid 的左边。
    • 我们不能排除 mid,所以我们将搜索区间的右边界缩小为 r = mid。新的搜索区间是 [l, mid]
  3. else l = mid + 1;
    • 如果 q[mid] 不满足条件(例如 q[mid] < x),说明 mid 一定不是答案,并且 mid 左侧的所有元素也一定不是答案。
    • 所以我们将搜索区间的左边界扩大为 l = mid + 1。新的搜索区间是 [mid + 1, r]

为什么不会死循环?

  • lr 只差 1 时,即 l = r - 1
  • 此时 mid = (l + l + 1) >> 1 = l
  • if (check(q[l]))r = l,循环结束。
  • elsel = l + 1l 变成 r,循环结束。
  • 两种情况都能正常退出。

模板 2:查找右边界

// 检查 q[mid] 是否 "小于等于 x"
// 区间 [l, r] 被划分为 [l, mid-1] 和 [mid, r]
// check 函数是判断要找的值是否符合条件
while (l < r) {
    int mid = l + r + 1 >> 1; // (l+r+1)/2,向上取整
    if (check(q[mid])) l = mid; // check为true,答案可能在[mid, r]
    else r = mid - 1; // check为false,答案一定在[l, mid-1]
}
// 循环结束时 l == r,即为答案

用途: 查找最后一个满足 check()true 的位置。

  • 例如:在一个升序数组中,找到最后一个 x\le x 的数。

流程解析:

  1. int mid = l + r + 1 >> 1;这是此模板的精髓! mid 向上取整
  2. if (check(q[mid])) l = mid;
    • 如果 q[mid] 满足条件(例如 q[mid] <= x),说明 mid 有可能是我们要找的那个 "最右侧" 的答案,或者答案在 mid 的右边。
    • 我们不能排除 mid,所以我们将搜索区间的左边界扩大为 l = mid。新的搜索区间是 [mid, r]
  3. else r = mid - 1;
    • 如果 q[mid] 不满足条件(例如 q[mid] > x),说明 mid 一定不是答案,并且 mid 右侧的所有元素也一定不是答案。
    • 所以我们将搜索区间的右边界缩小为 r = mid - 1。新的搜索区间是 [l, mid - 1]

为什么必须 + 1?(防止死循环)

  • 假设我们不加 + 1,即 mid = l + r >> 1(向下取整)。
  • 考虑当 lr 只差 1 时,即 l = r - 1
  • 此时 mid = (l + l + 1) >> 1 = l
  • if (check(q[l]))l = mid = llr 的值都没有改变
  • elser = l - 1,循环会结束。
  • 问题在于: 如果 check(q[l]) 总是为 truel 将永远等于 midlr 的值都不会变,while (l < r)无限循环
  • 解决方案:
  • 我们使用 mid = l + r + 1 >> 1(向上取整)。
  • l = r - 1 时,mid = (l + l + 1 + 1) >> 1 = (2l + 2) >> 1 = l + 1 = r
  • if (check(q[r]))l = mid = rl == r,循环结束。
  • elser = mid - 1 = r - 1 = ll == r,循环结束。
  • 两种情况都能正常退出。

总结 (二分):

  • 当你的更新逻辑是 l = mid 时,mid 必须向上取整+ 1)。
  • 当你的更新逻辑是 r = mid 时,mid 必须向下取整(不 + 1)。

6. 总结对比 (所有算法)

算法核心思想平均时间最坏时间空间稳定性备注
冒泡排序交换O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)稳定简单但低效,仅用于学习
快速排序分治O(nlogn)O(n \log n)O(n2)O(n^2)O(logn)O(\log n)不稳定原地排序,实现精妙
归并排序分治O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n)O(n)稳定时间稳定,但需额外空间
二分查找减治O(logn)O(\log n)O(logn)O(\log n)O(1)O(1)N/A前提:数组必须有序
std::sort混合排序O(nlogn)O(n \log n)!O(nlogn)O(n \log n)O(logn)O(\log n)不稳定(推荐) C++标准库
std::stable_sort归并排序O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n)O(n)稳定C++标准库

7. 例题

1. 排序

P1923 【深基9.例4】求第 k 小的数

题目描述

输入 nn1n<50000001 \le n < 5000000nn 为奇数)个数字 aia_i1ai<1091 \le a_i < {10}^9),输出这些数字的第 kk 小的数。最小的数是第 00 小。

请尽量不要使用 nth_element 来写本题,因为本题的重点在于练习分治算法。

输入格式

第一行有两个整数,分别表示 nnkk

第二行有 nn 个整数,第 ii 个数表示 aia_i

输出格式

一个整数,表示第 kk 小的数。

输入输出样例 #1

输入 #1

5 1
4 3 2 1 5

输出 #1

2
c++
#include<bits/stdc++.h>
using namespace std;
int miku[5000000];
void fufu(int a,int b){
    int mid=miku[(a+b)/2],i=a,j=b;
    while (i<=j){
        while(miku[i]<mid) i++;
        while(miku[j]>mid) j--;
        if (i<=j){
            swap(miku[i],miku[j]);
            i++;
            j--;
        }
    }
    if (i<b)fufu(i,b);
    if (j>a)fufu(a,j);
}
int main(){
    int n,k;
    scanf("%d %d",&n,&k);
    for (int i=0;i<n;i++){
        scanf("%d",&miku[i]);
    }
    fufu(0,n-1);
    printf("%d",miku[k]);
    return 0;
}

2. 二分

P1024 [NOIP 2001 提高组] 一元三次方程求解

题目描述

有形如:ax3+bx2+cx+d=0a x^3 + b x^2 + c x + d = 0 这样的一个一元三次方程。给出该方程中各项的系数(a,b,c,da,b,c,d 均为实数),并约定该方程存在三个不同实根(根的范围在 100-100100100 之间),且根与根之差的绝对值 1\ge 1。要求由小到大依次在同一行输出这三个实根(根与根之间留有空格),并精确到小数点后 22 位。

提示:记方程 f(x)=0f(x) = 0,若存在 22 个数 x1x_1x2x_2,且 x1<x2x_1 < x_2f(x1)×f(x2)<0f(x_1) \times f(x_2) < 0,则在 (x1,x2)(x_1, x_2) 之间一定有一个根。

输入格式

一行,44 个实数 a,b,c,da, b, c, d

输出格式

一行,33 个实根,从小到大输出,并精确到小数点后 22 位。

输入输出样例 #1

输入 #1

1 -5 -4 20

输出 #1

-2.00 2.00 5.00

说明/提示

【题目来源】

NOIP 2001 提高组第一题

c++
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
using namespace std;
double a,b,c,d;
int sum=0;
double check(double x){
    double s=a*x*x*x+b*x*x+c*x+d;
    return s;
}
int main(){
    cin>>a>>b>>c>>d;
    for(double i=-100.0;i<100.0;i+=0.001){
        if(check(i)-0<1e-4&&0-check(i)<1e-4){
            printf("%.2lf ",i);
            sum++;
            if(sum==3)break;
        }
    }
    return 0;
}

P2440 木材加工

题目背景

要保护环境。

题目描述

木材厂有 nn 根原木,现在想把这些木头切割成 kk 段长度ll 的小段木头(木头有可能有剩余)。

当然,我们希望得到的小段木头越长越好,请求出 ll 的最大值。

木头长度的单位是 cm\text{cm},原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。

例如有两根原木长度分别为 11112121,要求切割成等长的 66 段,很明显能切割出来的小段木头长度最长为 55

输入格式

第一行是两个正整数 n,kn,k,分别表示原木的数量,需要得到的小段的数量。

接下来 nn 行,每行一个正整数 LiL_i,表示一根原木的长度。

输出格式

仅一行,即 ll 的最大值。

如果连 1cm\text{1cm} 长的小段都切不出来,输出 0

输入输出样例 #1

输入 #1

3 7
232
124
456

输出 #1

114

说明/提示

数据规模与约定

对于 100%100\% 的数据,有 1n1051\le n\le 10^51k1081\le k\le 10^81Li108(i[1,n])1\le L_i\le 10^8(i\in[1,n])

c++
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e8+10;
int miku[N];
int n,k;
bool check(int x){
    int s=0;
    for(int i=0;i<n;i++){
        s+=miku[i]/x;
    }
    if(s>=k)return true;
    return false;
}
int main(){
    cin>>n>>k;
    for(int i=0;i<n;i++){
        cin>>miku[i];
    }
    sort(miku,miku+n);
    int l=0,r=miku[n-1];
    while(l<r){
        int mid=l+r+1>>1;
        if(!check(mid))r=mid-1;
        else l=mid;
    }
    cout<<l;
    return 0;
}