AtCoder Beginner Contest 466 ABCDE
A - Compromise
- 预估难度:
入门
题意
给定一个长度为 \(N\) 的整数数组,请判断这个数组是否全是负数。
数据范围
- \(2 \leq N \leq 10\)
- \(-100 \leq X_i \leq 100\)
代码
void solve()
{
int n;
cin >> n;
for(int i = 1; i <= n; i++)
{
int x;
cin >> x;
if(x >= 0)
{
cout << "No";
return;
}
}
cout << "Yes";
}
B - Representative Balls
- 预估难度:
入门 - 标签:计数思想
题意
有 \(M\) 种颜色,分别编号为 \(1, 2, \ldots, M\)。
有 \(N\) 个球,第 \(i\) 个球的颜色为 \(C_i\),大小为 \(S_i\)。
对于 \(k = 1, 2, \ldots, M\),请输出颜色为 \(k\) 的所有球当中,最大的球的大小。
而如果不存在颜色为 \(k\) 的球,则输出 -1。
数据范围
- \(1 \leq N, M \leq 100\)
- \(1 \leq C_i \leq M\)
- \(1 \leq S_i \leq 100\)
思路
可以借助一个计数数组 \(\text{ans}[i]\) 表示颜色为 \(i\) 的所有球的最大大小。
由于颜色不存在时要输出 \(-1\),并且每个球的大小都是一个正整数,因此可以将这个计数数组初始值设为 \(-1\)。
然后,每当出现一个颜色为 \(C\) 大小为 \(S\) 的球,就更新 \(\text{ans}[C]\) 为 \(\text{ans}[C]\) 和 \(S\) 中的较大值即可。
时间复杂度 \(O(N + M)\)。
代码
int ans[105];
void solve()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= m; i++)
ans[i] = -1;
for(int i = 1; i <= n; i++)
{
int c, s;
cin >> c >> s;
ans[c] = max(ans[c], s);
}
for(int i = 1; i <= m; i++)
cout << ans[i] << " ";
}
C - Count Close Pairs
- 预估难度:
普及- - 标签:双指针
题意
这是一道交互题。
在一条数轴上有 \(N\) 个点,按坐标从小到大的顺序分别编号为 \(1, 2, \ldots, N\)。
初始时,你只知道总点数为 \(N\)。程序开始运行时,你就可以输入这个整数。
接下来,你最多可以向交互程序询问 \({2N}\) 次以下问题:
- 选择两个整数 \(i, j\) 满足 \(1 \leq i < j \leq N\),然后将 \(i\) 和 \(j\) 两个整数按照
? i j的格式输出,表示询问点 \(i\) 与点 \(j\) 之间的距离是否不超过 \(1\)。 - 输出完成后,交互程序会给你一个字符串,字符串仅为
Yes或No的一种,你可以通过输入来获取这个字符串。
最后,请你输出距离不超过 \(1\) 的点对数量,也就是满足 \(1 \leq i < j \leq N\) 且点 \(i\) 与点 \(j\) 之间距离不超过 \(1\) 的整数对 \((i,j)\) 的数量。当你能够确定答案 \(X\) 时,请按照 ! X 的格式输出,然后结束你的程序。
数据范围
- \(2 \leq N \leq 10^3\)
思路
考虑枚举坐标较大的点 \(j\),那么最朴素的做法是再来一层循环 \(i = 1 \sim j-1\),检查每一对 \(i, j\) 是否符合条件。但这样的做法需要的询问次数是 \(\frac{N(N-1)}{2}\) 的,明显当 \(N\) 较大时远超 \(2N\) 次的限制。
由于题目保证了编号越大的点,在数轴上的坐标也越大。因此,如果我们对于每个点 \(j\),能够知道满足 \(i\) 和 \(j\) 之间的距离符合条件的最小的点 \(i\) 是哪个点的话,可以发现 \(i, i+1, \dots, j-1\) 这些点与 \(j\) 的距离一定都符合条件,答案可以直接加上 \(j-i\) 对。
于是可以想到二分查找,但二分的次数是 \(N\log N\) 级别的,相比于 \(2N\) 还是大。因此考虑双指针优化。
在枚举坐标较大的点 \(j\) 时,我们假设找出了满足 \(i\) 和 \(j\) 之间的距离符合条件的最小点为 \(i\)。那么当下一步继续枚举较大的点为 \(j+1\) 时,对应的符合条件的最小点一定是 \(i\) 或者是更右边的点,因此继续从上一步的 \(i\) 所在位置继续向右找即可。
这样,每次询问完成后 \(i, j\) 的其中某个数字一定会 \(+1\)(要么继续枚举下一个 \(j\),要么发现 \(i, j\) 距离不符合而看下一个 \(i\)),因此总询问次数便可以控制在 \(2N\) 及以内。
代码
// 交互,判断 l r 两点是否符合条件
bool check(int l, int r)
{
cout << "? " << l << " " << r << endl;
string s;
cin >> s;
return s == "Yes";
}
void solve()
{
int n, ans = 0;
cin >> n;
int l = 1; // 维护区间左端点
for(int r = 1; r <= n; r++) // 枚举区间右端点
{
while(l < r && !check(l, r))
l++;
// 此时 [l, r-1] 之间所有点与 r 的距离均满足条件
ans += r - l;
}
cout << "! " << ans << endl;
}
D - Placing Rooks
- 预估难度:
普及- - 标签:计数思想
题意
有一个 \(N\) 行 \(N\) 列的网格。
初始时,网格上没有放置任何棋子。
高桥将按顺序对网格执行 \(M\) 次操作。
第 \(i\) 次操作如下:
- 首先,移除第 \(R_i\) 行的所有棋子。
- 接着,移除第 \(C_i\) 列的所有棋子。
- 最后,在第 \(R_i\) 行第 \(C_i\) 列的格子上放置一个棋子。
请输出在执行完这 \(M\) 次操作后,网格上剩余的棋子总数。
数据范围
- \(1 \leq N \leq 3\times 10^5\)
- \(1 \leq M \leq 3\times 10^5\)
- \(1 \leq R_i \leq N\)
- \(1 \leq C_i \leq N\)
思路
讨论每次操作后添加的棋子到最后是否还存在。
明显对于第 \(t\) 次操作所添加的棋子所在位置 \((R_t, C_t)\),只有当后续所有操作涉及到的行与 \(R_t\) 均无关,涉及到的列与 \(C_t\) 也均无关时,这一次添加的棋子才会仍然存在,并对答案产生 \(1\) 的贡献。
因此我们可以先把所有操作存储下来,并用两个计数数组 \(\text{cntR}[i]\)、\(\text{cntC[j]}\) 先分别统计第 \(i\) 行与第 \(j\) 列会被移除的次数。
然后再按顺序看一遍所有操作。在第 \(t\) 次操作处理之前,先将第 \(t\) 次操作涉及的行和列对计数数组的影响删去,来保证两个计数数组里统计的信息表示的是在此次操作之后每行和每列被移除的次数即可。
时间复杂度 \(O(M)\)。
代码
int n, m;
int r[300005], c[300005];
int cntr[300005], cntc[300005];
void solve()
{
cin >> n >> m;
for(int i = 1; i <= m; i++)
{
cin >> r[i] >> c[i];
cntr[r[i]]++;
cntc[c[i]]++;
}
int ans = 0;
for(int i = 1; i <= m; i++)
{
cntr[r[i]]--;
cntc[c[i]]--;
if(cntr[r[i]] == 0 && cntc[c[i]] == 0) // 在这之后 当前的行列都不会被移除
ans++;
}
cout << ans;
}
E - Range Flip
- 预估难度:
普及 - 标签:动态规划
题意
有 \(N\) 张卡片排成一排,分别编号为 \(1, 2, \ldots, N\)。
第 \(i\) 张卡片的正面写有整数 \(A_i\),背面写有整数 \(B_i\)。
初始状态下,所有卡片均正面朝上。
你最多可以执行以下操作 \(K\) 次:
- 选择两个整数 \(l, r\) 满足 \(1 \leq l \leq r \leq N\),然后翻转编号在区间 \([l, r]\) 内的所有卡片。
- 这里的“翻转”是指将卡片原来朝下的一面翻转为朝上。
在执行完所有操作后,你可以将所有卡片朝上的一面写有的数字求和,记作 \(S\)。请求出 \(S\) 的最大可能值。
数据范围
- \(1 \leq N \leq 2 \times 10^5\)
- \(1 \leq K \leq 10\)
- \(1 \leq A_i, B_i \leq 10^9\)
思路
首先可以发现,每张卡片翻两次相当于没翻。
然后通过分类讨论可以发现,我们选择要翻转的区间 \([l, r]\) 是不会出现重合部分的,因为重合部分被翻了两次,和不翻没区别。证明:
- 如果两个区间 \([a, b]\) 和 \([c, d]\) 的重合部分仅包含单个端点,例如 \(a \le c \le b \le d\),此时交叉部分为 \([c, b]\)。那我们也可以将翻转的区间拆成 \([a, c-1]\) 和 \([b+1, d]\) 两段,在功能不变的同时使得两段区间不交叉。
- 如果两个区间 \([a, b]\) 和 \([c, d]\) 呈现包含关系,例如 \(a \le c \le d \le b\)。那我们也可以将翻转的区间拆成 \([a, c-1]\) 和 \([d+1, b]\) 两段,在功能不变的同时使得两段区间不交叉。
现在已知翻转区间不会重叠,也就是说每张卡片实际上最多只会翻转一次。那么我们的问题便可以变成:
- 在长度为 \(N\) 的序列中选出最多 \(K\) 段不相交的区间,区间内的数字取 \(B_i\) 作为答案,区间外的数字取 \(A_i\) 作为答案,问最大总和。
这是一个比较经典的动态规划问题。
记 \(\text{dp}[i][j][k=0/1]\) 表示在前 \(i\) 个数中,选出最多 \(j\) 个不相交的区间,并且 \(k=0/1\) 表示第 \(i\) 个数在区间外/内的情况下能获得的答案最大值。
对于状态转移,只需要分类讨论最后一个数与倒数第二个数的被选状态即可:
- 对于 \(\text{dp}[i][j][0]\),这一状态下的第 \(i\) 个数没有被区间选中。
- 如果第 \(i-1\) 个数被区间选中,可以发现 \(i-1\) 就是目前最后一段区间的最后一个数,那么第 \(i\) 个数的出现不会导致区间数量增加,上一个状态即 \(\text{dp}[i-1][j][1]\)。
- 如果第 \(i-1\) 个数没有被选中,那么最后这两个数就都不是区间内部的数字,第 \(i\) 个数的出现也不会导致区间数量的增加,上一个状态即 \(\text{dp}[i-1][j][0]\)。
- 对于 \(\text{dp}[i][j][1]\),这一状态下的第 \(i\) 个数被区间选中了。
- 如果第 \(i-1\) 个数也被选作答案,那么最后这两个数字可以当作同一段区间内的数字,那么第 \(i\) 个数的出现不会导致区间数量的增加,上一个状态即 \(\text{dp}[i-1][j][1]\)。
- 如果第 \(i-1\) 个数没有被选作答案,可以发现第 \(i\) 个数的出现会导致出现一段新的区间,并且第 \(i\) 个数是新的这段区间的第一个数字,上一个状态即 \(\text{dp}[i-1][j-1][0]\)。
综上,状态转移方程为:
代码
int n, k;
ll dp[200005][11][2];
// dp[i][j][k=0/1] 表示前 i 个数中最多选了 j 段区间,并且
// k = 0:第 i 个数不在区间内,贡献为 a[i]
// k = 1:第 i 个数在区间内,贡献为 b[i]
// 的最大答案
void solve()
{
cin >> n >> k;
for(int i = 1; i <= n; i++)
{
int a, b;
cin >> a >> b;
dp[i][0][0] = dp[i-1][0][0] + a; // 此前一段区间都没有,明显贡献为 a
for(int j = 1; j <= k; j++) // 枚举区间最大数量
{
dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1]) + a;
dp[i][j][1] = max(dp[i-1][j-1][0], dp[i-1][j][1]) + b;
}
}
cout << max(dp[n][k][0], dp[n][k][1]);
}

浙公网安备 33010602011771号