AtCoder Beginner Contest 243

比赛相关信息

比赛信息

比赛名称: AtCoder Beginner Contest 243
比赛地址: Atcode

比赛过程回顾

A B C D E F G Ex
提交次数 1 1 1 1 2
首次提交时间 06:01 11:20 26:48 1:12:25 1:47:49
首A时间 06:01 11:20 26:48 1:12:25

部分题解与小结

A -

小评

使用暴力的解法做的,想复杂了,做的较慢。

题意

家里有 \(V\) 升洗发水, \(\tt{}F,M,T\) 三个人依次使用(用量为 \(A,B,C\) ),谁会不够用。

一组样例, \(1≤V,A,B,C≤10^5\)

思路

赛时思路

暴力循环,直到不够用输出结果,跳出循环, \(\mathcal{O}(V)\)

最优思路

先对三人的总使用量取模,再判断余数会使得谁不够用, \(\mathcal{O}(1)\)

AC代码

点击查看代码
思路1
void Solve() 
{
    int v, a[3]; cin >> v >> a[0] >> a[1] >> a[2];
    for (int i = 0; ; ++ i) {
        v -= a[i % 3];
        if (v < 0) {
            cout << "FMT"[i % 3];
            return;
        }
    }
}
思路2
void Solve() 
{
    int v, x, y, z; cin >> v >> x >> y >> z;
    v %= (x + y + z);
    if (v < x) cout << "F";
    else if (v < x + y) cout << "M";
    else cout << "T";
}

B - Hit and Blow

小评

这题思路对了,码的速度还行。

题意

给出长度为 \(N\) 的序列 \(A,B\) ,保证同一个序列中不存在相同元素,输出以下内容:

  • \(A_i=B_i\) 的数量
  • \(i \ne j\)\(A_i = B_j\) 的数量

一组样例, \(1≤N≤1000,1≤A_i≤10^9,1≤B_i≤10^9\)

思路

第一个数量直接遍历即可,第二个数量可以使用暴力比对( \(\mathcal{O}(N^2)\) ),亦可使用桶排序方法记录( \(\mathcal{O}(N)\) )。

AC代码

点击查看代码
桶排序记录
int a[N], b[N], num1, num2;
map<int, int> m;

void Solve() 
{
    int n; cin >> n;
    FOR (i, 1, n) cin >> a[i];
    FOR (i, 1, n) cin >> b[i], m[b[i]] ++ ;
    
    FOR (i, 1, n) {
        if (a[i] == b[i]) ++ num1;
        if (m[a[i]] != 0) ++ num2; 
    }
    
    cout << num1 << endl << num2 - num1 << endl;
}
暴力比对
int a[N], b[N], num1, num2;

void Solve() 
{
    int n; cin >> n;
    for (int i = 1; i <= n; ++ i) cin >> a[i];
    for (int i = 1; i <= n; ++ i) cin >> b[i];
    
    for (int i = 1; i <= n; ++ i) {
        for (int j = 1; j <= n; ++ j) {
            if (a[i] == b[j]) {
                (i == j ? num1 : num2) ++ ; 
            }
        }
    }
    cout << num1 << endl << num2 << endl;
}

C - Collision 2

小评

稍有思考难度的模拟题。

题意

二维平面上有 \(N\) 个人,保证每个人都在不同的位置,现在,所有人会根据给出的命令向左或是向右移动,问是否会有两个人撞到。

一组样例, \(1≤N≤2*10^5,0≤X_i≤10^9,0≤Y_i≤10^9\)

思路

赛时思路

将所有人放到一个容器中,以 \(Y\) 坐标为主关键字, \(X\) 坐标为副关键字进行排序,而排序这一步可以借助 \(\tt{}pair\) 或是 \(\tt{}tuple\) 自带的内置排序实现。对于某一个人而言,如果他向左走,且他的左边有人,且与他位于同一行,那么会发生碰撞(向右走则同理), \(\mathcal{O}(N * log_2N)\)

时间复杂度最优思路

对于 \(Y\) 坐标相同的人,只需要找到“最左边的向右的人”和“最右边的向左的人”,比较这两个人的 \(X\) 坐标即可, \(\mathcal{O}(N)\) 。代码实现力可能不及上述思路。

AC代码

点击查看代码
vector<tuple<int, int, char> > ver;
int x[N], y[N];

void Solve() 
{
    int n; cin >> n;
    FOR (i, 1, n) cin >> x[i] >> y[i];
    string s; cin >> s;
    
    FOR (i, 1, n) ver.pb({y[i], x[i], s[i - 1]});
    sort(ALL(ver));
    
    int flag = 0;
    FOR (i, 0, n - 2) {
        auto [x, y, z] = ver[i];
        auto [xx, yy, zz] = ver[i + 1];
        if(x == xx && z == 'R' && zz == 'L') flag = 1;
    }
    
    if (flag == 1) yes;
    else no;
}

D - Moves on Binary Tree

小评

总的来说这道题没有难度,规律很好猜,实现也不难,赛时卡在了计算答案式子的一个符号问题上,导致查了很久的错误。

题意

给定一棵无限大的满二叉树以及树上某一起点的编号 \(X\) ,再给出一串长度为 \(N\) 的行动串(由 \(L,R,U\) 构成,分别代表向左、向右、向上移动),询问最终的位置编号。

一组样例,\(1≤N≤10^6,0≤X_i≤10^{18}\) ,保证最终的位置编号不超过 \(10^{18}\)

思路

赛时思路

\(L,R\) 行动后如果紧跟一个 \(U\) 行动,那么位置不变。这一思想与优先队列的出对入队思想一致,只需要维护一个数据结构(此处我使用了同时支持首尾操作的双向队列 \(\tt{}deque\) ),使其中的数据不包含上述的情况即可。最后再计算数据结构中剩余的指令。\(\mathcal{O}(N)\)

时间复杂度最优思路

我们注意到,之所以要采取上述的优化策略是因为如果直接计算会导致超数据范围,以及大整数计算的效率低下,而二进制则可以很好的规避这两个问题。

  • 对于 \(U\) ,只需要弹出最后一位即可;
  • 对于 \(L\) ,只需要在最后插上一位 \(0\) 即可;
  • 对于 \(R\) ,只需要在最后插上一位 \(1\) 即可。

上述三个操作的时间复杂度均为 \(\mathcal{O}(1)\) ,而这一方法的总时间复杂度仅为 \(\mathcal{O}(N + log X + log Ans)\) ,但是代码实现力可能不及上述思路(暂时没有找到适合的库函数来简化进制转换的问题)。

AC代码

点击查看代码
赛时思路
void Solve() 
{
    LL n, x; string s; cin >> n >> x >> s;
    deque<char> ch;
    
    for (auto it : s) {
        if (!ch.empty() && ch.back() != 'U' && it == 'U') ch.pop_back();
        else ch.push_back(it);
    }
    
    for (auto it : ch) {
        if (it == 'L') x *= 2;
        else if (it == 'R') x = x * 2 + 1;
        else if (it == 'U') x /= 2 ;
    }
    cout << x << endl;
}
时间复杂度最优思路

注:为了省力,此处直接套用了以前的高精度进制转换模板。

map<char, int> mp; //将字符转化为数字
map<int, char> mp2; //将数字转化为字符
string change(int a, int b, string s) //进制转换板子
{
    for(int i = 0; i < 10; i++) mp[(char)i + 48] = i, mp2[i] = (char)i + 48;
    // for(int i = 10; i < 36; i++) mp[(char)i + 55] = i, mp2[i] = (char)i + 55;
    // for(int i = 36; i < 62; i++) mp[(char)i + 61] = i, mp2[i] = (char)i + 61;

    string sh;
    vector<int> nums, ans;
    for(auto c : s) nums.push_back(mp[c]);
    reverse(nums.begin(), nums.end());
    while(nums.size()){ //短除法,将整个大数一直除 b ,取余数
        int remainder = 0;
        for(int i = nums.size() - 1; ~i; i--){
            nums[i] += remainder * a;
            remainder = nums[i] % b;
            nums[i] /= b;
        }
        ans.push_back(remainder); //得到余数
        while(nums.size() && nums.back() == 0) nums.pop_back(); //去掉前导 0
    }
    reverse(ans.begin(), ans.end());
    for(int i : ans) sh += mp2[i];
    return sh;
}
void Solve() 
{
    int n; string x, s; cin >> n >> x >> s;
    x = change(10, 2, x);
    
    for (auto it : s) {
        if (it == 'L') x += '0';
        else if(it == 'R') x += '1';
        else x.pop_back();
    }
    
    cout << change(2, 10, x) << endl;
}

尾声

值得一提的是,赛时思路的代码在做了类似于“双重 \(\tt{}auto\) ”,“使用 \(\tt{}deque\) 等尾插删友好的数据结构”等优化后,运行时间非常之迅速,Atcode数据可以到达恐怖的 18 ms,甚至优于大部分时间复杂度最优算法。可以说卡常数党大胜利。

E - Edge Deletion

小评

在比赛快结束的时候才做到这道题,属于基础的图论板子题,实现思路完全基于 \(\tt{}Floyd\) 最短路算法(也可以说是 \(\tt{}DP\) ),能够很好的考查图论基本素养。

题意

给定一张稠密图,你需要找到尽可能多的无用边并删除它们,使得删除之后任意两个点之间的最短距离保持不变、图仍联通。

一组样例,\(2≤N≤300,N - 1≤M<N^2/ 2\) ,边权 \(1≤C_i≤10^9\)

思路

首先跑一遍弗洛伊德,得到两点之间的最短距离。然后对于每一条边,遍历其余所有点,只要存在其他更短的路径,那么这条边就是无用边。

具体而言,弗洛伊德的本质是使用中转点 \(K\) 更新距离( d[i][j] = min(d[i][j], d[i][k] + d[k][j]) ),而我们也只需要找到中转点 \(K\) 去查询原边的必要性即可(如果 d[x][i] + d[i][y] <= d[x][y] ,说明边 {i,j} 是无用边)。

AC代码

点击查看代码
void floyd() {
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
void Solve() 
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;
            
    vector<pair<int, int> > ver;
    for (int i = 1; i <= m; ++ i) {
        int x, y, w; cin >> x >> y >> w;
        d[x][y] = d[y][x] = min(d[x][y], w);
        ver.pb({x, y});
    }
    floyd();
    int ans = 0;
    for (auto [x, y] : ver) {
        for (int i = 1; i <= n; ++ i) {
            if (d[x][i] + d[i][y] <= d[x][y] && i != x && i != y) {
                ++ ans;
                break;
            }
        }
    }
    cout << ans << endl;
}

G - Sqrt

小评

赛后补题时发现这道题并不是很难,属于较为基础的数论题。

题意

给定一个序列 \(A\) ,初始只含有一个元素 \(X\) 。每次操作都会将序列 \(A\) 中的最后一个元素取出,然后将 \(1\)\(\sqrt{A_{len}}\) (包含)之间的一个数字插入序列末尾,如此往复 \(10^{100}\) 次,一共可以得到多少个不同的序列?

\(1≤T≤20\) 组样例, \(1≤X≤9×10 ^{18}\) ,可以证明答案小于 \(2^{63}\)

思路

约定使用 \(F(X)\) 代表所求值。首先,我们很容易的可以想到 \(\mathcal{O}(T * \sqrt X)\) 的做法——由 \(F(X)=\sum_{i=1}^{\sqrt X} F(i)\) 。根据上述的公式,我们想到,如果能对 \(F(i)\) 再进行一样的计算,那么就可以把复杂度优化到\(\mathcal{O}(T * \sqrt[4] X)\)

分析 \(290\) 这一样例,我们不难得到这样的公式: \(F(X)=\sum_{i=1}^{\sqrt[4]X} (\sqrt X - i^2 + 1)*F(i)\) 。我们还能通过预处理的步骤将其优化到 \(\mathcal{O}(\sqrt[4] X)\)

290 ->
17 16 15 14 …… 9 8 7 …… 4 3 2 1
4321 4321 321 321 …… 321 21 21 …… 21 1 1 1

AC代码

点击查看代码
LL dp[N], s[N];
void force(int n)
{
	s[1] = 1;
	FOR (i, 1, n) {
		dp[i] = s[(int)sqrt(i)];
		s[i] = s[i - 1] + dp[i];
	}
}
LL cul(LL n) 
{
	LL ans = 0;
	for (LL i = 1; i * i <= n; ++ i)
		ans += dp[i] * (n - i * i + 1);
	return ans;
}
LL mysqrt(LL N) {
	LL x = sqrt(N) - 1;
	while (x + 1 <= N / (x + 1)) ++ x;
	return x;
}
void Solve() 
{
	force(N - 5);
	int T; cin >> T; while (T --) {
		LL n; cin >> n;
		cout << cul(sqrt(n)) << endl;
	}
}

文 / WIDA
2022.03.14 成文
首发于WIDA个人博客,仅供学习讨论


posted @ 2022-03-14 19:58  hh2048  阅读(81)  评论(0)    收藏  举报