AtCoder Beginner Contest 435 ABCDEF 题目解析
A - Triangular Number
题意
给定 \(N\),求 \(N\) 以内所有正整数的和。
代码
void solve()
{
int n;
cin >> n;
cout << n * (n + 1) / 2;
}
B - No-Divisible Range
题意
给定一个长度为 \(N\) 的正整数序列 \(A_1, A_2, \dots, A_N\)。
找有多少对整数对 \((l, r)\) \((1 \le l \le r \le N)\) 满足以下条件:
- 区间 \([l, r]\) 内的每个整数 \(A_i\) \((l \le i \le r)\) 都不是区间总和 \(A_l + A_{l+1} + \dots + A_r\) 的因数。
思路
循环嵌套枚举每个可能的区间,找出区间总和后再判断是否区间内存在总和的因数即可,计数后输出答案。
代码
int n, a[55];
void solve()
{
cin >> n;
for(int i = 1; i <= n; i++)
cin >> a[i];
int ans = 0;
for(int l = 1; l <= n; l++)
for(int r = l; r <= n; r++) // 枚举每个区间 [l, r]
{
int s = 0; // 区间总和
for(int i = l; i <= r; i++)
s += a[i];
bool flag = false; // 标记,判断区间内是否存在某个整数是 s 的因数
for(int i = l; i <= r; i++)
if(s % a[i] == 0)
{
flag = true;
break;
}
if(flag == false) // 区间符合条件
ans++;
}
cout << ans;
}
C - Domino
题意
在一条数轴上有 \(N\) 个多米诺骨牌。第 \(i\) 个多米诺骨牌位于坐标 \(i\) 处,且高度为 \(A_i\)。
当第 \(i\) 个多米诺骨牌向右侧倒下时,坐标范围在 \([i, i + A_i - 1]\) 以内的所有骨牌都会被推倒,然后也向右侧倒下。
若第一张骨牌向右侧倒下,那么到最后总共会有多少张骨牌倒下?
思路
因为多米诺骨牌每次倒下都会影响右侧的一段连续区间,因此最终所有倒下的骨牌应该都是左边连续的一段,要么最多从第 \(1\) 个骨牌开始倒到中间某一骨牌结束,要么所有骨牌全部倒下。
因此可以用一个变量来维护当前会倒下的骨牌当中最右侧的坐标 \(\text{mx}\),一开始只有第一张骨牌倒下,因此能影响到的坐标为 \(1 + A_1 - 1 = A_1\)。
然后按顺序从第二张骨牌开始看,当第 \(i\) 张骨牌确定会倒下,那么他会将影响范围扩大到 \(i + A_i - 1\) 的位置,以此更新 \(\text{mx}\) 变量求最大值。
而如果发现到达了某个位置 \(i\),满足 \(i \gt \text{mx}\),那也就说明当前骨牌及之后的所有骨牌都不会受到前面倒下的骨牌影响,答案即 \(\text{mx}\)。
代码
int n, a[500005];
void solve()
{
cin >> n;
for(int i = 1; i <= n; i++)
cin >> a[i];
int mx = a[1]; // 当前会倒下的位置中的最右侧坐标
for(int i = 2; i <= n; i++)
{
if(mx < i) // 影响不到这个骨牌,后面也就不用看了
break;
// 当前骨牌也会倒下,更新最右侧可能的坐标
mx = max(mx, i + a[i] - 1);
}
// 可能影响到的坐标会超过 n
cout << min(mx, n);
}
D - Reachability Query 2
题意
有一张包含 \(N\) 个点和 \(M\) 条边的有向图,点的编号从 \(1\) 到 \(N\),第 \(i\) 条边从点 \(X_i\) 指向点 \(Y_i\)。
一开始所有点都是白色的。你需要按顺序处理以下 \(Q\) 次询问,每次询问为以下两种格式之一:
1 v:将点 \(v\) 改为黑色。2 v:确定从点 \(v\) 出发沿着有向边走,是否有可能到达某个黑色点。
思路
在反向图中从某个点 \(u\) 出发所能够到达的所有点,在正向图里都能够正常走到此时的点 \(u\)。
开一个计数数组 \(\text{vis}[i]\) 表示从 \(i\) 点出发是否能够走到某个黑色点。
于是建立原图的反向图,每当一个点 \(v\) 被改为黑色,就在反向图中借助搜索从点 \(v\) 出发,将所有能到达的点全部标记一遍。而当搜到一个已经被标记过的点时,说明该点及其之后的点已经能够到达此前的某个黑色点,所以不需要重复搜索。
时间复杂度 \(O(N + M + Q)\)。
代码
int n, m;
vector<int> G[300005];
bool vis[300005];
// vis[i] 表示从 i 出发是否能够到达某个黑色点
// 从 u 出发把所有能到达的点全部标记
void dfs(int u)
{
if(vis[u])
return;
vis[u] = true;
for(int &v : G[u])
dfs(v);
}
void solve()
{
cin >> n >> m;
for(int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
G[v].push_back(u); // 反向图
}
int q;
cin >> q;
while(q--)
{
int op, v;
cin >> op >> v;
if(op == 1)
dfs(v);
else
{
if(vis[v])
cout << "Yes\n";
else
cout << "No\n";
}
}
}
E - Cover query
题意
有 \(N\) 个单元格,从左到右排成一行,分别编号为 \(1, 2, \dots, N\)。
一开始,所有单元格都被涂成白色。
你需要按顺序处理以下 \(Q\) 次询问,第 \(i\) 次询问如下:
给定两个整数 \(L_i, R_i\) \((1 \le L_i \le R_i \le N)\)。
将编号 \(L_i, L_{i+1}, \dots, R_i\) 这些单元格全部涂成黑色。
输出这 \(N\) 个单元格中还有多少个白色的单元格。
思路一
相当于每次给一段区间用于覆盖格子,然后问 \([1, n]\) 内还有多少格子没有被任何区间覆盖。
考虑维护所有被覆盖的格子所形成的多段区间。每当多一段新区间,则将其与原有的区间进行合并。那么答案就是 $N - $ 被覆盖的格子总数,也就是区间并集的总长度。我们可以用一个变量 \(\text{ans}\) 用于维护还有多少格子未被覆盖,初始值为 \(N\)。
每多一段新区间 \([L, R]\),为了快速找到并处理与该区间存在交集的所有区间,我们则需要一个数据结构能够用于实现对区间的高效排序、二分查找、插入以及删除操作,因此这里采用 pair 存储区间,set 维护区间。
首先第一段区间不会与任何区间冲突,可以直接放进 set,答案 \(\text{ans}\) 直接减去区间长度 \(R-L+1\)。
其后每多一段区间,首先先借助集合的 lower_bound 操作找到左端点 \(\ge L\) 的第一个区间,暂记作 \([x, y]\)。左端点 \(\ge L\) 的区间无非只有两种情况:要么和 \([L, R]\) 有交集(或者相邻),要么两区间完全没有关系。
如果两区间有交集,由于此时 \(L \le x\) 已满足,因此只需要满足 \(x \le R\);如果两区间相邻,则 \(x = R+1\)。
因此只要 \(x \le R + 1\),我们就可以尝试将区间 \([x, y]\) 合并到区间 \([L, R]\) 内,再将 \([x, y]\) 从集合内部删除。
- 删除一段区间时,我们可以先将该区间的长度加回答案 \(\text{ans}\) 内,之后在添加当前区间时再把覆盖的格子数减去。
- 合并两区间时如果出现 \(y \ge R\) 的情况,那么此时需要将区间 \([L, R]\) 的右端点向右更新到 \(y\) 的位置,即 \(R:= \max(R, y)\)。
这样一直重复合并区间,直到右侧没有任何区间,或是下一个区间与当前区间 \([L, R]\) 无任何关系时,右侧的处理即可结束,接下来考虑左侧。
左侧只需要找前面二分得到的左端点 \(\ge L\) 的区间的上一个区间即可,这便是唯一一个需要我们考虑的左侧区间,还是将其暂记作 \([x, y]\)。
同理,先判断两区间是否有交集。如果有交集,则 \(y \ge L\);而如果两区间相邻,则 \(y = L-1\)。
因此只要 \(y \ge L-1\),则可以将两区间合并。合并过程同上,更新 \(L := \min(L, x)\),然后将区间 \([x, y]\) 从集合内删除,并把区间长度加回 \(\text{ans}\) 内先。
照理来说,此时区间 \([L, R]\) 就是合并完成后所得到的大区间,只需要将其加入集合内部,然后将区间长度从答案 \(\text{ans}\) 内减去即可。
但还有一种情况没有考虑到,就是一开始的区间 \([L, R]\) 被包含在了集合内部某个已有的区间 \([x, y]\) 内(即 \(x \le L \le R \le y\))。此时按照我们上面的做法,除非 \(x=L\),否则右侧不会有可以合并的区间,外层区间的左端点会出现在 \(L\) 的左侧,因此可以把这种情况归类于向左合并的过程。
考虑合并的结果,此时我们直接取 \([x, y]\) 当作合并后的区间,因此只需要在向左合并的过程中,再加上一句 \(R:=\max(R, y)\) 来覆盖这种情况即可。
每次询问最多只会加一个新区间,每个区间也只会被删除一次,因此时间复杂度为 \(O(Q\log Q)\)。
代码一
typedef pair<int, int> pii;
set<pii> st; // 维护每段被涂黑的区间
void solve()
{
int n, q;
cin >> n >> q;
int ans = n;
while(q--)
{
int l, r;
cin >> l >> r;
if(st.empty()) // 第一段区间直接放进 set
{
st.insert(pii(l, r));
ans -= r - l + 1;
cout << ans << "\n";
continue;
}
auto it = st.lower_bound(pii(l, 0)); // 找左端点 >=l 的第一个区间
while(it != st.end())
{
pii p = *it;
if(p.first > r + 1) // 如果这段区间与 [l, r] 已经无交点(或相邻),则结束
break;
// 区间 [p.first, p.second] 的左端点已经 >= l
// 如果右端点 <= r,则这段区间直接删除即可
// 而如果右端点 > r,则可以合并两区间,更新 r 为 p.second
r = max(r, p.second);
ans += p.second - p.first + 1; // 被删除的这段区间对答案的影响先加回来
it = st.erase(it); // 删除该区间,获取后一个迭代器
}
if(it != st.begin()) // it 不是开头,往前找上一段区间是否与 [l, r] 有交集,有则合并
{
it--;
pii p = *it;
if(p.second >= l - 1) // 这段区间与 [l, r] 有交点或者刚好相邻
{
l = min(l, p.first); // 同上
r = max(r, p.second); // 有可能当前区间被完全包含在这段区间内部
ans += p.second - p.first + 1;
st.erase(it);
}
}
st.insert(pii(l, r));
ans -= r - l + 1;
cout << ans << "\n";
}
}
思路二
反过来维护没被涂黑的区间也可以,应该比直接维护被涂黑的区间稍微简单些。
在这种做法下的特殊情况为,如果某次要删除的区间 \([L, R]\) 可能被包含在集合中某个区间 \([x, y]\) 的内部,那么删除 \([L, R]\) 会导致区间 \([x, y]\) 被分为 \([x, L-1]\) 和 \([R+1, y]\) 两段。
其它维护方法同上。时间复杂度一致。
代码二
typedef pair<int, int> pii;
set<pii> st; // 维护每段没被涂黑的区间
void solve()
{
int n, q;
cin >> n >> q;
int ans = n;
st.insert(pii(1, n));
while(q--)
{
int l, r;
cin >> l >> r;
auto it = st.lower_bound(pii(l, 0)); // 找左端点 >=l 的第一个区间
while(it != st.end())
{
pii p = *it;
if(p.first > r) // 这段区间不会被 [l, r] 影响到
break;
else if(p.second > r) // 这段区间的左半部分会被 [l, r] 覆盖
{
ans -= r - p.first + 1;
// Problem: E - Cover query
// Contest: AtCoder - AtCoder Beginner Contest 435
// URL: https://atcoder.jp/contests/abc435/tasks/abc435_e
// Memory Limit: 1024 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// Template Ver.230403 - StelaYuri
// #pragma GCC optimize(3)
// #include<bits/stdc++.h>
#include<algorithm>
#include<bitset>
#include<cassert>
#include<cctype>
#include<chrono>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<functional>
#include<fstream>
#include<iomanip>
#include<iostream>
#include<map>
#include<queue>
#include<random>
#include<set>
#include<sstream>
#include<stack>
#include<string>
#include<unordered_map>
#include<utility>
#include<vector>
using namespace std;
//#include<ext/pb_ds/assoc_container.hpp>
//#include<ext/pb_ds/hash_policy.hpp>
//using namespace __gnu_pbds;
#define closeSync ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define multiCase int T;cin>>T;for(int cas=1;cas<=T;cas++)
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define repp(i,a,b) for(int i=(a);i<(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define perr(i,a,b) for(int i=(a);i>(b);i--)
#define REP(i,a,b,s) for(int i=(a);i<=(b);i+=(s))
#define REPP(i,a,b,s) for(int i=(a);i<(b);i+=(s))
#define PER(i,a,b,s) for(int i=(a);i>=(b);i-=(s))
#define PERR(i,a,b,s) for(int i=(a);i>(b);i-=(s))
#define all(a) (a).begin(),(a).end()
#define mst(a,b) memset(a,b,sizeof(a))
#define pb push_back
#define eb emplace_back
#define YES cout<<"Yes\n"
#define NO cout<<"No\n"
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
namespace BasicT {
const int INF=0x3f3f3f3f;
const ll LINF=0x3f3f3f3f3f3f3f3f;
const int dx[8]={0,1,0,-1,1,1,-1,-1},dy[8]={1,0,-1,0,1,-1,1,-1};
const ll mod=998244353;
mt19937 _randomSeed(std::chrono::system_clock::now().time_since_epoch().count());
template<typename T>T getRandom(T l,T r){return uniform_int_distribution<T>(l,r)(_randomSeed);}
template<typename T>T gcd(T a,T b){return b==0?a:gcd(b,a%b);}
ll qmul(ll a,ll b){ll r=0;while(b){if(b&1)r=(r+a)%mod;b>>=1;a=(a+a)%mod;}return r;}
ll qpow(ll a,ll n){ll r=1;while(n){if(n&1)r=(r*a)%mod;n>>=1;a=(a*a)%mod;}return r;}
ll qpow(ll a,ll n,ll p){ll r=1;while(n){if(n&1)r=(r*a)%p;n>>=1;a=(a*a)%p;}return r;}
}
namespace DebugT {
void debug(){cerr<<'\n';}template<typename T,typename... Args>void debug(T x,Args... args){cerr<<"[ "<<x<< " ] , ";debug(args...);}
template<typename Ta,typename Tb>istream& operator>>(istream &in,pair<Ta,Tb> &x){in>>x.first>>x.second;return in;}
template<typename Ta,typename Tb>ostream& operator<<(ostream &out,const pair<Ta,Tb> &x){out<<x.first<<' '<<x.second;return out;}
}
namespace FastIOT {
const int bsz=1<<18;char bf[bsz],*head,*tail;
inline char gc(){if(head==tail)tail=(head=bf)+fread(bf,1,bsz,stdin);if(head==tail)return 0;return *head++;}
template<typename T>inline void read(T &x){T f=1;x=0;char c=gc();for(;!isdigit(c);c=gc())if(c=='-')f=-1;for(;isdigit(c);c=gc())x=x*10+c-'0';x*=f;}
template<typename T>inline void print(T x){if(x<0)putchar(45),x=-x;if(x>9)print(x/10);putchar(x%10+48);}
template<typename T>inline void println(T x){print(x);putchar('\n');}
}
using namespace BasicT;
using namespace DebugT;
//using namespace FastIOT;
set<pii> st; // 维护每段没被涂黑的区间
void solve()
{
int n, q;
cin >> n >> q;
int ans = n;
st.insert(pii(1, n));
while(q--)
{
int l, r;
cin >> l >> r;
auto it = st.lower_bound(pii(l, 0)); // 找左端点 >=l 的第一个区间
while(it != st.end())
{
pii p = *it;
if(p.first > r) // 这段区间不会被 [l, r] 影响到
break;
else if(p.second > r) // 这段区间的左半部分会被 [l, r] 覆盖
{
ans -= r - p.first + 1;
st.insert(pii(r + 1, p.second)); // 将剩余部分放回集合
it = st.erase(it); // 删除该区间,获取后一个迭代器
break;
}
// 这段区间会被 [l, r] 完全覆盖
ans -= p.second - p.first + 1;
it = st.erase(it); // 删除该区间,获取后一个迭代器
}
if(it != st.begin()) // it 不是开头,往前找上一段区间是否与 [l, r] 有交集
{
it--;
pii p = *it;
if(p.second >= l && p.second <= r) // 这段区间的右半部分会被 [l, r] 覆盖
{
ans -= p.second - l + 1;
st.erase(it); // 删除这段区间
st.insert(pii(p.first, l - 1)); // 将剩余部分放回集合
}
else if(p.second > r) // 这段区间的中间部分会被 [l, r] 覆盖
{
ans -= r - l + 1;
st.erase(it); // 删除这段区间
st.insert(pii(p.first, l - 1)); // 将两侧剩余部分放回集合
st.insert(pii(r + 1, p.second));
}
}
cout << ans << "\n";
}
}
signed main()
{
closeSync;
//multiCase
{
solve();
}
return 0;
}
break;
}
// 这段区间会被 [l, r] 完全覆盖
ans -= p.second - p.first + 1;
it = st.erase(it); // 删除该区间,获取后一个迭代器
}
if(it != st.begin()) // it 不是开头,往前找上一段区间是否与 [l, r] 有交集
{
it--;
pii p = *it;
if(p.second >= l && p.second <= r) // 这段区间的右半部分会被 [l, r] 覆盖
{
ans -= p.second - l + 1;
st.erase(it); // 删除这段区间
st.insert(pii(p.first, l - 1)); // 将剩余部分放回集合
}
else if(p.second > r) // 这段区间的中间部分会被 [l, r] 覆盖
{
ans -= r - l + 1;
st.erase(it); // 删除这段区间
st.insert(pii(p.first, l - 1)); // 将两侧剩余部分放回集合
st.insert(pii(r + 1, p.second));
}
}
cout << ans << "\n";
}
}
思路三
可以考虑其它能够实现区间操作的数据结构,例如线段树。但由于本题定义域较大,因此可以离散化之后维护每段区间是否被覆盖,再借助线段树维护被覆盖的区间总长度即可。代码略。
F - Cat exercise
题意
有 \(N\) 座猫塔,从左到右排成一排,左数第 \(i\) 座 \((1\leq i\leq N)\) 猫塔的高度是 \(P_i\) 。保证 \((P_1,P_2,\ldots,P_N)\) 是 \((1,2,\ldots,N)\) 的排列。
左数第 \(i\) 座塔与第 \(j\) 座塔之间的距离定义为 \(\lvert i-j\rvert\) 。
有一只猫,最初位于高度为 \(N\) 的猫塔顶上。高桥希望通过反复地移走某座猫塔来锻炼这只猫。
当高桥移走一座塔时,猫的移动方式如下:
- 如果猫不在被移走的塔顶上,则不动。
- 如果猫在被移走的这座塔的塔顶上,并且该塔的左侧或右侧至少有一座相邻的塔存在,那么猫就会通过每次移动到相邻的塔上,以此前往目前它所能到达的高度最高的那座塔的位置。此时,猫移动的距离为移动前的塔楼与移动后的塔楼之间的距离(移动前、移动后和移动过程中的塔楼的高度并不重要)。
- 如果猫在被移走的这座塔的塔顶上,而该塔的左右两边没有相邻的塔,猫就会跳到高桥的怀里,练习到此结束。在这种情况下,不会发生移动。
在这里,在一座塔被移除后,原本与该塔左右相邻的两座塔(如果有)不会因此成为相邻的塔。
求猫在练习结束前移动的最大总距离。
思路
塔是可以根据我们的决策进行移除的,换句话说我们可以通过移除相邻的某座塔来决定猫的移动方向。例如猫在 \(i\) 位置,如果只想让猫向右移动,就只需要在移除塔 \(i\) 之前先把塔 \(i-1\) 移走即可。
又因为题目中提及“每次移除猫所在的塔后,猫回前往它所能到达的高度最高的那座塔的位置”,因此如果猫会从第 \(i\) 个位置的塔移动到第 \(j\) 个位置的塔,就需要保证 \(i \sim j\) 之间的所有其它塔都没有两侧这两座塔的高度高,否则明显猫有更好的选择。
因此,如果我们想让猫来到位置为 \(i\) 的塔,那么上一步猫所在的位置只可能是塔 \(i\) 的左右两侧第一个高于塔 \(i\) 的高度的塔(即左右两侧第一个 \(\gt P_i\) 的数所在位置)。这里便可以使用单调栈来对两侧第一个 \(\gt P_i\) 的数的信息进行维护。
最后因为猫总是从高的塔移动到低的塔上,所以可以倒序枚举塔的高度(\(i = n \sim 1\)),对于高度为 \(i\) 的塔,记 \(\text{dp}[i]\) 表示猫以高度为 \(i\) 的塔作为终点时,在前面的过程中能够移动的最大总距离。根据上面的讨论,此时 \(\text{dp}[i]\) 只会从左右两侧第一个高于 \(P_i\) 的塔转移过来。记上一座塔的位置为 \(p\),则转移方程为:
最后注意答案可能超过 int 范围。时间复杂度 \(O(N)\)。
代码
int n, a[200005], b[200005];
// b[i] 表示高度为 i 的塔的位置
int l[200005], r[200005];
// l[i], r[i] 表示左右两侧第一个 高于第i座塔 的塔所在位置
long long dp[200005];
// dp[i][j] 表示以高度为 i 的塔作为终点时 能移动的最远总距离
void solve()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
b[a[i]] = i;
}
stack<int> skl;
for(int i = 1; i <= n; i++)
{
while(!skl.empty() && a[skl.top()] < a[i])
skl.pop();
if(!skl.empty())
l[i] = skl.top();
skl.push(i);
}
stack<int> skr;
for(int i = n; i >= 1; i--)
{
while(!skr.empty() && a[skr.top()] < a[i])
skr.pop();
if(!skr.empty())
r[i] = skr.top();
skr.push(i);
}
for(int i = n - 1; i >= 1; i--) // 倒序看每个高度 i
{
int p = b[i]; // 获取高度为 i 的塔所在位置
if(l[p] != 0) // 左侧有高于 i 的塔
dp[i] = max(dp[i], dp[a[l[p]]] + abs(p - l[p]));
if(r[p] != 0) // 右侧有高于 i 的塔
dp[i] = max(dp[i], dp[a[r[p]]] + abs(p - r[p]));
}
cout << *max_element(dp + 1, dp + n + 1);
}

浙公网安备 33010602011771号