Codeforces Round #744 (Div. 3) 题解
A. Casimir’s String Solitaire
题意
给出一个仅由字符’A’,‘B’,‘C’构成的字符串,每次可以同时消去一个’A’和一个’B’,或同时消去一个’B’和一个’C’,问最终能否消去所有字符。
思路
只需’B’的个数与’A’,'C’的个数之和相等即可全部消除。
时间复杂度
O ( 1 ) O(1) O(1)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
A - Casimir’s String Solitaire | GNU C++17 | Accepted | 15 ms | 3600 KB |
#include <bits/stdc++.h>
using namespace std;
char s[55];
void solve() {
scanf("%s", s);
int n = strlen(s); //提前求出字符数量,可降低复杂度,但将strlen放到循环中也可以通过本题
int a = 0, b = 0; //a记录'A'与'C'的个数之和,b记录'B'的个数
for (int i = 0; i < n; ++i) {
if (s[i] == 'B') ++b;
else ++a;
}
if (a == b) printf("YES\n");
else printf("NO\n");
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}
B. Shifting Sort
题意
给出一个整数序列,每次可以选取一个区间 [ l , r ] [l,r] [l,r],选取一个正整数 d d d,并对区间 [ l , r ] [l,r] [l,r]循环左移 d d d个位置,要求在至多 n n n次操作后使序列从小到大有序,需要输出每一次操作。
思路
我们可以使用选择排序法,选取最小值,将其左移至第一个位置,然后在剩余的数中选取最小值,左移至第二个位置,依次类推即可完成。需要注意的是,如果最小值已经位于首位,则不需要任何操作。
时间复杂度
O ( n 2 ) O(n^2) O(n2)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
B - Shifting Sort | GNU C++17 | Accepted | 31 ms | 3600 KB |
#include <bits/stdc++.h>
using namespace std;
struct dt {
int l, r, d;
dt(int ll, int rr, int dd) { //构造函数,用于emplace_back
l = ll, r = rr, d = dd; //不写构造函数,换成push_back也可以做
}
};
int a[55], b[55];
void solve() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
vector<dt> v; //记录操作
for (int i = 1; i <= n; ++i) {
int mn = i; //记录最小值的位置
for (int j = i + 1; j <= n; ++j) if (a[j] < a[mn]) mn = j;
if (mn == i) continue; //最小值恰好是首个
int d = mn - i;
v.emplace_back(i, n, d); //记录操作
for (int j = i; j <= n; ++j) { //模拟循环移动,更新数组
if (j - d >= i) b[j - d] = a[j];
else b[n + 1 + j - d - i] = a[j];
}
for (int j = i; j <= n; ++j) a[j] = b[j];
}
printf("%d\n", v.size());
for (auto &i: v) printf("%d %d %d\n", i.l, i.r, i.d);
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}
C. Ticks
题意
给出一个 n n n行 m m m列的方格阵列,定义图案“对勾”:对 ∀ h ∈ [ 0 , d ] \forall h\in[0,d] ∀h∈[0,d],坐标 ( i − h , j ± h ) (i-h,j\pm h) (i−h,j±h)的方格均被涂黑,则称之为以 ( i , j ) (i,j) (i,j)为中心,高为 d d d的“对勾”。问给定的方格阵列能否由若干个高度不小于 k k k的“对勾”组成(不同对勾允许有重叠)。
思路
存在中心位于 ( i , j ) (i,j) (i,j),高为 d d d的“对勾”的充分必要条件是:从 ( i , j ) (i,j) (i,j)开始,在左上和右上方有至少 d d d个连续的黑色格子。同样,某个黑色格子属于某个“对勾”的充分必要条件是:从该位置 ( x , y ) (x,y) (x,y)开始向左下和右下方向寻找,能找到一个位置 ( i , j ) (i,j) (i,j),使得存在一个中心位于 ( i , j ) (i,j) (i,j),高不少于 x − i x-i x−i的“对勾”。因此,只需暴力预处理每个位置向左上、右上方向最长的连续黑格子数量,再暴力判断是否满足条件即可。
时间复杂度
O ( n 3 ) O(n^3) O(n3)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
C - Ticks | GNU C++17 | Accepted | 31 ms | 3700 KB |
#include <bits/stdc++.h>
using namespace std;
char s[20][20];
int a[20][20], b[20][20]; //数组a记录左上方最长连续黑格子数,数组b记录右上方的
int n, m, k;
bool check() {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (s[i][j] == '.') continue; //只针对黑色方格进行判断,白色方格直接忽略
bool ok = false;
int x, y, cnt;
x = i, y = j, cnt = 0;
while (x < n && y < m) {
if (a[x][y] >= max(k, cnt) && b[x][y] >= max(k, cnt)) { //左上右上需同时满足
ok = true;
break;
}
++cnt, ++x, ++y;
}
if (ok) continue;
x = i, y = j, cnt = 0;
while (x < n && y >= 0) {
if (a[x][y] >= max(k, cnt) && b[x][y] >= max(k, cnt)) {
ok = true;
break;
}
++cnt, ++x, --y;
}
if (!ok) return false;
}
}
return true;
}
void solve() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < n; ++i) scanf("%s", s[i]);
memset(a, 0, sizeof a);
memset(b, 0, sizeof b);
for (int i = 0; i < n - 1; ++i) //向右下方向递推,求出左上方向连续黑格子数
for (int j = 0; j < m - 1; ++j)
if (s[i][j] == '*' && s[i + 1][j + 1] == '*') a[i + 1][j + 1] = a[i][j] + 1;
for (int i = 0; i < n - 1; ++i) //向左下方向递推,求出右上方向连续黑格子数
for (int j = m - 1; j > 0; --j)
if (s[i][j] == '*' && s[i + 1][j - 1] == '*') b[i + 1][j - 1] = b[i][j] + 1;
if (check()) printf("YES\n");
else printf("NO\n");
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
D. Productive Meeting
题意
会议上有 n n n个人,第 i i i个人最多愿意讨论 a i a_i ai次,任意两个人都可以讨论任意次数。题目要求找出最大的讨论次数,并输出讨论的方案。
思路
贪心地找出当前愿意讨论次数最多的两个人,并使这两个人讨论一次,重复这一操作,直到无法再找到愿意讨论的两个人为止,即可完成此题。为了找到次数最多的人,使用堆(优先队列)即可。
时间复杂度
O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
D - Productive Meeting | GNU C++17 | Accepted | 109 ms | 7300 KB |
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
void solve() {
int n;
scanf("%d", &n);
priority_queue<pii> q; //大顶堆
for (int i = 1; i <= n; ++i) {
int p;
scanf("%d", &p);
if (p) q.emplace(p, i);
}
vector<pii> v; //用于记录过程
while (q.size() > 1) {
pii a = q.top(); //找到堆顶两个元素
q.pop();
pii b = q.top();
q.pop();
v.emplace_back(a.second, b.second); //记录一次讨论
--a.first, --b.first; //减少可讨论次数
if (a.first) q.push(a);
if (b.first) q.push(b);
}
printf("%d\n", v.size());
for (auto &i: v) printf("%d %d\n", i.first, i.second);
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}
E1. Permutation Minimization by Deque
题意
给出一个序列,依次加入一个空的双端队列,可以任选从头或尾加入。求可能的字典序最小的结果。请自行了解字典序的含义。
思路
采取贪心策略,出了第一个以外,新加入的元素若大于队头,在加入在队尾,否则加入在队头。模拟即可得到结果。
时间复杂度
O ( n ) O(n) O(n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
E1 - Permutation Minimization by Deque | GNU C++17 | Accepted | 109 ms | 4700 KB |
#include <bits/stdc++.h>
using namespace std;
void solve() {
int n;
scanf("%d", &n);
deque<int> q; //双端队列
int p;
scanf("%d", &p);
q.push_back(p); //直接加入第一个元素
for (int i = 1; i < n; ++i) {
scanf("%d", &p); //比较后加入其余元素
if (p > q.front()) q.push_back(p);
else q.push_front(p);
}
for (int i: q) printf("%d ", i); //输出
printf("\n");
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}
E2. Array Optimization by Deque
题意
给出一个序列,依次加入一个空的双端队列,可以任选从头或尾加入。求可能的逆序对最少的结果。请自行了解逆序对的含义。
思路
考虑最后一个元素,该元素不论在队头还是在队尾,产生的逆序对数量都与其他元素的顺序无关,因此贪心地选择产生逆序对数量较少的方案即可。随后,处理倒数第二个元素,方法与最后一个元素完全相同。注意:处理倒数第二个元素时,无需考虑它与最后一个元素是否会产生逆序对,因为已经在最后一个元素的计算过程中算过了。如此重复操作,处理所有元素即可。计算产生逆序对的数量,可以使用树状数组。本题数据范围较大,需要先离散化。
时间复杂度
O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
E2 - Array Optimization by Deque | GNU C++17 | Accepted | 280 ms | 11700 KB |
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int a[N], d[N], cnt; //cnt记录离散化后的数量
int lowbit(int x) {
return x & -x;
}
void upd(int x, int k) { //树状数组单点修改
for (int i = x; i <= cnt; i += lowbit(i)) d[i] += k;
}
int sum(int x) { //树状数组区间求和
int r = 0;
for (int i = x; i > 0; i -= lowbit(i)) r += d[i];
return r;
}
void solve() {
int n;
scanf("%d", &n);
map<int, int> m; //用于离散化
for (int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
m[a[i]];
}
cnt = 0;
for (auto &i: m) i.second = ++cnt;
for (int i = 1; i <= n; ++i) a[i] = m[a[i]], upd(a[i], 1); //离散化,并在树状数组中该位置加1
long long ans = 0;
for (int i = n; i >= 1; --i) {
int l = sum(a[i] - 1), r = sum(cnt) - sum(a[i]);
if (l > r) ans += r, upd(a[i], -1);
else ans += l, upd(a[i], -1);
}
printf("%lld\n", ans);
memset(d, 0, (cnt + 5) << 2); //记得清空树状数组
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}
F. Array Stabilization (AND version)
题意
现有一个仅由0和1组成的数组 a a a,记数组 a a a循环左移 d d d个位置后得到的序列为 a → d a^{\to d} a→d,现在对 a a a的每个元素 a i a_i ai,将其替换为 a i & a i → d a_i\&a_i^{\to d} ai&ai→d,称为一次操作。不断重复操作,直到整个序列不再变化,称作稳定状态。要求判断稳定状态下是否还存在1,若存在,输出-1,否则求出从初始状态到稳定状态需要的操作数。
思路
当且仅当 a i = a ( i − d + n ) % n = 1 a_i=a_{(i-d+n)\%n}=1 ai=a(i−d+n)%n=1时,经过一次操作后, a i a_i ai仍为1。因此我们只需暴力模拟跳跃 d d d个位置这一操作,找到最长的连续的1的数量,即可求得答案。在此过程中,有一个性质:令 k = g c d ( n , d ) k=gcd(n,d) k=gcd(n,d)(gcd表示最大公约数),则我们可以找到 k k k个独立的循环,每个循环的长度是 n k \frac{n}{k} kn。因此当连续的1的数量达到 n k \frac{n}{k} kn时,说明不论操作多少次,1都会存在。
友情提醒1:注意 d d d的数据范围, d = n d=n d=n是可能的,这将导致某种写法超时,可以通过特判 d = n d=n d=n解决。
友情提醒2:关于上述涉及的“性质”,可以通过离散数学中群和子群的方法严谨表述和证明,在此不作解释。
友情提醒3:本题数据范围很大,解法看似暴力,实则复杂度不大,可以放心写。
时间复杂度
O ( n ) O(n) O(n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
F - Array Stabilization (AND version) | GNU C++17 | Accepted | 249 ms | 11500 KB |
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int a[N], b[N];
void solve() {
int n, d;
scanf("%d%d", &n, &d);
memset(b, 0, (n + 5) << 2); //不要全memset,容易超时
for (int i = 0; i < n; ++i) scanf("%d", &a[i]);
if (d == n) { //特判d=n
for (int i = 0; i < n; ++i) {
if (a[i]) {
printf("-1\n");
return;
}
}
printf("0\n");
return;
}
int k = __gcd(n, d), lim = n / k, ans = 0;
for (int i = 0; i < k; ++i) {
int pos = i;
for (int j = 0; j < 2 * lim; ++j) { //要进行2轮模拟,避免因为首尾相接的问题出错
if (a[(pos + d) % n]) b[(pos + d) % n] = b[pos % n] + 1;
if (b[(pos + d) % n] >= lim) { //一定存在1的情形
printf("-1\n");
return;
}
pos = (pos + d) % n;
}
}
for (int i = 0; i < n; ++i) ans = max(ans, b[i]);
printf("%d\n", ans);
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}
G. Minimal Coverage
题意
在一个一维的数轴上有若干个线段,其中第一条线段的起点在0位置。给出线段的长度,相邻的线段必须首尾相接,但方向不一定要相同,求这些线段连接起来之后的最小总覆盖长度。
题意比较难懂,用数据举例:线段长度是 [ 7 , 8 , 6 ] [7,8,6] [7,8,6],第一条线段覆盖区间 [ 0 , 7 ] [0,7] [0,7],第二条线段覆盖区间 [ − 1 , 7 ] [-1,7] [−1,7],第三条线段覆盖区间 [ − 1 , 5 ] [-1,5] [−1,5],则总覆盖为 [ − 1 , 7 ] [-1,7] [−1,7],长度为8。
思路
总体思路采用二分策略,对答案进行二分,二分上界是 2 ⋅ m a x a i 2\cdot max\ a_i 2⋅max ai。对于每一个尝试的值 m i d mid mid,我们可以记录已处理的线段的尾端可能达到的位置,每加入一条新的线段,对可能的位置进行更新。由于该位置离起点不能超过 m i d mid mid个单位,我们需要删去超出范围的位置(更新时不加入即可)。
方法优化1:使用STL容器中的bitset来优化更新可能位置的操作,可以大幅提高效率,并且可以减少代码量(利用左移、右移、与、或运算)。
方法优化2:线段终点可能出现负数,因此直接用数组,或bitset来处理会很麻烦(需要加上一个适当的数,把负数转化为正数),实际上,第一条线段的起点在什么位置并不重要(也就是不一定要从0位置开始),因此不妨将初始状态设置为 m i d mid mid个1。该方法需要一定的理解能力,不能理解的可不采用,下面提供的代码采用了本方法。
时间复杂度
O ( n l o g 2 n ⋅ m a x a i ) O(nlog_2n\cdot max\ a_i) O(nlog2n⋅max ai)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
G - Minimal Coverage | GNU C++17 | Accepted | 46 ms | 3700 KB |
#include <bits/stdc++.h>
using namespace std;
int a[10005];
int n;
bool check(int x) {
bitset<2005> b, c;
for (int i = 0; i <= x; ++i) b[i] = c[i] = true; //左移代表线段方向为负,右移代表正
for (int i = 0; i < n; ++i) b = (b << a[i] | b >> a[i]) & c; //&c的目的是去掉超出范围的位置
return b.any();
}
void solve() {
scanf("%d", &n);
for (int i = 0; i < n; ++i) scanf("%d", &a[i]);
int l = 0, r = 2000, ans; //二分答案
while (l <= r) {
int mid = (l + r) >> 1;
if (check(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
}
printf("%d\n", ans);
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}