AtCoder Beginner Contest 425 ABCDEF 题目解析

A - Sigma Cubes

题意

给定一个正整数 \(N\),求出 \(\displaystyle \sum_{i=1}^N (-1)^i \times i^3\)

代码

void solve()
{
    int n, sum = 0;
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        if(i % 2 == 1)
            sum -= i * i * i;
        else
            sum += i * i * i;
    }
    cout << sum;
}

B - Find Permutation 2

题意

给定一个长度为 \(N\) 的整数序列 \(A=(A_1,A_2,\ldots,A_N)\) ,保证 \(A\) 内的整数要么是 \(-1\) ,要么是介于 \(1\)\(N\) 之间的整数。

请尝试将所有 \(-1\) 替换为任意 \(1\)\(N\) 之间的整数,使得最终整数序列是一个 \(N\) 的全排列。或是判断答案不存在。

思路

首先对于输入的数组,不存在答案的情况只可能是因为出现了多个重复的正整数。

所以可以先借助计数数组,或是循环嵌套,判断是否出现了除 \(-1\) 以外的重复数字。

如果答案存在,就对于每个 \(-1\),找一个没出现过的数字去将这个 \(-1\) 替代掉即可。这一步也可以借助计数数组+循环嵌套完成,或是双指针算法等。

代码

int n, a[15];
bool vis[15]; // 判断每种数字是否出现

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        if(a[i] == -1) // -1 暂时不处理
            continue;
        if(vis[a[i]] == true) // 如果 a[i] 已经出现过
        {
            cout << "No";
            return;
        }
        vis[a[i]] = true;
    }
    
    cout << "Yes\n";
    for(int i = 1; i <= n; i++)
    {
        if(a[i] == -1) // 找一个没用过的数字替代掉 -1
        {
            for(int j = 1; j <= n; j++)
                if(vis[j] == false) // j 没有用过
                {
                    vis[j] = true;
                    a[i] = j; // 放在 i 位置
                    break;
                }
        }
        cout << a[i] << " ";
    }
}

C - Rotate and Sum Query

题意

给你一个长度为 \(N\) 的整数序列 \(A=(A_1,A_2,\ldots,A_N)\)

请按顺序处理 \(Q\) 个查询。查询有两种类型,格式如下:

  • 1 c:重复执行 \(c\) 次操作,每次将 \(A\) 的第一个元素移动到末尾。
  • 2 l r:输出 \(\displaystyle \sum_{i=l}^r A_i\) 的值。

思路

考虑第一种操作,数组元素移动到末尾后,如果还想保持原本的下标关系,那我们就得把其它所有数字全部往前移动一位,这样的操作时间复杂度是 \(O(N)\) 的,当操作次数较多时会超时。

但发现移动 \(N\) 次之后数组会还原回一开始的样子,所以这里我们不考虑移动数组,而是采用循环队列的做法,用一个队首变量 \(f\) 来标记当前数组第一个元素的实际下标即可。每次执行操作一,相当于当前队首跑到了队尾,而原本队首的后一个数成为了队首,所以只需要让队首变量 \(f := f+1\) 即可。当 \(f\) 指向最后一个位置时,则回到第一个位置。

如果我们想要获得当前数组内第 \(i\) 个位置的元素,其实际下标便可以通过队首往后数 \(i+1\) 个位置得到,即 \(f + i - 1\)。但如果 \(f + i - 1 \gt n\),则需要回到第一个位置,对应下标为 \(f + i - 1 - n\)

发现对于是否超出第 \(n\) 个位置的讨论较为繁琐,所以这里可以稍微借助“化环为链”的技巧,将原本的 \(A\) 数组复制一份拼在 \(A\) 后面,变成一个长度为 \(2n\) 的数组 \(A_1, A_2, \dots, A_n, A_1, A_2, \dots, A_n\)。这样就可以不用去考虑分类讨论,直接就能用计算得到的 \(f + i - 1\) 下标。

然后考虑第二种操作,假如现在队首下标为 \(f\),要求你求出 \([l, r]\) 区间内的总和,那么对应的实际下标就是 \([f + l - 1, f + r - 1]\),静态区间求和,借助前缀和思想预处理后求解。

时间复杂度 \(O(N+Q)\)

代码

typedef long long ll;

int n, q;
int a[400005]; // 两倍空间
ll s[400005]; // a[] 的前缀和

void solve()
{
    cin >> n >> q;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        a[i + n] = a[i]; // 复制一份放在 a 后面
    }
    for(int i = 1; i <= 2 * n; i++)
        s[i] = s[i - 1] + a[i]; // 求前缀和
    
    int f = 1; // 当前第一个元素对应下标
    while(q--)
    {
        int op;
        cin >> op;
        if(op == 1)
        {
            int c;
            cin >> c;
            f += c;
            if(f > n) // 超过最后一个位置则回到第一个
                f = f - n;
        }
        else
        {
            int l, r;
            cin >> l >> r;
            // 求 [f + l - 1, f + r - 1] 的总和
            cout << s[f + r - 1] - s[f + l - 2] << "\n";
        }
    }
}

D - Ulam-Warburton Automaton

题意

有一个大小为 \(H \times W\) 的网格图。记第 \(i\) 行第 \(j\) 列的网格坐标为 \((i, j)\)

\(S_{i, j}\)#,则说明坐标 \((i, j)\) 的网格一开始是黑色的;如果 \(S_{i, j}\). 则说明其一开始是白色的。

请执行以下操作 \(10^{100}\) 次:

  • 将此时网格图中所有的四周仅有一个单元格为黑色白色单元格在同一时间全部涂成黑色。这里的“四周”表示上下左右四个方向的相邻格子。

求操作结束后,黑色单元格的总数。

思路

我们只会把白色单元格改成黑色的,所以涂色次数最多不会超过单元格总数。并且每次涂色会影响到的格子只有周围四个格子,所以可以采用宽搜解题。

但这题如果使用单个队列的普通宽搜做法,对于题目中的“在同一时间全部涂成黑色”这个条件可能就无法满足。也就是说,有可能网格图中某个白色单元格原本周围没有相邻的黑色网格,但在某一时刻周围一次性出现了两个及以上的黑色网格,这种情况根据题意是不会把这个白色单元格涂黑的。但我们如果采取普通宽搜的话,肯定是一个格子一个格子慢慢涂色的,有可能会导致误判。

所以本题的宽搜需要采取滚动队列等类似做法,拿两个队列 \(A, B\) 交替进行搜索,以保证“同时发生”这个条件得以满足。例如,一开始将所有要搜索的坐标先存到队列 \(A\) 中,然后开始搜索队列 \(A\) 内的每个网格。每次发现某个网格周围有一个白色单元格可以被涂黑时,在入队的时候先放到队列 \(B\) 中去,等到队列 \(A\) 内所有要搜索的网格全部搜完之后,再开始搜索队列 \(B\),以此交替执行。

特别需要注意,当发现有白色单元格可以被涂黑,在将其加入另一个队列的时候,我们可以标记这个位置已经被访问过了,但此时绝对不能直接改变这个位置的颜色,防止影响到当前队列其它网格的判断。直到当前队列搜索完之后,再把刚才需要发生颜色更改的所有网格统一改掉。这一步可以再借助 vector 等容器辅助存储。

时间复杂度 \(O(H \times W)\)

代码

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

struct point
{
    int x, y;
};

queue<point> q[2];

void solve()
{
    int n, m;
    cin >> n >> m;
    
    vector<vector<char>> mp(n + 1, vector<char>(m + 1));
    vector<vector<char>> vis(n + 1, vector<char>(m + 1, false)); // 标记数组
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin >> mp[i][j];
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            if(mp[i][j] == '#') // 从一开始的所有黑色格子出发搜索
            {
                q[0].push({i, j});
                vis[i][j] = true;
            }
    
    int cur = 0; // 当前正在搜索的是哪个队列 0/1
    while(!q[0].empty() || !q[1].empty()) // 只要至少有一个队列没搜完,就还能继续搜
    {
        int nxt = cur ^ 1; // 另一个队列的编号
        vector<point> G; // 当前这次搜索会让哪些格子变成黑色,先存起来,搜完后统一改颜色
        while(!q[cur].empty()) // 只要当前这个队列不为空
        {
            point u = q[cur].front();
            q[cur].pop();
            for(int i = 0; i < 4; i++) // 找周围可以影响到的下一个点
            {
                point v = {u.x + dx[i], u.y + dy[i]};
                
                if(v.x < 1 || v.y < 1 || v.x > n || v.y > m) // 越界
                    continue;
                if(mp[v.x][v.y] == '#') // 已经涂过色了
                    continue;
                if(vis[v.x][v.y] == true) // 已经搜索过了
                    continue;
                
                int cnt = 0; // 记录其周围有多少格子是黑色的
                for(int j = 0; j < 4; j++)
                {
                    point t = {v.x + dx[j], v.y + dy[j]};
                    if(t.x >= 1 && t.x <= n && t.y >= 1 && t.y <= m && mp[t.x][t.y] == '#')
                        cnt++;
                }
                if(cnt == 1) // 恰好周围只有一个黑色格子
                {
                    // 先标记并入队,但不要在这里直接改颜色,防止影响到当前这一次搜索过程中其他点的判断
                    q[nxt].push(v);
                    vis[v.x][v.y] = true;
                    G.push_back(v);
                }
            }
        }
        for(point &p : G) // 搜完后,把这一次搜索入队的每个点统一改成黑色
            mp[p.x][p.y] = '#';
        cur = nxt; // 交换队列
    }
    
    int ans = 0;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            ans += (mp[i][j] == '#');
    cout << ans;
}

E - Count Sequences 2

题意

给定一个正整数 \(N\) 和一个长度为 \(N\) 的正整数序列 \(C=(C_1,C_2,\ldots,C_N)\)

求满足以下所有条件的不同正整数序列的个数,输出其除以 \(M\) 的余数:

  • 序列的所有元素都在 \(1\)\(N\) 之间。
  • 对于每个 \(i=1,2,\ldots,N\),数 \(i\) 在序列中恰好出现 \(C_i\) 次。

有多组数据。模数 \(M\) 对于单个测试文件的所有测试数据是通用的。

思路

为避免公式混淆,下面将数字 \(i\) 出现的次数记作 \(T_i\) 次。

\(S = \sum\limits_{i=1}^N T_i\)

这是一个经典的多重集排列问题。先考虑全排列数量,即 \(A_S^S = S!\)

然后对于整数 \(i\),其在序列中恰好出现了 \(T_i\) 次,由于相同数字之间是不可区分的,所以需要除去该种数字内部的全排列数量,即 \(A_{T_i}^{T_i}\)。对于每种数字,均将其内部的重复方案数从总全排列方案中除去,故最终答案为:

\[\large \frac{A_S^S}{A_{T_1}^{T_1} \times A_{T_2}^{T_2} \times \dots \times A_{T_N}^{T_N}} = \frac{S!}{T_1! T_2! \dots T_N!} \]

如果模数 \(M\) 为一个大质数,我们可以很方便地通过递推预处理出每一项的阶乘,再通过费马小定理结合快速幂求出每项阶乘的逆元,直接计算即可得到答案。但本题的模数 \(M\) 并不是一个质数,因此该方法无效。

如果要求解的原值与模数 \(M\) 互质,我们还可以借助扩展欧几里得算法求解逆元。但本题 \(M\) 并没有保证与所有阶乘互质,因此该方法也无效。

所以最终只能从公式角度出发。这个公式是经典的“多项式系数”,描述多项式 \((x_1 + x_2 + \dots + x_m)^n\) 的展开式中,项 \(x_1^{n_1}x_2^{n_2}\dots x_m^{n_m}\) 的系数,即 \(\dfrac{(n_1+n_2+\dots+n_m)!}{n_1!n_2!\dots n_m!}\)

我们可以从该系数的定义入手。这个系数可以描述为将 \(S\) 个物品分为 \(N\) 组,每组分别有 \(T_1, T_2, \dots, T_N\) 个物品,问不同的分法数量。先从 \(S\) 个物品中选出 \(T_1\) 个物品分到第 \(1\) 组,再在剩下的 \(S-T_1\) 个物品中选出 \(T_2\) 个物品分到第 \(2\) 组,以此类推,方法数可以描述为:

\[\large C_S^{T_1} \times C_{S-T_1}^{T_2} \times C_{S-T_1-T_2}^{T_3} \dots C_{T_N}^{T_N} \]

结合杨辉三角预处理组合数后求解即可。

时间复杂度 \(O(S^2 + N)\),其中 \(S_{\max} = 5000\)

代码

typedef long long ll;

ll mod;
ll C[5005][5005];

// 预处理组合数
void init()
{
    for(int i = 0; i <= 5000; i++)
    {
        C[i][0] = C[i][i] = 1;
        for(int j = 1; j < i; j++)
            C[i][j] = (C[i-1][j-1] + C[i-1][j]) % mod;
    }
}

int a[300005];

void solve()
{
    int T;
    cin >> T >> mod;
    init();
    while(T--)
    {
        int n;
        cin >> n;
        int sum = 0;
        for(int i = 1; i <= n; i++)
        {
            cin >> a[i];
            sum += a[i];
        }
        ll ans = 1;
        for(int i = 1; i <= n; i++)
        {
            ans = ans * C[sum][a[i]] % mod;
            sum -= a[i];
        }
        cout << ans << "\n";
    }
}

F - Inserting Process

题意

给定一个长度为 \(N\) 且由小写字母组成的字符串 \(T\)

求满足以下所有条件的字符串序列 \(s=(s_0,s_1,\ldots,s_N)\) 的个数,输出其除以 \(998\,244\,353\) 的余数:

  • \(s_0\) 是空字符串。
  • 对于 \(i=1,2,\ldots,N\) 来说,字符串 \(s_{i}\) 是通过在 \(s_{i-1}\) 的任意位置(可能是开头或结尾)插入一个任意字符得到的。
  • \(s_N=T\)

思路

考虑与题意相反的操作,从 \(s_N = T\) 出发,每次任意删除一个字符,最后删到 \(s_0 = \text{空}\)

由于 \(N \le 22\),范围较小,考虑借助一个长度为 \(N\)二进制状态来描述每个字符目前是否已经被删除。\(0\) 表示已被删除,\(1\) 表示未被删除。接下来考虑状压 DP。

dp[sta] 表示将字符串删到 sta 这个二进制值所表示的状态时,所能得到的不同序列数量。

明显可以得到初始状态为 dp[(1 << n) - 1] = 1,表示一开始一个字符都还没删除的情况。

接下来考虑倒序枚举状态,对于状态 sta,如果其二进制上 \(2^j\) 这一位仍为 \(1\),则可以考虑删去该位所对应的字符,获得新状态 sta ^ (1 << j),并将上一状态的答案数量加入到新状态的答案中,即 dp[sta ^ (1 << j)] += dp[sta]

但在删除的过程中,我们需要注意一种特殊情况。例如对于字符串 \(\tt abbc\),删除第二个字符 \(\tt b\) 与删除第三个字符 \(\tt b\) 所能得到的新字符串均为 \(\tt abc\),两种删除情况各自对应的二进制状态分别为 \(\tt 1011\)\(\tt 1101\),但很明显我们不能把这两种情况当作是两种不同的状态。所以在过程中我们需要加一个限制条件:如果当前删除的这种字符在字符串中的出现情况是连续的一段,那么我们每次就只删除这段连续区间最左侧(或者只删除最右侧)的字符,以此保证不会重复计算方案。

时间复杂度 \(O(N \times 2^N)\)

代码

const int mod = 998244353;

int dp[1 << 22];

void solve()
{
    int n;
    string t;
    cin >> n >> t;
    
    dp[(1 << n) - 1] = 1; // 一个字符都还没删除的情况
    for(int sta = (1 << n) - 1; sta >= 0; sta--)
    {
        char pre = 0; // 保证不是小写英文字母 ASCII 即可,表示上一个未被删除的字符
        for(int i = 0; i < n; i++)
            if(sta >> i & 1) // 如果 i 这个字符还存在
            {
                if(t[i] != pre) // 与上一个字符不同,说明是新的一段连续字符的第一个位置,可以删除
                {
                    int nxt = sta ^ (1 << i); // 计算删除 2^i 位后的下个状态
                    dp[nxt] = (dp[nxt] + dp[sta]) % mod;
                }
                pre = t[i];
            }
    }
    cout << dp[0];
}
posted @ 2025-09-27 22:30  StelaYuri  阅读(231)  评论(0)    收藏  举报