2025年西华师范大学程序设计算法协会算法培训 & XCPC集训队专题训练赛 — 枚举优化专题
A - 江月诗的手机密码
题目背景
江月诗(jiangys)是xxxx大学的一个不得了的程序猿,不过你可能不知道,这位技术大神心里,其实悄悄装着一个人,我们就叫她舒妤吧!!!
那天,舒妤问江月诗:你的手机密码是多少?
江月诗:嘛?我仔细想想…
舒妤:你仿佛在逗我…
…
江月诗:我的手机设置过好多次密码,后来都是用指纹解锁,所以忘记密码辣。但是我记得可能是那几个密码
舒妤:那你务必告诉我…
江月诗:…...
题目描述
江月诗的手机密码是 \(6\) 位数字,但他只记得几个关键特征。舒妤拿到了一批候选密码,需要根据这些特征筛选出可能正确的密码。
已知密码需满足以下所有条件:
-
密码中所有数字均不包含 \(0\)(即每位数字只能是 \(1-9\));
-
密码的第一位数字与最后一位数字的乘积为 \(3\) 的倍数;
-
密码中所有数字的乘积能被 \(6\) 整除;
-
密码是回文数(从左到右读与从右到左读完全相同,例如 \(123321\))。
输入格式
第一行输入一个整数 \(N\)(\(1 \leq N \leq 100\)),表示候选密码的数量。
接下来 \(N\) 行,每行输入一个 \(6\) 位数字字符串(可能包含前导零,例如 "\(001234\)")。
输出格式
按输入顺序输出所有符合条件的密码,每个密码占一行。
若没有符合条件的密码,输出 N0ne。
输入样例
5
123321
237732
347743
627726
012210
输出样例
347743
627726
题解
本题就是一个简单的模拟题,直接按照题目要求模拟就行了,但是我们要注意,所有密码都不满足的时候我们是输出N0ne这个大家容易忽视,所以对于这种直接输出的题目,我们尽量还是复制题面。
Code:
#include <bits/stdc++.h>
#define I_can_AK int main
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using vi = vector<int>;
using vll = vector<ll>;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vst = vector<string>;
const int MAXN = 1e6 + 5;
void Solve(){
int n;
cin >> n;
vi t(6);
bool ok = 0;
while(n--){
bool bl = 0;
for(int i = 0; i < 6; i++){ //输入数据
char ch; cin >> ch;
t[i] = ch - '0';
if(t[i] == 0) bl = 1; //判断是否包含0
}
if(bl) continue;
if(!(t[0] == t[5] && t[1] == t[4] && t[2] == t[3])) bl = 1; //判断是否为回文数
if((t[0]*t[5])%3 != 0) bl = 1; //判断第一位与最后一位的乘积是否为3的倍数
if((t[0]*t[1]*t[2]*t[3]*t[4]*t[5])%6 != 0) bl = 1; //判断所有数的乘积是否能被6整除
if(bl) continue;
for(int i = 0; i < 6; i++) cout << t[i];
cout << "\n";
ok = 1;
}
if(!ok) cout << "N0ne";
return ;
}
I_can_AK(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int _ = 1;
//std::cin >> _;
while(_--) Solve();
return 0;
}
B - 江月诗的区间计数
题目背景
江月诗(jiangys)是 XXXX 大学 XCPC 集训队的主力队员,舒妤是江月诗的同学,两人常一起讨论算法题。某天舒妤在练习区间统计类题目时遇到瓶颈,找到江月诗请教。
舒妤指着屏幕:“月诗,这道区间计数题我总超时,你帮我看看思路哪里有问题?题目是找包含特定字符的子区间,数据量一大就卡壳。”
江月诗接过题面,指着关键条件:“这题可以用滑动窗口,再结合数组计数,时间复杂度能降到 \(\mathcal{O}(n)\),你试试按这个方向想?”
PS:本题就是一个变式。
题目描述
给定一个长度为 \(n\) 的字符串 \(s\)(仅包含 a、b、c 三种字符),请统计所有满足以下条件的子区间 \([l,r]\)(连续子串)的数量:
- 子区间内至少包含 \(1\) 个
a、\(1\) 个b、\(1\) 个c(顺序不限)。 - 子区间的长度不超过 \(k\) (若 \(k=0\) 则无长度限制)。
输入格式
第一行输入两个整数 \(n\) 和 \(k\)(\(1 \leq n \leq 10^5\),\(0 \leq k \leq n\))。
第二行输入字符串 \(s\)(长度为 \(n\))。
输出格式
一行一个整数表示满足条件的区间个数。
输入样例
7 5
abcabca
输出样例
12
说明/提示
【样例解释】
合法子区间需包含 a、b、c 各至少 \(1\) 个,且长度 \(\leq 5\)。
满足条件的区间有:\([1,3]\)、\([1,4]\)、\([1,5]\)、\([2,4]\)、\([2,5]\)、\([2,6]\)、\([3,5]\)、\([3,6]\)、\([3,7]\)、\([4,6]\)、\([4,7]\)、\([5,7]\)。
题解
这个就是一个简单的双指针(滑动窗口),只不过有一个窗口大小的限制 \(k\),还有就是统计区间数量的时候你要想好应该怎么计数,具体代码如下。
Code:
#include <bits/stdc++.h>
#define I_can_AK int main
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using vi = vector<int>;
using vll = vector<ll>;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vst = vector<string>;
const int MAXN = 1e6 + 5;
void Solve(){
int n, k;
cin >> n >> k;
string s;
cin >> s;
vi lst(3, -1); //lst[0],lst[1],lst[2]分别表示a,b,c最后一次出现的位置
ll ans = 0;
for (int r = 0; r < n; r++) {
// 更新当前字符的最后出现位置
if (s[r] == 'a') lst[0] = r;
else if (s[r] == 'b') lst[1] = r;
else if (s[r] == 'c') lst[2] = r;
// 检查三种字符是否都至少出现一次
if (lst[0] != -1 && lst[1] != -1 && lst[2] != -1) {
int mn_lst = min({lst[0], lst[1], lst[2]}); //找出当前的位置r至少包含a,b,c的最大的一个l,也就是最小的区间
int mn_l = 0; //找出当前位置r包含a,b,c的最小的l,也就是最大的区间
if (k > 0) { //最小的l需要满足区间的大小不超过k
mn_l = max(0, r - k + 1);
}
// 如果最小最后位置不小于左边界下限,则累加数量,也就是要满足区间大小不超过k
if (mn_lst >= mn_l) {
ans += (mn_lst - mn_l + 1);
}
}
}
cout << ans << "\n";
return ;
}
I_can_AK(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int _ = 1;
// std::cin >> _;
while(_--) Solve();
return 0;
}
C - 江月诗的书籍选取
题目描述
江月诗计划去图书馆借书。图书馆的书架上有许多书,每本书都有一个唯一的编号和所属的类别(如文学、历史、科学等)。为了满足他的需求,他希望借阅的书籍至少涵盖所有不同类别的书籍。
他需要在书架上选择一个连续的区间,确保这个区间的书籍包含图书馆内每个类别的至少一本书。他想要找到一个区间,使得该区间的长度最小。
这个区间的长度等于该区间最小和最大书籍编号之间的差(即区间的“大小”)。
现在请你帮助江月诗计算出一个包含每种不同书籍类别的最小区间大小。
输入格式
第一行:书籍的数量 \(N\)(\(1 \leq N \leq 50,000\))。
接下来的 \(N\) 行:每行包含两个用空格分隔的正整数,分别表示每本书的编号和所属的类别ID。这两个数字的最大值为 \(10^9\)。
输出格式
输出一行:包含每种不同书籍类别的最小区间大小。
样例输入
6
25 7
26 1
15 1
22 3
20 1
30 1
样例输出
4
说明/提示
【样例解释】
在这个图书馆的书架上,有 \(6\) 本书,编号分别为 \(25\)、\(26\)、\(15\)、\(22\)、\(20\)、\(30\),类别ID分别为 \(7\)、\(1\)、\(1\)、\(3\)、\(1\)、\(1\)。
从编号 \(22\) 到编号 \(26\) 的区间(总大小为 \(4\))包含了图书馆内每种不同类别的书籍:\(1\)、\(3\) 和 \(7\)。因此,这段区间的大小是最小的,且其大小为 \(4\)。
题解
这个题我们看见要找最小区间还是很容易就能想到使用双指针(滑动窗口)来维护,看这个区间内部是否已经包含了所有的品类,如果包含了所有的品类我们就记录这个区间是不是最小的区间,但是我们看到题目中的数据范围,品类的编号是 \(10^9\) ,我们该如何记录判断某个品类是否已经存在于这个区间里面呢?我们再观察题目数据,书籍的数量在 \(50000\) 以内,所以包含的品类数目不会超过 \(50000\) 所以在 \(10^9\) 里面可以用到的品类编号很少,我们可以用离散化的思想把品类编号映射到小的数据范围里面,此时我们就可以用一个数组来维护某个品类在这个区间出现了几次,具体代码如下。
Code:
#include <bits/stdc++.h>
#define I_can_AK int main
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using vi = vector<int>;
using vll = vector<ll>;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vst = vector<string>;
const int MAXN = 1e6 + 5;
struct ty {
int k; // 书籍的编号
int id; // 书籍类别ID(离散化后)
};
void Solve(){
int n, ff = 0;
cin >> n;
map<int, int> a; // 离散化映射:原始类别ID → 连续整数ID
vector<ty> f(n + 1); // 存储书籍信息(编号k和离散化类别id),后续会按k排序
for (int i = 1; i <= n; i++) {
int tmp;
cin >> f[i].k >> tmp;
if (!a.count(tmp)) { // 若类别未被离散化,分配新ID
ff++;
a[tmp] = ff;
}
f[i].id = a[tmp]; // 记录离散化后的类别ID
}
// 按书籍编号k从小到大排序
sort(f.begin() + 1, f.begin() + n + 1, [](ty a, ty b) -> bool {return a.k < b.k;});
// 初始化滑动窗口
int l = 1, r = 1, ans = INT_MAX;
vi c(50001, 0); // 计数数组:记录当前窗口中每个类别的出现次数
c[f[1].id]++; // 初始窗口包含第一本书的类别
int num = 1; // 当前窗口包含的类别数目
for (r = 2; r <= n; r++) {
if (!c[f[r].id]) { // 若该类别是首次加入窗口
num++; // 窗口类别数+1
}
c[f[r].id]++; // 该类别计数+1
// 收缩左边界:当左边界类别在窗口中出现次数>1时,可移除左边界元素
while (c[f[l].id] > 1) {
c[f[l].id]--; // 左边界类别计数-1
l++; // 左边界右移(缩小窗口)
}
// 若窗口包含所有类别,更新最小区间大小(区间大小 = 最大编号 - 最小编号)
if (num == ff) {
ans = min(ans, f[r].k - f[l].k);
}
}
cout << ans << "\n";
return ;
}
I_can_AK(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int _ = 1;
//std::cin >> _;
while(_--) Solve();
return 0;
}
D - 江月诗的无人机飞行路径
题目描述
江月诗正在进行一项无人机飞行训练。他的任务是控制多架无人机在飞行训练场地上执行精确的飞行任务。在这个特别宽广的飞行训练场地上,有 \(n\) 架无人机分别停在不同的位置,位置分别为 \(p_1 < p_2 < \cdots < p_n\)。江月诗坐在其中一架无人机旁,准备开始他的飞行训练。
每架无人机都会飞行到距离它当前位置第 \(k\) 近的无人机。具体来说,如果无人机位于位置 \(p_i\),它将飞行到与其他点 \(p_j\) 的距离从小到大排序后,第 \(k\) 小的一个位置,即:
解释:
-
严格比 $ p_j $ 近的位置 $ \leq k $(即 $ |{ p_a : |p_a - p_i| < |p_j - p_i| }| \leq k $);
-
小于等于 $ p_j $ 近的位置 $ > k $(即 $ |{ p_a : |p_a - p_i| \leq |p_j - p_i| }| > k $)。
如果选择的位置 \(p_j\) 不唯一,无人机会选择距离训练场起始点(位置 \(0\))最近的无人机。
现在,江月诗需要计算从每架无人机出发,经过 \(m\) 次飞行后,最终停留在哪一架无人机的位置。
输入格式
第一行:三个整数 \(n\)、\(k\) 和 \(m\)(\(1 \le k < n \le 1,000,000, 1 \le m \le 10^{18}\)),分别表示无人机数量、参数 \(k\) 和飞行的次数。
第二行:\(n\) 个整数 \(p_j\)(\(1 \le p_1 < p_2 < \cdots < p_n \le 10^{18}\)),表示无人机的位置。
输出格式
输出一行,包含 \(n\) 个整数 \(r_1, r_2, \cdots, r_n\),用空格分隔。每个数字 \(r_i\) 表示从输入顺序中的第 \(i\) 架无人机开始飞行 \(m\) 次后,最终停留的无人机编号。
输入样例
5 2 4
1 2 4 7 10
输出样例
1 1 3 1 1
说明/提示
【样例说明】
在飞行训练场地中,江月诗有 \(5\) 架无人机,位置分别为 \(1\)、\(2\)、\(4\)、\(7\)、\(10\)。江月诗需要为每一架无人机计算出,经过 \(m\) 次飞行后,最终它会停留在哪个位置,下图为样例的每个坐标的单次飞行方式。

题解
这个题我们很容易想到,可以使用滑动窗口来进行计算单次飞行后停留的位置,固定窗口的大小为 \(k+1\) 尽量保证当前的位置在窗口的中间,然后判断左右端点与当前位置的距离谁更大就选择哪个作为目标点,剩下就是 \(m\) 次飞行的模拟,我们可以看到 \(m\) 的数据范围为 \(1 \le m \le 10^{18}\) 如果我们直接模拟肯定超时,所以我们就应该想到使用倍增的方法来优化,把 \(m\) 按照二进制位分解,递推计算 \(2^i\) 次飞行后停留的位置,具体代码如下。
Code:
#include <bits/stdc++.h>
#define I_can_AK int main
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using vi = vector<int>;
using vll = vector<ll>;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vst = vector<string>;
const int MAXN = 1e6 + 5;
void Solve(){
ll n, k, m;
cin >> n >> k >> m;
vll a(n + 2), f(n + 2), ff(n + 2), ans(n + 2);
//a为无人机原始位置,f和ff为倍增计算的临时数组,ans为最终答案
for (int i = 1; i <= n; i++) cin >> a[i];
// 双指针法寻找第i架无人机的第k近目标
int l = 1, r = k + 1; // 初始左右指针,因位置有序,第k近目标初步范围在[l, r]
if (r > n) r = n; // 边界处理:若r超出范围则设为n
f[1] = r; // 第1架无人机的单次飞行目标初始化为r
// 计算每架无人机的单次飞行目标(f[i])
for (int i = 2; i <= n; i++) { // 从第2架无人机开始处理
// 调整左右指针:寻找i的第k近位置
// 当右指针可右移,且右移后距离i更近时,移动指针
while (r + 1 <= n && a[i] - a[l] > a[r + 1] - a[i]) {
if (l < r) l++; // 左指针右移(缩小左范围)
r++; // 右指针右移(扩大右范围) 这里保证我们的区间大小为k+1,因为是第k近的飞机所以移动的目标点一定在这个区间里面
if (r > n) {
r = n;
break;
}
}
// 确定第k近目标:若左侧距离≥右侧距离,选左侧(编号小,距离起点更近),否则选右侧
if (a[i] - a[l] >= a[r] - a[i]) f[i] = l;
else f[i] = r;
}
// 初始化结果数组:ans[i] = i 表示0次飞行时停在自身位置
for (int i = 1; i <= n; i++) ans[i] = i;
// 倍增法处理m次飞行
while (m) { // 分解m为二进制,处理每一位
if (m & 1) { // 若当前二进制位为1,累加对应次数的飞行
for (int i = 1; i <= n; i++) ans[i] = f[ans[i]];
// ans[i]更新为再飞2^b次后的位置(b为当前位数)
}
m >>= 1; // 右移一位,处理更高位
// 更新f数组:f[i]变为"飞行2^b次"的目标(原f是2^(b-1)次,更新后为2^b次)
for (int i = 1; i <= n; i++) ff[i] = f[i]; // 暂存当前f
for (int i = 1; i <= n; i++) f[i] = ff[ff[i]]; // 新f[i] = 先飞2^(b-1)次,再飞2^(b-1)次
}
for (int i = 1; i <= n; i++) cout << ans[i] << ' ';
cout << '\n';
return ;
}
I_can_AK(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
int _ = 1;
//std::cin >> _;
while(_--) Solve();
return 0;
}

浙公网安备 33010602011771号