牛客小白月赛110
A、智乃办赛
1、字母部分计算:每个字母对应500个编号。将输入的n减1后除以500得到字母的索引(0对应'A',1对应'B',以此类推)。
2、数字部分计算:n减1后对500取余,得到余数再加1,确保数字范围为1到500。使用printf的格式控制%03d保证数字部分为三位数,不足三位时补前导零。
3、组合输出:将字母和数字部分拼接,直接输出结果。
代码实现:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
// 计算字母部分:每500个数一个字母
int letter_index = (n - 1) / 500;
char letter = 'A' + letter_index;
// 计算数字部分:余数转换为1-500,补前导零
int number = (n - 1) % 500 + 1;
// 输出格式化为字母+三位数
printf("%c%03d", letter, number);
return 0;
}
B、智乃的Wordle
1、字符存在性判断:首先遍历 secret 单词,记录每个字符是否存在。这一步用布尔数组 exist 实现,exist[c] 表示字符 c 是否在 secret 中出现。
2、绿色标记处理:第一次遍历比较 secret 和 guess 的每一位,若字符相同则标记为 g,同时检查是否所有位置都正确。
3、黄色/红色标记处理:第二次遍历处理非绿色标记的位置。若该字符存在于 secret 中(无论是否被正确匹配过),则标记为 y;否则标记为 r。
4、结果输出:根据标记结果生成字符串,并输出是否完全正确的提示。
代码实现:
#include <iostream>
#include <string>
using namespace std;
int main() {
string secret, guess;
cin >> secret >> guess;
// 记录secret中的字符是否存在
bool exist[26] = {false};
for (char c : secret) {
exist[c - 'a'] = true;
}
string result(8, ' ');
bool allCorrect = true;
// 第一次遍历:处理绿色标记(g)
for (int i = 0; i < 8; ++i) {
if (secret[i] == guess[i]) {
result[i] = 'g';
} else {
allCorrect = false; // 存在不匹配的位置
// 暂时不处理黄/红标记,后续统一处理
}
}
// 第二次遍历:处理黄色和红色标记(y/r)
for (int i = 0; i < 8; ++i) {
if (result[i] != 'g') { // 非绿色位置需要进一步判断
if (exist[guess[i] - 'a']) {
result[i] = 'y';
} else {
result[i] = 'r';
}
}
}
// 输出结果
cout << result << endl;
cout << (allCorrect ? "congratulations" : "defeat") << endl;
return 0;
}
C、智乃的数字
(一)题目分析
我们需要找到第k大的“智数”。智数的定义是满足以下条件之一的奇数:
- 以5结尾。
- 各个数位之和是3的倍数(等价于该数能被3整除)。
通过分析,我们可以将问题转化为数学公式,并使用二分法快速确定第k个智数的位置。
(二)方法思路
1、数学公式推导:
- 以5结尾的奇数:构成等差数列5, 15, 25, ...,公差为10。
- 能被3整除的奇数:构成等差数列3, 9, 15, 21, ...,公差为6。
- 同时满足以上两个条件的数:构成等差数列15, 45, 75, ...,公差为30。
2、容斥原理:计算到某个数N时的智数总数,需减去重复计数的部分。
3、二分查找:通过二分法确定满足条件的最小N,使得前N个数中的智数总数等于k。
代码实现:
#include <iostream>
using namespace std;
typedef long long ll;
ll compute_count(ll n) {
if (n < 3) return 0;
ll count1 = (n >= 5) ? ((n - 5) / 10 + 1) : 0;
ll count2 = (n >= 3) ? ((n - 3) / 6 + 1) : 0;
ll count3 = (n >= 15) ? ((n - 15) / 30 + 1) : 0;
return count1 + count2 - count3;
}
ll find_kth(int k) {
ll low = 1, high = 1e18;
while (low <= high) {
ll mid = (low + high) / 2;
ll cnt = compute_count(mid);
if (cnt < k) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return low;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int k;
cin >> k;
cout << find_kth(k) << "\n";
}
return 0;
}
D、智乃与长短期主义者博弈
(一)题目分析
两位玩家轮流从一排数字的两端取数。短期主义者(先手)每次选择较大的数,若相等则选左边;长期主义者(后手)选择全局最优策略。我们需要计算两者的最终得分。
(二)思路解析
1、动态规划(DP)状态定义:
表示当剩余区间为
且当前为短期主义者取数时,当前玩家的净得分(当前得分减去对方后续得分)。
表示当剩余区间为
且当前为长期主义者取数时,当前玩家的净得分。
2、状态转移:
- 短期主义者的选择是确定的:取左右两端较大的数,若相等则取左边。
- 长期主义者需选择使全局最优的解,即比较取左或右后的净得分,取最大值。
3、初始条件:当区间长度为1时,玩家直接取该数,净得分为该数值。
4、计算总净得分:通过动态规划填充所有区间的状态,最终得到全局净得分 S,进而计算两位玩家的具体得分。
代码实现:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
int sum = 0;
for (int i = 0; i < n; ++i) {
cin >> a[i];
sum += a[i];
}
// 定义两个二维DP数组,分别表示当前玩家是短期(dp0)或长期(dp1)时的净得分
vector<vector<int>> dp0(n, vector<int>(n, 0)); // 短期玩家的DP表
vector<vector<int>> dp1(n, vector<int>(n, 0)); // 长期玩家的DP表
// 初始化区间长度为1的情况
for (int i = 0; i < n; ++i) {
dp0[i][i] = a[i];
dp1[i][i] = a[i];
}
// 从区间长度2开始,逐步处理更大的区间
for (int len = 2; len <= n; ++len) {
for (int left = 0; left <= n - len; ++left) {
int right = left + len - 1;
// 计算短期主义者的选择:取较大的一端,相等则左
if (a[left] >= a[right]) {
// 选择左端,剩余区间由长期主义者处理
dp0[left][right] = a[left] - dp1[left + 1][right];
} else {
// 选择右端,剩余区间由长期主义者处理
dp0[left][right] = a[right] - dp1[left][right - 1];
}
// 计算长期主义者的选择:取左右中更优的结果
int chooseLeft = a[left] - dp0[left + 1][right]; // 选左端的净得分
int chooseRight = a[right] - dp0[left][right - 1]; // 选右端的净得分
dp1[left][right] = max(chooseLeft, chooseRight); // 选择最优解
}
}
// 计算总净得分并得出两位玩家的最终得分
int totalDiff = dp0[0][n - 1];
int shortScore = (sum + totalDiff) / 2;
int longScore = (sum - totalDiff) / 2;
cout << shortScore << " " << longScore << endl;
return 0;
}
E、智乃的跳跃排序
(一)题目理解
给定一个长度为N的数组,元素互不相同。我们只能交换满足以下条件的一对元素:
- 下标差为k;
- 元素值的差为k。
要求判断是否可以通过这样的交换将数组排序为升序。
(二)关键点分析
- 交换规则的特殊性:不能随意交换元素,必须满足下标或值的差为k。
- 分组思想:若元素值的差为k,它们可以形成一条链式结构,这样的元素可以相互交换。
- 模k余数匹配:每个元素在排序后的正确位置的模k余数必须与原位置的模k余数在对应的链中保持一致。
(三)解题思路
- 排序数组:首先得到排序后的数组作为目标顺序。
- 余数分组:对于每个元素,计算其在原数组中的位置模k余数,以及在排序后数组中的位置模k余数。
- 数值链分析:遍历每个元素,找到所有与之数值差为k的元素形成的链。对于每个链中的元素,检查其原余数集合和排序后的余数集合是否相同(排序后顺序可不同,但集合元素必须相同)。
- 判断条件:若所有链的余数集合匹配,则可以排序成功,否则失败。
(四)具体步骤
- 排序目标数组:获得排序后的正确顺序。
- 建立映射关系: now:记录原数组中每个元素的位置模k余数。zhen:记录排序后每个元素的位置模k余数。
- 遍历数值链:使用哈希表标记已处理的元素。对于每个未被处理的元素,向上下延伸找到所有数值差为k的元素,形成链。
- 比较余数集合:将链中原余数和目标余数分别收集并排序,若不一致则输出"No"。
- 最终判断:所有链均通过检查则输出"Yes"。
代码实现:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n, k;
int arr[N], brr[N];
void solve() {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> arr[i];
brr[i] = arr[i]; // 复制数组用于排序
}
sort(brr + 1, brr + 1 + n); // 排序得到目标数组brr
unordered_map<int, int> r, zhen, now;
// r记录元素是否存在,zhen记录排序后的余数,now记录原数组的余数
for (int i = 1; i <= n; i++) {
r[arr[i]] = 1; // 标记元素存在
zhen[brr[i]] = i % k; // 排序后元素的位置模k余数
now[arr[i]] = i % k; // 原数组中元素的位置模k余数
}
auto idx = r.begin();
while (idx != r.end()) {
vector<int> ans, da; // ans保存当前链的原余数,da保存目标余数
int t = (*idx).first; // 当前处理的元素值
// 向上扩展:t, t+k, t+2k...
while (r[t + k] == 1) {
t += k;
ans.push_back(now[t]); // 原余数加入ans
da.push_back(zhen[t]); // 目标余数加入da
r.erase(t); // 标记已处理
}
t = (*idx).first;
// 向下扩展:t, t-k, t-2k...
while (r[t - k] == 1) {
t -= k;
ans.push_back(now[t]);
da.push_back(zhen[t]);
r.erase(t);
}
t = (*idx).first;
ans.push_back(now[t]); // 加入当前元素自身余数
da.push_back(zhen[t]);
r.erase(idx++); // 删除当前元素,迭代器后移
// 排序后比较余数集合是否一致
sort(ans.begin(), ans.end());
sort(da.begin(), da.end());
for (int i = 0; i < ans.size(); i++) {
if (ans[i] != da[i]) {
cout << "No" << endl;
return;
}
}
}
cout << "Yes" << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int T = 1;
while (T--) solve();
return 0;
}
F、智乃的数组乘积
(一)题目理解
给定一个长度为N的数组和一个质数P,要求通过最少次数的修改,使得修改后的数组满足:不存在任何子区间乘积模P等于给定值x。每次修改可将元素变为任意小于P的非负整数。
(二)关键点分析
- x=0的特殊处理:需确保数组中所有元素均不为0,否则存在单个元素为0的子区间。
- x≠0的动态维护:通过维护前缀积和危险值集合,避免后续乘积产生x的同余值。
- 打断策略:遇到可能产生危险值时,通过修改元素为0,切断前缀积链,重新开始计算,可以证明这样的修改次数一定是最少的。
(三)解题思路
- x=0的情况:将所有0元素改为非0数(如1),保证所有子区间乘积非0。修改次数等于原数组中0的个数。
- x≠0的情况:维护前缀积res和危险值集合book。遍历数组,计算当前前缀积与元素的乘积ans。若ans存在于book中,说明保留当前元素会导致存在危险子区间,需将其修改为0,并重置前缀积。否则,更新前缀积,并将新的危险值()加入集合。
代码实现:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 10;
const int INF = 1e18;
int arr[N], jc[N];
int n, p, x;
void solve() {
cin >> n >> p >> x;
for (int i = 1; i <= n; i++) {
cin >> arr[i];
}
if (x) {// 处理x≠0的情况
int res = 1; // 维护当前前缀积
unordered_map<int, int> book;
book[x] = 1;// 初始危险值为x
for (int i = 1; i <= n;) {
int ans = res * arr[i] % p; // 计算当前前缀积与当前元素的乘积
if (book[ans]) arr[i] = 0;// 若当前乘积可能导致后续出现x,修改当前元素为0以打断链条
if (arr[i] == 0) {
res = 1;// 重置前缀积
book.clear();// 清空危险值集合
book[x] = 1; // 重新初始化
i++;// 处理下一个元素
} else {
res = res * arr[i] % p;// 更新前缀积
book[res * x % p] = 1;// 将危险值加入集合
i++;// 继续处理下一个元素
}
}
for (int i = 1; i <= n; i++) cout << arr[i] << " ";
cout << endl;
} else {// 处理x=0的情况
for (int i = 1; i <= n; i++) {
if (arr[i] % p == 0) { // 将所有模p等于0的元素改为1
arr[i] = 1;
}
cout << arr[i] << " ";
}
cout << endl;
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int T = 1;
// cin>>T;
while (T--) solve();
return 0;
}