AtCoder Beginner Contest 400 ABCDEF 题目解析

A - ABC400 Party

题意

给定一个正整数 \(A\),问能否找到另一个正整数 \(B\),使得 \(A \times B = 400\)

思路

\(400\) 作为被除数,判断是否整除,若能则输出商即可。

代码

int a;
cin >> a;
if(400 % a == 0)
    cout << 400 / a;
else
    cout << -1;

B - Sum of Geometric Series

题意

给定两个正整数 \(N, M\),请判断 \(X = \sum\limits_{i=0}^M N^i = N^0 + N^1 + \dots + N^M\)\(10^9\) 间的大小关系。

如果 \(X\) 大于 \(10^9\),输出 inf,否则输出 \(X\)

思路

注意不要把答案算出来后再去判断大小,因为 \(N^M\) 这个数值非常大。

可以一遍计算一遍判断与 \(10^9\) 之间的关系,只要过程中超出了就直接输出 inf

注意使用 long long

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
    ll n, m;
    cin >> n >> m;
    
    ll x = 0, y = 1;
    for(int i = 0; i <= m; i++)
    {
        x += y; // 求总和
        y *= n; // 乘上 n 作为下一项
        if(x > 1e9)
        {
            cout << "inf";
            return 0;
        }
    }
    cout << x;
    
    return 0;
}

C - 2^a b^2

题意

给定一个正整数 \(N\),问 \(N\) 以内共有多少个正整数 \(X\) 满足:

  • 存在一对正整数 \((a, b)\) 使得 \(X = 2^a \times b ^2\)

思路

\(2\) 是一个质数,可以从 \(2^a\) 这一边入手。

\(a = 0\) 时,明显就是在求 \(N\) 以内有多少个完全平方数,答案为 \(\lfloor\sqrt{N}\rfloor\) 个。

\(a = 1\) 时,就是在求 \(\dfrac N 2\) 以内有多少个完全平方数,答案为 \(\lfloor\sqrt{\dfrac N 2}\rfloor\) 个。

\(a = 2\) 时,发现 \(2^2 \times b^2 = 2^0 \times (2^1\times b)^2\),答案已经在 \(a=0\) 中求出。

\(a = 3\) 时,发现 \(2^3 \times b^2 = 2^1 \times (2^1\times b)^2\),答案已经在 \(a=1\) 中求出。

\(a \ge 4\) 时,同理,可以得出答案已在 \(a = a\bmod 2\) 中求出。

因此答案就是 \(\lfloor\sqrt{N}\rfloor + \lfloor\sqrt{\dfrac N 2}\rfloor\)

小提示:\(N\) 比较大,建议使用更精确的 sqrtl 函数开方,或者手写二分求平方根。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
    ll n;
    cin >> n;
    ll d = sqrtl(n / 2);
    d += sqrtl(n / 4);
    cout << d;
    return 0;
}

D - Takahashi the Wall Breaker

题意

一张 \(H \times W\) 的网格图,其中 . 表示可以走的格子,# 表示障碍物。

在网格图中,每一步可以往上下左右四个方向走一格。如果相邻点是 .,可以直接走,不会消耗次数。而如果相邻点是 #,可以选择消耗一次次数使用“前踢”,这会使得面前两个格子的障碍物(如果有)直接被移除。

有个人想从 \((A, B)\) 走到 \((C, D)\),问他至少需要使用多少次“前踢”?

思路

范围太大,采用宽搜解题。

注意过程中如果使用了“前踢”,按照题意会导致地图被改变。但宽搜过程中如果修改了地图可能会导致其他位置在搜索时答案不正确。

所以我们这边可以把“前踢”直接理解为“穿墙”,也就是说,我们可以消耗一次次数,让当前这头牛往前最多穿 \(2\) 格的障碍物 #。这一点在宽搜过程中根据遇到的格子是否为 # 去处理是否需要消耗次数即可,一次性可以往同个方向处理两格。

但如果就这样使用单个队列直接宽搜,可能会导致超时。因为第一次遇到某个格子时,可能是消耗了比较多的次数“前踢”到达的,有可能在此之后会以更少的“前踢”次数到达这个格子。由于答案更优秀,所以需要重新从这个格子开始往外搜索,这将会导致同一个格子被访问多次。

解决方法(之一)是:可以采取两个队列交替搜索的方式。

定义当前队列为 cur,另一个队列为 nxt,每次从当前队列 cur 中取点出来继续搜索。如果搜索到的下一个点并没有消耗次数,那么则将下一个点重新放回当前队列 cur,类似普通队列的做法。而如果走到下一个点需要消耗一次“前踢”,则将下一个点放到 nxt 队列里去。直到当前队列中的元素全部搜完,切换两个队列重新开始搜索。

这种做法的好处是,我可以先把所有消耗次数为 \(0\) 的点全部搜完,之后再统一去搜消耗次数为 \(1\) 才能到达的点,然后再搜次数为 \(2\) 才能到达的,然后 \(3, 4, 5, \dots\)。这样可以保证图中每个点最多只会被搜索一次,时间复杂度控制在 \(O(H\cdot W)\)

代码

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

struct point
{
    int x, y, step; // step 表示使用的
};
point s, t;
queue<point> q[2];

int n, m;
char mp[1005][1005];
int dis[1005][1005]; // dis[i][j] 记录到达 (i, j) 需要消耗的最少前踢次数

const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = {0, 0, -1, 1};

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin >> mp[i][j];
    cin >> s.x >> s.y >> t.x >> t.y;
    
    memset(dis, 0x3f, sizeof dis); // 初始化为正无穷

    s.step = 0;
    q[0].push(s); // 起点进队
    dis[s.x][s.y] = 0;
    
    int cur = 0, nxt = 1; // 当前队列为 q[0],另一个为 q[1],记录下标
    
    while(!q[0].empty() || !q[1].empty())
    {
        while(!q[cur].empty()) // 只要当前队列不为空
        {
            point u = q[cur].front(), v;
            q[cur].pop();
            
            if(dis[u.x][u.y] != u.step)
                continue; // 当前这个point的步数与目前最优答案不同,直接排除
            
            for(int i = 0; i < 4; i++) // 方向
            {
                bool wall = false; // 判断是否遇到了墙
                for(int j = 1; j <= 2; j++) // 可以考虑一次性最多走两格
                {
                    v.x = u.x + dx[i] * j;
                    v.y = u.y + dy[i] * j;
                    
                    if(v.x < 1 || v.y < 1 || v.x > n || v.y > m)
                        continue;
                    
                    if(mp[v.x][v.y] == '#')
                        wall = true;
                    
                    int to; // 当前这个点 v 要放到哪个队列里
                    if(wall == true)
                    {
                        v.step = u.step + 1; // 遇到墙则需要让次数 +1
                        to = nxt; // 这个点需要到另一个队列里去
                    }
                    else
                    {
                        v.step = u.step;
                        to = cur;
                    }
                    if(dis[v.x][v.y] <= v.step) // v 的答案不优秀,排除
                        continue;
                    q[to].push(v);
                    dis[v.x][v.y] = v.step;
                }
            }
        }
        swap(cur, nxt); // 交换两队列
    }
    
    cout << dis[t.x][t.y]; // 输出到达终点的最少前踢次数
    
    return 0;
}

E - Ringo's Favorite Numbers 3

题意

\(Q\) 次询问,每次给定一个范围在 \([36, 10^{12}]\) 内的正整数 \(A\),问不超过 \(A\) 的所有正整数中最大的满足下列条件的整数 \(X\) 是什么:

  • \(X\) 严格只有 \(2\) 种质因子。
  • 假设 \(X\) 的两种质因子分别为 \(p, q\),并写成 \(X = p^a \times q^b\) 形式,此时需要满足 \(a\)\(b\) 均为偶数。

思路一

打表,找出 \(10^{12}\) 内所有满足上述条件的整数。

  1. 对于质因子而言,在答案中至少需要出现 \(2\) 次方,因此我们只需要先统计出所有 \(\sqrt{10^{12}} = 10^6\) 以内的质数。可以借助埃氏筛或欧拉筛。打表可以发现数量为 \(78498\) 个。
  2. 对于每个找出来的质数 \(p\),求出 \(p^2, p^4, p^6, \dots\) 等所有 \(p\) 的偶数次幂,存储在容器中(数组或 vector),然后排序。打表可以发现数量为 \(78734\) 个,增长不多。
  3. 对于找出来的所有质数的偶数次幂,直接采用 \(O(n^2)\) 的代码枚举两个不同的数字乘积作为答案。由于质数平方后每个数字都非常大,只要在过程中加上超范围就结束内层循环的判断,实际上根本不会跑满复杂度。最终答案总数为 \(288726\) 个。

最后对于每个询问,二分查找 \(\le A\) 的最大答案即可。

代码一

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

bool vis[1000005];
int prime[100005], pcnt = 0;

vector<ll> pvec;
vector<ll> ans;

void init() // 埃氏筛
{
    for(int i = 2; i <= 1000000; i++)
    {
        if(!vis[i])
        {
            prime[++pcnt] = i;
            if(i > 1000)
                continue;
            for(int j = i * i; j <= 1000000; j += i)
                vis[j] = true;
        }
    }
}

void init2()
{
    for(int i = 1; i <= pcnt; i++)
    {
        ll d = 1LL * prime[i] * prime[i], t = d; // d 是第 i 个质数的平方
        pvec.push_back(d);
        while(1.0 * t * d <= 1e12) // 只要 t*d 还没超过 1e12,继续乘平方
        {
            t *= d;
            pvec.push_back(t);
        }
    }
    sort(pvec.begin(), pvec.end()); // 排序
}

void init3()
{
    for(int i = 0; i < pvec.size(); i++) // 枚举第一个数字 pvec[i]
    {
        for(int j = i + 1; j < pvec.size(); j++) // 枚举第二个数字 pvec[j]
        {
            if(pvec[j] % pvec[i] == 0) // 要保证选择的两个数不能有共同质因子
                continue;
            if(1.0 * pvec[i] * pvec[j] > 1e12) // 重要,如果超范围则直接结束内层循环
                break;
            ans.push_back(pvec[i] * pvec[j]);
        }
    }
    sort(ans.begin(), ans.end());
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    
    init(); // 素数筛
    init2(); // 求素数的所有偶数次方
    init3(); // 求所有答案

    int q;
    cin >> q;
    while(q--)
    {
        ll n;
        cin >> n;
        // upper_bound 二分找 >n 的最小答案
        int p = upper_bound(ans.begin(), ans.end(), n) - ans.begin();
        // 上一个位置就是 <= n 的最大答案,即所求
        cout << ans[p - 1] << "\n";
    }
    
    return 0;
}

思路二

按照标程做法,由于最终找到的答案一定是一个完全平方数,所以不如直接对所有数字(包括输入的 \(A\))先开方,找到在一次方下的解,最后再平方输出即可。

我们可以使用类埃氏筛来找出所有 \(10^6\) 中包含恰好 \(2\) 个质因子的所有数字,统计起来。

最后每次输入一个 \(A\),同样借助二分找出 \(\le \sqrt{A}\) 的最大数字,输出其平方即是答案。

代码二

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

int cnt[1000005];
// cnt[i] 表示 i 有多少种质因子

vector<int> v;
// v 存储 1e6 以内所有包含两种质因子的数字

void init() // 类埃氏筛
{
    for(int i = 2; i <= 1000000; i++)
        if(!cnt[i]) // i 是质数
        {
            for(int j = i; j <= 1000000; j += i)
                cnt[j]++; // j 存在一个质因子 i
        }
}

int main()
{
    init();
    
    for(int i = 2; i <= 1000000; i++)
        if(cnt[i] == 2)
            v.push_back(i);
    
    int q;
    cin >> q;
    while(q--)
    {
        ll n;
        cin >> n;
        n = sqrt(n);
        // upper_bound 二分找 >n 的最小答案
        int p = upper_bound(v.begin(), v.end(), n) - v.begin();
        // 上一个位置就是 <= n 的最大答案,平方后输出
        cout << 1LL * v[p - 1] * v[p - 1] << "\n";
    }
    
    return 0;
}

F - Happy Birthday! 3

思路

给定正整数 \(N\) 以及两个长度为 \(N\) 的数组 \(\{C\}\)\(\{X\}\)

在一个环上总共有 \(N\) 个位置,每个位置按顺时针顺序分别编号为 \(1, 2, \dots, N\),其中编号为 \(i\) 的位置也可以被叫做编号 \(i+N\)。一开始,每个位置的颜色均为 \(0\)

你需要进行一些下述操作,使得最终位置 \(i\) 的颜色能够变成 \(C_i\),并且总花费最小,并求出这个最小花费:

  • 选择环上连续的一段,并将所有格子均涂成颜色 \(c\)。如果这一次涂色影响的位置总数量为 \(b\),则本次操作花费为 \(b + X_c\).

思路

首先考虑最极端的两种操作,一种是每个点都采用单独涂色的方法,一种是尽可能让同色的点都连起来一起涂。即使是采用后面的方案,我们的最终涂色方案肯定会在环上某个位置出现缺口(相邻两个数字不会被同时操作)。又因为这道题目是在环上进行的,可以借助化环为链的方法将数组扩大为原来的两倍,再在链上考虑问题。

根据数据范围,可以考虑区间 DP。

dp[l][r] 表示将区间 \([l, r]\) 全部涂成目标颜色所需要的最小花费。按照区间长度枚举每一个区间。

考虑状态转移方程。在处理 dp[l][r] 这个问题时,可以当作所有长度小于 \(r-l+1\) 的区间答案均已处理出。我们可以假设最右边的位置 \(r\) 是新涂色的位置,那么为了让他涂上对应的颜色,会出现两种情况:

  • 第一种情况,区间 \([l, r-1]\) 内没有任何一个位置与 \(r\) 位置同色,那么 \(r\) 只能采取单点涂色的方法,答案为 dp[l][r-1] + (x[c[r]] + 1)
  • 第二种情况,区间 \([l, r-1]\) 内存在至少一个位置与 \(r\) 位置同色,那么可以考虑让 \(r\) 这个位置与此前某个同色的位置一起涂。记想要一起涂的那个位置为 \(x\),那么可以在区间 \([x+1, r-1]\) 涂色之前就把这两个位置涂上颜色,花费就只需要加上 \(r\)\(x\) 的位置差值 r - x 即可(因为单点涂色的花费已经在 \(x\) 的位置计算过了)。此时,我们便可以把区间 \([l, r]\) 内问题的答案分为以下三部分之和:
    • 将区间 \([l, x]\) 涂色,花费 dp[l][x]
    • \(x\)\(r\) 连起来一起涂色,花费 r-x
    • 将区间 \([x+1, r-1]\) 涂色,花费 dp[x+1][r-1]

注意在上面的第二种情况当中,如果区间 \([l, r-1]\) 内存在多个位置与 \(r\) 位置同色,每个同色的位置都可以考虑让其与 \(r\) 一起涂。所以需要在区间 \([l, r-1]\) 内进行 for 循环找所有同色位置。

因此总时间复杂度为 \(O(n^3)\)

代码

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

int n, c[805], x[405];
ll dp[805][805];

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> c[i];
        c[i + n] = c[i]; // 化环为链
    }
    for(int i = 1; i <= n; i++)
        cin >> x[i];
    
    for(int len = 1; len <= n; len++) // 枚举长度
    {
        for(int l = 1; ; l++) // 枚举左端点
        {
            int r = l + len - 1;
            if(r > 2 * n)
                break;
            dp[l][r] = dp[l][r-1] + (x[c[r]] + 1); // r 单独涂色
            for(int x = l; x < r; x++)
                if(c[x] == c[r]) // r 与 x 可以一起涂色
                    dp[l][r] = min(dp[l][r], dp[l][x] + dp[x+1][r-1] + (r - x));
        }
    }
    
    ll ans = 1e18;
    for(int i = 1; i <= n; i++)
        ans = min(ans, dp[i][i + n - 1]); // 所有长度为 n 的区间取一个最小答案
    cout << ans;
    
    return 0;
}
posted @ 2025-04-12 23:39  StelaYuri  阅读(55)  评论(0)    收藏  举报