GESP 客观题评测系统

2025-12-Level-5

2025-12-Level-5

试卷解析总览,可直接查看每题答案与解析。

单选题(每题 2 分)

1 题(单选题

对如下定义的循环单链表,横线处填写()。

// 循环单链表的结点

struct Node {
    int data; // 数据域
    Node* next; // 指针域

    Node(int d) : data(d), next(nullptr) {
    };

    // 创建一个只有一个结点的循环单链表
    Node* createList(int value) {
        Node* head = new Node(value);
        head->next = head;
        return head;
    }

    // 在循环单链表尾部插入新结点
    void insertTail(Node* head, int value) {
        Node* p = head;
        while (p->next != head) {
            p = p->next;
        }
        Node* node = new Node(value);
        node->next = head;
        p->next = node;
    }

    // 遍历并输出循环单链表
    void printList(Node* head) {
        if (head == nullptr) return;
    }

    Node* p = head;
    // 在此处填入代码
    cout << endl;
}
A.
while (p != nullptr){
    cout << p->data << " ";
    p = p->next;
}
B.
while (p->next != nullptr){
    cout << p->data << " ";
    p = p->next;
}
C.
do {
    cout << p->data << " ";
    p = p->next;
} while (p != head);
D.
for (; p; p = p->next) {
    cout << p->data << " ";
}

正确答案C

解析详情

【答案】C

【考点】循环单链表的遍历

【解析】 循环单链表的末尾结点指向头结点。遍历时,如果使用 `while(p != head)`,则头结点在第一次判断时就会失效导致循环无法开始。使用 `do...while` 循环可以先执行一次输出头结点,然后再判断 `p != head` 是否成立,从而正确完成遍历。

【易错点】 直接使用 while 循环会导致循环条件初始就不满足,或者导致无限循环(如果逻辑错误)。

2 题(单选题

区块链技术是比特币的基础。在区块链中,每个区块指向前一个区块,构成链式列表,新区块只能接在链尾,不允许在中间插入或删除。下面代码实现插入区块添加函数,则横线处填写()。

//区块(节点)

struct Block {
    int index; // 区块编号(高度)
    string data; // 区块里保存的数据
    Block* prev; // 指向前一个区块
    Block(int idx, const string& d, Block* p) : index(idx), data(d), prev(p) {
    };
    // 区块链
    struct Blockchain {
        Block* tail;

        // 初始化
        void init() {
            tail = new Block(0, "Genesis Block", nullptr);
        }

        // 插入新区块
        void addBlock(const string& data) {
            // 在此处填入代码
        }

        // 释放内存
        void clear() {
            Block* cur = tail;
            while (cur != nullptr) {
                Block* p = cur->prev;
                delete cur;
                cur = p;
            }
            tail = nullptr;
        }
    }
}
A.
Block* newBlock = new Block(tail->index + 1, data, tail);
tail = newBlock->prev;
B.
Block* newBlock = new Block(tail->index + 1, data, tail);
tail = newBlock;
C.
Block* newBlock = new Block(tail->index + 1, data, tail->prev);
tail = newBlock;
D.
Block* newBlock = new Block(tail->index + 1, data, tail->prev);
tail = newBlock->prev;

正确答案B

解析详情

【答案】B

【考点】链表的尾部插入操作

【解析】 新区块 `newBlock` 的 `index` 应为当前 `tail` 的 `index + 1`,且其 `prev` 指针应指向旧的 `tail`。创建新区块后,需要更新 `tail` 指针,使其指向新创建的这个区块。选项 B 符合 `tail = newBlock` 的更新逻辑。

【易错点】 误将 tail 更新为 newBlock->prev 会导致链表结构断裂或回退。

3 题(单选题

下面关于单链表和双链表的描述中,正确的是()。

struct DNode {
    int data;
    DNode* prev;
    DNode* next;
};

// 在双链表中删除指定节点

void deleteNode(DNode* node) {
    if (node->prev) {
        node->prev->next = node->next;
    }
    if (node->next) {
        node->next->prev = node->prev;
    }
    delete node;
}

struct SNode {
    int data;
    SNode* next;
};

// 在单链表中删除指定节点

void deleteSNode(SNode* head, SNode* node) {
    SNode* prev = head;
    while (prev->next != node) {
        prev = prev->next;
    }
    prev->next = node->next;
    delete node;
}
A.
双链表删除指定节点是O(1),单链表是O(1)
B.
双链表删除指定节点是O(n),单链表是O(1)
C.
双链表删除指定节点是O(1),单链表是O(n)
D.
双链表删除指定节点是O(n),单链表是O(n)

正确答案C

解析详情

【答案】C

【考点】单链表与双链表删除结点的复杂度分析

【解析】 在已知要删除结点指针的情况下,双链表可以直接通过 `prev` 指针找到前驱结点,因此删除操作复杂度为 O(1)。单链表必须从头结点开始遍历以找到前驱结点,因此删除操作复杂度为 O(n)。

【易错点】 容易混淆“已知结点指针”和“查找特定值的结点”两种情况下的复杂度。

4 题(单选题

假设我们有两个数a=38a = 38b=14b = 14,它们对模mm同余,即ab(modm)a \equiv b \pmod{m}。以下哪个值不可能是mm?

A.
3
B.
4
C.
6
D.
9

正确答案D

解析详情

【答案】D

【考点】同余的定义与性质

【解析】 a ≡ b (mod m) 的充要条件是 m 能整除 (a - b)。在本题中,a - b = 38 - 14 = 24。24 可以被 3, 4, 6 整除,但不能被 9 整除。因此 m 不可能是 9。

【易错点】 误以为同余是 a 和 b 分别除以 m 的余数相同(虽然定义一致,但计算差值整除更快捷)。

5 题(单选题

下面代码实现了欧几里得算法。下面有关说法,错误的是()。

int gcd1(int a, int b) {
    return b == 0 ? a : gcd1(b, a % b);
}

int gcd2(int a, int b) {
    while (b != 0) {
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}
A.
gcd1() 实现为递归方式。
B.
gcd2() 实现为迭代方式。
C.
当 a 较大时,gcd1() 实现会多次调用自身,需要较多额外的辅助空间。
D.
当 a 较大时,gcd1() 的实现比 gcd2() 执行效率更高。

正确答案D

解析详情

【答案】D

【考点】递归与迭代的比较、欧几里得算法

【解析】 gcd1 使用递归实现,gcd2 使用迭代实现。虽然两者的时间复杂度相同,但递归实现(gcd1)需要消耗系统栈空间,在大数据量或深递归时效率通常略低于迭代实现。选项 D 称 gcd1 效率更高是错误的。

【易错点】 容易忽略递归带来的栈开销。

6 题(单选题

唯一分解定理描述的内容是()。

A.
任何正整数都可以表示为两个素数的和。
B.
任何大于1的合数都可以唯一分解为有限个质数的乘积。
C.
两个正整数的最大公约数总是等于它们的最小公倍数除以它们的乘积。
D.
所有素数都是奇数。

正确答案B

解析详情

【答案】B

【考点】唯一分解定理(算术基本定理)

【解析】 唯一分解定理指出:任何一个大于 1 的正整数(合数)都可以唯一地分解为有限个质数的乘积,且若不考虑质因数的顺序,这种分解方式是唯一的。选项 B 准确描述了这一内容。

【易错点】 不要将唯一分解定理与哥德巴赫猜想(两个素数之和)混淆。

7 题(单选题

下述代码实现素数表的线性筛法,筛选出所有小于等于 n 的素数,则横线上应填的代码是()。

vector<int> linear_sieve(int n) {
    vector<bool> is_prime(n + 1, true);
    vector<int> primes;

    is_prime[0] = is_prime[1] = 0; // 0 和 1 两个数特殊处理
    for (int i = 2; i <= n; ++i) {
        if (is_prime[i]) {
            primes.push_back(i);
        }
        // 在此处填入代码
        is_prime[i * primes[j]] = 0;
        if (i % primes[j] == 0)
            break;
    }
    return primes;
}
A.
for (int j = 0; j < primes.size() && i * primes[j] <= n; j++)
B.
for (int j = sqrt(n); j <= n && i * primes[j] <= n; j++)
C.
for (int j = 1; j <= sqrt(n); j++)
D.
for (int j = 1; j < n && i * primes[j] <= n; j++)

正确答案A

解析详情

【答案】A

【考点】线性筛法(欧拉筛)

【解析】 线性筛的核心思想是让每个合数只被其最小的质因子筛掉。内层循环需要遍历已经找到的素数表 `primes`,并且保证乘积 `i * primes[j]` 不超过范围 `n`。选项 A 提供了正确的循环边界和条件判断。

【易错点】 忽略 `i * primes[j] <= n` 的边界限制会导致数组越界。

8 题(单选题

下列关于排序的说法,正确的是()。

A.
快速排序是稳定排序
B.
归并排序通常是稳定的
C.
插入排序是不稳定排序
D.
冒泡排序不是原地排序

正确答案B

解析详情

【答案】B

【考点】排序算法的稳定性与空间复杂度

【解析】 归并排序在合并过程中通过判断条件可以保持相同元素的相对位置不变,因此是稳定排序(选项 B 正确)。快速排序和插入排序通常被认为是不稳定排序(虽然插入排序可以实现为稳定,但快排必不稳定)。冒泡排序是原地排序(空间复杂度 O(1))。

【易错点】 混淆稳定排序(相同元素顺序不变)与不稳定排序。

9 题(单选题

下面代码实现了归并排序。下述关于归并排序的说法中,不正确的是()。

void merge(vector<int>& arr, vector<int>& temp, int l, int mid, int r) {
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r) {
        if (arr[i] <= arr[j]) temp[k++] = arr[i++];
        else temp[k++] = arr[j++];
    }
    while (i <= mid) temp[k++] = arr[i++];
    while (j <= r) temp[k++] = arr[j++];
    for (int p = l; p <= r; p++) arr[p] = temp[p];
}

void mergeSort(vector<int>& arr, vector<int>& temp, int l, int r) {
    if (l >= r) return;
    int mid = l + (r - l) / 2;
    mergeSort(arr, temp, l, mid);
    mergeSort(arr, temp, mid + 1, r);
    merge(arr, temp, l, mid, r);
}
A.
归并排序的平均复杂度是O(n log n)。
B.
归并排序需要O(n)的额外空间。
C.
归并排序在最坏情况的时间复杂度是O(n²)。
D.
归并排序适合大规模数据。

正确答案C

解析详情

【答案】C

【考点】归并排序的复杂度与性质

【解析】 归并排序基于分治策略,无论是在最好、平均还是最坏情况下,其时间复杂度均为 O(n log n)。选项 C 称其最坏情况下为 O(n²) 是错误的。

【易错点】 容易将归并排序的最坏时间复杂度与快速排序的最坏情况(O(n²))搞混。

10 题(单选题

下述C++代码实现了快速排序算法,最坏情况的时间复杂度是()。

int partition(vector<int>& arr, int low, int high) {
    int i = low, j = high;
    int pivot = arr[low]; // 以首元素为基准
    while (i < j) {
        while (i < j && arr[j] >= pivot) j--;
        while (i < j && arr[i] <= pivot) i++;
        if (i < j) swap(arr[i], arr[j]);
    }
    swap(arr[i], arr[low]);
    return i;
}

void quickSort(vector<int>& arr, int low, int high) {
    if (low >= high) return;
    int p = partition(arr, low, high);
    quickSort(arr, low, p - 1);
    quickSort(arr, p + 1, high);
}
A.
O(n)O(n)
B.
O(logn)O(\log n)
C.
O(n2)O(n^{2})
D.
O(nlogn)O(n \log n)

正确答案C

解析详情

【答案】C

【考点】快速排序的最坏时间复杂度

【解析】 当输入的数组已经有序或逆序,且每次选择第一个元素作为基准(pivot)时,快速排序的分区会极度不平衡(一侧为 0 个元素,另一侧为 n-1 个元素),导致递归深度达到 n 层,总时间复杂度变为 O(n²)。

【易错点】 误记快排复杂度为 O(n log n) 而忽略其最坏情况。

11 题(单选题

下面代码尝试在有序数组中查找第一个大于等于 x 的元素位置。如果没有大于等于 x 的元素,返回 arr.size()。以下说法正确的是()。

int lower_bound(vector<int>& arr, int x) {
    int l = 0, r = arr.size();
    while (l < r) {
        int mid = l + (r - l) / 2;
        if (arr[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return l;
}
A.
上述代码逻辑正确
B.
上述代码逻辑错误,while 循环条件应该用lrl \leq r
C.
上述代码逻辑错误,mid 计算错误
D.
上述代码逻辑错误,边界条件不对

正确答案A

解析详情

【答案】A

【考点】二分查找的标准实现 (lower_bound)

【解析】 代码使用了左闭右开区间 `[0, arr.size())`。当 `arr[mid] >= x` 时,说明结果在左侧或就是 `mid`,因此令 `r = mid`;否则结果在右侧,令 `l = mid + 1`。最终 `l` 指向第一个满足条件的元素,逻辑完全正确。

【易错点】 对于二分查找的边界处理(是否加1,循环条件是 < 还是 <=)容易产生混淆。

12 题(单选题

小杨要把一根长度为 L 的木头切成 K 段,使得每段长度小于等于 x 。已知每切一刀只能把一段木头分成两段,他用二分法找到满足条件的最小 x (x 为正整数),则横线处应填写()。

// 判断:在不超过 K 次切割内,是否能让每段长度 <= x

bool check(int L, int K, int x) {
    int cuts = (L - 1) / x;
    return cuts <= K;
}

// 二分查找最小可行的 x

int binary_cut(int L, int K) {
    int l = 1, r = L;
    while (l < r) {
        int mid = l + (r - l) / 2;
    }
    // 在此处填入代码
    return l;
}

int main() {
    int L = 10; // 木头长度
    int K = 2; // 最多切 K 刀
    cout << binary_cut(L, K) << endl;
    return 0;
}
A.
if (check(L, K, mid))
    r = mid;
else
    l = mid + 1;
B.
if (check(L, K, mid))
    r = mid + 1;
else
    l = mid + 1;
C.
if (check(L, K, mid))
    r = mid + 1;
else
    l = mid - 1;
D.
if (check(L, K, mid))
    r = mid + 1;
else
    l = mid;

正确答案A

解析详情

【答案】A

【考点】二分查找求最小值

【解析】 这是典型的二分查找应用。如果当前的 `mid` 满足条件(`check(L, K, mid)` 为真),则说明 `mid` 可能是答案,也可能存在更小的答案,因此更新 `r = mid`;否则说明 `mid` 太小了,需要增大,更新 `l = mid + 1`。

【易错点】 二分更新时错误地将 `r = mid` 写成 `r = mid - 1` 可能漏掉正确答案。

13 题(单选题

下面给出了阶乘计算的两种方式。以下说法正确的是()。

int factorial1(int n) {
    if (n <= 1) return 1;
    return n * factorial1(n - 1);
}

int factorial2(int n) {
    int acc = 1;
    while (n > 1) {
        acc = n * acc;
        n = n - 1;
    }
    return acc;
}
A.
上面两种实现方式的时间复杂度相同,都为O(n)O(n)
B.
上面两种实现方式的空间复杂度相同,都为O(n)O(n)
C.
上面两种实现方式的空间复杂度相同,都为O(1)O(1)
D.
函数 factorial1() 的时间复杂度为O(2n)O(2^n),函数 factorial2() 的时间复杂度为O(n)O(n)

正确答案A

解析详情

【答案】A

【考点】递归与迭代的时间复杂度分析

【解析】 `factorial1` 递归调用 n 次,时间复杂度为 O(n);`factorial2` 循环 n 次,时间复杂度也为 O(n)。两者的执行时间随 n 线性增长。但 `factorial1` 需要额外的栈空间 O(n),而 `factorial2` 空间复杂度为 O(1)。

【易错点】 误以为递归算法的时间复杂度一定是指数级。

14 题(单选题

给定有n个任务,每个任务有截止时间和利润,每个任务耗时1个时间单位、必须在截止时间前完成,且每个时间槽最多做1个任务。为了在规定时间内获得最大利润,可以采用贪心策略,即按利润从高到低排序,尽量安排,则横线处应填写()。

struct Task {
    int deadline; //截止时间
    int profit; //利润
};

void sortByProfit(vector<Task>& tasks) {
    sort(tasks.begin(), tasks.end(),
                    [](const Task& a, const Task& b) {
                    return a.profit > b.profit;
                });
}

int maxProfit(vector<Task>& tasks) {
    sortByProfit(tasks);
    int maxTime = 0;
    for (auto& t : tasks) {
        maxTime = max(maxTime, t.deadline);
    }

    vector<bool> slot(maxTime + 1, false);
    int totalProfit = 0;

    for (auto& task : tasks) {
        for (int t = task.deadline; t >= 1; t--) {
            if (!slot[t]) {
                //在此处填入代码
            }
        }
    }

    return totalProfit;
}
A.
slot[t] = true;
totalProfit += task.profit;
B.
slot[t] = false;
totalProfit += task.profit;
C.
slot[t] = true;
totalProfit = task.profit;
D.
slot[t] = false;
totalProfit = task.profit;

正确答案A

解析详情

【答案】A

【考点】贪心算法在任务调度中的应用

【解析】 当找到一个空余的时间槽 `slot[t]` 时,应将其标记为已占用(`slot[t] = true`),并将当前任务的利润累加到总利润中。由于任务已按利润排序,我们应尽量让截止日期靠后的任务占用较晚的时间槽,以给其他截止日期早的任务留出空间。

【易错点】 忘记标记时间槽导致同一个时间槽被多个任务重复利用。

15 题(单选题

下面代码实现了对两个数组表示的正整数的高精度加法(数组低位在前),则横线上应填写()。

vector<int> add(vector<int> a, vector<int> b) {
    vector<int> c;
    int carry = 0;

    for (int i = 0; i < a.size() || i < b.size(); i++) {
        if (i < a.size()) carry += a[i];
        if (i < b.size()) carry += b[i];
        // 在此处填入代码
    }
    if (carry) c.push_back(carry);
    return c;
}
A.
c.push_back(carry / 10);
carry %= 10;
B.
c.push_back(carry % 10);
carry /= 10;
C.
c.push_back(carry % 10);
D.
c.push_back(carry);
carry /= 10;

正确答案B

解析详情

【答案】B

【考点】高精度加法运算

【解析】 在每一位的加法中,当前位的结果应该是累加和 `carry` 对 10 取模的结果(`carry % 10`),存入结果数组 `c` 中;而产生的进位则是 `carry` 除以 10 的商(`carry / 10`),用于下一位的计算。选项 B 正确实现了这一逻辑。

【易错点】 取模(%)和整除(/)的操作顺序或对象容易弄错。

判断题(每题 2 分)

1 题(判断题

数组和链表都是线性表。链表的优点是插入删除不需要移动元素,并且能随机查找。

正确答案错误

解析详情

【答案】错误

【考点】链表的特点与局限性

【解析】 链表不支持随机查找。查找链表中的某个元素必须从头结点开始顺序遍历,时间复杂度为 O(n)。数组才支持通过下标进行的 O(1) 随机查找。

【易错点】 混淆链表的“灵活插入删除”和“查找”特性。

2 题(判断题

假设函数 gcd() 函数能正确求两个正整数的最大公约数,则下面的lcm(a,b)\mathrm{lcm}(a, b)函数能正确找到两个正整数 a 和 b 的最小公倍数。

int lcm(int a, int b) {
    return a / gcd(a, b) * b;
}

正确答案正确

解析详情

【答案】正确

【考点】最大公约数与最小公倍数的关系

【解析】 最小公倍数 lcm(a, b) 与最大公约数 gcd(a, b) 满足关系:lcm(a, b) = (a * b) / gcd(a, b)。代码通过 `a / gcd(a, b) * b` 的写法先除后乘,可以有效防止中间过程溢出,逻辑正确。

【易错点】 忽略先除后乘在数值范围较大时的优越性。

3 题(判断题

在单链表中,已知指针 p 指向要删除的结点(非尾结点),想在O(1)O(1)删除 p,可行做法是用 p->next 覆盖 p 的值与 next,然后删除 p->next。

正确答案正确

解析详情

【答案】正确

【考点】单链表删除结点的特殊技巧

【解析】 由于无法直接获得 p 的前驱结点,常规删除需要 O(n)。但在非尾结点的情况下,可以将 p->next 结点的数据复制给 p,然后让 p 指向 p->next->next,最后释放 p->next 结点内存。这在逻辑上等同于删除了原 p 结点的数据,耗时为 O(1)。

【易错点】 若 p 是尾结点,此方法失效,因为没有 p->next 可以用来覆盖。

4 题(判断题

在求解所有不大于 n 的素数时,线性筛法(欧拉筛)都应当优先于埃氏筛法使用,因为线性筛法的时间复杂度为O(n)O(n),低于埃氏筛法的O(nloglogn)O(n \log \log n)

正确答案错误

解析详情

【答案】错误

【考点】素数筛法的效率比较

【解析】 虽然线性筛的时间复杂度理论上更优,但在实际应用(尤其是在 n 较小时)中,埃氏筛法的常数较小,代码更简单。并非在所有情况下都“应当优先”使用线性筛,需根据具体规模和常数考量。

【易错点】 误以为算法复杂度低就一定要在任何场景下取代复杂度稍高的算法。

5 题(判断题

二分查找仅适用于有序数据。若输入数据无序,当仅进行一次查找时,为了使用二分而排序通常不划算。

正确答案正确

解析详情

【答案】正确

【考点】二分查找的前提与排序代价

【解析】 排序的最优平均复杂度为 O(n log n),而顺序查找的复杂度为 O(n)。如果只进行一次查找,直接顺序查找比“先排序再二分”快。只有在多次查找的情况下,排序的成本才会被分摊,二分的优势才会体现。

【易错点】 忽略排序过程本身的高昂代价(O(n log n))。

6 题(判断题

通过在数组的第一个、最中间和最后一个这3个数据中选择中间值作为枢轴(比较基准),快速排序算法可降低落入最坏情况的概率。

正确答案正确

解析详情

【答案】正确

【考点】快速排序的优化策略(三数取中)

【解析】 快速排序的最坏情况发生在分区极度不平衡时。采用“三数取中法”选择枢轴,能极大地避免在有序或近乎有序的数据上选到最大或最小值作为基准,从而保持分区平衡,降低退化到 O(n²) 的风险。

【易错点】 三数取中虽有效但不能完全理论消除最坏情况,只是极大降低了概率。

7 题(判断题

贪心算法在每一步都做出当前看来最优的局部选择,并且一旦做出选择就不再回溯;而分治算法将问题分解为若干子问题分别求解,再将子问题的解合并得到原问题的解。

正确答案正确

解析详情

【答案】正确

【考点】算法思想的定义与区别

【解析】 题干准确描述了贪心算法的核心特征(局部最优、不回溯)和分治算法的基本流程(分解、解决、合并)。这是这两种基础算法思想的标准定义。

【易错点】 混淆贪心算法与动态规划(动态规划有状态转移,通常处理最优子结构但非简单局部最优)。

8 题(判断题

以下 fib 函数计算第 n 项斐波那契数(fib(0)=0\text{fib}(0) = 0fib(1)=1\text{fib}(1) = 1),其时间复杂度为O(n)O(n)

int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}

正确答案错误

解析详情

【答案】错误

【考点】斐波那契数列递归实现的复杂度分析

【解析】 该递归实现没有使用备忘录或记忆化,会导致大量的重复计算。其时间复杂度遵循递推式 T(n) = T(n-1) + T(n-2) + O(1),增长速度与斐波那契数列本身一致,为指数级 O(1.618ⁿ),而非 O(n)。

【易错点】 直觉上认为递归到 n 就是 O(n),而忽略了分叉带来的指数级增长。

9 题(判断题

递归函数一定要有终止条件,否则可能会造成栈溢出。

正确答案正确

解析详情

【答案】正确

【考点】递归函数的定义与安全性

【解析】 递归每次调用都会在内存栈中开辟新的函数帧。如果没有终止条件,递归将无限进行下去,最终耗尽系统为该进程分配的栈空间,触发 Stack Overflow(栈溢出)错误导致程序崩溃。

【易错点】 终止条件写错(如范围判断失误)也会导致同样的结果。

10 题(判断题

使用贪心算法解决问题时,通过对每一步求局部最优解,最终一定能找到全局最优解。

正确答案错误

解析详情

【答案】错误

【考点】贪心算法的局限性

【解析】 贪心算法只对具有“贪心选择性质”的问题有效。对于很多问题(如背包问题、最短路径的某些变体),局部最优的选择并不一定能推导出全局最优解。贪心算法是否正确需要严密的数学证明。

【易错点】 盲目相信贪心策略的普适性,忽略了对问题的贪心证明。