AtCoder Beginner Contest 407 ABCDEF 题目解析

A - Approximation

题意

求与分数 \(\dfrac A B\) 最接近的整数。

思路

注意精度,建议改为整数运算及判断。

即判断 \(x\)\(y\) 哪个与 \(\dfrac A B\) 更接近时,先写成 \(\dfrac{xB}B\)\(\dfrac{yB}{B}\),再直接整数比较分子即可。

代码

void solve()
{
    int a, b;
    cin >> a >> b;
    
    int ans = 0;
    
    for(int i = 0; i <= 407; i++) // 枚举答案
    {
        if(abs(i * b - a) < abs(ans * b - a)) // i 比目前的 ans 更接近
            ans = i;
    }
    
    cout << ans;
}

B - P(X or Y)

题意

有两个骰子,每个骰子都有六个面,每个面的点数分别为 \(1, 2, 3, 4, 5, 6\)

投掷一次这两个骰子,问有多大的概率满足以下两个条件中的至少一个条件:

  • 两骰子点数之和不小于 \(X\)
  • 两骰子点数之差不小于 \(Y\)

思路

对于一个事件,其概率定义为发生此事件的基本事件数量除以样本空间内的基本事件总数量

这道题的样本空间就是两个骰子可能投掷出的所有情况,共 \(6 \times 6 = 36\) 种。

那么我们只需要枚举每一种情况,统计有多少种情况满足题意的两个条件之一即可。

思路

void solve()
{
    int x, y, cnt = 0;
    cin >> x >> y;
    for(int i = 1; i <= 6; i++)
        for(int j = 1; j <= 6; j++) // i j 分别表示两个骰子的点数
        {
            if(i + j >= x || abs(i - j) >= y)
                cnt++;
        }
    printf("%.15f", cnt / 36.0);
}

C - Security 2

题意

有一个字符串,初始为空字符串。每次可以执行以下两种操作之一:

  • 往字符串后添加一个数字字符 \(0\)
  • 把当前字符串上的所有数字字符改为其后一个数字。即 \(0\)\(1\)\(1\)\(2\)。特殊的,\(9\) 会变成 \(0\)

给定一个字符串 \(S\),问至少要进行上述操作多少次才能够得到字符串 \(S\)

思路

可以反向思考,每次要么把所有字符改为其前一个字符(\(0\)\(9\),记作操作一),要么把最后一个字符删除(必须要保证是 \(0\),记作操作二)。

于是可以倒序看一遍整个字符串,用 \(sum\) 记录当前已经执行了多少次“操作一”。

对于每个位置的数字字符,先根据 \(sum\) 的值计算出变化后的数字。为了将此数字字符通过“操作二”删除,需要再执行几次“操作一”来将其变为 \(0\)。额外的操作次数即这个数字当前的数值。

一直做到字符串所有字符均被删除即可。

注意答案需要加上字符串原长度,表示“操作二”的执行次数。

代码

char s[500005];

void solve()
{
    cin >> (s + 1);
    int n = strlen(s + 1);
    int sum = 0; // 当前操作一的执行次数
    for(int i = n; i >= 1; i--)
    {
        int t = s[i] - '0';
        t = ((t - sum) % 10 + 10) % 10; // 计算反向操作 sum 次之后的该数数值
        sum += t; // 把当前数字变为 0 所需要额外进行的操作一次数
    }
    cout << sum + n;
}

D - Domino Covering XOR

题意

有一个 \(H \times W\) 的网格,每个网格内都有一个非负整数。记第 \(i\) 行第 \(j\) 列的非负整数为 \(A_{i, j}\)

现在可以在网格上随意放置多米诺骨牌,一块多米诺骨牌可以覆盖相邻的两个网格。每个网格最多只能被一块多米诺骨牌覆盖。

对于某种覆盖方案,其分数定义为未被覆盖的所有网格上的整数异或和

问在所有覆盖方案当中的分数最大值。

思路

发现 \(H \times W \le 20\) 这一条件,网格数量很少,考虑采用深度优先搜索将所有覆盖情况全部找出。

按照二维数组的深搜方式搜索,再开一个 vis[i][j] 数组记录每个网格目前是否已经被多米诺骨牌覆盖,防止重复覆盖的情况。

搜索过程中,对于每个位置 \((x, y)\),如果要在这个位置放骨牌,我们只考虑以其作为右下角的方案即可。即要么放置 \((x-1,y), (x, y)\) 这两个位置,要么放置 \((x, y-1), (x, y)\) 这两个位置。

如果能放,则当前位置数值不计入答案(注意把另外一个位置已经计入答案的部分借助异或运算去掉)。

如果不能放,那么当前位置先异或进答案,继续往后搜索。

直到所有网格全部搜完,找出所有情况下的答案最大值即可。

代码

typedef long long ll;

int n, m;
ll a[25][25];
bool vis[25][25];

ll ans = 0;

// 当前搜到 (x, y) 这个位置,且在此之前没有被覆盖的所有数字异或和为 sum
// 按先行后列的顺序搜索
void dfs(int x, int y, ll sum)
{
    if(x > n) // 超出最后一行,搜索结束
    {
        ans = max(ans, sum);
        return;
    }
    if(y > m) // 超出最后一列,调整为从下一行第一列开始搜索
    {
        dfs(x + 1, 1, sum);
        return;
    }
    if(x - 1 >= 1 && vis[x - 1][y] == false) // 如果上一行相邻的网格没有被覆盖,可以尝试放一块骨牌
    {
        vis[x - 1][y] = vis[x][y] = true;
        dfs(x, y + 1, sum ^ a[x - 1][y]); // 把另一个位置已经计入答案的部分借助异或去掉,再继续往后搜索
        vis[x - 1][y] = vis[x][y] = false;
    }
    if(y - 1 >= 1 && vis[x][y - 1] == false) // 如果上一列相邻的网格没有被覆盖,可以尝试放一块骨牌
    {
        vis[x][y - 1] = vis[x][y] = true;
        dfs(x, y + 1, sum ^ a[x][y - 1]);
        vis[x][y - 1] = vis[x][y] = false;
    }
    dfs(x, y + 1, sum ^ a[x][y]); // 不放骨牌,当前数值计入总和,继续往后搜索
}

void solve()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin >> a[i][j];
    dfs(1, 1, 0);
    cout << ans;
}

E - Most Valuable Parentheses

题意

给定一个长度为 \(2N\) 的非负整数数组 \(A = (A_1,\dots,A_{2N})\)

对于一个长度为 \(2N\) 且仅由 () 组成的括号匹配序列,其对应的答案为所有左括号 ( 所在位置对应的 \(A\) 数组数值之和。

问在所有可能的括号匹配序列当中,对应的答案最大值是多少?

思路

先假设当前有一个括号匹配序列是 ()()()()()...

即所有奇数位置全部计入答案,所有偶数位置全部删除。

但在从前往后处理 \(A\) 数组的过程中,每当处理到一个奇数位置 \(i\) 时,我们可以看看\(i\) 位置之前被删除的所有数字当中是否有比 \(A_i\) 更大的数字。

  • 如果没有,直接把 \(A_i\) 计入答案即可。
  • 如果有,记最大的那个数字所在位置为 \(j\),因为是被删除的数字,所以 \(j\) 位置当前一定对应的是右括号 )。我们可以让当前本应该出现在 \(i\) 位置的左括号和 \(j\) 位置的右括号交换。因为这个操作相当于是让右括号右移,所以可以保证交换后的括号序列一定是括号匹配的。交换完成后,我们则应该把 \(A_j\) 计入答案,而此时 \(i\) 位置的数字 \(A_i\) 则变成了被删除的数字。

对于这个处理过程,我们可以采取诸如“堆”等能够快速实现新增数字求最大值的数据结构辅助实现。

代码

typedef long long ll;

void solve()
{
    int n;
    cin >> n;
    
    ll sum = 0;
    priority_queue<int> q;
    
    for(int i = 1; i <= 2 * n; i++)
    {
        int d;
        cin >> d;
        if(i % 2 == 1) // 奇数位置,原本应该加入答案
        {
            if(!q.empty() && q.top() > d) // 如果前面被删除的数字内有比 d 更大的数字
            {
                sum += q.top(); // 被删除的最大数字加入答案
                q.pop();
                q.push(d); // 删除当前数字 d
            }
            else
                sum += d; // 如果没有则直接当前数字加入答案
        }
        else
            q.push(d); // 偶数位置默认删除
    }
    
    cout << sum << "\n";
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    int T;
    cin >> T;
    while(T--)
    {
        solve();
    }
    return 0;
}

F - Sums of Sliding Window Maximum

思路

有一个长度为 \(N\) 的非负整数数组 \(A = (A_1,\dots,A_N)\)

对于每个 \(k = 1, \dots, N\),解决以下问题:

  • 明显 \(A\) 中存在 \(N-k+1\) 个长度为 \(k\)连续子序列,问长度为 \(k\)每个连续子序列的最大值之和。

思路

我们可以考虑每个数字各自对答案的贡献。

对于 \(A_i\),如果要成为答案,那么对应的连续子序列应该包含 \(i\) 位置,且 \(A_i\) 必须是连续子序列中最大的数字。

换句话说,我们需要找出 \(A_i\) 左右两侧连续的且比 \(A_i\) 小的数字分别有多少个,这可以借助单调栈、链表等等算法实现。

  • 这里需要注意数值相同的情况。对于一个连续子序列如果内部有多个一样大的数值,我们可以把最左边或是最右边任意一个位置当作最大值,统计一次答案即可。下文以最左边的数字作为最大值,也就是往右统计时需要把相同大小的数字数量也计入。

\(i\) 左侧\(A_i\) 小的数字个数\(L_i\),右侧不小于 \(A_i\) 的数字个数\(R_i\)。于是对应的连续子序列下标区间的左端点则应该在 \([i-L_i, i]\) 范围内,共 \(L_i+1\) 种情况;对应右端点应该在 \([i, i+R_i]\) 范围内,共 \(R_i + 1\) 种情况。

也就是说以 \(A_i\) 作为最大值的子序列共有 \((L_i+1) \times (R_i+1)\) 种。

但本题我们需要对于每种长度的连续子序列,分别求出其最大值总和,于是我们先分析这种情况对每种长度的连续子序列的贡献。

暂时记 \(\text{minn}\) 表示 \((L_i, R_i)\) 的较小值,\(\text{maxx}\) 表示较大值。对于必须包含 \(i\) 位置,且左右端点分别落在 \([i-L_i, i]\)\([i, i+R_i]\) 范围内的所有区间,可以发现数量变化可以分为以下三段(也可以自己举个带常数的例子):

  • 当区间长度不超过 \(\text{minn} + 1\) 时,符合条件的区间个数会随着区间长度变大而慢慢变多。
    • 即长度为 \(1\) 的区间有 \(1\) 个,长度为 \(2\) 的区间有 \(2\) 个,……
  • 当区间长度超过 \(\text{minn}+1\) 且不超过 \(\text{maxx}+1\) 时,由于受到数量较少的这一侧的影响,我们无法再继续扩大区间个数,符合条件的区间个数会恒等于 \(\text{minn} + 1\)
  • 当区间长度超过 \(\text{maxx}+1\) 时,此时因为数量较多的这一侧即使全选也不大够,因此符合条件的区间个数会慢慢变少。
    • 即长度为 \(\text{maxx}+2\) 的区间有 \(\text{minn}\) 个,长度为 \(\text{maxx}+3\) 的区间有 \(\text{minn} - 1\) 个,……,长度为 \(\text{minn+maxx}+1\) 的区间有 \(1\) 个。

总结一下:

  • 区间长度在 \([1, \text{minn}+1]\) 范围内时,符合条件的区间个数分别为 \(1, 2, 3, \dots, \text{minn} + 1\) 个,是一个首项为 \(1\) 公差为 \(1\) 的等差数列。
  • 区间长度在 \([\text{minn}+2, \text{maxx}+1]\) 范围内时,符合条件的区间个数恒为 \(\text{minn} + 1\) 个。
  • 区间长度在 \([\text{maxx}+2, \text{minn+maxx}+1]\) 范围内时,符合条件的区间个数分别为 \(\text{minn}, \text{minn}-1, \dots, 2, 1\) 个,是一个首项为 \(\text{minn}\) 公差为 \(-1\) 的等差数列。

所以我们只需要能够快速对区间加上一个等差数列,即可完成本题。这可以用线段树或是树状数组维护差分数组来实现。维护等差数列也可以分为三步:

  • 区间长度在 \([1, \text{minn}+1]\) 范围内时,由于后一项总是比前一项多一个符合条件的区间,而当最大值为 \(A_i\) 时,可以看作后一项答案总是比前一项答案多一个 \(A_i\)。对于差分数组,只需要在这段区间每个位置都加上一个 \(A_i\) 即可。
  • 区间长度在 \([\text{minn}+2, \text{maxx}+1]\) 范围内时,由于每一项答案不变,无需修改。
  • 区间长度在 \([\text{maxx}+2, \text{minn+maxx}+1]\) 范围内时,后一项总是比前一项少一个符合条件的区间,换言之后一项答案总是比前一项答案少一个 \(A_i\)。对于差分数组,只需要在这段区间每个位置都减去一个 \(A_i\) 即可。
    • 但要注意这个变化是从 \(\text{maxx}+1 \rightarrow \text{maxx}+2\) 开始的,一直到 \(\text{minn+maxx}+1 \rightarrow \text{minn+maxx}+2\) 结束,又因为差分的右端点修改需要放在后一个位置上,因此实际操作的区间应该是 \([\text{maxx}+2, \text{minn+maxx}+2]\)

最后就是把差分数组借助前缀和转为原数组,第 \(i\) 项的实际答案为 \(1 \sim i\) 的总和。

因此只需要能够实现区间修改区间求和即可。

(如果使用的是普通差分或是树状数组,需要维护二维差分,但代码量可以少一些)

代码

typedef long long ll;

#define ls (p << 1)
#define rs (p << 1 | 1)

struct node
{
    int l, r;
    ll sum, lazy;
};

node tr[200005 << 2];

// 维护子树和
void push_up(int p)
{
    tr[p].sum = tr[ls].sum + tr[rs].sum;
}

// 懒惰标记下传
void push_down(int p)
{
    if(tr[p].lazy == 0)
        return;
    tr[ls].sum += tr[p].lazy * (tr[ls].r - tr[ls].l + 1);
    tr[rs].sum += tr[p].lazy * (tr[rs].r - tr[rs].l + 1);
    tr[ls].lazy += tr[p].lazy;
    tr[rs].lazy += tr[p].lazy;
    tr[p].lazy = 0;
}

// 建树
void build(int l, int r, int p = 1)
{
    tr[p].l = l;
    tr[p].r = r;
    tr[p].sum = tr[p].lazy = 0;
    if(l == r)
        return;
    int mid = (l + r) / 2;
    build(l, mid, ls);
    build(mid + 1, r, rs);
}

// 区间修改 +val
void update(int l, int r, ll val, int p = 1)
{
    if(l > r)
        return;
    if(l <= tr[p].l && tr[p].r <= r)
    {
        tr[p].sum += val * (tr[p].r - tr[p].l + 1);
        tr[p].lazy += val;
        return;
    }
    push_down(p);
    if(l <= tr[ls].r)
        update(l, r, val, ls);
    if(r >= tr[rs].l)
        update(l, r, val, rs);
    push_up(p);
}

// 区间求和
ll query(int l, int r, int p = 1)
{
    if(l > r)
        return 0;
    if(l <= tr[p].l && tr[p].r <= r)
        return tr[p].sum;
    push_down(p);
    ll res = 0;
    if(l <= tr[ls].r)
        res += query(l, r, ls);
    if(r >= tr[rs].l)
        res += query(l, r, rs);
    return res;
}

int n;
ll a[200005];
int L[200005], R[200005];
// L 表示左边比我小的数字个数  R 表示右边不超过我的数字个数(要考虑相同数字的影响)

// 单调栈求 L R 数组
void initLR()
{
    a[0] = a[n+1] = 1e9;
    stack<int> skl, skr;
    skl.push(0);
    for(int i = 1; i <= n; i++)
    {
        while(!skl.empty() && a[skl.top()] < a[i]) // 把左侧比我小的全部出栈
            skl.pop();
        L[i] = i - skl.top() - 1; // 栈顶就是左侧 >=a[i] 的最靠右的位置,计算比我小的数字数量
        skl.push(i);
    }
    skr.push(n + 1);
    for(int i = n; i >= 1; i--)
    {
        while(!skr.empty() && a[skr.top()] <= a[i]) // 把右侧不超过我的全部出栈
            skr.pop();
        R[i] = skr.top() - i - 1; // 栈顶就是右侧 >a[i] 的最靠左的位置,计算不超过我的数字数量
        skr.push(i);
    }
}

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    
    initLR();
    
    build(1, n+1);
    
    for(int i = 1; i <= n; i++)
    {
        int minn = min(L[i], R[i]), maxx = max(L[i], R[i]);
        update(1, minn + 1, a[i]);
        update(maxx + 2, minn + maxx + 2, -a[i]);
    }
    
    for(int i = 1; i <= n; i++)
        cout << query(1, i) << "\n"; // 差分数组的还原后的最终答案即 i 项前缀和
}
posted @ 2025-05-24 22:56  StelaYuri  阅读(124)  评论(0)    收藏  举报