Codeforces Round #731 (Div. 3) 题解
A. Shortest Path with Obstacle
题意
每次给出3个点A,B,F的坐标,求从A到B不经过F的最短路径长度。(每次只允许上下左右走1格)
思路
先不考虑障碍物,A到B的最短路径长度就是A和B的曼哈顿距离 ( ∣ x A − x b ∣ + ∣ y A − y B ∣ ) (|x_A-x_b|+|y_A-y_B|) (∣xA−xb∣+∣yA−yB∣)。现在考虑障碍物,只有A,B在同一x值或y值,且F恰好在A,B之间时,这条最短路径不可行,需要绕行2个单位长度。
时间复杂度
O ( 1 ) O(1) O(1)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
A - Shortest Path with Obstacle | GNU C++17 | Accepted | 15 ms | 0 KB |
#include <bits/stdc++.h>
using namespace std;
void solve() {
int x1, y1, x2, y2, x3, y3;
scanf("%d%d%d%d%d%d", &x1, &y1, &x2, &y2, &x3, &y3);
int add = 0;
if (x1 == x2 && x1 == x3 && y3 > min(y1, y2) && y3 < max(y1, y2))add = 2;
if (y1 == y2 && y1 == y3 && x3 > min(x1, x2) && x3 < max(x1, x2))add = 2;
printf("%d\n", abs(x1 - x2) + abs(y1 - y2) + add);
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
B. Alphabetical Strings
题意
定义一个长度不超过26的字符串 s s s为字典序的(alphabetical),满足条件: s s s由一个空字符串,每次在左端或右端添加一个字母得到,并且添加字母的顺序为字母表顺序 a , b , c , … , z a,b,c,\dots,z a,b,c,…,z(也就是说,长度为 l e n len len的字符串包含字母表中前 l e n len len个字母)。现给出一些长度不超过26的字符串,要求判断是不是字典序的。
思路
逆向思维,对于满足条件的字典序的字符串,长度为 l e n len len的时候,在字符串的第一个或最后一个位置,一定能找到字母表中第 l e n len len个字母。因此我们只需要不断地删去最大的字母即可,直到字符串长度为0(满足条件)或字符串两端均无法删去(不满足条件)。
时间复杂度
O ( n ) O(n) O(n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
B - Alphabetical Strings | GNU C++17 | Accepted | 30 ms | 0 KB |
#include <bits/stdc++.h>
using namespace std;
void solve() {
char s[50];
scanf("%s", s);
int n = (int) strlen(s);
int l = 0, r = n - 1; //初始化为原字符串的左右端
while (n--) {
if (s[l] == 'a' + n)++l; //左边符合,左端点右移
else if (s[r] == 'a' + n)--r; //右边符合,右端点左移
else { //都不符合,得出结论
printf("NO\n");
return;
}
}
printf("YES\n");
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
C. Pair Programming
题意
甲乙两人一起写一段代码,一开始已经有 k k k行代码,甲有 n n n个操作 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,…,an,乙有 m m m个操作 b 1 , b 2 , … , b m b_1,b_2,\dots,b_m b1,b2,…,bm,其中,操作若为0,则为新增一行代码,若不为0,则为修改操作的数值对应的行,此时要求修改的这一行存在。要求在不改变甲乙各自操作的相对顺序的情况下(即 a 1 a_1 a1一定在 a 2 a_2 a2之前, b 1 b_1 b1一定在 b 2 b_2 b2之前,以此类推),找到任何一种合理的方案,若不存在,输出 − 1 -1 −1。
思路
由于修改操作不影响代码行数,新增操作可以增加代码行数,因此贪心即可。每次检查甲的操作是否可行,若可行(也就是说甲的下一操作是0或者修改已经存在的行),则执行(如果是0的话要记得更新行数),不可行则判断乙的操作,与上述甲操作相同,若还是不可行,说明没有合适的方案。
时间复杂度
O ( n + m ) O(n+m) O(n+m)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
C - Pair Programming | GNU C++17 | Accepted | 77 ms | 200 KB |
#include <bits/stdc++.h>
using namespace std;
int a[1000], b[1000];
void solve() {
int k, n, m;
scanf("%d%d%d", &k, &n, &m);
for (int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
}
for (int i = 0; i < m; ++i) {
scanf("%d", &b[i]);
}
int i = 0, j = 0; //双指针,初始都是0
vector<int> v; //保存结果序列
while (i != n || j != m) {
if (i < n && a[i] <= k) { //尝试执行甲操作
v.push_back(a[i]);
if (!a[i])++k; //操作是0,要增加更新k
++i;
} else if (j < m && b[j] <= k) { //尝试执行乙操作
v.push_back(b[j]);
if (!b[j])++k;
++j;
} else { //都不可行,得出结论
printf("-1\n");
return;
}
}
for (auto &p:v)printf("%d ", p); //输出结果
printf("\n");
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
D. Co-growing Sequence
题意
定义序列 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,…,an为growing,满足条件:对于第1到n-1项,均满足 a i & a i + 1 = a i a_i\&a_{i+1}=a_i ai&ai+1=ai,即自身与下一项按位与后的结果等于自身。定义两个序列 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,…,an和 b 1 , b 2 , … , b n b_1,b_2,\dots,b_n b1,b2,…,bn为co-growing,满足条件:两序列各项按位异或后得到的序列是growing的。现给出序列 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,…,an,求字典序最小的序列 b 1 , b 2 , … , b n b_1,b_2,\dots,b_n b1,b2,…,bn,使得序列 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,…,an和 b 1 , b 2 , … , b n b_1,b_2,\dots,b_n b1,b2,…,bn为co-growing。
思路
对于每一项 a i a_i ai,与 a i + 1 a_{i+1} ai+1按位与运算后不改变值,等价于 a i a_i ai中的每一个二进制位,只要该位为1,则 a i + 1 a_{i+1} ai+1中对应的位为1。题目要求序列 b 1 , b 2 , … , b n b_1,b_2,\dots,b_n b1,b2,…,bn字典序最小,由于异或运算中,假定运算为 a x o r b a\ xor\ b a xor b, b b b中某位为1意味着运算结果中该位的值与 a a a中这一位的值不同,因此只需让异或得到的序列尽可能地接近原序列即可,最接近的值为按位或的结果,再根据异或操作的可逆性求出要求的序列。
时间复杂度
O ( n ) O(n) O(n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
D - Co-growing Sequence | GNU C++17 | Accepted | 93 ms | 2400 KB |
#include <bits/stdc++.h>
using namespace std;
int a[200005]; //读入的原序列
int b[200005]; //题目要求的序列
int c[200005]; //最优的合理序列
void solve() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
}
b[0] = 0; //第一项一定是0
c[0] = a[0];
for (int i = 1; i < n; ++i) {
c[i] = a[i] | c[i - 1]; //求出最优序列,用按位或
b[i] = a[i] ^ c[i]; //求出题目要求的序列,用按位异或
}
for (int i = 0; i < n; ++i) {
printf("%d ", b[i]);
}
printf("\n");
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
E. Air Conditioners
题意
长为 n n n的一维区间里有 k k k个空调,不存在重合的情况,每个空调能在其所属的位置造成温度为 t t t,每远离1单位距离,温度就上升1,一个点的温度是所有空调在该点造成温度的最小值。现给出区间长度和空调数量,以及每个空调的位置和设定温度,求每个点的温度。
思路
首先需要认识到一点:一个位于 x x x的空调,设定温度为 t t t,在向两侧传播的时候,相当于在 x − 1 x-1 x−1位置和 x + 1 x+1 x+1位置各新增一个温度设定为 t + 1 t+1 t+1的空调。利用这一点,我们把空调按设定温度从小到大排序,然后依次处理,直到所有单元都被处理过。处理时,先加入温度为 t 0 t_0 t0的所有空调,然后尝试使之向两侧传播,传播时,将传播得到的新的点也看作一个空调即可,利用队列依次处理完所有温度为 t 0 t_0 t0的情形后,开始处理温度为 t 0 + 1 t_0+1 t0+1的情形,重复以上操作即可。注意:本题使用优先队列会超时,必须使用队列等效率更高的实现方法。
时间复杂度
O ( n ) O(n) O(n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
E - Air Conditioners | GNU C++17 | Accepted | 233 ms | 12500 KB |
#include <bits/stdc++.h>
using namespace std;
int a[300005];
struct dt {
int x{}, t{}, d{}; //x表示位置,t表示温度,d表示传播方向
dt() = default;
dt(int xx, int tt, int dd) {
x = xx, t = tt, d = dd;
}
bool operator<(dt &o) const { //重载小于号,用于排序
return t < o.t;
}
} d[300005];
void solve() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) {
a[i] = 2e9; //初始化为无穷大
}
queue<dt> q; //运用队列提高效率
for (int i = 0; i < k; ++i) {
scanf("%d", &d[i].x);
}
for (int i = 0; i < k; ++i) {
scanf("%d", &d[i].t);
}
sort(d, d + k); //按温度排序
int cur = d[0].t; //当前正在处理的温度
int res = 1; //已经处理过的位置数
int point = 1; //已经加入的空调
a[d[0].x] = d[0].t; //先加入温度最低的空调
if (d[0].x > 1)q.push(dt(d[0].x - 1, d[0].t + 1, -1)); //尝试向左传播
if (d[0].x < n)q.push(dt(d[0].x + 1, d[0].t + 1, 1)); //尝试向右传播
while (res < n) {
while (point < k && d[point].t == cur) { //将等于当前正在处理温度的所有空调都加进来
if (d[point].t >= a[d[point].x]) { //这个位置已经被更新过了,直接跳过
++point;
continue;
}
a[d[point].x] = d[point].t;
++res; //更新成功,要记得计数已经更新过的位置数
if (d[point].x > 1)q.push(dt(d[point].x - 1, d[point].t + 1, -1));
if (d[point].x < n)q.push(dt(d[point].x + 1, d[point].t + 1, 1));
++point;
}
while (q.front().t == cur) {
auto p = q.front();
q.pop();
if (p.t >= a[p.x])continue; //不能降温,跳过
a[p.x] = p.t;
++res;
int to = p.x + p.d; //传播的目的地
if (to >= 1 && to <= n && p.t + 1 < a[to])q.push(dt(to, p.t + 1, p.d));
}
++cur;
}
for (int i = 1; i <= n; ++i) {
printf("%d ", a[i]);
}
printf("\n");
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
F. Array Stabilization (GCD version)
题意
给定一个正整数序列 a 0 , a 1 , … , a n − 1 a_0,a_1,\dots,a_{n-1} a0,a1,…,an−1,现定义操作:将序列中的每一项替换为该项与下一项的最大公因数,每一个替换是同时进行的,其中 a n − 1 a_{n-1} an−1的下一项是 a 0 a_0 a0。给出序列 a 0 , a 1 , … , a n − 1 a_0,a_1,\dots,a_{n-1} a0,a1,…,an−1,求经过多少次后,序列中的所有数都相等。
思路
首先考虑平衡状态:序列的最终平衡状态是所有数都等于整个序列的最大公因数。因此不妨先找出整个序列的最大公因数,每一项都除以该数,问题转化为多少次操作后,序列全为1。显然,如果此时所有数都是1,则操作为0次。除了这种情况外,操作次数一定在 [ 1 , n − 1 ] [1,n-1] [1,n−1]区间中。再次转化问题:如果一个长度为 l e n len len的区间的最大公因数不为1,那么至少需要 l e n len len次操作,才能使这段区间全部转化为1。令 f ( x ) = 0 / 1 f(x)=0/1 f(x)=0/1,表示序列中能否找到长度为 x x x的连续子序列( a n − 1 a_{n-1} an−1和 a 0 a_0 a0也认为是连续的),使得该段序列的最大公因数不为1。显然这是一个不严格的递增函数,因此可以使用二分解决。为了求出一个区间的最大公因数,需要写一个稀疏表来加速。
时间复杂度
O ( n ⋅ l o g 2 2 n ) O(n\cdot log_2^{\ 2}n) O(n⋅log2 2n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
F - Array Stabilization (GCD version) | GNU C++17 | Accepted | 764 ms | 16400 KB |
#include <bits/stdc++.h>
using namespace std;
int n;
int a[200005];
int st[200005][20]; //稀疏表(st表),记录区间gcd
int get(int x, int k) { //函数用于求从x开始长为k的区间的gcd
int p = 0;
while (1 << (p + 1) <= k)++p;
if (1 << p == k)return st[x][p];
else return __gcd(st[x][p], get((x + (1 << p)) % n, k - (1 << p)));
}
void solve() {
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
}
int g = a[0];
for (int i = 1; i < n; ++i) {
g = __gcd(g, a[i]); //C++版本比较低,不支持__gcd函数的请手写gcd函数
}
bool ok = true;
for (int i = 0; i < n; ++i) {
a[i] /= g;
if (a[i] > 1)ok = false;
}
if (ok) { //特判不需要操作的情况
printf("0\n");
return;
}
for (int i = 0; i < n; ++i) { //以下两个循环,预处理st表
st[i][0] = a[i];
}
for (int i = 1; 1 << i <= n; ++i) {
for (int j = 0; j < n; ++j) {
st[j][i] = __gcd(st[j][i - 1], st[(j + (1 << (i - 1))) % n][i - 1]);
}
}
int l = 1, r = n - 1; //二分左右界
int ans; //二分答案
while (l <= r) {
int mid = (l + r) >> 1; //二分中点
int res = 0;
for (int i = 0; i < n; ++i) {
if (get(i, mid) != 1) {
res = 1;
break;
}
}
if (!res)r = mid - 1;
else ans = mid, l = mid + 1;
}
printf("%d\n", ans);
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
G. How Many Paths?
题意
给出一个有向图,可能有自环,问从1号顶点到每个顶点的连通性。连通性有以下4种:
- 0:不连通
- 1:唯一连通路径
- 2:不唯一且有限的连通路径
- -1:无限种连通路径
思路
解题关键是找到环,只要经过了环,就意味着从这个点开始,路径有无数种。找环等价于找强连通分量,找到强连通分量后dfs即可。dfs时,遇到已经确定为无限路径的点,直接剪枝即可,唯一路径点有通向多条路径的点,同样可以剪枝,其余剪枝情况详见代码,这样剪枝可以保证,每一个点的状态最多经历从0到1到2到-1的更新过程,也就是最多搜索 3 n 3n 3n次即可完成。
时间复杂度
O ( n ) O(n) O(n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
G - How Many Paths? | GNU C++17 | Accepted | 701 ms | 51200 KB |
#include <bits/stdc++.h>
using namespace std;
vector<int> g1[400005], g2[400005]; //g1是正向建图,g2是反向建图
bool vis[400005]; //记录点有没有访问过
stack<int> s; //用于存储dfs序用于倒序遍历
bool f[400005]; //记录该点是否在环上
int ans[400005]; //用于记录该点和1号点的连通情况
void add(int x, int y) { //建边函数
g1[x].push_back(y);
g2[y].push_back(x);
}
void dfs1(int p) { //第一次搜索,在反图上搜索,并为后序遍历做准备,p是点的号码
if (vis[p])return;
vis[p] = true;
for (int i:g2[p]) {
dfs1(i);
}
s.push(p);
}
bool dfs2(int p, int x) { //第二次搜索,在原图上搜索,返回该点是否在环上,p是点的号码,x是环首
if (!vis[p])return false;
vis[p] = false;
bool res = false;
for (int i:g1[p]) { //后续能到达环首,或能到达在环上的点,则该点在环上
if (i == x)res = true;
else if (vis[i])res |= dfs2(i, x);
}
f[p] = res;
return res;
}
void dfs3(int p, int x) { //第三次搜索,在原图上搜索,处理每一个点的连通信息,p是点的号码,x是连通性
if (f[p])x = -1; //这个点在环上,那么一定有无数种路径
ans[p] = x;
for (int i:g1[p]) { //只有4种情况是要继续搜索的,其余情况一律剪枝
if (ans[i] == -1)continue; //下一个点在环上,直接不考虑,无穷大不管怎么加还是无穷大
if (ans[i] == 2 && x == -1)dfs3(i, -1); //当前点路径数无穷大,那么下一个点一定也是无穷大
if (ans[i] == 1 && (x == -1 || x == 2))dfs3(i, x); //当前点多种路径,下一个点也是多种路径
if (ans[i] == 1 && x == 1)dfs3(i, 2); //当前点唯一路径,下一个点路径增加一条
if (ans[i] == 0)dfs3(i, x); //下一个点无法到达,直接更新为当前点的连通性
}
}
void solve() {
int n, m;
scanf("%d%d", &n, &m);
while (m--) {
int x, y;
scanf("%d%d", &x, &y);
add(x, y); //建图
}
memset(vis, 0, sizeof vis);
for (int i = 1; i <= n; ++i) {
dfs1(i); //第一次搜索
}
memset(f, 0, sizeof f);
while (!s.empty()) {
dfs2(s.top(), s.top()); //第二次搜索
s.pop();
}
memset(ans, 0, sizeof ans);
dfs3(1, 1); //第三次搜索,1号点默认为1条路径
for (int i = 1; i <= n; ++i) {
printf("%d ", ans[i]);
}
printf("\n");
for (int i = 1; i <= n; ++i) { //用完记得把图清空
g1[i].clear();
g2[i].clear();
}
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}