AtCoder Beginner Contest 456 ABCDE 题目解析

A - Dice

  • 预估难度:入门

题意

有三个骰子,每个骰子有六个面,每个面上分别写有 \(1,2,3,4,5,6\)

当同时掷出这些骰子时,掷出的点数之和是否可能为 \(X\)

思路

能掷出的最小点数为 \(1+1+1=3\),能掷出的最大点数为 \(6+6+6 = 18\)

代码

void solve()
{
    int X;
    cin >> X;
    if(X >= 3 && X <= 18)
        cout << "Yes";
    else
        cout << "No";
}

B - 456

  • 预估难度:普及-
  • 标签:枚举、数学

题意

有三个骰子,每个骰子有六个面。
\(i\) 个骰子的第 \(j\) 个面上写有数字 \(A_{i,j}\)
对于每个骰子,每个面朝上的概率均为 \(\frac{1}{6}\)

当同时掷出这些骰子时,求掷出的点数中 \(4,5,6\) 各恰好出现一次的概率。

思路

由于每个骰子的每个面都是等概率朝上的,因此我们可以通过枚举法找出每一种可能的掷出情况,然后求出 \(4, 5, 6\) 恰好各出现一次的情况数,记作 \(\text{cnt}\)

由于总共只有三个骰子,每个骰子都有六种情况,因此总情况数量为 \(6^3 = 216\)

概率即 \(\dfrac{\text{符合条件的情况数}}{\text{总情况数}} = \dfrac{\text{cnt}}{216}\)

代码

// 判断 x y z 三个数字是否由 4 5 6 组成
bool check(int x, int y, int z)
{
    // 排序
    if(x > y) swap(x, y);
    if(y > z) swap(y, z);
    if(x > y) swap(x, y);
    return x == 4 && y == 5 && z == 6;
}

void solve()
{
    int a[4][7];
    for(int i = 1; i <= 3; i++)
        for(int j = 1; j <= 6; j++)
            cin >> a[i][j];
    
    int cnt = 0; // 能够掷出 4 5 6 的事件数量
    for(int i = 1; i <= 6; i++) // 枚举骰子一的下标
        for(int j = 1; j <= 6; j++) // 枚举骰子二的下标
            for(int k = 1; k <= 6; k++) // 枚举骰子三的下标
                if(check(a[1][i], a[2][j], a[3][k]))
                    cnt++;
    
    cout << fixed << setprecision(10) << cnt / 216.0 << "\n";
}

C - Not Adjacent

  • 预估难度:普及-
  • 标签:枚举

题意

给定一个由字符 abc 组成的字符串 \(S\)

\(S\) 中满足任意两个相邻字符都不相同的非空子串的数量,结果对 \(998244353\) 取模。

即使两个子串作为字符串是相同的,只要它们来自 \(S\) 中的不同位置,就被视为不同的子串。

思路

考虑枚举子串的右端点 \(i\),然后快速计算 \([1, i]\) 范围内有多少个符合条件的左端点。

假设在位置 \(i\) 及之前,上一次出现相邻且相同字符的位置分别为 \(\text{pre}-1\) 以及 \(\text{pre}\)。由于此时右端点固定,因此只要左端点 \(\lt \text{pre}\),子串内就一定会出现相邻且相同的字符,导致不符合题意。因此,符合条件的左端点应在 \([\text{pre}, i]\) 范围内,共 \(i - \text{pre} + 1\) 种。

至于 \(\text{pre}\) 变量,可在枚举右端点 \(i\) 的过程中顺便维护。初始时,由于需要保证左端点 \(\ge 1\),可将 \(\text{pre}\) 变量初始化为 \(1\)

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

代码

void solve()
{
    string s;
    cin >> s;
    int n = s.size();
    s = " " + s; // 下标从 1 开始
    
    long long ans = 0;
    
    int pre = 1;
    // 记录上一次出现相邻且相同字符的情况时 后一个字符的位置
    // 即前面最近的一对相邻且相同的子串位置为 pre-1 和 pre
    // 说明当右端点为 i 时,对应子串的左端点只能在 [pre, i] 内
    // 由于子串左端点下标不能 < 1,因此 pre 初始置为 1
    
    for(int i = 1; i <= n; i++) // 枚举子串右端点
    {
        if(s[i] == s[i - 1])
            pre = i;
        ans += i - pre + 1;
    }
    
    cout << ans % 998244353;
}

D - Not Adjacent 2

  • 预估难度:普及/提高-
  • 标签:动态规划

题意

给定一个由字符 abc 组成的字符串 \(S\)

\(S\) 中满足任意两个相邻字符都不相同的非空子序列的数量,结果对 \(998244353\) 取模。

即使两个子序列作为字符串是相同的,只要它们来自 \(S\) 中的不同位置,就被视为不同的子序列。

思路

子序列计数问题,可对每个字符的选或不选两种情况进行决策,因此考虑线性动态规划。

由于题目要求选择的相邻字符不能相同,因此状态中需要有一维用于控制选择的最后一个字符类型。

\(\text{dp}[i][j]\) 表示看到第 \(i\) 个位置过,且结尾字符为 \(j\) 的符合条件的子序列数量,其中 \(j = 0,1,2\) 分别对应 ab c

分类讨论当前最后一个选择的字符 \(j\)

  • \(j \ne S_i\),说明此时 \(S_i\) 这个字符必不选,方案数量可直接从前 \(i-1\) 个位置对应的状态直接继承:

\[\text{dp}[i][j] = \text{dp}[i-1][j] \]

  • \(j = S_i\),说明 \(S_i\) 这个字符可选可不选:
    • 如果要选,则前一个字符不能为 \(S_i\),方案数量即前 \(i-1\) 个位置中结尾字符不为 \(S_i\) 的方案总数再 \(+1\)(表示 \(S_i\) 单独形成一个子串);
    • 如果不选,则方案数量即前 \(i-1\) 个位置中结尾字符为 \(S_i\) 的方案数。

\[\text{dp}[i][j] = 1 + \sum_{k = 0,1,2} \text{dp}[i-1][k] \]

时间复杂度 \(O(N \cdot \sigma)\),本题 \(\sigma = 3\)

代码

typedef long long ll;

const ll mod = 998244353;

ll dp[300005][3];
// dp[i][j] 表示在 s[1 .. i] 中有多少个符合条件的子序列以 j 结尾

void solve()
{
    string s;
    cin >> s;
    int n = s.size();
    s = " " + s;
    
    for(int i = 1; i <= n; i++) // 看到第 i 个位置过
    {
        int x = s[i] - 'a'; // 当前字符
        // 当前字符不选,答案直接继承前 i-1 个位置的对应子序列数量
        for(int j = 0; j < 3; j++)
        {
            if(j != x)
                dp[i][j] = dp[i-1][j];
            else
                dp[i][j] = (dp[i-1][0] + dp[i-1][1] + dp[i-1][2] + 1) % mod;
        }
    }
    
    cout << (dp[n][0] + dp[n][1] + dp[n][2]) % mod;
}

E - Endless Holidays

  • 预估难度:普及+/提高
  • 标签:图论建图、拓扑排序 / 深度优先搜索 / 强连通分量

题意

AtCoder 王国有 \(N\) 个城市,编号为 \(1,2,\dots,N\)。有 \(M\) 条双向道路连接城市对,其中第 \(i\) 条道路连接城市 \(U_i\)\(V_i\)。任意两个城市之间都可以通过若干条道路相互到达。

在 AtCoder 王国,一周有 \(W\) 天。每周按星期 \(1,2,\dots,W\) 的顺序进行,星期 \(W\) 的后一天是星期 \(1\)

每个城市每星期都有一些特定的节假日。城市 \(i\) 的节假日信息由一个长度为 \(W\) 的字符串 \(S_i\) 给出:如果 \(S_i\) 的第 \(j\) 个字符是 o,则星期 \(j\) 是节假日;如果是 x,则星期 \(j\) 是工作日。

高桥会选择一个他喜欢的城市,并在第 \(1\) 天的中午访问该城市。之后的每个晚上,他反复选择留在当前城市或移动到一个与当前城市直接相连的城市。如果他能一直移动,使得每天中午所在的城市都是节假日,则输出 Yes,否则输出 No

思路

考虑建图,将”城市 \(i\) - 星期 \(j\)“记作二元组 \((i, j)\),将其哈希为一个 \(1 \sim N \times W\) 范围内的唯一数值,作为图中的点编号。明显这张图可以视作一张分层图,按星期天数进行分层,每层均为 \(N\) 个点,表示星期天数相同的每个城市。

由于只有当第 \(j\) 天城市 \(i\) 是节假日时,高桥才有可能在第 \(j\) 天走到城市 \(i\),那也就相当于此时 \((i, j)\) 所表示的点是可到达的;反之,则标记该点不可到达。

假设第 \(j\) 天高桥位于城市 \(i\),那么第 \(j+1\) 天(如果 \(j=W\) 则记作第 \(1\) 天)高桥有以下两种选择:

  • 留在城市 \(i\),这需要建立 \((i, j)\rightarrow (i, j+1)\) 这样一条单向边。
  • 走到其它相邻城市 \(t\),这需要建立 \((i, j) \rightarrow (t, j+1)\) 这样一条单项边。

如果高桥有办法在星期一从某个城市 \(x\) 开始,且接下来每天都能够走到一个有节假日的城市,因为城市数量有限但时间无限,明显图中一定会存在一个环,使得从点 \((x, 1)\) 开始能够走到这个环上。

有向图判环,可以使用拓扑排序判断,或是深搜标记判断,或是强连通分量判断均可。

单组数据时间复杂度 \(O((N+M)\cdot W)\)

代码(拓扑)

int n, m, w, u[100005], v[100005];

bool holiday[1000005]; // 记录每个点是否对应是节假日(是否可达)
vector<int> G[1000005]; // 存图
int ind[1000005]; // 记录每个点的入度

// 获取城市 i 在星期 j 的对应点编号
int id(int i, int j)
{
    return (j - 1) * n + i;
}

// 建一条 x -> y 的单向边
void addEdge(int x, int y)
{
    G[x].push_back(y);
    ind[y]++;
}

void solve()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
        cin >> u[i] >> v[i];
    cin >> w;
    for(int i = 1; i <= n; i++)
    {
        string s;
        cin >> s;
        for(int j = 1; j <= w; j++)
            holiday[id(i, j)] = (s[j - 1] == 'o');
    }
    
    // 清空
    for(int i = 1; i <= n * w; i++)
    {
        G[i].clear();
        ind[i] = 0;
    }
    
    // 建图
    for(int i = 1; i <= m; i++) // 经过边 u[i] <-> v[i]
    {
        int x = u[i], y = v[i];
        for(int j = 1; j <= w; j++)
        {
            int k = (j == w ? 1 : j + 1); // 第 j 天的后一天
            if(holiday[id(x, j)] && holiday[id(y, k)]) // 只在这两个点都对应放假时再建边
                addEdge(id(x, j), id(y, k));
            if(holiday[id(y, j)] && holiday[id(x, k)])
                addEdge(id(y, j), id(x, k));
        }
    }
    for(int i = 1; i <= n; i++) // 停在原城市不动
        for(int j = 1; j <= w; j++)
        {
            int k = (j == w ? 1 : j + 1); // 第 j 天的后一天
            if(holiday[id(i, j)] && holiday[id(i, k)])
                addEdge(id(i, j), id(i, k));
        }
    
    // 找环
    queue<int> q;
    for(int i = 1; i <= n * w; i++)
        if(holiday[i] && ind[i] == 0) // 将所有入度为 0 且可到达的点全部入队
            q.push(i);
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        for(int &v : G[u])
            if(--ind[v] == 0)
                q.push(v);
    }
    for(int i = 1; i <= n * w; i++)
        if(holiday[i] && ind[i] != 0) // 拓扑搜完后,如果还有某个可到达的点 存在入度,说明出现 环
        {
            cout << "Yes\n";
            return;
        }
    cout << "No\n";
}

代码(SCC)

int n, m, w, u[100005], v[100005];

bool holiday[1000005]; // 记录每个点是否对应是节假日(是否可达)
vector<int> G[1000005]; // 存图

// 获取城市 i 在星期 j 的对应点编号
int id(int i, int j)
{
    return (j - 1) * n + i;
}

// 建一条 x -> y 的单向边
void addEdge(int x, int y)
{
    G[x].push_back(y);
}

int dfn[1000005], low[1000005], dfs_clock = 0;
int scc[1000005], scc_cnt = 0, siz[1000005];
// scc[i] 表示 i 所在强连通分量编号
// siz[i] 表示编号为 i 的强连通分量内部点数
stack<int> sk;

void tarjan(int u)
{
    dfn[u] = low[u] = ++dfs_clock;
    sk.push(u);
    
    for(int &v : G[u])
    {
        if(!dfn[v])
        {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        }
        else if(!scc[v]) // 该点已被搜过但还在栈内
            low[u] = min(low[u], low[v]);
    }
    
    if(dfn[u] == low[u])
    {
        scc_cnt++;
        int x; // 最后出栈的点
        do
        {
            x = sk.top();
            sk.pop();
            
            scc[x] = scc_cnt;
            siz[scc_cnt]++;
        }while(x != u);
    }
}

void solve()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
        cin >> u[i] >> v[i];
    cin >> w;
    for(int i = 1; i <= n; i++)
    {
        string s;
        cin >> s;
        for(int j = 1; j <= w; j++)
            holiday[id(i, j)] = (s[j - 1] == 'o');
    }
    
    // 清空
    dfs_clock = scc_cnt = 0;
    for(int i = 1; i <= n * w; i++)
    {
        G[i].clear();
        dfn[i] = low[i] = scc[i] = siz[i] = 0;
    }
    
    // 建图
    for(int i = 1; i <= m; i++) // 经过边 u[i] <-> v[i]
    {
        int x = u[i], y = v[i];
        for(int j = 1; j <= w; j++)
        {
            int k = (j == w ? 1 : j + 1); // 第 j 天的后一天
            if(holiday[id(x, j)] && holiday[id(y, k)]) // 只在这两个点都对应放假时再建边
                addEdge(id(x, j), id(y, k));
            if(holiday[id(y, j)] && holiday[id(x, k)])
                addEdge(id(y, j), id(x, k));
        }
    }
    for(int i = 1; i <= n; i++) // 停在原城市不动
        for(int j = 1; j <= w; j++)
        {
            int k = (j == w ? 1 : j + 1); // 第 j 天的后一天
            if(holiday[id(i, j)] && holiday[id(i, k)])
                addEdge(id(i, j), id(i, k));
        }
    
    // 找环
    for(int i = 1; i <= n; i++) // 枚举起点城市
        if(holiday[id(i, 1)] && !dfn[id(i, 1)])
            tarjan(id(i, 1));
    for(int i = 1; i <= scc_cnt; i++)
        if(siz[i] > 1)
        {
            cout << "Yes\n";
            return;
        }
    cout << "No\n";
}
posted @ 2026-05-02 22:09  StelaYuri  阅读(46)  评论(0)    收藏  举报