蓝桥杯题型
蓝桥杯题型
蓝桥杯题型分类
语法基础
艺术与篮球(日期问题)
#include <bits/stdc++.h>
using namespace std;
map<char,int>myMap;
int basketball,calligraphy;
//日期是否合法模板
int months[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
bool check(int date)
{
int year=date/10000,month=date%10000/100,day=date%100;
if(!month||month>=13||!day)return false;
if(month!=2&&day>months[month])return false;
if(month==2)
{
int leap=(year%100!=0&&year%4==0)||year%400==0;
if(day>28+leap)return false;
}
return true;
}
int main()
{
// 插入数字与笔画数
myMap['0'] = 13;
myMap['1'] = 1;
myMap['2'] = 2;
myMap['3'] = 3;
myMap['4'] = 5;
myMap['5'] = 4;
myMap['6'] = 4;
myMap['7'] = 2;
myMap['8'] = 2;
myMap['9'] = 2;
// 遍历日期范围,从2000年1月1日到2024年4月13日
for(int i=20000101;i<=20240413;i++)
{
if(check(i))
{
string s=to_string(i);
int num=0;
for(auto j:s)
{
num+=myMap[j];
}
if(num>50)
{
basketball++;
}
}
}
cout<<basketball;
return 0;
}
时间显示(时间问题)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main()
{
ll x;
cin >> x;
// 计算小时、分钟、秒数
ll hh = (x / 3600000) % 24; // 小时数,取 24 小时制
ll mm = (x / 60000) % 60; // 分钟数,取 60 分钟
ll ss = (x / 1000) % 60; // 秒数,取 60 秒
// 输出时间格式
printf("%02lld:%02lld:%02lld", hh, mm, ss);
return 0;
}
跑步计划(日期问题)
#include <bits/stdc++.h>
using namespace std;
int months[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // 每个月的天数
// 检查数字是否包含1
bool check(int x) {
while (x) {
if (x % 10 == 1) {
return true; // 如果数字中包含1,返回true
}
x /= 10; // 去除最后一位
}
return false; // 如果没有1,返回false
}
int main() {
int ans = 0; // 总跑步的千米数
int week = 6; // 2023年1月1日是星期天,所以初始化为6(星期天)
// 遍历每个月
for (int i = 1; i <= 12; i++) {
// 遍历每个月的每一天
for (int j = 1; j <= months[i]; j++) {
week = (week % 7) + 1; // 更新星期几,确保在1到7之间循环(星期天为7)
// 如果日期、月份或星期几包含1,跑5千米
if (check(i) || check(j) || check(week)) {
ans += 5;
} else {
ans += 1; // 否则跑1千米
}
}
}
cout << ans; // 输出小蓝总共跑的千米数
return 0;
}
偶串(字符)
使用 Map 或者 unordered_map。
遍历字符串中的每个字符。对每个字符,检查它是否已经在你的数据结构中。如果是,增加它的计数。
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
string str;
cin >> str; // 输入字符串
unordered_map<char, int> char_count; // 用哈希表存储每个字符的出现次数
// 遍历字符串并统计每个字符的出现次数
for (char c : str) {
char_count[c]++;
}
// 检查是否每个字符出现次数为偶数
bool is_even = true;
for (auto& pair : char_count) {
if (pair.second % 2 != 0) { // 如果出现次数是奇数
is_even = false;
break;
}
}
// 输出结果
if (is_even) {
cout << "YES" << endl;
} else {
cout << "NO" << endl;
}
return 0;
}
创建一个大小为26的整数数组(假设为 cnt),用于存储每个小写字母的出现次数。数组的索引
0−25 分别对应字母 a-z。
遍历字符串的每一个字符(假设为 c):
将字符 c 转为其 ASCII 值。
通过计算 c - 'a' 来得到一个从 0
0 到 25 的索引,这个索引对应于字符 c。
使用这个索引来增加 cnt 数组中对应元素的值。
遍历结束后,cnt 数组中的每个元素就存储了对应字母在字符串中的出现次数。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
string s;
vector<int> cnt(26);
cin >> s;
for (auto c : s)
{
cnt[c - 'a']++;
}
for (int i = 0; i < 26; ++i)
{
if (cnt[i] % 2)
{
cout << "NO" << '\n';
return 0;
}
}
cout << "YES" << '\n';
return 0;
}
最长子序列(字符)
#include <iostream>
#include <string>
using namespace std;
int main() {
string s, t;
int num = 0;
cin >> s >> t;
for (char ch : t) {
// 查找当前字符ch在s中的位置
size_t pos = s.find(ch);
if (pos == string::npos) {
// 如果找不到字符,直接输出已找到的匹配数并结束程序
cout << num;
return 0;
} else {
// 如果找到了字符,更新字符串s并增加计数
num++;
s = s.substr(pos + 1); // 从pos+1开始截取s
}
}
// 输出最终匹配的字符数
cout << num;
return 0;
}
字母数(进制转换)
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
int a[1000];
char ch[] = { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' };
bool solve(int x)//十进制转换为16进制
{
string ans;
while(x)
{
if(ch[x%16]>='A')
{
ans+=ch[x%16];
x/=16;
}
else
{
return false;
}
}
reverse(ans.begin(),ans.end());
return true;
}
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int i=2022;
while(true)
{
i++;
if( solve(i))
{
cout<<i;
return 0;
}
}
return 0;
}
6个0(进制转换)
#include <bits/stdc++.h>
using namespace std;
bool check(int x)
{
// 检查最低的 5 位是否都为 0
for(int i = 0; i < 5; i++)
{
if(((x >> i) & 1) != 0) // 如果第 i 位不为 0,返回 false
{
return false;
}
}
return true;
}
int main()
{
int x = 2022; // 从 2022 开始查找
while(true)
{
x++; // 从 2023 开始检查
if(check(x)) // 如果 x 的最低 5 位全为 0
{
cout << x;
return 0;
}
}
return 0;
}
优秀的拆分(位运算)
输入输出样例
示例 1
输入
6
输出
4 2
示例 2
输入
7
输出
-1
7的二进制数为(0111),6的二进制数为(0110),可以发现7的二进制位的最低位(第0位)为1,值为
2 0 2^0
2
0 ,所以只要最低位为1,就不是优秀的拆分。我的从最高位开始遍历,只要第i位为1,我们就输出 1«i ,即为
2 i 2^i
2
i
#include <bits/stdc++.h>
using namespace std;
int main() {
int num;
cin >> num;
// 如果最低位为 1,输出 -1
if (((num >> 0) & 1) == 1) {
cout << -1 << endl;
return 0;
}
// 从最高位开始遍历,检查每一位
for (int i = 30; i >= 0; i--) {
// 如果当前位为 1,输出 2^i
if (((num >> i) & 1) == 1) {
cout << (1 << i) << " ";
}
}
return 0;
}
异或数列(位运算)
示例 1
输入
4
1 1
1 0
2 2 1
7 992438 1006399 781139 985280 4729 872779 563580
输出
1
0
1
1
解题思路
我们设在游戏结束后 Alice 的数变为
c
,Bob 的数变为
d
。
我们先来解决平局的情况:
根据异或性质可得:若
c = d
,则
c ⊕ d = 0
。
而
c ⊕ d = X1 ⊕ X2 ⊕ ⋯ ⊕ Xn
,所以要使
c = d
,当且仅当
X1 ⊕ X2 ⊕ ⋯ ⊕ Xn = 0
。
接下来定输赢:
我们将
c, d
转换成二进制数。对于二进制数的比较,我们是从高位往低位开始的。所以要使自己的数最大,就需要从高位开始。
设当前枚举到二进制的第
i
位。设
X1, X2, X3, ..., Xn
中一共有
cnt1
个数在该位的值为
1
,
cnt2
个数在该位的值为
0
。(
cnt1 + cnt2 = n
)
结论一 :
如果
cnt1
为偶数,则 Alice 和 Bob 无法在该位分出胜负。
证明方法和上述平局情况相同。
或者也可以这么想:
cnt1
为偶数,那么 Alice 和 Bob 要么都从这
cnt1
个数中分到偶数个,要么 Alice 和 Bob 都在这
cnt1
个数中分到奇数个;所以无论怎么分配,
c, d
在该位的异或值都必然相同。
反之当
cnt1
为奇数时,必然能决出胜负。证明方法和上述类似,就不再给出。
那么
cnt1
为奇数时如何判断谁输谁赢呢?
我们先定义,对于当前第
i
位,如果能让自己的数值从
0 → 1
,或者能让对手的数值从
1 → 0
,则自己的胜率
+1
;如果让自己的数值从
1 → 0
,或者让对手的数值从
0 → 1
,则自己的胜率
-1
;如果既不改变自己的数值,也不改变对手的数值,则自己的胜率不变。显然,游戏结束时,胜率越高的一方获胜。
结论二 :
当
cnt1
为奇数,
cnt2 = 0
时,先手必胜。
证明:
模拟一下可以发现先手后手走的每一步都必然是让自己胜率增加的一步。由于
cnt1
为奇数,所以先手可以比后手多走一步,所以先手的胜率必然会比后手高。
那么什么情况下必然会使自己的胜率减少呢?即当自己的数值为
1
,且对手的数值为
0
,且公共数列中只有
1
可以选取时。
结论三 :
谁的胜率率先减少,则谁必败。
证明:
由于一方胜率减少了,所以可得公共数列中只有
1
可以选取,没有
0
可以选取。
设胜率率先减少的
Alice
,那么此时
Alice
和
Bob
的数值只有两种可能:
Alice
的数值为0
,Bob
的数值为0
;Alice
的数值为1
,Bob
的数值为1
。
由于
Alice
和
Bob
的数值相同,所以公共序列中使用的
1
的个数必然为偶数,剩余的
1
的个数必然为奇数。且此时是
Bob
先手,根据结论二,
Bob
必胜,
Alice
必败,证明完毕。
那么谁的胜率会先减少的呢?
结论四 :
当
cnt1
、
cnt2
为奇数时且
cnt1 > 1
时,先手的胜率会率先减少。
证明:
当
cnt2
为奇数时,先手第一步只能选取
0
或是
1
:
- 若先手先取
1
则后手取0
。此时先手的数值为1
,后手的数值为0
。为了不让自己的胜率降低,先手只能取0
,而后手也接着取0
。由于0
个为奇数,所以先手将率先无法取0
,只能取1
,使得自己的胜率降低。 - 若先手取
0
则后手也取0
。此时场面还是1
的个数为奇数,0
的个数为奇数的情况。若先手率先取了1
,则就回到了上述的情况,先手必败。所以先手只能不断取0
,而后手也跟着不断取0
。最后先手取完0
,将剩余奇数个1
,回到了结论三的情况。由于此时到了后手的轮次,所以先手必败。
结论五 :
当
cnt1 = 1
时,先手必胜。
证明略。
根据上述五个结论,模拟一遍即可。
复杂度为
O(22 ∑ i=1 Tni)
。
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int a[N];
signed main() {
int T = 1;
cin >> T;
while(T--) {
int n, sum = 0, ans = 0;
cin >> n;
// 读取输入并计算异或和
for(int i = 1; i <= n; i++) {
cin >> a[i];
sum ^= a[i];
}
// 结论1:如果异或和为 0,则平局
if (!sum) {
cout << 0 << '\n';
continue;
}
// 对每一位进行分析
for (int j = 20; j >= 0; j--) {
int one = 0, zero = 0;
// 统计当前位上的1和0的数量
for (int i = 1; i <= n; i++) {
if (a[i] >> j & 1) one++;
else zero++;
}
// 结论2 如果当前位有奇数个1,则确定胜负
if (one & 1) {
if (zero % 2 && one != 1) ans = -1; //结论4 不满足条件,Bob 获胜
else ans = 1; // 满足条件,Alice 获胜
break;
}
}
cout << ans << '\n'; // 输出结果
}
return 0;
}
幸运数字的个数(预计算)
样例输入
6
1 10
1 16
1 17
10 100
100 1000
1000 10000
样例输出
10
15
16
11
13
14
说明
对于所有评测数据:
1 ≤ T ≤ 1 0 5 , 1 ≤ l i ≤ r i ≤ 1 0 12 。 1≤T≤10^5,1≤l_i ≤r_i≤10^{12} 。
1
≤
T
≤
1
0
5
,
1
≤
l
i
≤
r
i
≤
1
0
12
。
要用到的思想是先“离线”预计算所有可能的幸运数字,再用二分查找快速计算每个查询区间内的幸运数字数量。具体做法如下:
先枚举所有“十六进制中由同一字符重复”的数字,排除超过 10^12 的值,并将这些数字存储到一个数组并排序;
对每次给定的范围 [l, r],使用二分查找定位区间上下界,从而快速统计落在该区间内的幸运数字个数。
#include <bits/stdc++.h>
using namespace std;
static const long long MAX_VAL = 1000000000000LL; // 1e12
// 预先生成所有在 [1, 1e12] 范围内 "十六进制由同一数字重复" 的幸运数字
// 注意:digit 取值范围是 [0..15],长度取值范围适当即可(1~16足够覆盖1e12)
vector<long long> generateLuckyNumbers() {
vector<long long> luckyNums;
// 十六进制最大可用字符:0~f (共16个)
for (int digit = 0; digit < 16; ++digit) {
for (int length = 1; length <= 16; ++length) {
// 构建长度为 length 的重复字符
// 例如若 digit = 12 (十六进制 c),length = 4,则是"cccc"
// 然后转为十进制,判断是否 <= 1e12
// digit 转成对应的16进制字符
char hexDigit;
if (digit < 10) {
hexDigit = char(digit + '0');
} else {
hexDigit = char(digit - 10 + 'a');
}
// 构建重复串
string hexStr(length, hexDigit);
// 转成十进制
// stoll(hexStr, nullptr, 16) 有可能超范围,用更安全方式
// 这里用 64位整型计算
long long num = 0;
for (char c : hexStr) {
// digitVal 可以用 c - '0' 或 c - 'a' + 10
// 但我们已经知道是同一个字符
int val;
if (isdigit(c))
val = c - '0';
else
val = c - 'a' + 10;
num = num * 16 + val;
// 若已经超过范围就中断
if (num > MAX_VAL) break;
}
if (num > 0 && num <= MAX_VAL) {
luckyNums.push_back(num);
}
}
}
// 去重并排序
sort(luckyNums.begin(), luckyNums.end());
luckyNums.erase(unique(luckyNums.begin(), luckyNums.end()), luckyNums.end());
return luckyNums;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 预先生成所有可能的幸运数字
static vector<long long> luckyNumbers = generateLuckyNumbers();
int T;
cin >> T;
while (T--) {
long long l, r;
cin >> l >> r;
// 在 luckyNumbers 中,用二分查找统计区间 [l, r] 内的元素个数
auto leftIt = lower_bound(luckyNumbers.begin(), luckyNumbers.end(), l);
auto rightIt = upper_bound(luckyNumbers.begin(), luckyNumbers.end(), r);
cout << (rightIt - leftIt) << "\n";
}
return 0;
}
填空
握手问题
对于第一个人来说 除了自己以外要跟其他49人握手 所以第一个是49 ,对于第二个人来说 第一个人主动跟我握手了 有一次不算 所以第二个是48.。 以此类推 第43个人就是7 到了最后七个人呢 因为互相都没有握手 并且7个人都被前面的人握过手了 所以都是0
#include <iostream>
using namespace std;
int main(){
int sum=0;
for(int i=7;i<=49;i++) sum+=i;
cout<<sum;
return 0;
}
报数问题
第1-10个: 20 24 40 48 60 72 80 96 100 120
第11-20个:140 144 160 168 180 192 200 216 220 240
第21-30个:260 264 280 288 300 312 320 336 340 360
第31-40个:380 384 400 408 420 432 440 456 460 480
思路一:发现第10个数,第20个数,第30个数,第40个数......(每十个数为一轮)等等都是120的倍数,
既然题目要求第202420242024个数,那我们不妨先求第202420242020个数,然后再往后再多求4个数就行。
也就是202420242020/10*120=202429042904240,找它之后的四个能被20或24整除的数,也就是
2429042904288
思路二:通过观察发现,第奇数位个数是20的倍数,第偶数位个数是24的倍数。所以第202420242024个数
就是24的倍数,那我们直接除以2(判断是这个数是第几个24的倍数),然后再乘24就行。
也就是202420242024÷2×24=2429042904288
杂题
游戏专家(零和博弈)
输入格式
一行一个字符串
s(1≤∣s∣≤1000)由小写英文字母组成。
样例输入
bazabyakslfd
样例输出
zbybzazazaza
分治思想(交替处理策略)先行者和后行者对应偶数和奇数
我们轮流让小蓝和小桥修改字符串,小蓝尽量将字符变成字典序最大的字母 z,小桥尽量将字符变成字典序最小的字母 a。
#include <bits/stdc++.h>
using namespace std;
int main()
{
string s;
cin >> s;
int n = s.size();
// 轮流修改字符串
for (int i = 0; i < n; i++)
{
if (i % 2 == 0) // 小蓝的回合,尽量使字典序最大
{
if (s[i] != 'z') // 如果当前字符不是'z',则将其改为'z'
{
s[i] = 'z';
}
}
else // 小桥的回合,尽量使字典序最小
{
if (s[i] != 'a') // 如果当前字符不是'a',则将其改为'a'
{
s[i] = 'a';
}
}
}
cout << s; // 输出最终字符串
return 0;
}
大衣的异或回文对(回文判断)
样例输入1
4
13 27 12 26
样例输出1
8
样例输入2
3
2 2 2
样例输出2
6
使用字符判断回文
// 判断整数 x 是否是回文
bool isPalindrome(int x) {
string s = to_string(x);
string rev_s(s.rbegin(), s.rend());
return s == rev_s;
}
使用数字判断回文
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e4 + 10;
ll a[N];
ll ans;
// 判断是否是回文数
bool hw(ll n) {
ll sum = 0;
ll k = n;
while (n != 0) {
sum = (sum * 10) + (n % 10); // 反转数字
n /= 10;
}
return sum == k; // 如果反转的数字等于原数字,则为回文数
}
int main() {
ll n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
// 遍历所有对 (i, j) 计算异或并判断是否是回文数
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++) {
if (hw(a[i] ^ a[j])) {
ans++;
}
}
}
cout << ans << endl;
return 0;
}
寻找至宝的奥秘(数学)
最大公因数的基本概念 :
最大公因数(GCD)是指两个数的最大共有因子。比如,
gcd(12, 15) = 3
,因为
12
和
15
都能被
3
整除,而没有比
3
更大的共同因子。
思路分析 :
最大公因数的性质 :
- 假设我们有两个正整数
a
和b
,如果它们的 GCD 很大,那么这两个数的因子也应该尽量重合。 - 如果我们选取
a = n
和b = n / 2
,那么它们的 GCD 通常会比较大。具体来说,gcd(n, n/2)
总是n/2
(这是因为n/2
是n
的因子,并且它们共享n/2
作为共同因子)。
- 假设我们有两个正整数
为什么选择
n
和n / 2
:选择
n
和n / 2
作为候选 :- 如果我们选择两个数
a = n
和b = n / 2
,这两个数之间的最大公因数是n / 2
。这是因为:n
是n / 2
的倍数,n
和n / 2
的最大公因数就是n / 2
。
- 例如,当
n = 10
时,n = 10
和n / 2 = 5
,这两个数的 GCD 是 5。
- 如果我们选择两个数
为什么
n / 2
会是最大值 :- 当我们选择两个数时,我们希望它们的最大公因数尽可能大。
n / 2
是n
最大的因子之一,所以选择n
和n / 2
总是能得到最大的 GCD。
- 当我们选择两个数时,我们希望它们的最大公因数尽可能大。
其他可能的组合 :
- 如果选择
a = n
和b = n - 1
,它们的最大公因数一般会较小,因为相邻的整数的 GCD 总是1
。 - 同理,其他的一些数对,如
a = n
和b = n - 2
等,都会比n / 2
和n
的 GCD 小。
- 如果选择
例子 :
例子 1 :
输入:
n = 10
- 我们选择
a = 10
和b = 10 / 2 = 5
,计算gcd(10, 5)
,结果是5
。 - 因为
10
和5
的最大公因数是5
,这是最大的 GCD。
- 我们选择
例子 2 :
输入:
n = 12
- 我们选择
a = 12
和b = 12 / 2 = 6
,计算gcd(12, 6)
,结果是6
。 - 因为
12
和6
的最大公因数是6
,这是最大的 GCD。
- 我们选择
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
// 输出 n / 2 即可得到最大公因数
cout << n / 2 << endl;
return 0;
}
小蓝的战斗计划
- 优先尝试消灭需要 2 个单位时间的怪物:遍历排序后的所有时间段 b[i],如果 b[i] ≥ 2,就尽可能使用该时间段来消灭“需要 2 个单位时间”的怪物(min(cnt2, b[i]/2))。在此操作中,消灭多少头怪物,就从 cnt2 和 b[i] 各自对应的“数量”中减去相应数值。
- 然后尝试消灭需要 1 个单位时间的怪物:再次遍历同一个排序后的时间段数组,如果 b[i] ≥ 1,就用这个时间段去消灭尽量多的 cnt1 怪物(min(cnt1, b[i]))。
- 最后检查 cnt1 和 cnt2 是否都被消灭(即 cnt1 == 0 && cnt2 == 0)。若全部消灭则输出 “Y”,否则输出 “N”。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int cnt1 = 0, cnt2 = 0;
int a[N], b[N];
int main() {
cin >> n >> m;
// 输入怪物的战斗时间,并统计需要时间1和时间2的怪物数量
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
if (x == 1) {
cnt1++;
} else {
cnt2++;
}
}
// 输入时间段的长度
for (int i = 1; i <= m; i++) {
cin >> b[i];
}
// 对时间段进行降序排序
sort(b + 1, b + 1 + m, [](int u, int v) { return u > v; });
// 优先使用时间段消灭需要时间2的怪物
for (int i = 1; i <= m && cnt2 > 0; i++) {
if (b[i] >= 2) { // 如果时间段长度>=2,可以消灭一个需要时间2的怪物
int x = min(cnt2, b[i] / 2); // 计算能消灭多少个怪物
cnt2 -= x; // 更新需要消灭时间2的怪物数量
}
}
// 使用剩余的时间段消灭需要时间1的怪物
for (int i = 1; i <= m && cnt1 > 0; i++) {
if (b[i] >= 1) { // 如果时间段长度>=1,可以消灭一个需要时间1的怪物
int x = min(cnt1, b[i]); // 计算能消灭多少个怪物
cnt1 -= x; // 更新需要消灭时间1的怪物数量
}
}
// 判断是否所有怪物都被消灭
if (cnt1 == 0 && cnt2 == 0) {
cout << "Y" << endl;
} else {
cout << "N" << endl;
}
return 0;
}
二分
123
- 小区间的构成
假设数列的构成是如下形式:
- 第 1 个区间包含 1 个元素(
1
)。 - 第 2 个区间包含 2 个元素(
1 2
)。 - 第 3 个区间包含 3 个元素(
1 2 3
)。 - 第 4 个区间包含 4 个元素(
1 2 3 4
)。 - …
第
i
个小区间包含
i
个元素。我们将这些小区间连起来形成整个数列。
- 数组
a[j]
的定义
数组
a[j]
表示前
j
个小区间的总元素数,同时也能表示每个小区间的和。例如:
a[1] = 1
(表示前 1 个小区间有 1 个元素)a[2] = 1 + 2 = 3
(表示前 2 个小区间共有 3 个元素)a[3] = 1 + 2 + 3 = 6
(表示前 3 个小区间共有 6 个元素)a[4] = 1 + 2 + 3 + 4 = 10
(表示前 4 个小区间共有 10 个元素)
注意,数组
a[j]
是单调递增的,因为每个小区间的元素个数都在增加。
关键点:
k = i - a[j]
- 数列中的位置
i
是在第j+1
个区间中的某个元素。 - 前
j
个区间包含了a[j]
个元素,也就是说,第j+1
个区间的第一个元素出现在位置a[j] + 1
。
因此,位置
i
在第
j+1
个区间的具体位置是:
- 第
j+1
个区间的第k
个元素 :k
就是位置i
相对于第j+1
个区间开始位置的偏移量。
由于前
j
个区间包含了
a[j]
个元素,第
j+1
个区间从位置
a[j] + 1
开始。所以位置
i
在第
j+1
个区间中的具体位置是:
k = i - a[j]
#include <iostream>
using namespace std;
using ll=long long;
const int N=1414215;
ll a[N],s[N];
ll persum(ll i)
{
ll l=0,r=N;
while(l<r)
{
ll mid=(l+r+1)>>1;
if(a[mid]<i)l=mid;
else r=mid-1;
}
return s[l]+a[i-a[l]];
}
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
for(int i=1;i<N;i++)
{
a[i]=a[i-1]+i;
s[i]=s[i-1]+a[i];
}
int t;
cin>>t;
while(t--)
{
ll l,r;
cin>>l>>r;
cout<<persum(r)-persum(l-1)<<endl;
}
return 0;
}
双指针
小齐的奶牛配对挤奶计划
样例输入
3
1 8
2 5
1 2
样例输出
10
评测数据规模
1 ≤ M ≤ 1 , 000 , 000 , 000 , M 为偶数, 1 ≤ N ≤ 100 , 000 1≤M≤1,000,000,000,M 为偶数,1≤N≤100,000
1
≤
M
≤
1
,
000
,
000
,
000
,
M
为偶数,
1
≤
N
≤
100
,
000
#include <bits/stdc++.h>
using namespace std;
/*
问题描述:
给定若干组输入 (x, y),表示有 x 头奶牛,其挤奶产量为 y。
这些 input 的 x 之和为 M(总奶牛数,且 M 为偶数)。
将所有 M 头奶牛分成 M/2 对,并行挤奶时的总耗时,取决于所有配对 (A, B) 的挤奶时间 A+B 的最大值。
目标是找到所有可能配对中,使 max(A+B) 最小的方案,并输出这个值。
解决思路(双指针):
1. 将每种产量 y 与其数量 x 记录下来,并按照 y 升序排序。
2. 设置两端指针:left 指向最小产量,right 指向最大产量。
3. 每次取尽可能多的奶牛对,数量为 min(左侧剩余奶牛数, 右侧剩余奶牛数)。
4. 该批次配对的时间为 left 产量 + right 产量,用其更新全局最大值。
5. 逐步减少两侧数量并移动指针,直至全部奶牛被配对完成。
时间复杂度主要在对产量排序上,为 O(N log N),其中 N 最多为 100000(不按单头奶牛数量计)。
*/
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
// 记录 (y, x)
vector<pair<long long, long long>> cows;
cows.reserve(N);
long long totalCount = 0; // 用于计算奶牛总数 M
for (int i = 0; i < N; ++i) {
long long x, y;
cin >> x >> y;
cows.push_back({y, x});
totalCount += x;
}
// 按产量升序排序
sort(cows.begin(), cows.end(), [](auto &a, auto &b){
return a.first < b.first;
});
// 双指针:l 指向产量最小,r 指向产量最大
int l = 0, r = (int)cows.size() - 1;
long long res = 0;
while (l <= r) {
if (l == r) {
// 剩余都在同一个产量上
// 这时必然剩余的奶牛数为偶数,可以两两配对
// 配对时间为 2 * cows[l].first
// 但实际只需要一次就能给出最终答案
res = max(res, 2LL * cows[l].first);
break;
}
// 本轮可以配对的奶牛数
long long num = min(cows[l].second, cows[r].second);
// 对应配对时间
long long sumTime = cows[l].first + cows[r].first;
res = max(res, sumTime);
// 扣除配对过的奶牛数
cows[l].second -= num;
cows[r].second -= num;
// 如果左侧产量用完,则左指针右移
if (cows[l].second == 0) {
++l;
}
// 如果右侧产量用完,则右指针左移
if (cows[r].second == 0) {
--r;
}
}
cout << res << "\n";
return 0;
}
卓儿探寻全球变暖
样例输入
5 3
1 3 5 1 3
0 2 4
样例输出
1 2 1
1 ≤ n , d ≤ 1 0 5 , 1 ≤ h i ≤ 1 0 9 1≤n,d≤10^5,1≤h_i≤10^9
1
≤
n
,
d
≤
1
0
5
,
1
≤
h
i
≤
1
0
9
暴力做法
变量含义:
• n 表示大楼数量,d 表示要查询的天数。
• 数组 h 存储每栋大楼的高度,数组 t 存储每个查询日对应的海平面高度。
• 布尔数组 st 标记某天是否“未被淹没”(true 为未淹没)。
对每个查询日的处理:
(1) 将大楼中高 <= 当前海平面的全部标记为 false(表示已淹没)。
(2) 随后扫描所有大楼,累积计算未淹没大楼所形成的连续“区域”数量:
如果遇到一段连续的 true(未淹没大楼),则算作一个区域;
当连续的 true 被一个 false(淹没大楼)打断时,再出现下一段 true 时,就会有一个新的区域。
(3) 将最终区域数输出。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 10;
// h[] 用于存储每栋大楼的高度
// t[] 用于存储不同查询日的海平面高度
// st[] 用于标记某栋大楼今日是否仍未被淹没
vector<int> h(N);
vector<int> t(N);
bool st[N];
int main() {
int n, d;
// 输入 n(大楼数量)和 d(查询天数)
cin >> n >> d;
// 读取大楼高度
for (int i = 1; i <= n; i++) {
cin >> h[i];
}
// 读取查询海平面天数数组
for (int i = 1; i <= d; i++) {
cin >> t[i];
}
// 将 st[] 初始化为 true,表示初始默认所有大楼都未被淹没
memset(st, true, sizeof(st));
// 对每个查询天数分别进行处理
int region = 0; // 表示当前查询日下,未淹没大楼所形成的区域数量
for (int i = 1; i <= d; i++) {
int day = t[i]; // 当前海平面高度
region = 0; // 每次查询前重置区域数
bool flag = false; // 标记是否在扫描大楼时已经遇到一个“连续未淹没区域”
// 1) 更新 st[j]: 若大楼高度 <= 当前海平面,则标记为已被淹没 (false)
for (int j = 1; j <= n; j++) {
if (h[j] <= day) {
st[j] = false;
}
}
// 2) 统计当前未淹没楼形成的连续区域数
for (int j = 1; j <= n; j++) {
if (st[j]) {
// 如果此楼未淹没并且尚未记录一个新区域,则区域数加一
if (!flag) {
region++;
flag = true; // 进入新区域
}
} else {
// 如果此楼已被淹没,则结束之前的未淹没区域标记
flag = false;
}
}
// 输出当日的区域数
cout << region << " ";
}
return 0;
}
双指针+排序
首先,将所有大楼 (高度, 下标) 按高度从小到大排序,便于后续根据海平面高度逐步淹没。
随后,有一个双指针循环:当海平面上升到 t[i] 时,就把所有高度 ≤ t[i] 的大楼“标记”为淹没(分别存储到 drown[i] 列表)。
利用一个布尔数组 st[] 来标记下标为 x 的大楼是否已被淹没;给 0 与 n+1 这两个“边界”强制设为已淹没状态(true),方便识别某大楼左右是否都是淹没状态。
遍历 drown[i],对于每一个新淹没的大楼 x:
• 若 x 左右均尚未淹没,则此次淹没会把原来的一个区域分割成两个,因此区域计数 ans 增加 1。
• 若 x 左右均已经淹没,则原先的两个淹没区在 x 处“连接”为一个区,区域计数 ans 减少 1。
• 最后将 x 标记为已淹没。
每次处理完后,输出当前 ans 值,即此刻剩余未淹没大楼的整体区域数量。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 10;
bool st[N];
int main() {
int n, d;
cin >> n >> d;
vector<pair<int,int>>h(n+1);// h[]数组存储(大楼高度, 其原始下标),下标从1开始使用
for (int i = 1; i <= n; i++)
{
cin>>h[i].first;// 读入大楼高度
h[i].second=i; // 存储对应的原始位置
}
// t[]数组存储每个查询的海平面高度(总共有d次查询)
vector<int> t(d);
for (int i = 0; i < d; i++)
{
cin >> t[i]; // 读入第i次查询的海平面高度
}
// 将 h 中的大楼数据按高度升序进行排序
sort(h.begin() + 1, h.end());
// drown[i]存储在第i次查询中「新被淹没」的大楼下标
vector<vector<int>>drown(d);
// 双指针:i遍历查询,j遍历大楼列表
// 若 h[j+1].first <= t[i] => 说明该楼在第i次查询时已经被淹没
// 则记录其位置到drown[i]中表示本轮新增被淹没的楼
for (int i = 0, j = 0; i < d; i++) {
while (j + 1 <= n && h[j + 1].first <= t[i]) {
j++;
drown[i].emplace_back(h[j].second);
}
}
// st[x]用于标记下标为x的楼是否已被淹没
// 在边界0和n+1处设置为true,方便判断左右是否淹没
st[0] = true; // 边界视为已淹没
st[n + 1] = true; // 边界视为已淹没
int ans = 1; // 当前未淹没的大楼形成的区域数,初始设为1
// 遍历每个查询,将在该日新增被淹没的楼进行处理
for(auto &u:drown)
{
// u中存储了本次查询“刚好在海平面下”的大楼索引
for(auto x:u)
{
// 情况1:若左右都未被淹,则淹没x会把一个连续区域分成两部分 => 区域数 +1
if(!st[x-1]&&!st[x+1])
{
ans+=1;
}
// 情况2:若左右都已淹,则淹没x会将两个淹没区“连接”,相当于减少一个区域 => 区域数 -1
if (st[x - 1] && st[x + 1]) {
ans -= 1;
}
st[x]=true;
}
cout<<ans<<" ";
}
return 0;
}