Luogu P1012 [NOIP 1998 提高组] 拼数
题目核心
给定 \(n\) 个正整数,我们需要将它们按照某种顺序拼接起来,形成一个数值上最大的整数。
思路分析
解决这类“组合最优”问题时,我们往往会尝试一些直观的贪心策略。让我们从一个看似合理但实际有缺陷的思路开始,逐步推导出正确的解法。
错误思路:按字典序降序排序
既然是拼接成一个大数,我们很自然地会想到处理字符串。如果把所有数字看作字符串,然后按照字典序从大到小排序,再拼接起来,结果会是最大的吗?我们用一个例子来检验这个策略。对于 7, 13, 4, 246(样例 #2):
- 将数字转换为字符串:
"7","13","4","246"。 - 按字典序降序排序:
"7">"4">"246">"13"。 - 排序后的顺序是:
7, 4, 246, 13。 - 拼接结果:
7424613。
这个结果与样例输出完全一致,这使得该策略看起来非常有希望。但是,我们却找到一个反例来证明或证伪它。考虑两个数:3 和 31。
- 转换为字符串:
"3"和"31"。 - 按字典序比较:
"31"的字典序大于"3"。(因为首位'3'相同,比较第二位时,"31"有'1',而"3"已经结束了。在很多标准库实现中,较长的字符串被认为字典序更大)。 - 按字典序降序排序结果:
31, 3。 - 拼接结果:
313。
然而,我们肉眼可见,将它们拼接为 331 会得到一个更大的数。因此,简单的按字典序降序排序是错误的。
错误根源:字典序比较的是单个字符串本身,它没有考虑到一个字符串作为“前缀”时,对后续拼接产生的影响。"3" 虽然在字典序上可能不如 "31",但它作为开头,为后续数字留下了更好的可能性。
正确思路:自定义排序规则(贪心)
既然简单的比较规则不行,我们就必须设计一个能真正服务于我们最终目标的比较规则。
我们的目标是让最终拼接的整数最大。这引导我们思考一个根本问题:对于任意两个数 \(a\) 和 \(b\),我们如何决定谁应该排在前面?
假设我们正在决定 \(a\) 和 \(b\) 的相对顺序。它们在最终序列中是相邻的。有两种拼接可能:
- \(a\) 在前,\(b\) 在后,形成
...ab... - \(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\) 表示字符串拼接,我们比较的是拼接后字符串的字典序大小(这等价于比较它们代表的整数大小)。让我们用这个新规则来重新审视之前的反例:
-
比较
3和31:A="3",B="31"A+B->"3" + "31"->"331"B+A->"31" + "3"->"313"- 因为
"331">"313",所以我们定义3\(\succ\)31。在排序时,3应该排在31前面。
-
比较
312和343:A="312",B="343"A+B->"312343"B+A->"343312"- 因为
"343312">"312343",所以我们定义343\(\succ\)312。在排序时,343应该排在312前面。
这个规则完美地解决了我们遇到的所有问题。我们只需要将所有给定的数字,按照这个新的 \(\succ\) 关系进行降序排序,然后依次拼接,就能得到最终的答案。
正确性证明(交换论证法)
为什么这个贪心策略是正确的?我们可以用交换论证法来证明。
-
定义排序规则:我们的排序规则是:对于任意两个数 \(a\) 和 \(b\),若字符串
a+b>b+a,则 \(a\) 排在 \(b\) 前面。 -
反证法:假设通过我们的排序规则得到的最终拼接结果 \(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\),然后利用这个关系对所有数字进行排序,最终拼接得到的就是全局最优解。这个思想在处理类似的拼接、组合以获得最优解的问题时非常有用。解题的关键步骤是:
- 将所有输入数字视为字符串。
- 定义一个自定义比较规则,用于确定任意两个字符串的先后顺序。
- 使用该规则对字符串列表进行排序。
- 将排序后的结果顺序拼接即为答案。

浙公网安备 33010602011771号