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])\)。
得出状态转移方程为:
再考虑 \(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";
}

浙公网安备 33010602011771号