Skip to content

D. Points and Powers of Two - 题解

比赛与标签

比赛: Codeforces Round 486 (Div. 3) 标签: brute force, math 难度: *1800

题目大意喵~

主人你好呀~!是小猫娘我哦!今天我们来分析一道非常有趣的题目,喵~

题目是这样子的:在一条数轴上,我们有 n 个坐标各不相同的点。我们需要从这些点中选出一个子集,这个子集要满足一个奇妙的条件:子集中任意两个点的距离都必须是2的非负整数次幂(比如 1, 2, 4, 8, ...)。我们的目标是找到满足这个条件的、包含点数最多的子集,并把它打印出来。

简单来说,就是找一个最大的点集,使得里面任意两点 ab 的距离 |a - b| 都等于 2^d(不同的点对,d 可以不同)。

解题思路,来一起推理吧!

这道题看起来有点吓人呐,n 最大有 20 万,如果暴力枚举所有子集肯定是不行的说。但是只要我们静下心来分析,就会发现一个超级重要的突破口哦!

我们来分析一下,满足条件的子集最大能有多大呢?

  • 大小为 1 的子集: {a} 一个点自己跟自己玩,当然满足条件啦,所以答案至少是1喵。

  • 大小为 2 的子集: {a, b} 两个点的话,只要它们的距离 |a - b| 是2的幂次方就可以啦,比如 {3, 5},距离是2,也就是 2^1

  • 大小为 3 的子集: {a, b, c} 这可是最关键的部分!我们来仔细研究一下,喵~ 为了方便,我们假设 a < b < c。那么,b-a, c-b, c-a 这三个距离都必须是2的幂。 我们设 b - a = 2^k 并且 c - b = 2^m。 那么 c - a = (c - b) + (b - a) = 2^m + 2^k。 现在问题来了:两个2的幂相加,什么时候结果还是2的幂呢? 我们来试试看:

    • 如果 k = m,那么 2^k + 2^k = 2 * 2^k = 2^(k+1)。Bingo!这是一个2的幂!
    • 如果 k < m,那么 2^k + 2^m = 2^k * (1 + 2^(m-k))。因为 m-k > 0,所以 2^(m-k) 是个偶数,1 + 2^(m-k) 就是个大于1的奇数。一个大于1的奇数不可能是2的幂! 所以呀,我们得出了一个惊人的结论:要让 2^k + 2^m 成为2的幂,当且仅当 k = m

    这意味着 b - a 必须等于 c - b!也就是说,b 必须是 ac 的中点! 所以,一个大小为3的合法子集,必定是 {p - d, p, p + d} 这样的形式,其中 d 是2的幂。 我们来验证一下:

    • |p - (p - d)| = d = 2^k (OK!)
    • |(p + d) - p| = d = 2^k (OK!)
    • |(p + d) - (p - d)| = 2d = 2 * 2^k = 2^(k+1) (OK!) 完美!所有距离都是2的幂。比如例题中的 {3, 5, 7},就是 {5 - 2^1, 5, 5 + 2^1}
  • 大小为 4 的子集: {a, b, c, d} 那...四个点呢?还能更多吗? 我们假设 a < b < c < d。 根据上面的结论,子集 {a, b, c} 必须是 {b - 2^k, b, b + 2^k} 的形式。所以 a = b - 2^k, c = b + 2^k。 同理,子集 {b, c, d} 也必须满足条件,所以它们也得是 {c - 2^m, c, c + 2^m} 的形式。所以 b = c - 2^m, d = c + 2^m。 联立一下:b = (b + 2^k) - 2^m,可以得到 2^k = 2^m,所以 k = m。 那么这四个点就是:a = b - 2^k, b, c = b + 2^k, d = c + 2^k = (b + 2^k) + 2^k = b + 2 * 2^k = b + 2^(k+1)。 这个集合是 {b - 2^k, b, b + 2^k, b + 2^(k+1)}。 让我们来检查一下 ad 的距离: |d - a| = |(b + 2^(k+1)) - (b - 2^k)| = 2^(k+1) + 2^k = 2 * 2^k + 2^k = 3 * 2^k。 呜喵!3 * 2^k 里面有个讨厌的3,它绝对不是2的幂!所以,大小为4的子集是不可能存在的

最终策略

所以,答案的最大可能就是 3

这下问题就变得超级简单了,我们的策略就很清晰啦:

  1. 优先找大小为3的子集。遍历每一个输入的点 p,把它当作中间点。然后遍历所有可能的2的幂 d = 2^k,检查 p - dp + d 是否都在输入的点集中。只要找到一组,就是我们的答案!
  2. 如果找不到大小为3的,我们再找大小为2的子集。同样,遍历每个点 p,检查 p + d 是否存在。找到一个就可以啦。
  3. 如果连大小为2的都找不到,那说明任意两点距离都不是2的幂,答案就只能是大小为1的子集了。随便输出一个点就好。

为了快速判断一个点是否存在,我们先把所有点都放进一个哈希集合(C++里的 std::unordered_set)里,这样查询起来平均就是O(1)的啦,超级快!

代码实现,Nya!

cpp
#include <iostream>
#include <vector>
#include <unordered_set>
#include <algorithm>

void solve() {
    int n;
    std::cin >> n;
    std::vector<long long> x(n);
    // 使用哈希集合来快速查找一个点是否存在,喵~
    std::unordered_set<long long> s;
    for (int i = 0; i < n; ++i) {
        std::cin >> x[i];
        s.insert(x[i]);
    }

    // 优先级最高:寻找大小为3的集合。这是我们能找到的最大的集合了!
    for (long long p : x) {
        // 遍历所有可能的2的幂,d从0到31就足够覆盖所有情况了
        // 2^31 > 2*10^9,比题目中任意两点的最大距离都大
        for (int d = 0; d <= 31; ++d) {
            long long pow2 = 1LL << d; // 计算 2^d
            // 检查 p-pow2 和 p+pow2 是否都存在于原始点集中
            if (s.count(p - pow2) && s.count(p + pow2)) {
                // 找到了!直接输出然后结束程序,喵!
                std::cout << 3 << "\n";
                std::cout << p - pow2 << " " << p << " " << p + pow2 << "\n";
                return;
            }
        }
    }

    // 如果没找到大小为3的,那就退而求其次,找大小为2的
    for (long long p : x) {
        for (int d = 0; d <= 31; ++d) {
            long long pow2 = 1LL << d;
            // 检查 p+pow2 是否存在
            if (s.count(p + pow2)) {
                // 找到了!这是当前能找到的最好的答案
                std::cout << 2 << "\n";
                std::cout << p << " " << p + pow2 << "\n";
                return;
            }
        }
    }

    // 如果大小为2和3的都没找到,那答案肯定是1了
    // 随便输出一个点就行
    std::cout << 1 << "\n";
    std::cout << x[0] << "\n";
}

int main() {
    // 加速输入输出,是个好习惯哦
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    solve();
    return 0;
}

复杂度分析

  • 时间复杂度: O(N * log(MAX_VAL)) 的说。 我们首先需要 O(N) 的时间把所有点读入并存入哈希集合。然后,我们最多会遍历 N 个点,对于每个点,我们再遍历大约32个2的幂(log(MAX_VAL),其中 MAX_VAL 是坐标的最大差值)。每次查询哈希集合是 O(1) 的。所以总的时间复杂度是 O(N * log(MAX_VAL))。这个速度对于 N = 2*10^5 来说是绰绰有余的!

  • 空间复杂度: O(N) 的说。 我们主要用了一个哈希集合 s 和一个向量 x 来存储输入的 N 个点,所以空间复杂度是 O(N)。

知识点与总结

这道题最核心的地方就是通过数学推导,将问题的规模大大简化了呢!

  1. 数学洞察力: 证明满足条件的集合大小最大为3,是解决本题的关键。这一下就把一个看起来很复杂的组合问题,变成了一个简单的、有固定上限的搜索问题。
  2. 贪心策略: 我们的解法其实是一种贪心。因为我们知道最好的答案是3,所以我们优先寻找大小为3的解。如果找不到,再寻找次好的解(大小为2),以此类推。这种从最优解开始尝试的思路在很多问题中都很有用。
  3. 哈希集合的应用: std::unordered_set 是处理这类“判断元素是否存在”问题的神器!它能提供平均 O(1) 的查询效率,避免了在数组或排序向量中进行 O(N) 或 O(logN) 的查找,大大提升了算法性能。

希望这篇题解能帮到主人哦!下次遇到难题也不要怕,静下心来仔细分析,总能找到线索的!加油喵~!

Released under the MIT License.