AtCoder Beginner Contest 412 ABCDEF 题目解析

A - Task Failed Successfully

题意

总共有 \(N\) 天,高桥的目标是在第 \(i\) 天完成 \(A_i\) 项任务,实际上完成了 \(B_i\) 项。

问有多少天完成的任务比目标多?

思路

比较有多少个 \(B_i \gt A_i\) 即可。

代码

void solve()
{
    int n;
    cin >> n;
    int s = 0;
    for(int i = 1; i <= n; i++)
    {
        int a, b;
        cin >> a >> b;
        if(b > a)
            s++;
    }
    cout << s;
}

B - Precondition

题意

给定两个由大小写英文字母组成的字符串 \(S\)\(T\)

请判断字符串 \(S\) 是否满足以下条件:

  • 对于 \(S\) 中的每个大写字母,只要这个字母不在 \(S\) 的开头,那么这个字母一定出现在字符串 \(T\) 中。

思路

字符串长度较短,计数或循环判断均可。

循环遍历 \(S\),每次遇到一个不是开头的大写英文字母时,就去检查是否 \(T\) 中有出现它的前一个字符即可。

代码一 循环

char s[105], t[105];

void solve()
{
    cin >> s >> t;
    int lens = strlen(s), lent = strlen(t);
    for(int i = 1; i < lens; i++) // 从第二个字母开始
    {
        if(s[i] >= 'A' && s[i] <= 'Z')
        {
            // 检查 s[i-1] 是否出现在 t 内
            bool flag = false;
            for(int j = 0; j < lent; j++)
            {
                if(t[j] == s[i-1])
                {
                    flag = true;
                    break;
                }
            }
            if(flag == false) // 没出现则不符合条件
            {
                cout << "No";
                return;
            }
        }
    }
    cout << "Yes";
}

代码二 计数

char s[105], t[105];
bool vis[128]; // 计数判断字母是否出现

void solve()
{
    cin >> s >> t;
    int lens = strlen(s), lent = strlen(t);
    
    for(int i = 0; i < lent; i++)
        vis[t[i]] = true;
    
    for(int i = 1; i < lens; i++) // 从第二个字母开始
    {
        if(s[i] >= 'A' && s[i] <= 'Z')
        {
            // 检查 s[i-1] 是否出现在 t 内
            if(vis[s[i-1]] == false) // 没出现则不符合条件
            {
                cout << "No";
                return;
            }
        }
    }
    cout << "Yes";
}

C - Giant Domino

题意

\(N\) 块多米诺骨牌,分别编号为 \(1, 2, \dots, N\),第 \(i\) 块的大小为 \(S_i\)

考虑从中选出一些骨牌,从左往右排成一排,然后将他们推倒。

当某块大小为 \(S\) 的骨牌往右倒时,如果与其相邻的右边的骨牌大小不超过 \(2S\),那么右边这块骨牌接下来也会往右倒。

你的任务是选出两块及以上的多米诺骨牌,并排成一排,满足以下条件:

  • 最左边的骨牌必须是原来的 \(1\) 号。
  • 最右边的骨牌必须是原来的 \(N\) 号。
  • 把最左边的骨牌往右推倒后,最右边的骨牌也会被推倒。

问满足条件所需要选出的骨牌最少数量。

思路

由于最左和最右两块骨牌已经固定,我们只需要考虑往这两块中间插入其它骨牌即可。

只要右边的骨牌大小不超过左边的两倍,就可以接着被推倒。因此如果一开始最右边的骨牌就已经是最左边的骨牌大小的两倍以内,就只需要选出这两块骨牌即可满足条件。

否则,最右边的骨牌大小一定是最左边的骨牌大小的两倍以上。那么我们就需要每次在中间插入一块大小不超过左侧骨牌大小两倍的新骨牌,直到最右边的骨牌大小不超过新插入的骨牌大小的两倍为止。

又因为本题需要让选出的骨牌数量最少,因此贪心可知,每次要插入的新骨牌,一定是在不超过左侧骨牌大小两倍的所有未选择的骨牌当中所选出的一个最大的骨牌。

过程可以借助二分等方式快速确定要插入的新骨牌大小,直到满足题意,或者找不到任何符合条件的骨牌为止。

代码

int n;
int s[200005];

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> s[i];
    
    // x 表示目前除了最右侧骨牌以外的最大骨牌
    // y 表示最右侧骨牌大小
    int x = s[1], y = s[n], ans = 2;
    sort(s + 1, s + n + 1);
    
    while(x * 2 < y)
    {
        // 找 <= 2x 的最大骨牌
        int p = upper_bound(s + 1, s + n + 1, x * 2) - s - 1; // >2x 的最小下标 取前一个位置
        // 如果找到的骨牌是与前一块骨牌相同大小(有可能是同一块)
        // 说明 [x+1, 2x] 范围内已经找不到任何能够让最大的骨牌继续变大的新骨牌
        // 此时直接判断不存在即可
        if(s[p] == x)
        {
            cout << -1 << "\n";
            return;
        }
        // 否则,取当前这块骨牌插入
        ans++;
        x = s[p];
    }
    
    cout << ans << "\n";
}
signed main()
{
    int T;
    cin >> T;
    while(T--)
        solve();
    return 0;
}

D - Make 2-Regular Graph

题意

给定一张包含 \(N\) 个点 \(M\) 条边的简单无向图,你可以进行以下两种操作任意次:

  • 任意在图中加一条边
  • 任意删去图中的一条边

问至少进行多少次操作,可以在保证这张图还是简单无向图的基础上,让每个点的度数恰好为 \(2\)

思路

简单无向图即无自环无重边。又因为每个点的度数恰好为 \(2\),很明显这张图最终应该是由一个或多个环组成,并且每个环的长度至少为 \(3\)(因为长度为 \(1\) 的环视作自环,长度为 \(2\) 的环视作重边)。

再根据图的度数关系(总度数 = 边数 \(\times 2\)),可知最终图的点数与边数应当是相同的。

注意本题的数据范围 \(N \le 8\),所以我们可以尝试枚举出这张图最终每一种可能的情况,然后逐一判断操作次数,取最小即可。

枚举最终的图的方式有很多种,这里采取的方法是暂时把无向图当作有向图,并且每个点的入度与出度均为 \(1\)。这样我们就可以去枚举从每个点出发的下一个点是什么,且到达的点不会重复,这就是一个 \(N\) 的全排列。

全排列枚举过程可以采用深度优先搜索或是 next_permutation 直接构造。

总时间复杂度为 \(O(N!\cdot N)\)

代码

int n, m;
bool edge[10][10]; // 邻接矩阵存图

int p[10];

void solve()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        int a, b;
        cin >> a >> b;
        edge[a][b] = edge[b][a] = true;
    }
    
    for(int i = 1; i <= n; i++)
        p[i] = i;
    
    int ans = n + m; // 记最小答案
    do
    {
        bool flag = true;
        for(int i = 1; i <= n; i++)
            if(p[i] == i || p[p[i]] == i) // 长度 <= 2 的环
            {
                flag = false;
                break;
            }
        if(flag)
        {
            int cnt = 0; // 统计有多少条需要的边与现有的边是相同的
            for(int i = 1; i <= n; i++)
                if(edge[i][p[i]])
                    cnt++;
            // 要删除的边即 m-cnt,要新增的边即 n-cnt
            ans = min(ans, (m - cnt) + (n - cnt));
        }
    } while(next_permutation(p + 1, p + n + 1));
    
    cout << ans;
}

E - LCM Sequence

题意

定义 \(F_n\) 表示 \(1, 2, 3, \dots, n\) 这些正整数的最小公倍数

给定两个正整数 \(L, R\),问在 \(\{F_L, F_{L+1}, \dots, F_R\}\) 当中总共出现了多少种不同的整数。

思路

首先,\(F\) 序列一定是一个单调非递减序列,后一个值要么与前一个值相同,要么变大。

已知最小公倍数就是对于每一种质因子 \(p\),即在所有数字当中质因子 \(p\) 出现的最大幂次为 \(x\),那么该质因子对最小公倍数的贡献就是 \(p^x\)

考虑什么时候 \(F_{i-1} \rightarrow F_i\) 会变大。很明显就是在 \(i\) 这个新加进来的数字当中,出现了某个质因子的幂次比前面 \(1 \sim i-1\) 中出现的最大幂次更大的情况。

此时的 \(i\) 只会有两种情况:

  • \(i\) 是一个质数。即 \(i\) 本身就是一个新质因子。
  • \(i\) 是某个质数的 \(x\) 次幂 \((x \ge 2)\)。即 \(i\) 仅包含一种质因子。

第一种情况显而易见,第二种情况其实也很好证明。通过反证法,如果 \(i\) 中包含了至少两种质因子 \(p^a \times q^b \times \dots\),那么我们可以把每种质因子单独拆出来当作新的数字 \(p^a, q^b, \dots\),这些新数字一定都比 \(i\) 本身要小,说明在此之前每种质因子的最大幂次已经确定,不可能因为 \(i\) 的出现导致最小公倍数发生改变。

考虑统计答案数量。基于题目的问题,我们可以假设 \(F_L\) 就是一种新数字,然后考虑统计 \([L+1, R]\) 范围内有多少个位置符合上述两种情况即可。

对于第一种情况,这是素数筛的区间筛做法,如果 \(10^{14}\) 范围内的某个数字不是质数,那么它一定会有一个不超过 \(\sqrt{10^{14}} = 10^7\) 的质因数。因此我们只需要把 \(10^7\) 范围内的所有质数全部筛出,然后用这些数字去尝试把 \([L, R]\) 范围内的非质数全部筛去即可。埃氏筛写法就是对于每种质数 \(p\),从 \(\lceil \dfrac L p \rceil \times p\) 开始到 \(\lfloor \dfrac R p \rfloor \times p\) 结束,把每个 \(p\) 的倍数全部筛去。最后统计 \([L+1,R]\) 内未被筛去的数字数量。

对于第二种情况,同样只需要把 \(10^7\) 范围内的所有质数全部筛出,然后统计对于每种质数 \(p\) 有多少种不同的 \(x\) 满足 \(L \le p^x \le R\) 即可。可以直接暴力求解。

\(W = 10^7, D = R-L+1\),假设素数筛采用埃氏筛,时间复杂度为 \(O(W\log\log W + D\log\log D)\)

代码

typedef long long ll;

// 素数筛
bool vis[10000005];
int pr[1000005], pcnt = 0; // ~ 67w

// 区间筛  vis2[i] 表示 i+L 这个数字是否被筛去
bool vis2[10000005];

// 素数筛求 1e7 内质数
void init()
{
    for(int i = 2; i * i <= 10000000; i++)    
        if(!vis[i])
        {
            for(int j = i * i; j <= 10000000; j += i)
                vis[j] = true;
        }
    for(int i = 2; i <= 10000000; i++)
        if(!vis[i])
            pr[++pcnt] = i;
}

void solve()
{
    init();
    
    ll l, r, ans = 1;
    cin >> l >> r;
    
    for(int i = 1; i <= pcnt; i++)
    {
        if(pr[i] > r)
            break;
        
        // 情况二 统计有多少个 x 满足 pr[i]^x in [l+1, r]
        ll t = pr[i]; // 从 1 次方开始枚举
        while(t <= r)
        {
            if(t > l)
                ans++;
            t *= pr[i];
        }
        
        // 区间筛素数
        ll x = (l + pr[i] - 1) / pr[i] * pr[i];
        ll y = r / pr[i] * pr[i];
        for(ll j = x; j <= y; j += pr[i]) // 筛去区间内所有合数
            vis2[j - l] = true;
    }
    
    // 情况一 统计 [l+1, r] 内的质数数量
    for(ll j = l + 1; j <= r; j++)
    {
        if(vis2[j - l] == false)
            ans++;
    }
    
    cout << ans;
}

F - Socks 4

题意

\(N\) 种颜色的袜子被放在同一个抽屉里,每种颜色分别编号为 \(1, 2, \dots, N\),其中第 \(i\) 种颜色的袜子共有 \(A_i\) 只。

一开始,高桥在抽屉外放了一只颜色为 \(C\) 的袜子。接下来他会一直进行以下操作:

  • 随机从抽屉中取一只袜子出来,然后比较此时抽屉外的两只袜子颜色:
    • 如果两只袜子颜色相同,结束操作。
    • 如果两只袜子颜色不同,高桥可以选择将其中一只袜子重新放回抽屉。但他在做这个操作时,会尽量让自己接下来的操作次数的期望值是最少的。

问直到结束操作为止的抽袜子次数的期望值,对 \(998244353\) 取模。

思路

注意一开始抽屉外的这只袜子不包含在 \(A_C\) 内。

分数取模即 \(\dfrac p q \equiv p \cdot q^{\text{998244353}-2}\ (\bmod 998244353)\)

首先,如果取出的两只袜子颜色不同,为了让接下来的操作次数期望最小,很明显高桥会贪心选择保留出现次数更多的那一种袜子,这会让结束操作的概率更大。

\(S\) 表示所有袜子总数,\(E_i\) 表示如果此时抽屉外的袜子颜色为 \(x\),从现在开始操作直到结束的过程中抽袜子次数的期望值,我们可以把期望分为三部分:

  • 下一次抽取的袜子颜色与 \(x\) 相同,此时期望抽取次数为 \(1\),概率为 \(\dfrac{A_x-1}{S-1}\)
  • 下一次抽取的袜子颜色与 \(x\) 不同,记其颜色为 \(y\),继续分类:
    • 如果颜色 \(y\) 的袜子数量比 \(x\) 的袜子数量更多,接下来会把 \(x\) 放回抽屉,期望抽取次数即 \(E_y+1\),概率为 \(\dfrac{A_y}{S-1}\)
    • 如果颜色 \(y\) 的袜子数量比 \(x\) 的袜子数量更少,接下来会把 \(y\) 放回抽屉,期望抽取次数即 \(E_x+1\),概率为 \(\dfrac{A_y}{S-1}\)

综上,可以得到以下公式:

\[\begin{aligned} E_x &= \dfrac{A_x-1}{S-1}\times 1 + \sum\limits_{A_y \gt A_x} \dfrac{A_y\cdot (E_y + 1)}{S-1} + \sum\limits_{A_y \lt A_x} \dfrac{A_y\cdot (E_x + 1)}{S-1} \\ \\ E_x &= \dfrac{A_x-1}{S-1} + \sum\limits_{A_y \gt A_x} \dfrac{A_y\cdot (E_y + 1)}{S-1} + (E_x + 1) \cdot \sum\limits_{A_y \lt A_x} \dfrac{A_y}{S-1} \\ \\ E_x &= \dfrac{A_x-1}{S-1} + \sum\limits_{A_y \gt A_x} \dfrac{A_y\cdot (E_y + 1)}{S-1} + E_x \cdot \sum\limits_{A_y \lt A_x} \dfrac{A_y}{S-1} + \sum\limits_{A_y \lt A_x} \dfrac{A_y}{S-1} \\ \\ E_x \cdot (1 - \sum\limits_{A_y \lt A_x} \dfrac{A_y}{S-1}) &= \dfrac{A_x-1}{S-1} + \sum\limits_{A_y \gt A_x} \dfrac{A_y\cdot (E_y + 1)}{S-1}+ \sum\limits_{A_y \lt A_x} \dfrac{A_y}{S-1} \\ \\ E_x &= \frac{\dfrac{A_x-1}{S-1} + \sum\limits_{A_y \gt A_x} \dfrac{A_y\cdot (E_y + 1)}{S-1}+ \sum\limits_{A_y \lt A_x} \dfrac{A_y}{S-1}}{1 - \sum\limits_{A_y \lt A_x} \dfrac{A_y}{S-1}} \\ \\ E_x &= \frac{(A_x-1) + \sum\limits_{A_y \gt A_x} {A_y\cdot (E_y + 1)} + \sum\limits_{A_y \lt A_x} {A_y}}{(S-1) - \sum\limits_{A_y \lt A_x} {A_y}} \end{aligned} \]

根据公式可知,我们需要按照袜子的数量从大到小排序,然后按顺序依次处理每种袜子的期望即可。

至于处理过程,因为我们是先处理数量更多的袜子,因此 \(\sum\limits_{A_y \gt A_x} {A_y\cdot (E_y + 1)}\) 这部分可以边处理边取前缀和来快速维护,而 \(\sum\limits_{A_y \lt A_x} {A_y}\) 这部分也可以在过程中借助一个变量直接维护。

最后讨论如果取出的袜子颜色与当前袜子不同,但两种袜子的数列又是相同的情况。其实这种情况不需要特殊考虑,因为如果下一只袜子与当前袜子数量相同,那么这一次不论放回哪只袜子都是可以的,因此我们可以任意把相同数量不同颜色的袜子归类于上面讨论的第二或第三两部分中任意一部分(就是既可以当作新抽取的袜子数量偏少,也可以当作偏多,对答案无影响)。所以我们排序后直接按照下标顺序处理即可。

最终答案即 \(E_C\)。时间复杂度 \(O(N\log N)\)

代码

struct node
{
    int cnt, pos;
    bool operator < (const node &nd) const
    {
        return cnt > nd.cnt; // 按袜子数量从大到小排序
    }
} A[300005];

ll E[300005];

void solve()
{
    int n, x;
    cin >> n >> x;
    
    int S = 0;
    for(int i = 1; i <= n; i++)
    {
        cin >> A[i].cnt;
        A[i].pos = i;
        S += A[i].cnt;
    }
    
    // 把一开始放在外面的这只袜子考虑进来
    A[x].cnt++;
    S++;
    
    sort(A + 1, A + n + 1);
    
    ll suffix = 0; // 1 ~ i-1 的 A[j] * (E[j] + 1) 总和
    int C = S; // i+1 ~ n 的 A[j] 总和
    for(int i = 1; i <= n; i++)
    {
        C -= A[i].cnt; // 先把当前袜子数量减去
        
        ll p = ((A[i].cnt - 1) + suffix + C) % mod;
        ll q = (S - 1) - C;
        E[A[i].pos] = p * qpow(q, mod - 2) % mod;
        
        suffix = (suffix + A[i].cnt * (E[A[i].pos] + 1)) % mod; // 处理前缀和
    }
    
    cout << E[x];
}
posted @ 2025-06-28 22:57  StelaYuri  阅读(543)  评论(0)    收藏  举报