AtCoder-ABC-410 题解 | 致谢:侍同学、胡同学、黄同学、杨同学

在这里插入图片描述

比赛速览

  • A. 模拟
  • B. 模拟(苏州,小学,侍**同学)
  • C. 位置换算(苏州,初二,胡**同学、河北,初一,杨 **同学)
  • D. Dijkstra 变形应用(苏州,初一,黄**同学)
  • E. DP 与 优化
  • F. 《最大全1子矩阵》变形题
  • G. 线段树 与 最大不相交区间数

A - 模拟

在AtCoder王国中,正在举行 N N N场赛马比赛。
年龄在 A i A_i Ai岁及以下的马可以参加第 i i i场比赛。
请问一匹 K K K岁的马可以参加这 N N N场比赛中的多少场?

B - 模拟

有编号为 1 , 2 , … , N 1,2,\dots,N 1,2,,N N N N个盒子。初始时所有盒子都是空的。
将会有 Q Q Q个球按顺序到来。Takahashi会按照序列 X = ( X 1 , X 2 , … , X Q ) X=(X_1,X_2,\dots,X_Q) X=(X1,X2,,XQ)将这些球放入盒子中。具体来说,对于第 i i i个球,他会执行以下操作:

  • 如果 X i ≥ 1 X_i \ge 1 Xi1:将这个球放入编号为 X i X_i Xi的盒子
  • 如果 X i = 0 X_i = 0 Xi=0:将这个球放入当前球数最少的盒子中编号最小的那个

请确定每个球最终被放入哪个盒子。

参考程序(苏州,小学,侍**同学)

#include<bits/stdc++.h>
using namespace std;
long long n,q;
long long vis[110];
long long find_min()
{
	long long mn=LONG_LONG_MAX,cnt=0;
	for(int i=1;i<=n;i++)
	{
		if(vis[i]<mn)
		{
			mn=vis[i];
			cnt=i;
		}
	}
	return cnt;
}
int main()
{
	cin>>n>>q;
	for(int i=1;i<=q;i++)
	{
		long long x;
		cin>>x;
		if(x>=1)
		{
			vis[x]++;
			cout<<x<<" ";
		}
		else if(x==0)
		{
			long long t=find_min();
			vis[t]++;
			cout<<t<<" ";
		}
	}
	return 0;
}

C - 位置换算

有一个初始长度为 N N N的整数序列 A A A,初始时 A i = i A_i = i Ai=i。需要处理以下 Q Q Q种类型的查询:

  • 类型1:将 A p A_p Ap的值修改为 x x x
  • 类型2:输出 A p A_p Ap的当前值
  • 类型3:将序列 A A A进行 k k k次"将第一个元素移动到末尾"的操作
    • 具体来说,就是将序列 A A A替换为 ( A 2 , A 3 , … , A N , A 1 ) (A_2,A_3,\dots,A_N,A_1) (A2,A3,,AN,A1),共执行 k k k

请根据查询要求进行相应的处理。

本题的难点在于如何处理移动对于移动的操作复杂度不允许我们直接模拟,所以我们必须通过记录总的移动次数来反向推算数据此时的位置。

例如对于当前的 A p A_p Ap, 在移动 k k k次之前对应的位置是 ( p + k ) (p+k) % N (p+k), 因此因此我们实际操作的位置是 ( p + k ) (p+k) % N (p+k)

但是这里有一个坑点,就是当 ( p + k ) (p+k) %N == 0 (p+k)的时候,我们所对应的位置不是 0 0 0位置,而是 N N N 位置。

参考程序(苏州,初二,胡**同学)

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn = 1e6 + 5;
int a[maxn], temp = 0;
int n, q;
int mod = 0;
int mo(int x){return (x % n + n) % n;}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin >> n >> q;
    // for(int i = 1;i <= 1e9;i ++){
    //     if(n * i > 1e9){mod = n * (i - 1);break;}
    // }
    // cout << mod << endl;
    for(int i = 1;i <= n;i ++)a[i] = i;
    // cout << q << ' ';
    while(q --){
        int tp;cin >> tp;
        // cout << tp << endl;
        int k;cin >> k;
        if(tp == 1){
            int ans;cin >> ans;
            a[mo(k + temp) == 0 ? n : mo(k + temp)] = ans;
        }
        else if(tp == 2){
            // cout << k << ' ' << k - temp << ' ' << mo(k - temp) << ' ';
            cout << a[mo(k + temp) == 0 ? n : mo(k + temp)] << endl;
        }
        else {
            temp += k;
            temp %= n;
        }
    }
}

方法二

我们也可以在一开始就将所有的位置从0开始使用。这样就不需要去额外判断。对于题目操作的 A p A_p Ap, 此时对应就是 A p − 1 A_{p-1} Ap1, 在移动 k k k次之前对应的位置是 ( p − 1 + k ) (p-1+k) % N (p1+k)

参考程序(河北,初一,杨**同学)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAX 1000005
int n,q,k=0;
int a[MAX];
int main()
{
    cin>>n>>q;

    for (int i=0;i<n;i++){
        a[i]=i+1;
    }
    for (int i=1;i<=q;i++){
        int x;
        cin>>x;
        if (x==1){
            int p,b;
            cin>>p>>b;
            a[(p+k-1)%n]=b;
        }
        if (x==2){
            int p;
            cin>>p;
            cout<<a[(p+k-1)%n]<<endl;
        }
        if (x==3){
            int p;
            cin>>p;
            k+=p;
            k%=n;
        }
    }
    return 0;
}

D - Dijkstra 变形应用

有一个包含 N N N 个顶点和 M M M 条边的有向图,其中顶点编号为 1 1 1 N N N,边编号为 1 1 1 M M M。第 i i i 条边是从顶点 A i A_i Ai 指向顶点 B i B_i Bi 的带权边,权重为 W i W_i Wi

要求找到从顶点 1 1 1 到顶点 N N N 的某条路径中,所经过边的权重的按位异或( m a t h r m X O R \\mathrm{XOR} mathrmXOR 的最小值 。

本题与408E题很相似。但是做法不同

本题我们考虑异或操作的特性,并没有明确的贪心思路,同时考虑复杂度允许N^2。然后我们观察到所有边权都是1024以内的,最终的边权路径异或和也是1024以内。因此把所有的路径结果都算出来是否存在,就可以找到最小的。

  • dis[i][j] 表示从1点到i号点,路径异或和为j的路径是否存在。
  • 初始化:dis[1][0] = 1
  • 然后就可以延用Dijkstra的思路开始搜索扩展。

参考程序(苏州,初一,黄**同学)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll NN = 1e3 + 5;
const ll INF = 1e18;
queue<pair<ll, ll>> Q;
vector<pair<ll, ll>> G[NN];
ll N, M, u, v, w, dis[NN][1024], minxor = 1024;
int main() {
    cin >> N >> M;
    for(ll i = 0; i < M; i ++){
        cin >> u >> v >> w;
        G[u].push_back({v, w});
        //G[v].push_back({u, w});
	}   
    for(ll i = 1; i <= N; i ++) for(ll j = 0; j < 1024; j ++) dis[i][j] = INF;
    dis[1][0] = 0;
    Q.push({1, 0});
    while(!Q.empty()){
        auto cnt = Q.front(); Q.pop();
        ll u = cnt.first, cntxor = cnt.second;
        for(auto xx : G[u]){
            ll v = xx.first, w = xx.second, newxor = w ^ cntxor;
            if(dis[v][newxor] == INF){
                dis[v][newxor] = dis[u][cntxor] + 1;
                Q.push({v, newxor});
            }
        }
    }
    for(ll i = 0; i < 1024; i ++) if(dis[N][i] != INF && i < minxor) minxor = i;
    if(minxor == 1024) cout << -1 << endl;
    else cout << minxor << endl;
}

E - DP 与 优化

Takahashi 将要按顺序挑战 N N N 只怪物。初始时,Takahashi 的生命值 H H H魔法值 M M M

在与第 i i i 只怪物战斗时,他可以选择以下两种行动之一:

  • 不使用魔法战斗:只有当他的生命值至少为 A i A_i Ai 时才能选择。选择后,他的生命值减少 A i A_i Ai,并击败该怪物。
  • 使用魔法战斗:只有当他的魔法值至少为 B i B_i Bi 时才能选择。选择后,他的魔法值减少 B i B_i Bi,并击败该怪物。

游戏会在所有 N N N 只怪物被击败或无法采取任何行动时结束。问:在游戏结束前,他最多能击败多少只怪物?

方法一

这是一个非常明显的 DP 问题,如果数据量比较小,我们可以很容易的写出 DP 的解法。

dp[i][j][k] 表示打死第i个怪兽,还剩余j点血和k点魔法,这个情况是否存在。

但是这显然是个N^3做法。接下来我们对此进行优化。

稀疏的状态: 当我们考虑这个三层 DP状态的时候我们可以发现大部分情况都是0,只有少部分的情况是1。因此我们做的大部分转移都是无效转移。

于是我们考虑反过来用当前状态来更新产生的新状态。这时候我们发现对于任何一个当前的状态,我所能产生的新状态只有两个。即选择使用 A 还是选择使用 B。

所以我们初始的状态是 dp[0][H][W] = 1, 其他都是0。

我们在转移的时候只会使用当前存在的状态去产生新状态。对于当前是0的状态,它没有必要去更新任何状态,它也产生不了新的状态。由此,我们就可以写出一个 BFS 的更新过程。

事实上我们没有必要保留三维,我们只需要保留后面两个维度就可以了。

方法二

dp[i][j] 表示打败第i个怪兽,血剩余j点是,剩余最多的魔力值是多少。

然后就是常规的转移了。

#include <bits/stdc++.h>
using namespace std;

int n,h,m;
int dp[3005][3005];
int a[3005];
int b[3005];

int main() {
    scanf("%d %d %d",&n,&h,&m);
    memset(dp,-1,sizeof(dp));
    for(int i=0;i<=h;i++) {
        dp[0][i]=m;
    }
    for(int i=h+1;i<=3000;i++) {
        dp[0][i]=-1;
    }
    for(int i=1;i<=n;i++) {
        scanf("%d %d",&a[i],&b[i]);
    }
    int ret=0;
    for(int i=1;i<=n;i++) {
        for(int j=0;j<=3000;j++) {
            if (dp[i-1][j]<0) {
                continue;
            }
            if (j>=a[i]) {
                dp[i][j-a[i]]=max(dp[i][j-a[i]],dp[i-1][j]);
            }
            if (dp[i-1][j]>=b[i]) {
                dp[i][j]=max(dp[i][j],dp[i-1][j]-b[i]);
            }
        }
        bool flag=false;
        for(int j=0;j<=3000;j++) {
            if (dp[i][j]>=0) {
                flag=true;
                break;
            }
        }
        if (!flag) {
            printf("%d",i-1);
            return 0;
        }
    }
    printf("%d",n);
}

F - 《最大全1子矩阵》变形题

给定一个 H × W H \times W H×W 的网格,每个单元格包含 #.
网格信息由 H H H 个长度为 W W W 的字符串 S 1 , S 2 , … , S H S_1, S_2, \dots, S_H S1,S2,,SH 给出,其中第 i i i 行第 j j j 列的单元格的符号与 S i S_i Si 的第 j j j 个字符相同。

要求计算满足以下条件的矩形区域的数量:

  • 该矩形区域内 # 的数量和 . 的数量相等。

形式化地说,求满足以下所有条件的整数四元组 ( u , d , l , r ) (u, d, l, r) (u,d,l,r) 的数量:

  • 1 ≤ u ≤ d ≤ H 1 \leq u \leq d \leq H 1udH
  • 1 ≤ l ≤ r ≤ W 1 \leq l \leq r \leq W 1lrW
  • 从第 u u u 行到第 d d d 行、第 l l l 列到第 r r r 列的子网格中,#. 的数量相同。

给定 T T T 个测试用例,对每个测试用例输出答案。

本题依然是一个比较模版的题目。如果你做过最大全1 子矩阵就知道应该枚举上下界,然后转换成子段和的问题来处理。

如果我们枚举了上下界,那么只要确定左右界就可以了。我们可以把子矩阵压缩成一个数组,# 用-1代替,. 用1代替,求和压缩即可。问题变成了求一个数组中有多少个子段的和为0.变成了有多少组位置前缀和相同。

G - 线段树 与 最大不相交区间数

在一个圆周上按顺时针方向排列着 2 N 2N 2N 个互不相同的点,编号依次为 1 , 2 , 3 , … , 2 N 1, 2, 3, \dots, 2N 1,2,3,,2N

给定 N N N 条弦,其中第 i i i 条弦连接点 A i A_i Ai 和点 B i B_i Bi,且所有 2 N 2N 2N 个端点 A 1 , … , A N , B 1 , … , B N A_1,\ldots,A_N,B_1,\ldots,B_N A1,,AN,B1,,BN 互不相同。

按顺序执行以下操作:

  1. 从现有的 N N N 条弦中,选择若干条互不相交的弦,并删除其余弦;
  2. 自由添加一条新弦到圆周上。

求操作完成后,所有弦之间的最大可能交点数。

image.png

由于最后任意添加弦,所以可以和每条弦都相交,问题变成了,求最多能选多少条弦,保证无交点。

问题转化成区间上就是多少个不相交的区间。

如果我们按照右端点排序,那么嵌套的区间的L应当是一个下降子序列。所以最多的嵌套层数就是L最长下降子序列的长度。


赛后交流
微信号:jikecheng11,赛后大家可以一起交流做题思路,分享做题技巧,欢迎大家的加入!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值