1617 字
4 分钟
Atcoder_Beginner_Contest_457(A~E题)
2026-05-11
统计加载中...

尝试打打abcabc写写题解。
只写了AEA-E的,后两题一点思路没有也就没补了()。
题目链接:https://atcoder.jp/contests/abc457/tasks

A - Array#

按照题目要求写即可。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define endl '\n'
#define mod 998244353
typedef pair<int, int> pii;
int a[110];
void solved(){
int n, x;
cin >> n;
for(int i = 1; i <= n;i++){
cin >> a[i];
}
cin >> x;
cout << a[x] << endl;
}
int main(){
ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
int t = 1;
while(t--){
solved();
}
return 0;
}

B - Arrays#

这里其实是要输入一个每行元素数量不同的特殊二维数组,然后输出对应索引的元素,我这里用了mapmap

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define endl '\n'
#define mod 998244353
typedef pair<int, int> pii;
map<int, vector<int>> mp;
void solved(){
int n;
cin >> n;
for(int i = 1; i <= n;i++){
int t;
cin >> t;
while(t--){
int x;
cin >> x;
mp[i].push_back(x);
}
}
int x, y;
cin >> x >> y;
cout << mp[x][y - 1] << endl;
}
int main(){
ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
int t = 1;
while(t--){
solved();
}
return 0;
}

C - Long Sequence#

这里构造的二维数组跟上一题一样。然后要我们构造一个由每个AiA_i序列的元素按CiC_i个轮次反复压入数组而成BB数组。我们按它的要求真的压入就会超时。所以我们的选择是判断KK落在哪个AiA_i序列压入的范围。让(KK -前面压入的总范围),再对这个序列的元素个数取模,求出具体在单个AiA_i序列中这个KK的索引是多少。然后输出这个索引对应的元素即可。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define endl '\n'
#define mod 998244353
typedef pair<int, int> pii;
map<int, vector<ll>> mp;
ll c[200010];
void solved(){
ll n, k;
cin >> n >> k;
for(int i = 1; i <= n;i++){
int t;
cin >> t;
while(t--){
int x;
cin >> x;
mp[i].push_back(x);
}
}
for(int i = 1; i <= n;i++){
cin >> c[i];
}
for(int i = 1; i <= n;i++){
ll size = mp[i].size();
if(k > size * c[i]){
k -= size * c[i];
}
else if(k == size * c[i]){
cout << mp[i][size - 1] << endl;
break;
}
else{
ll temp = k % size;
if(temp == 0){ k = size - 1;}
else {k = temp - 1;}
cout << mp[i][k] << endl;
break;
}
}
}
int main(){
ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
int _ = 1;
while(_--){
solved();
}
return 0;
}

D - Raise Minimum#

这题我们需要转变一下思路。我们如果想要去模拟这个操作的过程肯定是会超时的。但是我们完全可以不去模拟,而是尝试去猜答案,看这个答案是否能够符合条件,然后不断找到一个能满足条件的最大值。而这其实就是二分法。
我们这里用的贪心二分答案。这里的检查逻辑是如果AiA_i小于目前猜的最小值就进行操作计数。我这里的判断逻辑不是ii的倍数的就取商加11,是就直接取商。(其实还可以写简便一点)然后比较需要的操作次数是否小于等于最大的操作次数限制,是就继续往大的值猜,否就往小的猜。
tipstips:这里的二分法右端最大值应该取maxa+kmaxa + k。我们考虑一个极端情况。对于A1A_1,假设它和最大值maxamaxa相同,然后全部操作kk都用在它上面,那么最大值也只能去到maxa+kmaxa + k。而这就是序列最小值的能达到的极限最大值了。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define endl '\n'
#define mod 998244353
typedef pair<int, int> pii;
ll a[200010];
ll n, k;
ll mina = 1e18 + 10, maxa = 0;
bool check(ll x){
ll cnt = 0;
for(int i = 1; i <= n;i++){
if(a[i] < x){
if((x - a[i]) % i != 0){
cnt += ((x - a[i]) / i + 1);
}
else{
cnt += ((x - a[i]) / i);
}
if(cnt > k) return false;
}
}
return cnt <= k;
}
void solved(){
cin >> n >> k;
for (int i = 1; i <= n;i++){
cin >> a[i];
if(a[i] > maxa) {maxa = a[i];}
if(a[i] < mina) {mina = a[i];}
}
ll l = mina, r = maxa + k;
ll ans = mina;
while(l <= r){
ll mid = l + (r - l) / 2;
if(check(mid)){
ans = mid;
l = mid + 1;
}
else{
r = mid - 1;
}
}
cout << ans << endl;
}
int main(){
ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
int _ = 1;
while(_--){
solved();
}
return 0;
}

E - Crossing Table Cloth#

这题依旧是二分题。但是要复杂很多。
核心思路是对于每一次查询时的左(右)端点,我们用一个数组存入它所有右(左)端点的下标。然后通过二分查找找到第一个满足条件的右(左)端点。也就是进行两次查找。(一次以左端点为准,找满足条件的右端点,另一次以右端点为准,找满足条件的左端点)然后通过条件判断。
1.1.找不到满足条件的右(左)端点。
2.2.两个区间断开了。
3.3.满足条件的只有一个区间(这个区间的左端点就是ss,右端点就是tt,且没有完全包含在内的子区间)。
这三种情况就直接nono掉,如果满足就是yesyes了。
这里的前两个条件都很好判断,详细都写在注释里了。
但是这里的判断是否有子区间就比较困难。如果正常地循环去找子区间就是O(n2)O(n^2)会直接超时,这里我们引入了树状数组来优化这种区间查询的情况(可以实现O(logN)O(logN))。最终我们时间复杂度可以压到O((M+Q)logN)O((M + Q)logN)可以通过题目。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define endl '\n'
#define mod 998244353
typedef pair<int, int> pii;
int n, m;
// 树状数组
struct BIT {
int n;
vector<int> bit;
BIT(int n_) : n(n_), bit(n_ + 2, 0) {} //初始化结构体
void add(int i, int v) {
for(; i <= n; i += i & -i) bit[i] += v; //先给当前的区间加值,然后不断给父区间(包含这个区间的大区间)也加值
}
int sum(int i){
int s = 0;
for (; i > 0; i -= i & -i) s += bit[i]; //从最大区间开始,不断查找小区间,累加小区间的值计入总和,然后返回这个总和
return s;
}
};
void solved(){
cin >> n >> m;
vector<vector<int>> L(n + 1), R(n + 1); //对应每一个左(右)端点,向数组中存入对应右(左)端点的下标
for(int i = 0; i < m;i++){
int l, r;
cin >> l >> r;
L[l].push_back(r);
R[r].push_back(l);
}
//保证二分的单调性
for(int i = 1; i <= n;i++){
sort(L[i].begin(), L[i].end());
sort(R[i].begin(), R[i].end());
}
//查询
int q;
cin >> q;
vector<array<int, 3>> Q(q);
//对应的每次查询i,Q[i][0]表示左端点,Q[i][0]右端点,Q[i][2]为此时对应的下标i
for (int i = 0; i < q; i++){
cin >> Q[i][0] >> Q[i][1];
Q[i][2] = i; //保存下标的原因是因为后续要用树状数组需要重新排序,但对应结果要存入正确的下标。
}
vector<array<int, 3>> sorted_Q = Q; //复制一个排序的Q方便树状数组操作
sort(sorted_Q.begin(), sorted_Q.end(), [](auto& a, auto& b){
return a[0] > b[0]; //对Q[i][0]从大到小排序,后续我们记录右端点要从大到小进行。
});
BIT bit(n); //创建树状数组
int cur = n; //cur记录当前操作到的下标。
vector<int> cnt_within(q, 0); //记录每次查询时,对应给出的区间内含有多少个子区间(包含它给出的这个区间本身)
for(auto & [s, t, idx] : sorted_Q){
while(cur >= s){ //当目前操作到的cur比目前的s大的话就继续向L[cur]里存有的右端点记入树状数组中,因为此时左端点>=cur的值都已经被加入BIT里了。
for (int r : L[cur]){
bit.add(r, 1); //往r下标在树状数组中的区间加1(也就是bit[r]的值加1),然后会在BIT里面继续也给父区间加1。
}
cur--; //当前cur的右端点都计入了之后就下一个cur
}
cnt_within[idx] = bit.sum(t); //对于一个已经完成的查询内的s(所有右端点已经计入),我们就计算它和它的子区间的总个数和(也就是我们存入的值)
}
//针对原来正常顺序的每一次查询进行二分
for(auto & [s, t, idx] : Q){
auto& vecL = L[s];
auto itR = upper_bound(vecL.begin(), vecL.end(), t); //找到第一个大于t的右端点itR,后续会itR--(也就是对应小于或者等于t的那个下标)
if(itR == vecL.begin()) { //说明第一个元素就比t大,没有符合条件的itR,直接no掉
cout << "No" << endl;
continue;
}
itR--;
int maxR = *itR; //存入此时itR对应的值(也就是满足条件的最大的右端点t的下标)
auto& vecR = R[t];
auto itL = lower_bound(vecR.begin(), vecR.end(), s); //找到第一个大于等于左端点s的itL,所以不用减。
if(itL == vecR.end()){ //如果最后一个元素都不能满足就直接no掉
cout << "No" << endl;
continue;
}
int minL = *itL; //存入此时的itL的值(也就是满足条件的最小的左端点的s的下标)
//如果两个区间没有连接在一起就no掉
if(maxR < minL - 1) {
cout << "No" << endl;
continue;
}
//判断我们找到的两个区间是否最左端点是S,最右端点是T,并且区间内有两个及以上的子区间(包含本身)。是则yes
bool has_st = (maxR == t && minL == s);
if(has_st && cnt_within[idx] == 1){
cout << "No" << endl;
}
else{
cout << "Yes" << endl;
}
}
}
int main(){
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
solved();
return 0;
}

树状数组#

我们拿这道题目用到的BITBIT来说明一下。
我们首先要理解bit[i]bit[i]里的ii代表的其实是一个区间。比如66其实代表的是[5,6][5,6]这个区间,77代表的是[7,7][7,7]这个区间。它是这样理解的。66对应的区间长度由它的二进制从右往左第一次出现11的位置对应的22的幂值来决定。66的二进制为110110,所以它对应的区间长度就是21=22^1 = 277的是111111,所以它的区间长度是20=12^0 = 1,同理88就是23=82^3 = 8。所以我们可以通过这种方式划分区间:
11对应[1,1][1,1]22对应[1,2][1,2]33对应[3,3][3,3]44对应[1,4][1,4],以此类推,[5,5][5,5][5,6][5,6][7,7][7,7][1,8][1,8]
那么我们就会发现一个规律,每到一个二次幂就是一个大区间,这个大区间我们就可以存放对应区间[1,n][1,n]含有的所有的值。所以我们在插入值时,我们就可以利用i+=i&ii += i \& -i(本质上就是根据区间长度进行跳转)这个条件来不断跳转我们要赋值的ii对应的bits[i]bits[i]。然后我们发现一旦它跳转到了一个大区间(二次幂对应的区间),它接下来就只会在大区间里继续向上跳了。(因为大区间的区间长度都是二次幂,跳转也是这个大小)因此通过这种方式,我们小区间内的值都会被传递到大区间内然后不断向上传递,这里其实就是addadd插入值的原理,显然是一个O(logN)O(logN)级别的插入。
同理我们来看看查询一个对应区间内的总值(也就是sumsum)应该怎么查询。我们查询一个右端点,也就是最大的ii所在的区间。我们通过向下跳转i=i&i i -= i \& -i,会不断向下面的区间跳转。每跳转一个区间我们就将其中的值bit[i]bit[i]加入总和ss中。然后一旦跳转到一个大区间(二次幂对应的区间),下一步就会直接结束循环了,而此时大区间对应的值就是前面[1,n][1, n]所有的总值了,(插入的原理)我们本次的查询就结束了。显然通过这种跳转的方式,查询也是一个O(logN)O(logN)级别的查询。
在本题中树状数组的优化就显得十分高效,利用方式也完全基于这个原理。只不过区间最小端点变成了查询时的ss,而不是11

NOTE

这里的i&ii \& -i其实就是原数和补码取合,然后就会取到二进制里从右往左第一个为11的位对应的二次幂。

// 树状数组
struct BIT {
int n;
vector<int> bit;
BIT(int n_) : n(n_), bit(n_ + 2, 0) {} //初始化结构体
void add(int i, int v) {
for(; i <= n; i += i & -i) bit[i] += v; //先给当前的区间加值,然后不断给父区间(包含这个区间的大区间)也加值
}
int sum(int i){
int s = 0;
for (; i > 0; i -= i & -i) s += bit[i]; //从最大区间开始,不断查找小区间,累加小区间的值计入总和,然后返回这个总和
return s;
}
};
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Atcoder_Beginner_Contest_457(A~E题)
https://mkrari.cn/posts/abc_457/
作者
Mkrari
发布于
2026-05-11
许可协议
CC BY-NC-SA 4.0