Luogu P1012 [NOIP 1998 提高组] 拼数

题目核心

给定 \(n\) 个正整数,我们需要将它们按照某种顺序拼接起来,形成一个数值上最大的整数。

思路分析

解决这类“组合最优”问题时,我们往往会尝试一些直观的贪心策略。让我们从一个看似合理但实际有缺陷的思路开始,逐步推导出正确的解法。

错误思路:按字典序降序排序

既然是拼接成一个大数,我们很自然地会想到处理字符串。如果把所有数字看作字符串,然后按照字典序从大到小排序,再拼接起来,结果会是最大的吗?我们用一个例子来检验这个策略。对于 7, 13, 4, 246(样例 #2):

  1. 将数字转换为字符串: "7", "13", "4", "246"
  2. 按字典序降序排序: "7" > "4" > "246" > "13"
  3. 排序后的顺序是: 7, 4, 246, 13
  4. 拼接结果: 7424613

这个结果与样例输出完全一致,这使得该策略看起来非常有希望。但是,我们却找到一个反例来证明或证伪它。考虑两个数:331

  1. 转换为字符串: "3""31"
  2. 按字典序比较: "31" 的字典序大于 "3"。(因为首位'3'相同,比较第二位时,"31"有'1',而"3"已经结束了。在很多标准库实现中,较长的字符串被认为字典序更大)。
  3. 按字典序降序排序结果: 31, 3
  4. 拼接结果: 313

然而,我们肉眼可见,将它们拼接为 331 会得到一个更大的数。因此,简单的按字典序降序排序是错误的

错误根源:字典序比较的是单个字符串本身,它没有考虑到一个字符串作为“前缀”时,对后续拼接产生的影响。"3" 虽然在字典序上可能不如 "31",但它作为开头,为后续数字留下了更好的可能性。

正确思路:自定义排序规则(贪心)

既然简单的比较规则不行,我们就必须设计一个能真正服务于我们最终目标的比较规则。

我们的目标是让最终拼接的整数最大。这引导我们思考一个根本问题:对于任意两个数 \(a\)\(b\),我们如何决定谁应该排在前面?

假设我们正在决定 \(a\)\(b\) 的相对顺序。它们在最终序列中是相邻的。有两种拼接可能:

  1. \(a\) 在前,\(b\) 在后,形成 ...ab...
  2. \(b\) 在前,\(a\) 在后,形成 ...ba...

为了让最终结果最大,我们显然应该选择能产生更大“局部值”的那个顺序。也就是说:

  • 如果拼接结果 a + b(字符串拼接)形成的数大于 b + a 形成的数,那么 \(a\) 就应该排在 \(b\) 的前面。
  • 反之,如果 b + a > a + b,那么 \(b\) 就应该排在 \(a\) 的前面。

我们将这个规则形式化:定义一种新的“大于”关系,我们记作 \(\succ\)。对于任意两个数(作为字符串)\(A\)\(B\):$ A \succ B \iff A+B > B+A $。

其中,\(A+B\) 表示字符串拼接,我们比较的是拼接后字符串的字典序大小(这等价于比较它们代表的整数大小)。让我们用这个新规则来重新审视之前的反例

  • 比较 331

    • A="3", B="31"
    • A+B -> "3" + "31" -> "331"
    • B+A -> "31" + "3" -> "313"
    • 因为 "331" > "313",所以我们定义 3 \(\succ\) 31。在排序时,3 应该排在 31 前面。
  • 比较 312343

    • A="312", B="343"
    • A+B -> "312343"
    • B+A -> "343312"
    • 因为 "343312" > "312343",所以我们定义 343 \(\succ\) 312。在排序时,343 应该排在 312 前面。

这个规则完美地解决了我们遇到的所有问题。我们只需要将所有给定的数字,按照这个新的 \(\succ\) 关系进行降序排序,然后依次拼接,就能得到最终的答案。

正确性证明(交换论证法)

为什么这个贪心策略是正确的?我们可以用交换论证法来证明。

  1. 定义排序规则:我们的排序规则是:对于任意两个数 \(a\)\(b\),若字符串 a+b > b+a,则 \(a\) 排在 \(b\) 前面。

  2. 反证法:假设通过我们的排序规则得到的最终拼接结果 \(S = s_1s_2...s_n\) 不是最优解。那么,一定存在一个最优解 \(S'\),它和 \(S\) 不完全相同。既然 \(S'\)\(S\) 不同,那么在 \(S'\) 中必然至少存在一对相邻的元素 \(s'_i\)\(s'_{i+1}\),它们的顺序与我们在 \(S\) 中的排序规则相悖。也就是说,按照我们的规则,\(s'_{i+1}\) 应该排在 \(s'_i\) 的前面,即 \(s'_{i+1} \succ s'_i\)(也就是字符串 \(s'_{i+1} + s'_i\) > \(s'_i + s'_{i+1}\))。

    现在,我们构造一个新的序列 \(S''\),它通过交换 \(S'\) 中的 \(s'_i\)\(s'_{i+1}\) 得到:

    \[S' = s'_1...s'_{i-1} \ s'_i \ s'_{i+1} \ s'_{i+2}...s'_n \]

    \[S'' = s'_1...s'_{i-1} \ s'_{i+1} \ s'_i \ s'_{i+2}...s'_n \]

    比较 \(S'\)\(S''\) 的大小。它们的前缀 \(s'_1...s'_{i-1}\) 和后缀 \(s'_{i+2}...s'_n\) 完全相同。决定它们大小的只有中间部分:\(s'_is'_{i+1}\)\(s'_{i+1}s'_i\)

    根据我们的假设,\(s'_{i+1} + s'_i > s'_i + s'_{i+1}\),这意味着 \(S''\) 代表的整数比 \(S'\) 代表的整数更大。这与我们“\(S'\) 是最优解”的假设相矛盾!因为我们找到了一个比 \(S'\) 更大的解 \(S''\)

    因此,假设不成立。通过我们的排序规则得到的解 \(S\) 必定是最优解。我们可以不断地在任意非最优解中寻找这种“逆序对”并交换它们,每一步都会使结果变得更大,最终必然会达到我们贪心策略所构造的序列 \(S\)

代码实现详解

基于上述思路,我们可以着手编写代码。核心就是实现一个自定义的排序。

Python 代码解析

from functools import cmp_to_key
# 1. 导入 cmp_to_key 工具

n = int(input())
# 2. 读取数字个数 n (虽然在此代码中 n 未被直接使用,但题目格式要求读取)

a = input().split()
# 3. 读取 n 个整数,并将它们作为字符串存储在列表 a 中
#    例如,输入 "13 312 343",a 会变成 ['13', '312', '343']
#    注意:必须作为字符串处理,才能方便地进行 a+b 拼接操作。

# 4. 核心排序步骤
a.sort(key=cmp_to_key(lambda a, b: 1 if a + b < b + a else -1))
#    这行代码是最关键的部分,我们来分解它:
#    a.sort(...): 对列表 a 进行原地排序。默认是升序。
#    key=...: 指定排序的依据。这里不是简单地按元素本身大小,而是通过一个 key 函数的返回值来决定。
#    cmp_to_key(...): 这是一个转换函数。它将一个“比较函数”(comparison function) 转换为一个“键函数”(key function)。
#                     老式比较函数 cmp(a, b) 的行为是:
#                     - 如果 a < b,返回负数
#                     - 如果 a == b,返回 0
#                     - 如果 a > b,返回正数
#    lambda a, b: 1 if a + b < b + a else -1:
#                     这是一个匿名的比较函数,它接收两个元素 a 和 b。
#                     - `a + b < b + a`: 这对应我们之前定义的 `b` $\succ$ `a`。
#                       此时返回 1 (正数),表示在比较中 `a` "大于" `b`。
#                       在默认的升序排序中,"大"的元素会被排到后面。
#                       所以,如果 `b` 应该在 `a` 前面,就让 `a` "大于" `b`,这样 `a` 就会被排到 `b` 的后面。
#                     - `else -1`: 这对应 `a + b >= b + a`,即 `a` $\succ$ `b` 或 `a` 与 `b` 等价。
#                       此时返回 -1 (负数),表示在比较中 `a` "小于" `b`。
#                       在升序排序中,"小"的元素会被排到前面。
#                       所以,如果 `a` 应该在 `b` 前面,就让 `a` "小于" `b`,这样 `a` 就会被排到 `b` 的前面。
#    总结一下这行排序:它实际上是按照我们的规则 `A` $\succ$ `B` $\iff$ `A+B > B+A` 进行了一次 **降序** 排序。
#    排序后,列表 a 中的元素顺序就是我们想要的最终拼接顺序。

print("".join(a))
# 5. 将排序后的字符串列表中的所有元素拼接成一个字符串并输出。
#    例如,排序后的 a 是 ['343', '312', '13']
#    "".join(a) 的结果就是 "34331213"

C++ 代码实现

C++ 的 sort 函数可以直接接受一个自定义的比较器(通常是 lambda 表达式),这让实现更加直观。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

// 自定义比较函数
// 如果 a 应该排在 b 前面,返回 true
bool compare(const string& a, const string& b) {
    return a + b > b + a;
}

int main() {
    // 提高cin/cout效率
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;

    vector<string> nums(n);
    for (int i = 0; i < n; ++i) {
        cin >> nums[i];
    }

    // 使用 sort 和自定义比较函数进行排序
    // 第三个参数是一个比较器。它需要一个返回 bool 值的函数或 lambda。
    // sort 会将 comp(a, b) 为 true 的 a 排在 b 前面。
    // 我们希望 a+b > b+a 时 a 排在前面,所以比较器直接就是 return a + b > b + a;
    sort(nums.begin(), nums.end(), compare);

    // 或者使用 Lambda 表达式,更简洁
    /*
    sort(nums.begin(), nums.end(), [](const string& a, const string& b) {
        return a + b > b + a;
    });
    */

    for (int i = 0; i < n; ++i) {
        cout << nums[i];
    }
    cout << endl;

    return 0;
}

总结

本题是一道经典的贪心算法问题,其精髓在于将全局最优问题转化为对局部最优的正确定义。我们通过定义一种新颖的、符合问题目标的比较关系 \(A \succ B \iff A+B > B+A\),然后利用这个关系对所有数字进行排序,最终拼接得到的就是全局最优解。这个思想在处理类似的拼接、组合以获得最优解的问题时非常有用。解题的关键步骤是:

  1. 将所有输入数字视为字符串。
  2. 定义一个自定义比较规则,用于确定任意两个字符串的先后顺序。
  3. 使用该规则对字符串列表进行排序。
  4. 将排序后的结果顺序拼接即为答案。
posted @ 2025-08-09 14:54  AFewMoon  阅读(20)  评论(0)    收藏  举报