AtCoder Beginner Contest 365 题解

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(因为这一轮他赢了)。
  • 如果青木出的是『剪刀』(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,则意味着从 ij 的子数组在这一位上的异或和为 1

首先,遍历数组 a,计算 sum 数组,其中 sum[i] 表示从 a[1]a[i] 的异或累积和。

对于每一位 bit029,做如下操作:

  • 重新计算 a 数组在该位上的值,即 (sum[i] >> bit) & 1
  • 统计在该位上为 0 和为 1 的前缀和数组 per
  • 计算在该位上为 1 的子数组贡献数 num,通过遍历 per 数组,对于每一个 inum 增加 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

思路:
  1. 构建线段树:线段树是一种数据结构,可以高效地查询和更新区间内的信息。每个节点维护一个区间 [l, r] 的信息,根节点覆盖整个范围 [1, n],叶节点对应具体的线段信息。
  2. 节点结构:每个节点包含的信息有:
    • fl, fr:表示线段的固定左右端点。
    • gl, gr:表示线段的可变动左右端点。
    • _c:表示在当前节点区间内,点到线段的最短距离的累加和。
  3. 合并操作:当两个子节点区间合并时,需要计算新的区间信息和最短距离累加和。具体操作包括更新 fl, fr, gl, gr_c
  4. 更新操作:当需要更新某个区间 [q, q] 内的线段信息时,递归地将信息更新到相应的叶子节点,并在返回时更新路径上的所有父节点。
  5. 查询操作:查询区间 [ql, qr] 内的线段信息,递归地查询覆盖 [ql, qr] 的子区间,并将结果合并。
  6. 处理查询:对于每个查询 (sx, sy, tx, ty),使用线段树查询区间 [sx, tx] 的信息,找到最近的点 p,计算 |ty - p|,并加上线段树中存储的最短距离累加和。
时间复杂度
  • 构建线段树:O(n log n)
  • 查询操作:O(log n)
  • 更新操作:O(log n)
注意事项
  • 线段树的构建和查询需要对区间边界和条件仔细处理,以避免错误。
  • 在合并节点时,需要正确计算最短距离累加和 _c,以及更新可变动端点 glgr

代码:

太长了不贴这了,去看看这里

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Digital_Enigma

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值