#7317. 2026/6/12/YZR笔记(逻辑思维)

2026/6/12/YZR笔记(逻辑思维)

课堂笔记:构造、数论、位运算与 MEX

0. 今日知识地图

模块 对应题目 核心方法
构造排列 The 67th Permutation Problem 从两端取数,按规律输出一个合法排列
数论构造 The 67th OEIS Problem 质数筛 + 相邻质数乘积构造
位运算暴力 The 67th XOR Problem 枚举两两异或,取最大值
排序 + MEX Shifted MEX 排序去重,找最长连续段

一、构造题:The 67th Permutation Problem

1. 题型特征

题目要求输出一个长度为 3n 的排列。

构造题的关键不是“搜索答案”,而是:

  • 先观察样例和限制;
  • 找到一种稳定的输出规律;
  • 保证每个数只出现一次;
  • 保证输出范围是 1 ~ 3n

2. 最终思路

最终通过代码使用了“双指针构造”:

int l = 1, r = n * 3;
while (l < r) {
    cout << l << " " << r << " " << r - 1 << " ";
    l++;
    r -= 2;
}

每次输出 3 个数:

  1. 当前最小的 l
  2. 当前最大的 r
  3. 当前次大的 r - 1

然后:

l++;
r -= 2;

这样每一轮刚好消耗 3 个不同的数。


3. 为什么不会重复?

初始时:

l = 1;
r = 3 * n;

每轮取:

l, r, r - 1

下一轮:

l 变成 l + 1
r 变成 r - 2

所以左边依次取:

1, 2, 3, ...

右边依次取:

3n, 3n-1, 3n-2, 3n-3, ...

左右两边不会撞在一起,直到刚好输出 3n 个数。


4. 关键代码

#include<bits/stdc++.h>
using namespace std;

int T, n;

int main() {
    cin >> T;
    while (T--) {
        cin >> n;
        int l = 1, r = n * 3;

        while (l < r) {
            cout << l << " " << r << " " << r - 1 << " ";
            l++;
            r -= 2;
        }
        cout << "\n";
    }
    return 0;
}

5. 易错点

今天这题尝试次数较多,主要问题集中在:

  • cnt 更新规律不稳定,容易让数字重复或越界;
  • 没有从“每次消耗 3 个数”这个角度设计构造;
  • 构造题不要靠随机试,最好先证明每个数是否只出现一次。

建议做构造题时先问自己三件事:

  1. 输出的数是否都在合法范围内?
  2. 是否每个数只出现一次?
  3. 是否一共输出了正确数量的数?

二、数论构造:The 67th OEIS Problem

1. 题型特征

题目要求构造一个长度为 n 的数列,使相邻两个数的 gcd 满足特定规律。

从最终代码可以看出,这题的核心是:

用质数制造相邻位置之间可控的最大公因数。


2. 核心构造

最终输出规律:

if (i == 1 || i == 2) cout << i << " ";
else cout << zs[i - 2] * zs[i - 1] << " ";

也就是:

a1 = 1
a2 = 2
a3 = 第1个质数 * 第2个质数 = 2 * 3 = 6
a4 = 第2个质数 * 第3个质数 = 3 * 5 = 15
a5 = 第3个质数 * 第4个质数 = 5 * 7 = 35
...

这样相邻两项会共享一个质因子。

例如:

1, 2, 6, 15, 35, 77, ...

相邻 gcd:

gcd(1, 2) = 1
gcd(2, 6) = 2
gcd(6, 15) = 3
gcd(15, 35) = 5
gcd(35, 77) = 7

刚好得到一串递增的质数或特殊起点。


3. 质数筛模板

这题需要先筛出足够多的质数。

int a[1001000], zs[1001000];
int cnt = 0;

a[0] = 1;
a[1] = 1;

for (int i = 2; i <= 1000000; i++) {
    if (a[i] == 0) {
        zs[++cnt] = i;
        for (int j = i * 2; j <= 1000000; j += i) {
            a[j] = 1;
        }
    }
}

含义:

  • a[i] == 0:当前 i 是质数;
  • a[i] == 1:当前 i 不是质数;
  • zs[cnt]:存第 cnt 个质数。

4. 完整关键代码

#include<bits/stdc++.h>
#define int long long
using namespace std;

int T, n;
int a[1001000], zs[1001000];

signed main() {
    a[0] = 1;
    a[1] = 1;

    int cnt = 0;
    for (int i = 2; i <= 1000000; i++) {
        if (a[i] == 0) {
            zs[++cnt] = i;
            for (int j = i * 2; j <= 1000000; j += i) {
                a[j] = 1;
            }
        }
    }

    cin >> T;
    while (T--) {
        cin >> n;
        for (int i = 1; i <= n; i++) {
            if (i == 1 || i == 2) cout << i << " ";
            else cout << zs[i - 2] * zs[i - 1] << " ";
        }
        cout << "\n";
    }

    return 0;
}

5. 今日易错点

从提交记录看,这题中间出现过几个典型问题:

错误 1:筛法循环写错

错误倾向:

for (int j = i + a[i]; j <= 10000; j += a[i])

这里 a[i] 是标记数组,不是步长。筛倍数时步长应该是 i

正确:

for (int j = i * 2; j <= 1000000; j += i)

错误 2:质数标记含义反了

建议统一记法:

a[i] == 0 表示还没有被筛掉,也就是质数
a[i] == 1 表示合数

并且一开始要写:

a[0] = a[1] = 1;

错误 3:乘法可能爆 int

最终代码用了:

#define int long long

这是必要的。因为质数乘质数可能比较大,普通 int 有溢出风险。


三、位运算:The 67th XOR Problem

1. 题型特征

给一个长度为 n 的数组,要求找出两个数异或后的最大值。

异或符号是:

^

例如:

5 ^ 3

表示 53 按二进制逐位异或。


2. 数据范围决定做法

这题 n 大约在 3105 级别,可以直接枚举两两组合。

时间复杂度:

O(n^2)

n ≈ 3000 时:

3000 * 3000 = 9,000,000

这个数量可以接受。


3. 核心代码

int ans = INT_MIN;

for (int i = 1; i <= n; i++) {
    for (int j = i + 1; j <= n; j++) {
        ans = max(ans, a[i] ^ a[j]);
    }
}

注意第二层循环从:

j = i + 1

开始,避免自己和自己异或,也避免重复枚举。


4. 完整关键代码

#include<bits/stdc++.h>
using namespace std;

int T, n, a[3110];

int main() {
    cin >> T;
    while (T--) {
        cin >> n;
        for (int i = 1; i <= n; i++) {
            cin >> a[i];
        }

        int ans = INT_MIN;
        for (int i = 1; i <= n; i++) {
            for (int j = i + 1; j <= n; j++) {
                ans = max(ans, a[i] ^ a[j]);
            }
        }

        cout << ans << "\n";
    }

    return 0;
}

5. 易错点

  • ^ 是异或,不是乘方。
  • 如果题目要选两个不同位置,内层循环应从 i + 1 开始。
  • 看到 n <= 3000,优先考虑 O(n^2) 暴力,不要一上来想复杂数据结构。

四、排序 + MEX:Shifted MEX

1. 题型特征

题目和 MEX 有关。

MEX 指一个序列中没有出现过的最小非负整数。

例如:

MEX([1, 2, 0, 5]) = 3
MEX([1, 2, 4, 9]) = 0

这题允许整体平移数组,也就是所有数同时加上同一个 x

目标是让平移后的数组 MEX 尽量大。


2. 核心转化

如果平移后想让:

0, 1, 2, ..., k-1

都出现,那么平移前数组中必须存在一段连续的数:

x, x+1, x+2, ..., x+k-1

所以问题转化为:

在原数组中找最长的连续整数段长度。

例如:

原数组:2 4 1 0 -1
排序后:-1 0 1 2 4
最长连续段:-1, 0, 1, 2
答案:4

3. 做法

  1. 排序;
  2. 跳过重复数字;
  3. 如果当前数等于前一个数 +1,连续长度加一;
  4. 否则更新答案,重新开始计数。

4. 关键代码

sort(a + 1, a + n + 1);
a[n + 1] = -2e9;

int s = 1, ans = 1;

for (int i = 2; i <= n + 1; i++) {
    if (a[i] == a[i - 1]) continue;

    if (a[i] == a[i - 1] + 1) {
        s++;
    } else {
        ans = max(ans, s);
        s = 1;
    }
}

cout << ans << "\n";

这里加了一个哨兵:

a[n + 1] = -2e9;

它的作用是强制最后一段连续段也被结算。


5. 完整关键代码

#include<bits/stdc++.h>
using namespace std;

int T, n;

int main() {
    cin >> T;
    while (T--) {
        cin >> n;
        int a[3010] = {0};

        for (int i = 1; i <= n; i++) {
            cin >> a[i];
        }

        sort(a + 1, a + n + 1);
        a[n + 1] = -2e9;

        int s = 1, ans = 1;
        for (int i = 2; i <= n + 1; i++) {
            if (a[i] == a[i - 1]) continue;

            if (a[i] == a[i - 1] + 1) {
                s++;
            } else {
                ans = max(ans, s);
                s = 1;
            }
        }

        cout << ans << "\n";
    }

    return 0;
}

6. 今日易错点

易错点 1:重复数字不能打断连续段

例如:

1 1 2 3

最长连续段仍然是:

1, 2, 3

重复的 1 应该跳过:

if (a[i] == a[i - 1]) continue;

易错点 2:最后一段要记得更新答案

如果没有哨兵,循环结束后还要补一句:

ans = max(ans, s);

今天最终代码用的是哨兵写法,也可以。


五、今日整体复盘

1. 做得好的地方

  • 4 道题最终都拿到了 100
  • 能从调试尝试中逐步修正方向。
  • O(n^2) 暴力的判断比较准确,XOR 题很快通过。
  • MEX 题最终抓住了“最长连续段”这个核心转化。

2. 需要加强的地方

1)构造题要先证明,再提交

构造题最怕“看起来像对的”。

提交前至少检查:

范围对不对?
数量对不对?
有没有重复?
能不能覆盖边界 n=1、n=2?

2)数论筛法要写熟

质数筛是基础模板,建议背熟下面这个版本:

vis[0] = vis[1] = 1;
for (int i = 2; i <= N; i++) {
    if (!vis[i]) {
        prime[++cnt] = i;
        for (int j = i * 2; j <= N; j += i) {
            vis[j] = 1;
        }
    }
}

特别注意:

j += i

不是 j += vis[i],也不是 j += a[i]


3)MEX 题常常要转化

看到 MEX,不一定直接模拟 MEX。

这题的关键不是找当前 MEX,而是问:

平移之后,怎样让 0,1,2,... 尽量完整出现?

于是转化成了原数组中的最长连续段。


六、课后练习建议

必练 1:手写质数筛

要求不看模板,写出:

vis[0] = vis[1] = 1;
for (int i = 2; i <= N; i++) {
    if (!vis[i]) {
        prime[++cnt] = i;
        for (int j = i * 2; j <= N; j += i) {
            vis[j] = 1;
        }
    }
}

并回答:

  • vis[i] = 0 表示什么?
  • 为什么 ji * 2 开始?
  • 为什么步长是 i

必练 2:排序后找最长连续段

给数组:

4 2 3 6

排序后:

2 3 4 6

最长连续段是:

2 3 4

答案是:

3

再练一个带重复的:

1 1 2 2 3 5

答案应该是:

3

因为最长连续段是:

1 2 3

必练 3:判断复杂度

如果 n <= 3000,两层循环大约是:

9 * 10^6

一般可以接受。

所以 XOR 最大值这类题可以直接:

for (int i = 1; i <= n; i++) {
    for (int j = i + 1; j <= n; j++) {
        ans = max(ans, a[i] ^ a[j]);
    }
}

七、一句话总结

今天的重点不是单个语法点,而是四种常见竞赛思维:

构造:找规律并证明不重复
数论:用质数和 gcd 设计性质
位运算:根据数据范围选择 O(n^2)
MEX:把目标转化成最长连续段