AtCoder Beginner Contest 433 ABCDEF 题目解析

A - Happy Birthday! 4

题意

给定正整数 \(X,Y,Z\)

高桥和青木目前分别是 \(X\) 岁和 \(Y\) 岁。

在每年的一月一日,高桥和青木会同时衰老 \(1\) 岁。

求未来(包括今年)是否有一个时刻,高桥的年龄正好是青木年龄的 \(Z\) 倍。

思路一

即求是否存在非负整数 \(i\) 满足 \(\dfrac{X+i}{Y+i} = Z\)

由于 \(Z\) 的取值范围最小为 \(2\),因此 \(X \le Y\) 时一定无解。

\(X\gt Y\) 时,随着 \(i\) 增大,比值会不断减小,直到无限趋近于 \(1\)

因此我们可以从小到大枚举 \(i\),直到比值 \(\lt Z\) 时退出枚举并判为无解即可。

代码一

void solve()
{
    int x, y, z;
    cin >> x >> y >> z;
    for(int i = 0; (x + i) / (y + i) >= z; i++)
    {
        if((x + i) % (y + i) == 0 && (x + i) / (y + i) == z)
        {
            cout << "Yes";
            return;
        }
    }
    cout << "No";
}

思路二

或者推公式:

\[\begin{aligned} \frac{X+i}{Y+i} &= Z \\ X+i &= Y\cdot Z + Z\cdot i \\ i &= \frac{Y\cdot Z - X}{1 - Z} \end{aligned} \]

代码二

void solve()
{
    int x, y, z;
    cin >> x >> y >> z;
    if((y * z - x) % (1 - z) == 0 && (y * z - x) / (1 - z) >= 0)
        cout << "Yes";
    else
        cout << "No";
}

B - Nearest Taller

题意

\(N\) 个人从左到右站成一排,左数第 \(i\) 个人编号为 \(i\),其身高为 \(A_i\)

对于每个 \(i=1,2,\ldots,N\) ,判断在编号为 \(i\) 的人的左边是否存在比他高的人。如果存在,找出其中位置最接近他的人。

代码

int n, a[105];

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        
        int p = -1; // 往前找第一个 > a[i] 的位置
        for(int j = i - 1; j >= 1; j--)
            if(a[j] > a[i])
            {
                p = j;
                break;
            }
        cout << p << "\n";
    }
}

C - 1122 Substring 2

题意

如果字符串 \(T\) 满足以下所有条件,那么它就是1122-字符串

  • \(T\) 是一个由数字组成的非空字符串。
  • \(T\) 的长度是偶数。
  • \(T\) 的前半段(\(1\)\(\frac{|T|}2\))是相同的字符。
  • \(T\) 的后半段(\(\frac{|T|}2 + 1\)\(|T|\))是相同的字符。
  • \(T\) 的前半段字符 \(+1\) 后与后半段字符相同。

给定一个由数字组成的字符串 \(S\),求 \(S\) 中有多少个子串1122-字符串

思路

考虑枚举答案字符串的右半段第一个字符 \(S_i\),那么其左侧的字符即 \(S_{i-1}\),这两个字符需要满足题意中的第五个条件(\(S_{i-1}+1=S_i\))。

然后由于要保证目标字符串的前半段字符相同、后半段字符也相同,因此这里可以从 \(i-1\) 位置开始往前查找连续相同的字符数量,记作 \(x\);再从 \(i\) 位置开始往后查找连续相同的字符数量,记作 \(y\)

明显此时以 \(S_i\) 为右半段第一个字符的答案字符串数量为 \(\min(x, y)\)

由于每个字符最多只会被从前往后以及从后往前各扫一次,因此时间复杂度为 \(O(|S|)\)

代码

void solve()
{
    string s;
    cin >> s;
    
    int n = s.size();
    int ans = 0;
    
    for(int i = 1; i < n; i++)
    {
        // 枚举 s[i-1] | s[i] 表示目标字符串的分割线
        if(s[i - 1] + 1 != s[i])
            continue;
        
        int j = i - 1; // 往前找与 s[i-1] 相同的连续字符最靠前的位置
        while(j - 1 >= 0 && s[j - 1] == s[i - 1])
            j--;
        
        int k = i; // 往后找与 s[i] 相同的连续字符最靠后的位置
        while(k + 1 < n && s[k + 1] == s[i])
            k++;
        
        // 前半段字符有 (i-1) - j + 1 个,后半段字符有 k - i + 1 个
        ans += min(i - j, k - i + 1);
    }
    cout << ans;
}

D - 183183

题意

对于正整数 \(x,y\) ,定义 \(f(x,y)\) 如下:

  • \(x,y\) 以不带前导零的形式像字符串一样前后拼接所得到的新数字。

给出正整数 \(N,M\)\(N\) 个正整数 \(A=(A_1,A_2,\ldots,A_N)\)

求满足以下条件的整数对 \((i,j)\) 数量。

  • \(1\le i,j\le N\)
  • \(f(A_i,A_j)\)\(M\) 的倍数。

思路

发现 \(f(x, y)\) 也可以看作是 \(x \times 10^{|y|} + y\),其中 \(|y|\) 表示数字 \(y\) 的位数。

由于要满足 \(f(A_i,A_j)\)\(M\) 的倍数,也就是 \((A_i \times 10^{|A_j|} + A_j) \bmod M = 0\),我们可以单独考虑加法的前后两部分对 \(M\) 取模的结果。

考虑枚举前一个数的位置 \(i\),再枚举 \(A_j\) 的位数 \(x\),这里我们将 \((A_i \times 10^x) \bmod M\) 的值暂记作 \(y\)

由于此时后一个数的位数已知,我们只需要按位数对所有数字进行分组,然后在所有位数为 \(x\) 的整数中找出除以 \(M\) 的余数恰好为 \((M-y)\bmod M\) 的整数数量,这些整数均可以与前面枚举的 \(A_i\) 形成整数对。因此将该数量加入答案即可。

时间复杂度 \(O(N\log_{10}\max(A_i))\)

代码

typedef long long ll;

int a[200005];
unordered_map<int, int> cnt[12];
// cnt[i][j] 表示位数为 i 且 % M = j 的整数数量

int getLen(int n)
{
    int r = 0;
    while(n)
    {
        n /= 10;
        r++;
    }
    return r;
}

void solve()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        cnt[getLen(a[i])][a[i] % m]++;
    }
    
    ll ans = 0;
    for(int i = 1; i <= n; i++)
    {
        ll y = a[i] % m;
        for(int j = 1; j <= 10; j++) // 枚举第二个数的位数
        {
            y = y * 10 % m; // 每多一位则前一个数 * 10
            ans += cnt[j][(m - y) % m];
        }
    }
    cout << ans;
}

E - Max Matrix 2

题意

给定两个整数 \(N,M\) ,一个长度为 \(N\) 的整数序列 \(X=(X_1,X_2,\ldots,X_N)\) 和一个长度为 \(M\) 的整数序列 \(Y=(Y_1,Y_2,\ldots,Y_M)\)

判断是否存在满足以下条件的大小为 \(N \times M\) 的整数矩阵 \((A_{i, j})\),若存在则找出任意一个:

  • \(1\le A_{i,j} \le N\times M\)
  • 所有整数互不相同
  • \(i\) 行的最大值为 \(X_i\)
  • \(j\) 列的最大值为 \(Y_j\)

思路

首先明显不可能有两行或者两列的最大值是相同的,即 \(X, Y\) 两序列内的整数理应互不相同。

由于对单行单列的限制只有其最大值,因此数字越大限制就越多,所以可以考虑按从大到小的顺序依次将所有整数填入矩阵。

明显最大值 \(N \times M\) 的位置最好确定,找哪一行哪一列的最大值为 \(N \times M\) 即可。

然后我们假设正在填整数 \(x\),考虑该整数受到的限制情况:

  • 如果存在 \(X_i = x\)\(Y_j = x\),明显此时 \(x\) 只能够填在 \(A_{i, j}\) 上。
  • 如果仅存在 \(X_i = x\),那么 \(x\) 必须填在第 \(i\) 行,此时可以任意找没填过数字的一列 \(j\) 满足 \(Y_j \gt x\),然后将 \(x\) 填在这列上。
  • 如果仅存在 \(Y_j = x\),那么 \(x\) 必须填在第 \(j\) 列,此时可以任意找没填过数字的一行 \(i\) 满足 \(X_i \gt x\),然后将 \(x\) 填在这行上。
  • 如果 \(x\)\(X, Y\) 序列完全无关,任意未填数字的位置均可以填 \(x\)

因为我们是按照降序填数字的,因此在填 \(x\) 之前,\(x + 1 \sim N \times M\) 之间的所有数字均已经填完。以上述第二种情况为例,此时我们任意找到满足 \(Y_j \gt x\) 的一列,可以保证这里的第 \(j\) 列某一行上一定已经填上了 \(Y_j\) 这个最大值。

因此上述第二、三、四种情况中我们任选出来的位置均可以任意填入当前整数 \(x\)

至于代码实现,由于随着要填的数字 \(x\) 慢慢变小,能够用来填写该数字的位置一定是慢慢变多的,因此我们可以对于整个矩阵内的每个位置 \((i, j)\),先算出该位置从哪个数字填完后开始可以被用来任意填写其它数字(即 \(\min(X_i, Y_j)\),填完后将不再受任何限制),然后按值来存储所有可行的位置,这里用 G[i] 用于存储填完 \(i\) 之后可以被用来任意填写数字的所有位置。

然后在过程中可以使用队列等数据结构持续维护当前还有哪些位置可以任意填数。

如果待填写的整数满足上述条件一,位置固定直接填即可;如果满足条件四,则队列内任意取一个位置填即可。

重点是在满足条件二或三时需要讨论一下。以满足条件二为例,即 \(X_i = x\),这时要找一列 \(j\) 满足 \(Y_j \gt x\) 用来填 \(x\) 这个整数。但明显此时 \(x = \min(X_i, Y_j)\),也就是说 \(x\) 现在可以直接取上面预处理出来的 G[x] 内任意位置填数。

单组数据时间复杂度 \(O(N \times M)\)

代码

void solve()
{
    int n, m;
    cin >> n >> m;
    
    vector<int> X(n + 1);
    vector<int> Y(m + 1);
    vector<int> XX(n * m + 1, 0); // XX[i] 存哪一行限制了最大值为 i
    vector<int> YY(n * m + 1, 0); // YY[i] 存哪一列限制了最大值为 i
    
    for(int i = 1; i <= n; i++)
        cin >> X[i];
    for(int i = 1; i <= m; i++)
        cin >> Y[i];
    
    for(int i = 1; i <= n; i++)
    {
        if(XX[X[i]] != 0)
        {
            cout << "No\n";
            return;
        }
        XX[X[i]] = i;
    }
    for(int i = 1; i <= m; i++)
    {
        if(YY[Y[i]] != 0)
        {
            cout << "No\n";
            return;
        }
        YY[Y[i]] = i;
    }
    
    vector<vector<int>> ans(n + 1, vector<int>(m + 1, 0)); // 答案矩阵
    vector<vector<pii>> G(n * m + 1); // G[i] 存有哪些位置从填完 i 这个数开始可以被用来任意填数
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            G[min(X[i], Y[j])].push_back(pii(i, j));
    
    queue<pii> q; // 维护当前可以任意被用来填数的所有位置
    
    for(int i = n * m; i >= 1; i--)
    {
        if(XX[i] != 0 && YY[i] != 0)
        {
            // 此时 i 的位置需固定为 (XX[i], YY[i])
            ans[XX[i]][YY[i]] = i;
            for(pii &p : G[i])
            {
                if(p.first != XX[i] || p.second != YY[i]) // 只要该位置不是当前填 i 用的位置
                    q.push(p); // 加到队列内,之后任意填数
            }
        }
        else if(XX[i] != 0 || YY[i] != 0)
        {
            // 此时 i 必须填在 XX[i] 行或 YY[i] 列(取决于哪个 != 0)
            // 假设 XX[i] != 0,那么明显此时 i == X[XX[i]]
            // 需要再找一列 j 满足 Y[j] > i,即 i == min(X[XX[i]], Y[j]) 成立
            // 所以只需要取 G[i] 中的任一位置来放 i 即可
            if(G[i].empty())
            {
                cout << "No\n";
                return;
            }
            pii p = G[i].back();
            G[i].pop_back(); // 填完后去掉这个位置
            ans[p.first][p.second] = i;
            for(pii &p : G[i])
                q.push(p); // 剩余位置加入队列
        }
        else
        {
            // 此时 i 可以任意填,队列内选一个位置出来即可
            if(q.empty())
            {
                cout << "No\n";
                return;
            }
            pii p = q.front();
            q.pop(); // 填完后去掉这个位置
            ans[p.first][p.second] = i;
            for(pii &p : G[i])
                q.push(p); // 剩余位置加入队列
        }
    }
    
    cout << "Yes\n";
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= m; j++)
            cout << ans[i][j] << " ";
        cout << "\n";
    }
}
signed main()
{
    ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
    int T;
    cin >> T;
    while(T--)
    {
        solve();
    }
    return 0;
}

F - 1122 Subsequence 2

题意

如果字符串 \(T\) 满足以下所有条件,那么它就是1122-字符串

  • \(T\) 是一个由数字组成的非空字符串。
  • \(T\) 的长度是偶数。
  • \(T\) 的前半段(\(1\)\(\frac{|T|}2\))是相同的字符。
  • \(T\) 的后半段(\(\frac{|T|}2 + 1\)\(|T|\))是相同的字符。
  • \(T\) 的前半段字符 \(+1\) 后与后半段字符相同。

给定一个由数字组成的字符串 \(S\),求 \(S\) 中有多少个子序列1122-字符串

思路

同样考虑枚举答案字符串的左半段最后一个字符位置或是右半段第一个字符位置,然后考虑统计在该字符必选的情况下,有多少种符合条件的子序列。

以枚举左半段的最后一个字符为例。假设 \(S_i\) 为此时答案左半段的最后一个字符,记 \(i\) 的左侧与 \(S_i\) 相同的字符数量为 \(x\)(包含 \(S_i\)),记 \(i\) 的右侧与 \(S_i+1\) 相同的字符数量为 \(y\),明显此时可以取到的最长答案子序列长度为 \(2 \times \min(x, y)\)

假设我们取 \(2 \times z\) 作为答案子序列的长度,左侧可以从除了 \(S_i\) 的剩余 \(x-1\) 个字符中任选 \(z-1\) 个字符作为答案,右侧可以从 \(y\) 个字符中任选 \(z\) 个作为答案,则可选择的子序列方案共有 \(\text{C}_{x-1}^{z-1}\cdot \text{C}_{y}^z\) 种。

则以 \(S_i\) 作为答案左半段的最后一个字符时,符合条件的子序列总数共有:

\[\sum_{z = 1}^{\min(x, y)} \text{C}_{x-1}^{z-1}\cdot \text{C}_{y}^z \]

由于 \(\text{C}_{a}^b = \text{C}_{a}^{b-a}\),上式等于:

\[\sum_{z = 1}^{\min(x, y)} \text{C}_{x-1}^{x-z}\cdot \text{C}_{y}^z \]

根据范德蒙德恒等式 \(\text{C}_{m+n}^k = \sum\limits_{i=0}^k \text{C}_m^i \cdot \text{C}_{n}^{k-i}\),上式等于:

\[\text{C}_{x+y-1}^x \]

预处理阶乘后借助公式求解组合数即可。

时间复杂度 \(O(|S|)\)

代码

typedef long long ll;

ll fac[2000005], inv[2000005];

void init()
{
    fac[0] = 1;
    for(int i = 1; i <= 2000000; i++)
        fac[i] = fac[i - 1] * i % mod;
    inv[2000000] = qpow(fac[2000000], mod - 2);
    for(int i = 1999999; i >= 0; i--)
        inv[i] = inv[i + 1] * (i + 1) % mod;
}

ll getC(int n, int m)
{
    return fac[n] * inv[m] % mod * inv[n - m] % mod;
}

int pre[128], suf[128];
// 记录当前前缀/后缀中各种字符出现次数

void solve()
{
    init();
    
    string s;
    cin >> s;
    
    ll ans = 0;
    
    for(char c : s)
        suf[c]++; // 先假设所有字符都在答案后半段
    
    for(char c : s)
    {
        pre[c]++;
        suf[c]--;
        // 假设当前字符 c 是答案前半段的最后一个字符,且必用
        int x = pre[c], y = suf[c + 1];
        ans = (ans + getC(x + y - 1, x)) % mod;
    }
    cout << ans;
}
posted @ 2025-11-22 22:51  StelaYuri  阅读(333)  评论(0)    收藏  举报