AtCoder Beginner Contest 408 ABCDEF 题目解析

A - Timeout

题意

有一位老人一直在睡觉,只有在当服务员拍了拍他肩膀时,他才会清醒 \(S+0.5\) 秒。

一开始,老人是清醒的,有一位服务员刚刚拍了他的肩膀。

从现在开始,服务员会拍老人 \(N\) 次,第 \(i\) 次拍肩膀的时间是在 \(T_i\) 秒之后。

请问从现在开始一直到 \(T_N\) 秒之后,老人是否一直保持清醒?

思路

按顺序模拟即可,只有当下一次拍肩的时间 \(T_i\) 超过上一次拍肩至少 \(S+0.5\) 秒,则输出 No

代码

void solve()
{
    int n, s;
    cin >> n >> s;
    int pre = 0; // 上一次拍肩的时间
    for(int i = 1; i <= n; i++)
    {
        int t;
        cin >> t;
        if(t > pre + s) // 都是整数,可以忽略 0.5
        {
            cout << "No";
            return;
        }
        pre = t;
    }
    cout << "Yes";
}

B - Compression

题意

给定一个长度为 \(N\) 的正整数数组,请完成排序以及去重的操作。

思路一 STL

借助 STL 的 sort 函数排序,再借助 unique 函数去重。

代码一

int n, a[105];

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    sort(a + 1, a + n + 1);
    n = unique(a + 1, a + n + 1) - (a + 1);
    cout << n << "\n";
    for(int i = 1; i <= n; i++)
        cout << a[i] << " ";
}

思路二 计数

借助计数数组统计 出现过多少种数字 以及 哪些数字出现过,最后统一输出。

代码二

int n;
bool vis[105];
// vis[i] 表示 i 是否出现过

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        int d;
        cin >> d;
        vis[d] = true;
    }
    
    int cnt = 0;
    for(int i = 1; i <= 100; i++)
        if(vis[i] == true)
            cnt++;
    
    cout << cnt << "\n";
    for(int i = 1; i <= 100; i++)
        if(vis[i] == true)
            cout << i << " ";
}

C - Not All Covered

题意

\(N\) 座城堡排成一排,编号分别为 \(1, 2, \dots, N\),它们由 \(M\) 座炮塔守护着。

\(i\) 座炮塔可以守护编号范围在 \(L_i\)\(R_i\) 内的所有城堡。

至少需要摧毁多少座炮塔,才能够使得至少一座城堡没有被任何炮塔守护着。

思路

换句话说,对于每座城堡,求它被多少座不同的炮塔守护着。从守护的炮塔数量最少的城堡入手,也就是找一个最小值作为答案。

接下来就是借助计数数组统计每座城堡被多少座炮塔守护,也就是对于每座炮塔,把区间 \([L_i, R_i]\) 内每个位置全部 \(+1\),这可以借助差分+前缀和来快速解决。

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

代码

int cnt[1000005];

void solve()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        int l, r;
        cin >> l >> r;
        cnt[l]++;
        cnt[r + 1]--;
    }
    int ans = m;
    for(int i = 1; i <= n; i++)
    {
        cnt[i] += cnt[i - 1]; // 差分求前缀和
        ans = min(ans, cnt[i]); // 取最小值作为答案
    }
    cout << ans << "\n";
}

D - Flip to Gather

题意

\(T\) 组数据,每次给定一个长度为 \(N\)\(01\) 串。

你可以执行任意次操作,每次操作选择 \(01\) 串中的任意一个位置,将其进行 \(0/1\) 翻转(\(0\)\(1\)\(1\)\(0\))。

问至少执行多少次操作,才能够使得整个字符串内所有 \(1\) 全部连在一块(或者字符串内不存在任意一个 \(1\))。

思路

假如说我们需要进行一些操作,使得最终区间 \([L,R]\) 内全都是字符 \(1\),其余位置全都是字符 \(0\),那我们可以把这个问题看作三部分:

  • 区间 \([1, L-1]\) 内的所有 \(1\) 都变成 \(0\)
  • 区间 \([L,R]\) 内所有 \(0\) 都变成 \(1\)
  • 区间 \([R+1, N]\) 内的所有 \(1\) 都变成 \(0\)

我们可以借助前缀和,来快速求解一段区间内的 \(1\) 的数量,那么 \(0\) 的数量可以类似地用 区间长度 - 区间内 \(1\) 的数量 来求解。

假如我们定义 sum[i] 表示 \(1 \sim i\) 内的字符 \(1\) 的数量,那么为了使得最终只有区间 \([L,R]\) 存在字符 \(1\),上面三部分各自的操作次数分别为:

  • sum[L-1] 表示 \(1 \sim L-1\)\(1\) 的数量
  • R - L + 1 - (sum[R] - sum[L-1]) 表示 \(L \sim R\)\(0\) 的数量
  • sum[N] - sum[R] 表示 \(R+1 \sim N\)\(1\) 的数量

我们的最终答案即这三部分之和,可以写作 sum[N] + R - L + 1 + 2 * sum[L-1] - 2 * sum[R]

整理一下,也可以写作是 (sum[N] + 1) + (R - 2 * sum[R]) + (2 * sum[L-1] - L)。我们的目标是找到某段区间,使得这个公式的值最小。

接下来考虑如何快速求解所有符合条件的区间 \([L, R]\) \((1 \le L \le R \le N)\) 的答案。

循环枚举右端点 \(R\),那么整个公式只有左端点 \(L\) 是不确定的。但因为要让整个公式的值最小,所以可以在循环枚举的过程中同时开一个变量记录前面所有位置(2 * sum[L-1] - L) 的最小值。

这样就可以在 \(O(N)\) 的时间复杂度内完成本题。

代码

int n;
char s[200005];
int sum[200005]; // 前缀字符 1 的数量

void solve()
{
    cin >> n;
    cin >> (s + 1);
    for(int i = 1; i <= n; i++)
        sum[i] = sum[i - 1] + (s[i] == '1');
    
    int ans = sum[n]; // 假如所有 1 全变为 0
    int preMin = 0; // 统计前面所有位置的 2*sum[i-1]-i 的最小值
    for(int i = 1; i <= n; i++)
    {
        preMin = min(preMin, 2 * sum[i - 1] - i); // 假如 i 是左端点
        ans = min(ans, (sum[n] + 1) + (i - 2 * sum[i]) + preMin); // 假如 i 是右端点
    }
    cout << ans << "\n";
}

int main()
{
    int T;
    cin >> T;
    while(T--)
        solve();
	return 0;
}

E - Minimum OR Path

题意

给定一张包含 \(N\) 个点以及 \(M\) 条边的连通无向图,保证不存在自环。

\(i\) 条边连接 \(u_i, v_i\) 两个点,且权值为 \(w_i\)

问在所有从点 \(1\) 到点 \(N\) 的简单路径中,经过的所有边边权按位或的最小值是多少?

思路

按二进制考虑每一位。

对于二进制下的同一个高位而言,高位为 \(1\) 的数字一定比高位为 \(0\) 的数字更大,因此我们的目标是尽可能让答案的高位为 \(0\)

我们可以从最高位 \(2^{29}\) 开始,往低位一位一位看过去。

假设现在看到了 \(2^i\) 这一位,我们可以贪心假设如果当前这一位为 \(0\),还能否找出一条从 \(1\)\(N\) 的简单路径。

因为我们暂时的目的只有让当前枚举到的这一位为 \(0\),即使后面更低位全都为 \(1\),答案也会是更小的,因此我们可以直接假设 \(2^{0 \sim i-1}\) 这些二进制位都可以任意取,然后借助搜索判断是否存在简单路径即可。

  • 如果仍然存在,则当前 \(2^i\) 这一位可以为 \(0\),为了让答案更小,所以我们就定这一位为 \(0\)
  • 如果不存在,则当前 \(2^i\) 这一位必须为 \(1\)

代码实现方面,记 \(ans\) 表示已经确定要取 \(1\)那些高位之和。每次假设 \(2^i\) 不取时,可以让 \(ans\) 加上 \(2^i-1\),以此当作边权最大值,在边权均为该数字二进制子集的子图上求解简单路径的存在性即可。

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

代码

int n, m;
vector<pii> G[200005];
bool vis[200005];

// 判断从 p 出发是否存在一条到 n 的简单路径
// 且过程中遇到的边权二进制必须是 mx 的子集
bool dfs(int p, int mx)
{
    if(p == n)
        return true;
    vis[p] = true;
    for(pii &p : G[p])
    {
        int &v = p.first, &w = p.second;
        if(vis[v]) // 已访问过
            continue;
        if((mx | w) != mx) // w 不是 mx 的子集
            continue;
        if(dfs(v, mx))
            return true;
    }
    return false;
}

// 检查是否存在一条 1 到 n 的简单路径
// 且路径上所有边权的二进制都是 mx 的子集
bool check(int mx)
{
    memset(vis, false, sizeof vis);
    return dfs(1, mx);
}

void solve()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        G[u].push_back(pii(v, w));
        G[v].push_back(pii(u, w));
    }
    int ans = 0; // 当前已经确定必须放 1 的二进制位总和
    for(int i = 29; i >= 0; i--)
    {
        // 假设不放 1<<i 这一位,那么后面的二进制位可以随便放
        // 可以让当前 ans 加上 2^i-1(即让 2^0~2^(i-1) 均为 1)
        // 之后检查是否存在一条简单路径,边权二进制均是这个数的子集即可
        if(!check(ans + (1 << i) - 1)) // 如果不放 1<<i 发现找不到路径,则当前这一位必须放 1
            ans += 1 << i;
    }
    cout << ans << "\n";
}

F - Athletic

题意

给定三个正整数 \(N, D, R\) 以及一个 \(N\) 的排列 \(H_1, H_2, \dots, H_N\)

你可以选择任意一个位置作为起点,然后每次跳到其它位置上。

假如当前位置为 \(i\),下一个想去的位置为 \(j\),只有当以下两个条件满足时,你才能够从 \(i\) 跳到 \(j\)

  • \(H_j \le H_i - D\),即下一个位置的数字必须至少比上一个位置的数字小 \(D\)
  • \(1 \le |i-j| \le R\),即下一个位置与上一个位置之间的距离不能超过 \(R\)

问最多可以跳多少次?

思路

因为 \(D\ge 1\),很明显我们每次一定是从大的数字跳向小的数字的。

因此可以考虑动态规划,记 \(dp[i]\) 表示如果最终停在数字 \(i\) 上,在此之前最多可以经过多少个不同的位置(如果这里就记跳跃次数的话可能还得处理一下无法跳跃的情况,建议先直接记经过多少个位置)。

因为给定的是一个 \(N\) 的排列,因此我们可以用计数数组 \(pos[i]\) 记录 \(i\) 这个数字出现的位置。

考虑状态转移,假如我们暂时不考虑 \(H_j \le H_i - D\) 这个条件,只看 \(1 \le |i-j| \le R\) 这个条件,很明显如果以数字 \(i\) 作为终点,也就表示最终我们停在 \(pos[i]\) 的位置上。

换句话说,上一步我们可能出现的位置一定在 \(\max(1, pos[i]-R) \sim \min(N, pos[i] + R)\) 这段区间的范围内。

对于区间内的每个位置 \(j\),如果 \(j\) 位置就是我们上一步所在的位置,那么便可以得到 \(dp[i] = \max(dp[i], dp[j + 1])\)

得出状态转移方程为:

\[dp[i] = \max_{j = \max(1, pos[i]-R)} ^{\min(N, pos[i] + R)} \quad dp[j] + 1 \]

再考虑 \(H_j \le H_i - D\) 这个条件,也就是说我们还要保证上一步所在位置的数字至少得是 \(i + D\)

所以我们还需要实现一个功能,就是当我们在做数字 \(i\) 的状态转移时,只能够取数字范围在 \(i + D \sim N\) 之间的这个数字的答案来转移。

因此除了 \(dp\) 数组以外,我们还需要再来一个 \(temp\) 数组辅助进行动态规划。\(temp\) 数组的含义以及内部的值与 \(dp\) 数组近乎一致,唯一不同的是当我们处理到数字 \(i\) 时,\(temp\) 数组内只有 \(i + D \sim N\) 之间的这些数字所在的位置是有答案的(相当于比 \(dp\) 数组的答案更新慢了 \(D\) 步)。

对于 \(temp\) 数组的维护,我们可以在处理到数字 \(i\) 时,再去执行 \(temp[pos[i+D]] := dp[pos[i+D]]\),以此更新 \(i+D\) 这个数字所在位置的答案。

但因为上述动态规划存在区间求最大值的操作,因此最终我们可以借助线段树等数据结构来维护这个 \(temp\) 数组辅助进行动态规划,实现单点修改及区间查询即可。

最后取 \(dp\) 数组最大值作为最终答案即可。注意答案要 \(-1\),因为上面我们定义数组的含义表示的是经过多少个数字,而不是跳了多少次。

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

代码

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

struct node
{
    int l, r;
    ll mx;
};

node tr[500005 << 2];

void push_up(int p)
{
    tr[p].mx = max(tr[ls].mx, tr[rs].mx);
}

void build(int l, int r, int p = 1)
{
    tr[p].l = l;
    tr[p].r = r;
    tr[p].mx = 0;
    if(l == r)
        return;
    int mid = l + r >> 1;
    build(l, mid, ls);
    build(mid + 1, r, rs);
}

// 更新 pos 位置为 val
void update(int pos, int val, int p = 1)
{
    if(tr[p].l == tr[p].r)
    {
        tr[p].mx = val;
        return;
    }
    if(pos <= tr[ls].r)
        update(pos, val, ls);
    else
        update(pos, val, rs);
    push_up(p);
}

// 求 [l, r] 的最大值
int query(int l, int r, int p = 1)
{
    if(l <= tr[p].l && tr[p].r <= r)
        return tr[p].mx;
    int res = 0;
    if(l <= tr[ls].r)
        res = max(res, query(l, r, ls));
    if(r >= tr[rs].l)
        res = max(res, query(l, r, rs));
    return res;
}

int n, d, r;
int h[500005];
int pos[500005];
int dp[500005];

void solve()
{
    cin >> n >> d >> r;
    for(int i = 1; i <= n; i++)
    {
        cin >> h[i];
        pos[h[i]] = i; // 统计每个数字所在位置
    }
    
    build(1, n);
    
    int ans = 0;
    for(int i = n; i >= 1; i--)
    {
        if(i + d <= n) // 处理到数字 i 时再去更新 i+D 这个数字所在位置的答案
            update(pos[i + d], dp[i + d]);
        int x = max(1, pos[i] - r);
        int y = min(n, pos[i] + r);
        dp[i] = query(x, y) + 1;
        ans = max(ans, dp[i]); // dp 数组最大值即最终答案
    }
    
    cout << ans - 1 << "\n";
}
posted @ 2025-05-31 22:12  StelaYuri  阅读(84)  评论(0)    收藏  举报