【题目全解】ACGO排位赛#12

ACGO 排位赛#12 - 题目解析

别问为什么没有挑战赛#11,因为挑战赛#11被贪心的 Yuilice 吃掉了(不是)。

本次挑战赛难度相比较前面几次有所提升。

爆料:小鱼现在已经入职了研发部门,排位赛的算分级制将在未来的几场排位赛中做出重大改变。之后就不会出现大家段位都很低的情况了。对于程度好的同学,稍微打一两把排位赛段位就会有很大的提升。

第一题 - 50%𝐴𝐼, 50%𝐻𝑢𝑚𝑎𝑛

题目链接跳转:点击跳转

STL 大法真好,用自带的 string.size() 方法就可以快速求出一个字符串的长度。具体思路见代码,根据题目要求模拟就好了。

本题的 AC 代码如下:

cpp 复制代码
#include <iostream>
#include <cmath>
using namespace std;

/*
PS: 信心倍增题目。
写完这道题会让你觉得这场比赛特别简单。但其实不是。
*/

int n, cnt;
string str;

int main(){
    cin >> n;
    for (int i=1; i<=n; i++){
        cin >> str;
        if (str.size() > 5) cnt++;
    }
    int ans = ceil(1.0 * cnt / n * 100);
    printf("%d%%AI, %d%%Human\n", ans, 100 - ans);
    return 0;
}

第二题 - 统计区间内奇数与偶数的数量

题目链接跳转:点击跳转

对于这种区间的问题,可以先考虑一部分。如果要求出从 \([1, L]\) 区间内满足条件的数字我们应该怎么办?假设 \(L\) 是一个偶数,那么 \([1, L]\) 区间的奇数和偶数就应该是 \(L / 2\)。相同地,假设 \(L\) 是一个奇数,那么 \([1, L]\) 区间的奇数和偶数就分别是 \(L / 2 + 1\) 和 \(L / 2\)。注意到 \(L / 2 + 1\) 和 \(L / 2\) 两个公式都可以被简写成 \((L + 1) / 2\)。

设 \(odd(L)\) 和 \(even(L)\) 分别为区间 \([1, L]\) 的奇数个数和偶数个数。那么可以得出结论,如果要求 \([L, R]\) 区间的奇数个数,答案就应该是 \(odd(R) - odd(L-1)\) 和 \(even(R) - even(L-1)\)。

本题的 AC 代码如下,代码并没有使用函数,见谅:

cpp 复制代码
#include <iostream>
using namespace std;

/*
思维水题。
稍微想一下找找规律就好了。
*/

int q, l, r, k;

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> q;
    while(q--){
        cin >> k >> l >> r;
        if (k == 1) cout << ((r + 1) >> 1) - ((l) >> 1) << endl;
        else cout << (r >> 1) - ((l - 1) >> 1) << endl;
    }
    return 0;
}

第三题 - 火星背包 II

题目链接跳转:点击跳转

一道反向 \(0/1\) 背包的模板题目。通常,\(0/1\) 背包问题的目标是选择若干物品,使得这些物品的总重量不超过背包容量,并且这些物品的总价值最大化。而反向 \(0/1\) 背包问题则是反其道而行之,其目标是选择若干物品,使得这些物品的总重量不低于给定的最小值,并且这些物品的总价值最小化。

主要就是状态的定义比较难:设 \(dp[j]\) 表示恰好凑出总重量为 \(j\) 的最小代价。那么我们可以得到如下的状态转移方程:

\[dp_j = \min(dp_j, dp_{j-w_i} + v_i); \]

根据状态的定义,我们将 \(dp\) 数组一开始全部初始化为正无穷大,同时另 \(dp_0 = 0\),表示恰好凑出重量为 \(0\) 的物品的最小代价为 \(0\),正无穷大代表暂时还没有答案,需要在后续的循环中更新。

之后就是跑一遍正常的 Knapsack 01 代码就好了。在输出答案的时候,我们需要找到一个满足条件 \((dp[i] \le m)\)​ 的最大值。用一个 for 循环就可以搞定了。

本题的 AC 代码如下:

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;

/*
反向 01 背包题目。
问题不大,X03 的同学们在集训营做过类似的题目(maybe 仅限杭州八月一期)。
*/

int n, m, sum;
int dp[100005], ans;
int w[1005], v[1005];

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n >> m;
    for (int i=1; i<=n; i++){
        cin >> w[i] >> v[i];
        sum += v[i];
    }
    memset(dp, 0x7f, sizeof dp);
    dp[0] = 0;
    for (int i=1; i<=n; i++){
        for (int j=sum; j>=v[i]; j--){
            dp[j] = min(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    for (int i=1; i<=sum; i++){
        if (dp[i] <= m) ans = i;
    }
    cout << ans << endl;
    return 0;
}

第四题 - 可分数列

题目链接跳转:点击跳转

题目很简单,但需要仔细想一会儿才行。这道题的解决思路就是 找规律 !没错,就是找一下规律就好了。我们只需要关注如何将这 \(4N + 2\) 个数列在去掉两个元素后可以被平均分成 \(N\) 个等差数列。

通过手动模拟的方式,注意到我们只需要把这些数字竖着排列就可以解决问题。另每一列有四个元素,但其中有两列有五个元素(代表 \(4N + 2\) 的 \(+2\) 部分),共有 \(N\) 列。例如数列 \([1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27]\)。竖着排列之后就是这个样子的:

text 复制代码
1 7 13 19 25
3 9 15 21 27
5 11 17 23

可以看到这些元素构成了三个等差数列,且两个等差数列的长度为 \(5\)。那么对于长度为 \(4\) 的等差数列我们完全可以不用管,直接输出就好了。而对于这两个长度为 \(5\) 的等差数列,我们考虑从这两个等差数列中各删除一个数字来构成新的等差数列。综合来看,我们发现删除元素 \(3\) 和 \(25\) 后,新的两个等差数列依旧满足题目限定的条件。

再多列举几个例子,发现规律也是相通的。那么因此我们可以得出结论,对于任意一个长度为 \(4N + 2\) 的等差数列,只需要删除这个等差数列的第 \(2\) 项和第 \(4N + 1\) 项,剩下的 \(4N\) 个元素一定可以组成 \(N\) 个长度为 \(4\) 的等差数列。

关于本题的更多信息,可以阅读 アイドル 老师的题解作为补充:链接跳转

找到规律后代码就很好写了,本题的 AC 代码如下:

cpp 复制代码
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;

int n, x, d;

signed main(){
    cin >> n >> x >> d;
    if (n == 1){
        cout << -1 << endl;
        return 0;
    }
    cout << 2 << " " << 4 * n + 1 << endl;
    for (int i=0; i<n; i++){
        int t = x + i * d;
        if (i == 1) t += n * d;
        for (int j=0; j<4; j++)
            cout << t + j * n * d << " ";
        cout << endl;
    }
    return 0;
}

第五题 - 花火大会

题目链接跳转:点击跳转

这道题的输出不是故意为难大家,因为输出实在太大了,超出了 CF 限定了 64MB 的限制,因此只能将输出地图改为了输出地图的哈希值(对于没有学过哈希的同学来说,输出答案也是一个比较困难的事情)。

本道题的难度其实不大,就是一个多源不同权的无权最短路问题。但由于数据量比较大,使用普通的优先队列维护会超时(别问我是怎么知道的,我一开始就用了优先队列,然后在 #13 测试点就 TLE 了),因此我们需要考虑如何找到一个 workaround 来替换优先队列。

注意到我们只需要另外再使用一个 while,在每次获取头节点的时候判断某一个烟花是否刚好在该时间点燃放,如果烟花正好燃放,我们就把这个烟花的坐标加入到队列之中。

cpp 复制代码
while(arr[cnt].z <= t.z && cnt <= k){
    vis[arr[cnt].x][arr[cnt].y] = 1;
    que.push(arr[cnt]); cnt ++;
}

由于每一个烟花每一个单位时间只会影响附近的一个格子,说明在放入烟花的时候队列中队头和队尾的权重是相同的,这样就保证队列内的元素权重一定是单调递增的。这这种情况下,这道题就转换成了使用 BFS 来求多源无权最短路的条件。

本题的 AC 代码如下:

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int MOD = 998244353;

int n, m, k, cnt = 2;
struct node{
    int x, y, z;
} arr[1005]; 
queue<node> que;
int dis[5005][5005];
int vis[5005][5005];
int dx[] = {0, 1, -1, 0};
int dy[] = {1, 0, 0, -1};

bool cmp(node a, node b){
    return a.z < b.z;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);

    cin >> n >> m >> k;
    for (int i=1, x, y, z; i<=k; i++)
        cin >> arr[i].x >> arr[i].y >> arr[i].z;

    sort(arr+1, arr+1+k, cmp);
    que.push(arr[1]);
    vis[arr[1].x][arr[1].y] = 1;

    while(!que.empty()){
        node t = que.front();
        que.pop();
        if (dis[t.x][t.y]) continue;
        dis[t.x][t.y] = t.z;
        while(arr[cnt].z <= t.z && cnt <= k){
            vis[arr[cnt].x][arr[cnt].y] = 1;
            que.push(arr[cnt]); cnt ++;
        }
        for (int i=0; i<4; i++){
            int cx = dx[i] + t.x;
            int cy = dy[i] + t.y;
            if (cx < 1 || cy < 1 || cx > n || cy > m) continue;
            if (dis[cx][cy]) continue; 
            if (vis[cx][cy]) continue;
            vis[cx][cy] = 1;
            que.push((node){cx, cy, t.z+1});
        }
    }
    
    long long ans = 0, p = 1;
    for (int i=1; i<=m; i++) 
        p = (p * 233) % MOD;
    for (int i=1; i<=n; i++){
        for (int j=1; j<=m; j++){
            p = (p * 233) % MOD;
            ans = (ans + (dis[i][j] * p) % MOD) % MOD;
        }
    }

    cout << ans << endl;
    return 0;
}

第六题 - 剑之试炼

题目链接跳转:点击跳转

这道题超级恶心,我发自内心地对那些在比赛过程中 AC 此题目的同学表示尊敬。

思路上非常好想,由于所有的怪兽都要打,且打每一个怪兽的时间都是固定的,因此我们可以在一开始就先预处理出打完所有怪兽的时间,需要优化的就只有赶路的时间了。

先考虑每一层的情况,对于任意一层来说,关键点就是找到一个最优的路径(从某一个起点出发,经过所有的点后再传送到下一层的最短路径)即可。学习过状态压缩动态规划的同学可以很容易想到模板题【最短 Hamilton 路径】(洛谷链接:链接跳转)。

对于每一层来说,具体的实现方法和函数如下:

  1. cover(dist, grid, "#*");

    • distgrid中标记为"#""*"的所有障碍物进行覆盖处理。这一步的作用是标记出所有不可通行的区域,为后续的距离计算或路径规划提供基础。
  2. getMinDist(dist, grid, "#");

    • 计算从当前所在位置到grid中标记为"#"(障碍物)位置的最短距离。这一步的作用是为后续的路径规划获取到障碍物的距离信息,方便下一步进行路径选择。
  3. hamilton(dist, grid);

    • distgrid上执行哈密顿路径(Hamiltonian Path)的计算,目的是找到一条经过所有目标点的路径。这一步的作用是根据前面的距离信息,找到一条经过所有必经点的路径。
  4. cover(dist, grid, "#.");

    • distgrid中标记为"#"(障碍物)和"."(空地)的部分进行覆盖处理。这一步是为了确保后续的距离计算排除已标记的障碍物,并识别可通行的路径。
  5. getMinDist(dist, grid, "#");

    • 再次计算从当前所在位置到grid中标记为"#"的最短距离。这一步是为了更新经过路径覆盖处理后的最短距离,确保得到最新的距离信息。

除此之外就是一些小的细节优化了,具体请参阅代码注释。

本题的 AC 代码如下(本代码参考了黄老师的 std,并加以修改(我是不会说我数组传来传去把自己传死了,讨厌指针的一天)),若需要更详细的解答,可以 参考本文

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <queue>
#include <cstring>
#include <vector>
using namespace std;

int n, m, k;
string map[30][105];
int dist[105][105];
struct node{
    int x, y, w;
    bool friend operator < (node a, node b){
        return a.w > b.w;
    }
};
const int dx[] = {1, 0, -1, 0};
const int dy[] = {0, 1, 0, -1};

/*
PS:本来想用纯数组加指针的方式做的。
后来写着写着把自己写死掉了,还是 vector 方便。
普通的二维数组传来传去简直要我的命。
::: 虽然我的编译器见不得 c++11 的标准,每次都给我提 warning。头大。
*/

// 转换,将每一个怪兽的血量都转换成对应的数字。
int calc(char c){
    if (c == '.') return 0;
    if (c == 'R') return 1;
    if (c == 'B') return 3;
    if (c == 'D') return 8;
    if (c == 'S') return 24;
    if (c == 'G') return 36;
    return 0x7f7f7f7f;
}

// 最普通的广度优先搜索算法:
// 记录从 sx, sy 出发,到当前层 grid 的每一个点的最短路径。
// 其中 block 代表无法走的格子。
vector<vector<int> > bfs(vector<string> &grid, int sx, int sy, string block){
    vector<vector<int> > dist(n, vector<int>(m, 0x7f7f7f7f));
    dist[sx][sy] = 0;
    struct node{
        int x, y;
    }; queue<node> que;
    que.push((node){sx, sy});
    while(!que.empty()){
        node t = que.front();
        que.pop();
        for (int i=0; i<4; i++){
            int cx = t.x + dx[i];
            int cy = t.y + dy[i];
            if (cx < 0 || cy < 0 || cx >= n || cy >= m) continue;
            // 字符串函数,判断当前格子是否是障碍物,如果是障碍物的话就忽略。
            if (block.find(grid[cx][cy]) != string::npos) continue;
            if (dist[cx][cy] < 0x7f7f7f7f) continue;
            dist[cx][cy] = dist[t.x][t.y] + 1;
            que.push((node){cx, cy});
        }
    }
    return dist;
}

// 将 dist 数组根据 grid 复原。
// 如果当前位置无法被走到,则将 dist 更新为无穷大。
void cover(vector<vector<int> > &dist, vector<string> &grid, string block){
    for (int i=0; i<n; i++){
        for (int j=0; j<m; j++){
            if (block.find(grid[i][j]) != std::string::npos)
                dist[i][j] = 0x7f7f7f7f;
        }
    }
    return ;
}

// dijkstra 最短路算法。
// 主要作用是计算并更新每个节点到其余节点的最短距离,并通过广度优先搜索(BFS)算法来实现。
// dist[i][j] 是从一开始设定的起点到达 (i, j) 的累积代价,考虑了路径上经过的每个格子的代价。
void getMinDist(vector<vector<int> > &dist, vector<string> &grid, string block){
    priority_queue<node> que;
    // 一开始把所有起点都入队。
    for (int i=0; i<n; i++){
        for (int j=0; j<m; j++){
            que.push((node){i, j, dist[i][j]});
        }
    }

    while(!que.empty()){
        node t = que.top();
        que.pop();
        if (dist[t.x][t.y] < t.w) continue;
        for (int i=0; i<4; i++){
            int cx = t.x + dx[i];
            int cy = t.y + dy[i];
            if (cx < 0 || cy < 0 || cx >= n || cy >= m) continue;
            if (block.find(grid[cx][cy]) != string::npos) continue;
            // 如果已经存在更优解了,就忽略。
            if (dist[cx][cy] <= t.w + 1) continue;
            dist[cx][cy] = t.w + 1;
            que.push((node){cx, cy, t.w + 1});
        }
    }
}

// 计算汉密尔顿路径
// 即从起点出发走完所有的点最后再回来的路径最短路。
void hamilton(vector<vector<int> > &dist, vector<string> &grid){
    struct node{
        int x, y;
    }; vector<node> vec;
    for (int i=0; i<n; i++){
        for (int j=0; j<m; j++){
            if (grid[i][j] == '*')
                vec.push_back((node){i, j});
        }
    }

    int k = vec.size();
    vector<vector<int> > f(k);
    // f 数组用于计算每一个关键节点(怪兽)之间的最短路。
    for (int i=0; i<k; i++){
        int sx = vec[i].x;
        int sy = vec[i].y;
        auto toOther = bfs(grid, sx, sy, "#");
        for (int j=0; j<k; j++){
            f[i].push_back(toOther[vec[j].x][vec[j].y]);
        }
    }

    // 对于 Hamilton 路径不熟悉的,直接去搜洛谷模板题先看一眼。
    // X04-01 的同学应该是学过的(毕竟是我挑的题目)。
    vector<vector<int> > dp(1 << k, vector<int>(k, 0x7f7f7f7f));
    for (int i=0; i<k; i++){
        int sx = vec[i].x;
        int sy = vec[i].y;
        dp[1 << i][i] = dist[sx][sy];
    }
    for (int i=0; i<(1<<k); i++){
        for (int j=0; j<k; j++){
            if (~i >> j & 1) continue;
            for (int l=0; l<k; l++){
                if (~(i ^ 1 << j) >> l & 1) continue;
                dp[i][j] = min(dp[i][j], dp[i ^ 1 << j][l] + f[l][j]);
            }
        }
    }
    for (int i=0; i<k; i++){
        int sx = vec[i].x;
        int sy = vec[i].y;
        dist[sx][sy] = dp[(1 << k) - 1][i];
    }
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n >> m >> k;
    vector<vector<string> > grids(k, vector<string>(n));
    for (auto &grid : grids){
        for (auto &row : grid){
            cin >>  row;
        }
    }
    int cost = k - 1;
    for (auto &grid : grids){
        for (auto &row : grid){
            for (auto &c : row){
                if (c != '#' && c != '.'){
                    cost += calc(c);
                    c = '*';
                }
            }
        }
    }

    vector<vector<int> > dist = bfs(grids[0], 0, 0, "#*");
    
    /*
        先排除掉某些障碍物和关键点,以计算初步的最短路径。
        再使用 hamilton 函数来处理关键点之间的路径规划,得到所有关键点的最优路径。
        最后进一步排除空白区域和障碍物,重新计算网格中各点之间的最短路径,得到最终结果。
    */
    for (auto &grid : grids){
        cover(dist, grid, "#*");
        getMinDist(dist, grid, "#");
        hamilton(dist, grid);
        cover(dist, grid, "#.");
        getMinDist(dist, grid, "#");
    }

    // 计算答案。
    int ans = 0x7f7f7f7f;
    for (int i=0; i<n; i++){
        for (int j=0; j<m; j++){
            ans = min(ans, dist[i][j]);
        }
    }

    cout << ans + cost << endl;
    return 0;
}