AtCoder Beginner Contest 413 ABCDEFG 题目解析
A - Content Too Large
题意
高桥有 \(N\) 个物品,每个物品的大小为 \(A_1, A_2, \dots, A_N\)。
他只有一个袋子,袋子能装得下的总大小为 \(M\)。
问他能否把所有物品全部装进袋子里?
思路
求 \(A_i\) 总和并判断是否不超过 \(M\) 即可。
代码
void solve()
{
int n, m, sum = 0;
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int x;
cin >> x;
sum += x;
}
if(sum <= m)
cout << "Yes";
else
cout << "No";
}
B - cat 2
题意
给定 \(N\) 个字符串 \(S_1, S_2, \dots, S_N\)。
你可以任意执行以下操作:
- 选择两个不同的整数 \(i, j\) \((1 \le i, j \le N)\),然后将 \(S_i\) 和 \(S_j\) 首尾拼接成为一个新字符串。
问以上操作总共能够得到多少种不同的字符串?
思路
模拟题。
如果采用字符数组枚举,对于字符串的拼接等操作可以自己手写,或者借助 strcpy / strcat / strcmp 等字符串操作函数。
或者直接借助 STL 的 string / set 完成拼接与去重。
代码一 STL
int n;
string s[105];
set<string> st;
void solve()
{
cin >> n;
for(int i = 1; i <= n; i++)
cin >> s[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(i != j)
st.insert(s[i] + s[j]);
cout << st.size();
}
代码二 字符数组函数
int n;
char s[105][12]; // 输入的每个字符串
char t[22]; // 暂时存储拼接出来的新字符串
char ans[10005][22]; // 拼接得到的所有互不相同的字符串
int cnt = 0; // 答案数量
// 将 s[i] 和 s[j] 拼接,放在 t 数组内
void merge(int i, int j)
{
strcpy(t, s[i]); // 把 s[i] 复制到 t 内
strcat(t, s[j]); // 把 s[j] 拼接到 t 后面
}
// 检查 t 数组内字符串是否与 ans 内已有的字符串相同
// 如果均不相同,则将其加入 ans 内
void check()
{
for(int i = 1; i <= cnt; i++)
if(strcmp(t, ans[i]) == 0) // 完全相同,说明已经出现过这种字符串
return;
// 如果均不相同,则将 t 加入到 ans 后面
cnt++;
strcpy(ans[cnt], t);
}
void solve()
{
cin >> n;
for(int i = 1; i <= n; i++)
cin >> s[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++) // 枚举要拼接的两字符串下标
if(i != j)
{
merge(i, j);
check();
}
cout << cnt;
}
代码三 手写
int n;
char s[105][12]; // 输入的每个字符串
char t[22]; // 暂时存储拼接出来的新字符串
char ans[10005][22]; // 拼接得到的所有互不相同的字符串
int cnt = 0; // 答案数量
// 将 s[i] 和 s[j] 拼接,放在 t 数组内
void merge(int i, int j)
{
int leni = strlen(s[i]);
int lenj = strlen(s[j]);
for(int k = 0; k < leni; k++)
t[k] = s[i][k];
for(int k = 0; k < lenj; k++)
t[k + leni] = s[j][k];
t[leni + lenj] = '\0'; // 手动放一个终止符
}
// 检查 t 数组内字符串是否与 ans 内已有的字符串相同
// 如果均不相同,则将其加入 ans 内
void check()
{
int lent = strlen(t);
for(int i = 1; i <= cnt; i++)
{
int leni = strlen(ans[i]);
if(lent != leni) // 长度不同直接跳过
continue;
bool flag = true; // 是否完全相同
for(int j = 0; j < lent; j++)
if(t[j] != ans[i][j])
{
flag = false;
break;
}
if(flag == true) // 完全相同,说明已经出现过这种字符串
return;
}
// 如果均不相同,则将 t 加入到 ans 后面
cnt++;
for(int j = 0; j <= lent; j++)
ans[cnt][j] = t[j];
}
void solve()
{
cin >> n;
for(int i = 1; i <= n; i++)
cin >> s[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++) // 枚举要拼接的两字符串下标
if(i != j)
{
merge(i, j);
check();
}
cout << cnt;
}
C - Large Queue
题意
给定一个空序列 \(A\)。
有 \(Q\) 次操作,请按顺序处理,操作有两种类型:
1 c x:在 \(A\) 序列的末尾添加 \(c\) 个 \(x\)。2 k:输出 \(A\) 序列当前的前 \(k\) 个数字之和,然后删除前 \(k\) 个数。
思路
因为每次加数字加在最后面,删数字又是删除最前面,考虑用队列进行维护。
因为数字个数特别多,因此这里我们不能用队列存储每一个数字。但又因为每一次的添加操作加进来的数字全都是相同的,所以我们可以直接存储一整组的数字,也就是把单次添加操作中的 (值,连续出现次数)这个二元组存储在队列中。
当要删除数字时,不断取队列最前面的那一组数字,判断队列内这组数字的剩余数量与我们想要删除的数字数量之间的大小关系,不断地做下去就可以。
- 如果这组数字的剩余数量不大于我们想要删除的数字数量,那就把整组数字的总和全部加到这一次的答案中,这组数字出队,继续看下一组。
- 如果这组数字的剩余数量大于我们想要删除的数字数量,那就只取需要删除的这部分数量并将总和加入答案中,修改这组数字在队列中的剩余数量即可。
时间复杂度 \(O(Q)\)。
代码
struct node
{
int val; // 这组数字的值
int cnt; // 这组数字的个数
};
node q[200005]; // 队列
int f = 1, e = 0;
void solve()
{
int Q;
cin >> Q;
while(Q--)
{
int op;
cin >> op;
if(op == 1)
{
node nd;
cin >> nd.cnt >> nd.val;
q[++e] = nd;
}
else
{
int k;
cin >> k;
long long sum = 0; // 总和
while(k > 0) // 只要还有数字要拿
{
if(q[f].cnt <= k) // 最前面这组的剩余数量不大于 k
{
sum += 1LL * q[f].cnt * q[f].val;
k -= q[f].cnt;
f++; // 出队
}
else
{
sum += 1LL * k * q[f].val;
q[f].cnt -= k; // 这组剩余数字数量 -k
k = 0;
}
}
cout << sum << "\n";
}
}
}
D - Make Geometric Sequence
题意
给定一个长度为 \(N\) 且不包含 \(0\) 的整数序列 \(A = (A_1, A_2, \dots, A_N)\),问是否存在一个 \(A\) 的排列,使其为一个等比数列?
思路
考虑公比 \(q\),因为题目保证 \(A_i \ne 0\),因此可以排除 \(q = 0\) 的情况。
当 \(q \gt 0\) 时,所有整数应当是同号的(全负或全正),此时可以直接按数值对整个数组排序,判断排序后的数组是否已经是等比数列即可。
当 \(q \lt 0\) 时,分两种情况讨论:
- \(q = -1\):如果能构成等比数列,此时数组内所有元素的绝对值应当是相同的,但正负数的个数会因为数组长度的奇偶性而出现不一致。当长度为偶数时,正负数个数一定相等;当长度为奇数时,开头的元素与结尾元素是同号的,因此某个符号的数字个数会多 \(1\)。此时只需要判断数组内正负数元素个数的差值是否在 \(1\) 以内即可。
- \(q \ne -1\):此时等比数列每一项绝对值都不同,且相邻两项符号不同。直接按数值绝对值对整个数组排序,判断排序后的数组是否已经是等比数列即可。
最后注意判断等比数列的写法,最好将除法改为乘法计算。
单组数据时间复杂度 \(O(N\log N)\)。
代码
typedef long long ll;
int n;
ll a[200005];
bool cmp(ll &x, ll &y)
{
return abs(x) < abs(y);
}
void solve()
{
cin >> n;
for(int i = 1; i <= n; i++)
cin >> a[i];
bool f = true; // 判断是否所有数字绝对值均相同
for(int i = 2; i <= n; i++)
if(abs(a[i]) != abs(a[1]))
{
f = false;
break;
}
if(f) // 公比只能是 -1 或 1
{
int x = 0, y = 0; // 统计正负数个数
for(int i = 1; i <= n; i++)
if(a[i] < 0)
x++;
else
y++;
if(abs(x - y) <= 1 || x == 0 || y == 0) // 如果正负数个数差值不超过 1,或者所有元素同号且值相同
cout << "Yes\n";
else
cout << "No\n";
return;
}
sort(a + 1, a + n + 1);
if((a[1] > 0) != (a[n] > 0)) // 最小值与最大值不同号,说明公比只能是负数
sort(a + 1, a + n + 1, cmp);
for(int i = 2; i < n; i++)
{
// a[i-1] / a[i] == a[i] / a[i+1]
if(a[i] * a[i] != a[i-1] * a[i+1])
{
cout << "No\n";
return;
}
}
cout << "Yes\n";
}
signed main()
{
int T;
cin >> T;
while(T--)
solve();
return 0;
}
E - Reverse 2^i
题意
给定一个 \((1, 2, \dots, 2^N)\) 的排列 \(P\)。
你可以进行任意次以下操作:
- 选择一个 \(0 \le b \le N\),将排列 \(P\) 从前往后每 \(2^b\) 个数分为一个块,然后任意选择某个块,将其内部元素进行首尾翻转。
请找出操作后能得到的所有序列方案中,字典序最小的序列。
思路
首先注意到,除非我们选择 \(b = N\) 然后翻转整个序列,否则此时在序列前一半的所有数字都不可能通过其它操作到达序列的后一半,反之亦然。
因此对于 \(b = N\) 的操作,明显要么不做,要么只做一次即可,超过一次没有意义。
在考虑完 \(b = N\) 的操作是否需要进行之后,我们接着看序列的前一半或者后一半子序列,发现对于它们来说,\(b = N-1\) 这个操作也是一样,要么不做,要么只做一次。接下来继续递归看当前序列的前一半和后一半,发现每次要讨论的问题都是相同的,因此本题可以考虑借助递归解决。
然后我们考虑对于某段序列,什么时候才需要做一次翻转操作。明显只有当整个序列的最小值出现在后半段而不是前半段时,我们才需要通过翻转来将最小值移动到序列的前半段,然后如果我们接着递归看前半段,继续进行如上讨论,一定能够将当前这个最小值移动到序列的最前面,使得字典序最小。
所以整题我们只需要从整个序列开始,按子序列长度从大到小,递归分类讨论即可。
单组数据时间复杂度 \(O(N\log N)\)。
代码
int n;
int a[270000];
void dfs(int l, int r)
{
if(l == r)
return;
int mid = (l + r) / 2;
int lmin = *min_element(a + l, a + mid + 1);
int rmin = *min_element(a + mid + 1, a + r + 1);
if(rmin < lmin) // 最小值出现在右半段
reverse(a + l, a + r + 1); // 翻转 [l, r] 整段
dfs(l, mid);
dfs(mid + 1, r);
}
void solve()
{
cin >> n;
for(int i = 1; i <= (1 << n); i++)
cin >> a[i];
dfs(1, 1 << n);
for(int i = 1; i <= (1 << n); i++)
cout << a[i] << " ";
cout << "\n";
}
signed main()
{
int T;
cin >> T;
while(T--)
solve();
return 0;
}
F - No Passage
题意
给定一个 \(H \times W\) 的网格图,其中有 \(K\) 个点是终点。
高桥和青木在网格图上玩游戏,棋盘上一开始有一个棋子。
他们会依次重复进行以下操作,直到棋子被移动到终点:
- 青木选择上下左右某个方向,禁用这个方向。
- 高桥选择上下左右某个方向移动棋子,但不能和青木所选的方向相同。注意如果当这一次移动操作会导致棋子被移出棋盘,则当作无事发生。
高桥的目标是让棋子移动到任意一个终点上,并且移动次数越少越好。而青木的目标是阻止棋子被移动到终点,如果实在无法阻止,则会让棋子的移动次数越多越好。
已知它们两人每次都会以各自的最优策略操作。
问对于每一个可能的棋子的起始位置 \((i, j) (1 \le i \le N, 1 \le j \le M)\),以下问题的总和是多少:
- 从 \((i, j)\) 出发,到棋子被移动到终点的总移动次数。如果无法移动到终点,则记移动次数为 \(0\)。
思路
高桥的目的是最小化移动次数,也就是尽可能地去找最短路。记 dis[i][j] 表示从格子 \((i, j)\) 出发,在目前题意限制下到达任意一个终点所需要移动的次数。
首先很明显,原本就是终点的那些位置的 dis 为 \(0\)。
如果我们不考虑青木的操作,所有方向随便走,那么要求的就是整张图每个点到达任意一个起点的最短路长度,代码上可以将现在的终点当作搜索的起点,向外直接借助 SPFA 等最短路算法即可实现。
考虑青木的操作。对于当前棋子所在位置周围的四个格子,由于青木每次只能禁用一个方向,所以他肯定会禁用 dis 最小的相邻点所在方向,因此高桥每次只能挑周围四个格子中 dis 次小值对应的格子作为我们下一步要移动的点。
综上,在求解整张图最短路的时候,我们每次选择周围四个方向的次小值所对应位置来作为我们要转移最短路的位置即可。
时间复杂度 \(O(H \times W)\)。
代码
const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = {0, 0, -1, 1};
int n, m, k;
int dis[3005][3005];
bool vis[3005][3005];
// 求 (x, y) 周围四个方向的 dis 次小值
int cal(int x, int y)
{
vector<int> G = {dis[x][y-1], dis[x][y+1], dis[x-1][y], dis[x+1][y]};
sort(G.begin(), G.end());
return G[1];
}
void solve()
{
cin >> n >> m >> k;
queue<pii> q;
memset(dis, 0x3f, sizeof dis);
for(int i = 1; i <= k; i++)
{
int x, y;
cin >> x >> y;
dis[x][y] = 0;
q.push(pii(x, y));
}
while(!q.empty())
{
int x = q.front().first, y = q.front().second;
q.pop();
for(int i = 0; i < 4; i++)
{
int px = x + dx[i], py = y + dy[i];
if(px < 1 || px > n || py < 1 || py > m)
continue;
int d = cal(px, py);
if(dis[px][py] > d + 1) // 存在更优秀的次短走法
{
dis[px][py] = d + 1;
q.push(pii(px, py));
}
}
}
long long sum = 0;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
if(dis[i][j] != 0x3f3f3f3f)
sum += dis[i][j];
cout << sum << "\n";
}
G - Big Banned Grid
题意
给定一个 \(H \times W\) 的网格图,其中有 \(K\) 个点是障碍物。
高桥会从 \((1, 1)\) 出发,每次走上下左右四个方向,目标是到达 \((H, W)\),但过程中不能经过障碍物。
问他能否实现目标?
数据:起点和终点保证不存在障碍物,且 \(1 \le H, W \le 2 \times 10^5\)。
思路
从连通块的角度思考,就是在判断是否 \((1, 1)\) 和 \((H, W)\) 两网格点是否还处于同一个连通块内。
换言之,就是判断给定的这 \(K\) 个障碍物是否把起点和终点分割为了两个连通块。
如果能将其分为两个连通块,那么这些障碍物一定是从整张图 左/下 边缘一直到整张图 右/上 边缘呈现八连通的情况的。(对于障碍物而言,在 \(2\times 2\) 网格内的斜对角线上放置两个障碍物,就会影响到四连通的走法,所以障碍物可以是八连通的。)
因此我们只需要从地图的 左/下 边缘任意找一个障碍物开始搜索,检查是否能够搜到 右/上 边缘即可。
至于存图方面,虽然整张图的 行/列 总数很大,但障碍物的数量也是 \(2 \times 10^5\) 级别的,所以可以借助 set/map 等 STL 辅助存储每个位置是否有障碍物。
时间复杂度 \(O(K\log K)\)。
代码
const int dx[8] = {-1, 1, 0, 0, -1, -1, 1, 1};
const int dy[8] = {0, 0, -1, 1, -1, 1, -1, 1};
int n, m, k;
set<pii> st;
vector<int> R; // 记录第一列上有哪些行存在障碍物
vector<int> C; // 记录最后一行上有哪些列存在障碍物
bool flag = true; // 左上与右下是否还在同一个连通块
void dfs(int x, int y)
{
if(flag == false) // 已经能够判断不连通
return;
if(st.find(pii(x, y)) == st.end()) // 当前点已经访问过,或者当前点不存在障碍物
return;
st.erase(pii(x, y)); // 清除障碍物
if(x == 1 || y == m) // 到达 上边缘/右边缘
{
flag = false;
return;
}
for(int i = 0; i < 8; i++)
{
int px = x + dx[i], py = y + dy[i];
if(px < 1 || px > n || py < 1 || py > m)
continue;
dfs(px, py);
}
}
void solve()
{
cin >> n >> m >> k;
for(int i = 1; i <= k; i++)
{
int x, y;
cin >> x >> y;
st.insert(pii(x, y));
if(x == n)
C.push_back(y);
if(y == 1)
R.push_back(x);
}
for(int &x : R)
dfs(x, 1); // 从左边缘开始
for(int &y : C)
dfs(n, y); // 从下边缘开始
if(flag)
cout << "Yes";
else
cout << "No";
}

浙公网安备 33010602011771号