AtCoder Beginner Contest 365 题解
ABC365的比赛题解,我做出G题了,但暂时不发,需要的请私信
私信前请看:https://www.luogu.com.cn/paste/lowgb1lx
A - Leap Year
思路
-
输入年份:首先输入一个整数
y
,代表需要判断的年份。 -
条件判断:
- 如果
y
不是4的倍数,直接输出365。 - 如果
y
是4的倍数但不是100的倍数,输出366。 - 如果
y
是100的倍数但不是400的倍数,输出365。 - 如果
y
是400的倍数,输出366。
- 如果
代码
void solve() {
int y;
cin >> y;
if(y % 4 != 0)
cout << 365 << endl;
else if(y % 4 == 0 && y % 100 != 0)
cout << 366 << endl;
else if(y % 100 == 0 && y % 400 != 0)
cout << 365 << endl;
else if(y % 400 == 0)
cout << 366 << endl;
}
B - Second Best
思路
首先输入是必须滴(确信)
有一种神奇叫做暴力出奇迹,于是:
我们可以将所有数均输入后排序(本人用的sort
)然后直接输出第二大(即倒数第二个)
但这么做会出现一个失误:顺序(编号)错了
所以我们不能排序,或者是要先保存每个数的原本序号,可能用到结构体,这样会很麻烦
正确思路:
循环遍历数组,用一个 m a x n maxn maxn 来储存最大数, s c sc sc 来储存第二大元素,每次循环判断是否比原来的第二大元素大且不等于最大的元素,最后输出编号(我用 X X X 来存储)即可
代码
int a[N];
void solve() {
int n;
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
int maxn = 0;
for (int i = 0; i < n; i++)
if (a[i] > maxn)
maxn = a[i];
int sc = 0;
int x;
for (int i = 0; i < n; i++)
if (a[i] > sc && a[i] != maxn) {
sc = a[i];
x = i+1;
}
cout << x << endl;
}
C - Transportation Expenses
先说题目大意
将一个给定的总量 m m m 分配到一个数组 A A A 的每个元素上,使得每个元素分配到的量不超过其原始值,同时尽可能让更多的元素达到其原始值
如果总量 m m m 足够覆盖所有元素的总和,那么分配方案有无限多种,因为每个元素都可以得到等于其原始值的分配。
如果 m m m 不足以覆盖所有元素的总和,代码将找出一个最大的分配值 k k k,使得每个元素分配到的量不超过 k k k,并且总的分配量不超过 m m m
思路
计算数组元素的总和:
sum
:所有元素的总和。
检查是否存在无限解:
- 如果
sum
小于等于m
,表示所有元素都可以分配到其原始值,存在无限种分配方案,输出"infinite"
。
二分查找:
-
初始化二分搜索的边界:
l = 0
,r = 1e16
。 -
当
l
小于r
时,继续循环。 -
计算中间值
mid
,使用(l + r + 1) >> 1
来确保mid
总是偏向右边界。 -
调用 f l a g ( ) flag() flag()函数检查是否可以将每个元素分配到最多 m i d mid mid 的量而不超过 m m m 的总量。
-
如果
flag(mid)
返回true
,说明mid
是可行的,因此将左边界l
更新为mid
。 -
否则,将右边界
r
更新为mid - 1
。
-
输出结果:
- 输出二分查找的结果
l
,即每个元素最多能分配到的量。
f l a g flag flag 函数解析:
- f l a g flag flag 函数接受一个参数 k k k,表示考虑将每个元素分配最多 k k k 的量。
- 遍历数组 A A A,计算如果每个元素分配 m i n ( a [ i ] , k ) min(a[i], k) min(a[i],k) 的量,总的分配量是否超过 m m m。
- 如果总分配量超过 m m m,返回 f a l s e false false;否则返回 t r u e true true。
代码(加了点小注释~)
int a[N]; // Array for storing elements
int m, n; // Variables for problem inputs
// Check function to see if it's possible to distribute 'k' or less to each element
bool flag(int k)
{
int sum = 0;
for (int i = 1; i <= n; i++)
sum += min(a[i], k);
if (sum > m) return false;
return true;
}
// Main solving function
void solve()
{
int sum = 0;
cin >> n >> m; // Read the number of elements and the total distribution limit
for (int i = 1; i <= n; i++)
{
cin >> a[i]; // Read the elements
sum += a[i]; // Calculate the total sum of elements
}
if (sum <= m) // If the total sum is already less than or equal to 'm', there's an infinite solution
{
cout << "infinite" <<endl;
return;
}
int l = 0, r = 1e16, mid; // Binary search bounds
while (l < r)
{
mid = (l + r + 1) >> 1; // Calculate the middle value
if (flag(mid)) l = mid; // If 'flag' returns true, move the left bound
else r = mid - 1; // Otherwise, move the right bound
}
cout << l << endl; // Output the result
}
虽然是AI帮我加的
D - AtCoder Janken 3
前置知识(以免某些人不知道这个游戏咋玩)
石头剪刀布游戏的胜负关系如下:
- 石头(R)胜剪刀(S)
- 剪刀(S)胜纸(P)
- 纸(P)胜石头(R)
思路:
dp[i][j]
表示在第i
轮游戏结束后,高桥以动作j
结束时,他能取得的最大胜利次数。j
的值有四种可能:1
表示高桥出『石头』(R)。2
表示高桥出『剪刀』(S)。3
表示高桥出『纸』(P)。-INF
是一个不可能的状态,用于标记在某轮游戏结束时高桥不能以某种动作结束的情况。
对于每一轮游戏 i
:
- 如果青木出的是『石头』(R):
- 高桥出『纸』(P)时,他可以赢得比赛,所以
dp[i][1]
更新为前一轮中出『剪刀』(S)或『纸』(P)的最大胜利次数。 - 高桥出『剪刀』(S)时,他将与青木打平,但为了满足条件,不能与前一轮重复,所以
dp[i][2]
设置为-INF
。 - 高桥出『石头』(R)时,他将输掉比赛,但为了满足条件,他可以选择与青木打平,所以
dp[i][3]
更新为前一轮中出『剪刀』(S)或『石头』(R)的最大胜利次数加上1(因为这一轮他赢了)。
- 高桥出『纸』(P)时,他可以赢得比赛,所以
- 如果青木出的是『剪刀』(S)或『纸』(P),同理可得:
- 『剪刀』(S):高桥出『石头』(R)时赢,出『纸』(P)时平,出『剪刀』(S)时输。
- 『纸』(P):高桥出『剪刀』(S)时赢,出『石头』(R)时平,出『纸』(P)时输。
代码
(其实这道题我一开始是想用string
的,但是老是报错,一气之下改成char
了)
int n,dp[200007][4];
char c;
void solve() {
cin>>n;
for(int i=1; i<=n; i++) {
cin>>c;
if(c=='R') {
dp[i][1]=max(dp[i-1][2],dp[i-1][3]);
dp[i][2]=-INF;
dp[i][3]=max(dp[i-1][2],dp[i-1][1])+1;
}
if(c=='S') {
dp[i][2]=max(dp[i-1][1],dp[i-1][3]);
dp[i][3]=-INF;
dp[i][1]=max(dp[i-1][2],dp[i-1][3])+1;
}
if(c=='P') {
dp[i][3]=max(dp[i-1][1],dp[i-1][2]);
dp[i][1]=-INF;
dp[i][2]=max(dp[i-1][1],dp[i-1][3])+1;
}
}
cout<<max(dp[n][1],max(dp[n][2],dp[n][3]))<<endl;
}
E - Xor Sigma Problem
思路:
关键思路
对于任意子数组 a[i..j]
,其异或和可以通过 sum[j] ^ sum[i-1]
来计算,其中 sum[k]
是从 a[1]
到 a[k]
的异或累积和。
由于异或操作在每一位上独立进行,我们可以针对每一位分别计算贡献,然后将每位的贡献乘以对应的 2^bit
再相加。
对于每一位,我们统计所有子数组中该位为 1
的贡献。总的来说,如果在某一位上 sum[j]
和 sum[i-1]
的异或结果为 1
,则意味着从 i
到 j
的子数组在这一位上的异或和为 1
。
首先,遍历数组 a
,计算 sum
数组,其中 sum[i]
表示从 a[1]
到 a[i]
的异或累积和。
对于每一位 bit
从 0
到 29
,做如下操作:
- 重新计算
a
数组在该位上的值,即(sum[i] >> bit) & 1
。 - 统计在该位上为
0
和为1
的前缀和数组per
。 - 计算在该位上为
1
的子数组贡献数num
,通过遍历per
数组,对于每一个i
,num
增加per[i - 2][!a[i]]
。
将每一彼特位上的贡献数乘以 2^bit
累加起来,得到最终答案。
这种方法利用了异或运算的性质和位操作技巧,通过计算每一位的贡献来避免直接计算所有子数组的异或和,显著提高了算法的效率。由于每一位的计算都是独立的,整个算法的时间复杂度为 O ( n × l o g ( m a x ( a [ i ] ) ) O(n \times log(max(a[i])) O(n×log(max(a[i])),其中 l o g ( m a x ( a [ i ] ) ) log(max(a[i])) log(max(a[i])) 是为了处理每一位的计算。
代码:
(bit
不算关键词吧)
int n, a[N], sum[N], per[N][2];
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
sum[i] = (sum[i - 1] ^ a[i]);
}
int ans = 0;
for (int bit = 0; bit < 30; bit++) {
int num = 0;
for (int i = 1; i <= n; i++) {
a[i] = (sum[i] >> bit) & 1;
}
per[0][0] = 1;
for (int i = 1; i <= n; i++) {
per[i][0] = per[i - 1][0] + !a[i];
per[i][1] = per[i - 1][1] + a[i];
}
for (int i = 2; i <= n; i++) {
num += per[i - 2][!a[i]];
}
ans += (1LL << bit) * num;
}
cout << ans << endl;
}
F - Takahashi on Grid
思路:
- 构建线段树:线段树是一种数据结构,可以高效地查询和更新区间内的信息。每个节点维护一个区间
[l, r]
的信息,根节点覆盖整个范围[1, n]
,叶节点对应具体的线段信息。 - 节点结构:每个节点包含的信息有:
fl
,fr
:表示线段的固定左右端点。gl
,gr
:表示线段的可变动左右端点。_c
:表示在当前节点区间内,点到线段的最短距离的累加和。
- 合并操作:当两个子节点区间合并时,需要计算新的区间信息和最短距离累加和。具体操作包括更新
fl
,fr
,gl
,gr
和_c
。 - 更新操作:当需要更新某个区间
[q, q]
内的线段信息时,递归地将信息更新到相应的叶子节点,并在返回时更新路径上的所有父节点。 - 查询操作:查询区间
[ql, qr]
内的线段信息,递归地查询覆盖[ql, qr]
的子区间,并将结果合并。 - 处理查询:对于每个查询
(sx, sy, tx, ty)
,使用线段树查询区间[sx, tx]
的信息,找到最近的点p
,计算|ty - p|
,并加上线段树中存储的最短距离累加和。
时间复杂度
- 构建线段树:
O(n log n)
- 查询操作:
O(log n)
- 更新操作:
O(log n)
注意事项
- 线段树的构建和查询需要对区间边界和条件仔细处理,以避免错误。
- 在合并节点时,需要正确计算最短距离累加和
_c
,以及更新可变动端点gl
和gr
。
代码:
太长了不贴这了,去看看这里吧