K - Kindred Sums - 题解
标签与难度
标签: 格基规约 (Lattice Reduction), Meet-in-the-middle, 子集和, 数论, 构造 难度: 2900
题目大意喵~
主人你好呀,喵~ 这道题是这样的:
我们有 N = 1000 个非常大的正整数 ,还有一个同样非常大的模数 。
我们的任务是,从这1000个数的下标 {1, 2, ..., N} 中,找出两个互不相交并且不相同的子集 和 。这两个子集需要满足一个特殊的条件:它们对应元素的和之差是 的倍数。
用数学公式表达就是:
题目保证一定有解,我们只需要输出任意一个解就好啦。
输出格式有点特别哦:我们要输出一个长度为 N 的字符串。
- 如果第
i个元素在集合 中,字符串第i位就是1。 - 如果第
i个元素在集合 中,字符串第i位就是2。 - 如果第
i个元素哪儿都不在,那一位就是0。
解题思路分析
这道题看起来就像一个巨大的背包或者子集和问题,但是 N 和数值都太大了,直接搜索肯定会超时的说! 种可能性,就算是超级计算机也顶不住呀,喵~
所以,我们需要更聪明的办法!让我带你一步步揭开谜底吧!
换个角度看问题喵
首先,我们把题目要求的目标稍微变个形。寻找集合 和 ,其实等价于为每个 分配一个系数 ,使得:
这里的系数 和集合 的关系是:
因为题目要求 和 是不同的非空集合(至少不能同时为空),所以我们不能让所有的 都为 0。我们需要一个非平凡解!
鸽巢原理的启示
想一想,我们有 个数,可以构成 个不同的子集。 是一个天文数字,远远大于 。
根据鸽巢原理,如果我们计算所有 个子集的元素和并对 取模,必然会产生碰撞!也就是说,一定存在两个不同的子集 和 ,使得:
移项一下,就是:
这不就是我们想要的形式吗?我们可以定义 和 。这样 和 就互不相交,并且因为 ,所以 和 不会同时为空,找到了一个合法的解!
这个原理证明了解一定存在,但并没有告诉我们怎么高效地找到它。毕竟,我们不能真的去计算 个子集和呀!
格基规约 (Lattice Reduction) 的大智慧
这个问题的本质,其实是寻找一组整数系数 使得一个线性组合为 。在数学上,这类问题被称为整数关系发现(Integer Relation Discovery),而解决它的有力工具就是格基规约(Lattice Reduction),其中最著名的算法是 LLL 算法。
我们可以构造一个格(Lattice)。想象一个 维空间,其中的向量是这样的:
- ...
- (这里的 是一个缩放因子,用来平衡数值大小,可以暂时忽略)。
这个格里所有的向量都可以写成 的形式。我们想找一个短的、非零的向量。如果能找到一个向量 ,并且它的长度很小,那么它的各个分量 和 都会很小。如果我们能找到一个 的向量,那就意味着:
这正是我们想要的!LLL算法就能帮我们找到这样的短向量。
不过,从头实现一个能处理1000维大数的LLL算法也太为难人了,喵~ 我们可以用一个更直观、更易于实现的简化版思想。
简化版的“规约” + Meet-in-the-middle
我们的核心策略分为两步:
规约 (Reduction): 我们有1000个初始“向量”,每个向量代表一个 和它的来源。我们通过不断地两两相减,来制造出新的、数值上更“小”的向量。比如,如果我们有两个向量 和 ,它们的值分别是 和 。那么新向量 的值就是 ,它的系数向量就是 ( 是第i个单位向量)。通过不断重复这个过程,我们可以把1000个向量规约成一个更小的集合(比如40个),这些新向量的值都相对较小,但它们仍然是原始 的线性组合。
中间相遇 (Meet-in-the-middle): 当我们把向量数量减少到可以处理的范围(比如 个)时,就可以用经典的中间相遇攻击来寻找最终解了。
- 把这40个向量分成两组,每组20个。
- 第一组: 计算这20个向量所有 个子集的和。对于每个子集,我们记录下它的
(sum % M, 对应的系数向量),存入一个哈希表map中。 - 第二组: 同样计算这20个向量的所有 个子集的和。对于每个子集的和
current_sum,我们去哈希表中查找是否存在一个target_sum = (M - current_sum % M) % M。 - 如果找到了!那就意味着我们找到了两部分子集,它们的和加起来恰好是 的倍数!把它们对应的系数向量合并起来,就得到了最终的答案
c向量。 - 我们也可以查找
current_sum是否直接在哈希表中。如果找到,说明sum1 % M == sum2 % M,那么sum1 - sum2是M的倍数。这样构造出的解,系数是{-1, 0, 1}。
这个两步走的策略,既利用了格基规约的核心思想(通过线性组合降低向量范数),又使用了经典的搜索技巧,非常巧妙地解决了这个难题!
代码实现
好啦,理论讲完了,一起来看看代码怎么写吧,喵~
我将采用上面提到的“规约 + 中间相遇”的思路来写这份代码。
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
#include <map>
// 为了处理大数,我们使用 __int128
using int128 = __int128_t;
const int N = 1000;
const int128 M = 1000000000000000000LL;
// 定义一个结构体来表示我们的“向量”
// 它包含一个数值 value 和一个表示其来源的系数向量 coeffs
struct Vector {
int128 value;
std::vector<int> coeffs;
Vector() : value(0), coeffs(N, 0) {}
// 定义向量减法
Vector operator-(const Vector& other) const {
Vector result;
result.value = this->value - other.value;
for (int i = 0; i < N; ++i) {
result.coeffs[i] = this->coeffs[i] - other.coeffs[i];
}
return result;
}
// 定义向量加法
Vector operator+(const Vector& other) const {
Vector result;
result.value = this->value + other.value;
for (int i = 0; i < N; ++i) {
result.coeffs[i] = this->coeffs[i] + other.coeffs[i];
}
return result;
}
};
// 用于排序的比较函数
bool compareVectors(const Vector& a, const Vector& b) {
return a.value < b.value;
}
// 用于从标准输入读取 __int128
std::istream& operator>>(std::istream& is, int128& val) {
long long input_val;
is >> input_val;
val = input_val;
return is;
}
// 用于向标准输出打印 __int128
std::ostream& operator<<(std::ostream& os, const int128& val) {
if (val == 0) return os << "0";
std::string s = "";
int128 tmp = val;
bool neg = false;
if (tmp < 0) {
neg = true;
tmp = -tmp;
}
while (tmp > 0) {
s += (tmp % 10) + '0';
tmp /= 10;
}
if (neg) s += '-';
std::reverse(s.begin(), s.end());
return os << s;
}
int main() {
// 加速输入输出
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
std::vector<Vector> basis(N);
for (int i = 0; i < N; ++i) {
std::cin >> basis[i].value;
basis[i].coeffs[i] = 1;
}
// --- 阶段一:规约 ---
// 我们将向量数量规约到 REDUCED_BASIS_SIZE
const int REDUCED_BASIS_SIZE = 40;
while (basis.size() > REDUCED_BASIS_SIZE) {
// 按 value 排序,让值相近的向量排在一起
std::sort(basis.begin(), basis.end(), compareVectors);
std::vector<Vector> next_basis;
// 保留第一个向量
next_basis.push_back(basis[0]);
// 其他向量都通过与前一个向量相减来更新
for (size_t i = 1; i < basis.size(); ++i) {
next_basis.push_back(basis[i] - basis[i-1]);
}
basis = next_basis;
// 为了避免系数过大,可以进行一些额外的规约步骤,但这个简化版也足够找到解
}
// --- 阶段二:中间相遇搜索 ---
int k = basis.size();
int k1 = k / 2;
int k2 = k - k1;
// 存储第一半向量组合的哈希表
// key: sum % M, value: 对应的向量
std::map<int128, Vector> first_half_sums;
// 遍历第一半向量的所有子集
for (int i = 0; i < (1 << k1); ++i) {
Vector current_sum;
for (int j = 0; j < k1; ++j) {
if ((i >> j) & 1) {
current_sum = current_sum + basis[j];
}
}
int128 mod_val = current_sum.value % M;
if (mod_val < 0) mod_val += M;
// 如果已经有一个组合的和是0的倍数,直接就是答案了!
if (mod_val == 0 && i != 0) {
first_half_sums[mod_val] = current_sum;
break;
}
if (first_half_sums.find(mod_val) == first_half_sums.end()) {
first_half_sums[mod_val] = current_sum;
}
}
Vector solution;
bool found = false;
// 遍历第二半向量的所有子集
for (int i = 0; i < (1 << k2); ++i) {
if (found) break;
Vector current_sum;
for (int j = 0; j < k2; ++j) {
if ((i >> j) & 1) {
current_sum = current_sum + basis[k1 + j];
}
}
int128 mod_val = current_sum.value % M;
if (mod_val < 0) mod_val += M;
// 寻找 sum1 + sum2 = 0 (mod M)
int128 target = (M - mod_val) % M;
if (first_half_sums.count(target)) {
Vector s1 = first_half_sums[target];
solution = s1 + current_sum;
if (solution.value != 0 || std::any_of(solution.coeffs.begin(), solution.coeffs.end(), [](int c){ return c != 0; })) {
found = true;
break;
}
}
// 寻找 sum1 = sum2 (mod M)
if (first_half_sums.count(mod_val)) {
Vector s1 = first_half_sums[mod_val];
// 确保是不同的组合
if (s1.value != current_sum.value || s1.coeffs != current_sum.coeffs) {
solution = s1 - current_sum;
if (solution.value != 0 || std::any_of(solution.coeffs.begin(), solution.coeffs.end(), [](int c){ return c != 0; })) {
found = true;
break;
}
}
}
}
// --- 输出结果 ---
for (int i = 0; i < N; ++i) {
if (solution.coeffs[i] == 1) {
std::cout << '1';
} else if (solution.coeffs[i] == -1) {
std::cout << '2';
} else {
std::cout << '0';
}
}
std::cout << std::endl;
return 0;
}复杂度分析
时间复杂度:
- 规约阶段: 我们大概需要 轮来把向量数量从
N降到k。每一轮都需要排序,复杂度是 。向量操作是 。所以规约阶段总的来说比较快。 - 搜索阶段: 中间相遇是主要瓶颈。我们将 k 个向量分成两半。对每一半,我们都需要生成 个子集和。每次生成和合并系数向量需要 的时间。哈希表操作接近 。所以搜索阶段的复杂度是 。在我们的代码中,k 大约是40,所以 约一百万,这是可以接受的。
- 规约阶段: 我们大概需要 轮来把向量数量从
空间复杂度:
- 主要开销是中间相遇搜索中存储第一半结果的哈希表。它最多会存储 个项,每个项包含一个长度为
N的系数向量。
- 主要开销是中间相遇搜索中存储第一半结果的哈希表。它最多会存储 个项,每个项包含一个长度为
知识点总结
这道题真是一次奇妙的冒险呢,喵~ 我们用到的知识点有:
- 问题转化: 把寻找集合 的问题,转化为寻找一组系数 的线性组合问题。这是解决组合问题时常用的思路。
- 鸽巢原理: 它是我们信念的基石!它告诉我们解一定存在,所以我们的方向是对的。
- 格基规约 (Lattice Reduction): 这是解决此类问题的标准数学工具。虽然我们没有直接用复杂的 LLL 算法,但我们通过排序和相减来模拟其核心思想——通过线性组合找到“更短”的向量,这大大降低了问题的规模。
- 中间相遇 (Meet-in-the-middle): 当问题规模被缩减到可以处理的范围时(比如40个元素),中间相遇就成了我们的终极武器。它通过空间换时间,将指数级的搜索复杂度 降低到 ,非常有效!
希望我的讲解对你有帮助哦!下次遇到难题,也请不要放弃,多动动脑筋,一定能找到思路的!喵~