A. In Search of Convenience
题意
路由器位于整数坐标点 ,信号覆盖半径为整数 。找出任意一个整数坐标点,使它与路由器的距离恰好等于 。
思路
沿水平方向移动 ,直接选择 。
这个点的横坐标差为 ,纵坐标差为 ,所以距离为:
由于 、、 都是整数,答案的坐标也都是整数,满足要求。
时间复杂度为 ,额外空间复杂度为 。
代码
void Solve() {
int x0, y0, R;
std::cin >> x0 >> y0 >> R;
std::cout << x0 + R << ' ' << y0 << '\n';
}B. Did Not Go to Print
题意
有 份文档,第 条指令对应编号为 的文档。打印机的内存采用后进先出的方式存放文档,指令有三种:
1:把文档 放到内存顶部。2:内存非空时,打印并移除顶部文档;内存为空时,打印文档 。3:直接打印文档 ,不改变内存中的文档。
找出所有没有被打印的文档编号,并按升序输出。
思路
“放到顶部、取出顶部”正好对应栈,因此用 std::stack<int> 模拟打印机内存。
另外,用布尔数组 v 记录每份文档是否被打印:
- 遇到
1,将当前编号 入栈。 - 遇到
2且栈非空,将栈顶编号标记为已打印,然后弹出栈顶。 - 其余情况,即
2且栈为空,或指令为3,将当前编号 标记为已打印。
所有指令处理完后,从 到 扫描 v,把未打印的编号放进答案数组。扫描顺序已经是升序,无需额外排序。
例如,指令为 12:先将文档 入栈,再打印栈顶文档 ,因此文档 没有被打印。
每条指令只进行常数次栈操作,时间复杂度为 ,空间复杂度为 。
代码
void Solve() {
int n; std::string s; std::cin >> n >> s;
std::stack<int> st;
std::vector<bool> v(n + 1, false);
for (int i = 1; i <= n; ++i) {
if (s[i - 1] == '1') {
st.push(i);
} else if (s[i - 1] == '2' && !st.empty()) {
v[st.top()] = true;
st.pop();
} else {
v[i] = true;
}
}
std::vector<int> ans;
for (int i = 1; i <= n; ++i) {
if (!v[i]) {
ans.push_back(i);
}
}
std::cout << ans.size() << '\n';
for (int x : ans) {
std::cout << x << ' ';
}
std::cout << '\n';
}C. Unrequited Love
题意
有 个琴键,第 个琴键的数值为 。起点为 的三和弦使用琴键 、、,产生的数值为:
统计有多少对不同的三和弦,它们的数值相同,并且没有共用任何琴键。每对只计算一次,不区分选择顺序。
思路
首先确定哪些三和弦会共用琴键。设两个起点为 ,它们使用的琴键集合分别是:
这两个集合相交,当且仅当 为 或 :
- 相差 ,共用两个琴键。
- 相差 ,共用一个琴键。
- 相差 或 ,两组琴键的奇偶性不同,没有交集。
- 相差至少 ,两组琴键的范围没有交集。
因此,问题转化为:统计数值相等、起点差不为 或 的数对。
从左到右处理三和弦,记录前面各个数值出现的次数。处理当前起点 时:
cnt[b[i]]是前面所有与当前三和弦等值的三和弦数量,先加入答案。- 将当前数值的出现次数加一。
- 如果与起点 的三和弦等值,减去这一对。
- 如果与起点 的三和弦等值,也减去这一对。
每一对都只在处理较大起点时统计,所以不会重复。
由于 ,三和弦数值的范围为 。数组下标不能为负,因此统一加上 30000,把范围变成 。统一平移不影响两个数是否相等。
每组时间复杂度为 ,空间复杂度为 ;计数数组只在程序中初始化一次。
代码
void Solve() {
int n; std::cin >> n;
std::vector<int> a(n);
for (auto& i : a) {
std::cin >> i;
}
std::vector<int> b(n - 4);
static int cnt[60001];
int ans = 0;
for (int i = 0; i < n - 4; i++) {
b[i] = a[i] + a[i + 2] - a[i + 4] + 30000;
ans += cnt[b[i]]++;
if (i >= 2 && b[i] == b[i - 2]) {
ans--;
}
if (i >= 4 && b[i] == b[i - 4]) {
ans--;
}
}
for (const auto& i : b) {
cnt[i] = 0;
}
std::cout << ans << '\n';
}D. Precision Alignment
题意
有 个实验室,每个实验室有三个读数 ,它们的顺序固定。一次操作可以在一个实验室内执行以下三种调整之一:
其中,正数的符号为 ,负数的符号为 ,零的符号为 。每次操作都使用当时的读数。
所有实验室合计最多进行 次操作,希望最大化调整后各实验室三数之和的最小值。
思路
先解决一个实验室的问题:设初始总和为 ,将总和提高到至少 ,最少需要多少次操作。
如果 ,无需操作。下面只考虑 。
情况一:三个数全相等。
如果 ,三种操作的增量都是 ,无法改变任何读数,也就无法提高总和。
情况二:存在逆序。
如果 ,可以固定 ,不断执行 ;如果 ,可以固定 ,不断执行 。
只要不满足 ,上述两种逆序至少存在一种。所以每次操作都可以使总和增加 ,提高到 的最少操作数就是 。
情况三:,但不全相等。
此时还没有能使总和增加的操作,必须先制造逆序。可以选择:
- 连续减小 ,直到 ,需要 次。
- 当 时,连续减小 ,直到 ,需要 次。
由于不全相等,,所以第一种方式总能执行。如果 ,第一种方式只需一次,也是最优;否则两种方式都能执行。因此最少需要先减少:
次,才能制造逆序。
这个次数也是下界:首次出现逆序前,没有增加操作;要让 或 ,至少得把对应的初始间隔缩小到负数,而一次操作至多将这个间隔缩小 。
做完 次减少操作后,总和从 变为 。之后利用逆序,每次增加 ,还需要 次。因此最少操作数为:
例如 的初始总和为 。先将 减到 ,得到 ,再让 增加两次,就能让总和达到 ,总共需要 次操作。
接下来二分最终最小和 。要让所有实验室的总和都不低于 ,各实验室所需的最少操作数可以独立计算;这些费用之和不超过 ,目标就可行。
目标越大,需要的操作越多,因此可行性具有单调性。设初始最小和为 :
- 不做操作就能达到 ,它是可行下界。
- 一次操作至多使某个实验室的总和增加 ,所以答案不会超过 。
二分区间为 。判定时逐项从剩余预算 rst 中扣除费用,费用超出预算就立即返回,避免把所有费用累加后溢出。
时间复杂度为 ,最多约 次判定;空间复杂度为 。
代码
void Solve() {
int n, k; std::cin >> n >> k;
std::vector<std::pair<int, int>> v(n);
int l = LLONG_MAX;
for (auto &[s, w] : v) {
int a, b, c; std::cin >> a >> b >> c;
s = a + b + c; w = 0;
if (a == b && b == c) {
w = -1;
} else if (a <= b && b <= c) {
w = 2 * (std::min(b - a, c - b) + 1);
}
l = std::min(l, s);
}
auto check = [&](int x) {
int rst = k;
for (auto [s, w] : v) {
if (x <= s) {
continue;
}
if (w == -1) {
return false;
}
int nd = x - s + w;
if (nd > rst) {
return false;
}
rst -= nd;
}
return true;
};
int r = l + k;
while (l < r) {
int mid = l + (r - l + 1) / 2;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
std::cout << l << '\n';
}E. Repentance Is Already on the Way
题意
两条平行道路上各有 座建筑。记第一行的第 座建筑为 ,第二行的第 座建筑为 ,所属公司的编号分别为 。
可用道路只有竖边 ,以及相邻列之间的两条斜边 、。同公司建筑之间的道路长度为 ,不同公司之间为 。
从 出发,恰好访问每座建筑一次,可以在任意建筑结束,求路线的最大总长度。
解题思路
虽然题目要求寻找一条经过全部建筑的路径,但这张图的结构使候选路线只有 条:每个终点 对应唯一一条路线。
首先,所有边都连接两行之间的建筑,所以沿路径访问的建筑一定在 行和 行之间交替。总共访问 座建筑,从 开始,终点必然在 行。
固定终点为 。考虑第 列与第 列之间的切口,只有两条道路能跨过这个切口:
- 当 时,起点和终点位于切口两侧,路径跨过切口的次数必须为奇数。因此只能使用一条跨边。左侧的 座建筑会先被连续访问,从 开始交替访问偶数座建筑后,离开左侧时必然位于 行,所以使用的是 。
- 当 时,起点和终点都在切口左侧,但还必须访问右侧建筑,跨过切口的次数必须为正偶数。因此两条跨边都要使用。
结合每个中间建筑必须连接两条路径边的要求,可以确定这条路线的全部边:
- 所有 ,其中 。
- 前 列的竖边 。
- 最后一列的竖边 。
- 从第 个切口开始的另一条斜边 ,其中 。
对应的走法是:先按 访问前缀;随后沿斜边向右,到最后一列通过竖边转向,再沿另一侧斜边向左,最终到达 。
例如 时,路线为:
设这条路线的长度为 。先计算 的情况:使用所有相邻列之间的两条斜边,加上最后一列的竖边。
这里 表示条件 成立时为 ,否则为 。
当终点从 改为 时,边集合只发生一个替换:删除斜边 ,加入竖边 。因此:
存 ,然后按这个差值递推所有 ,维护最大值即可。 时没有斜边,答案就是唯一竖边的长度。
时间复杂度为 ,空间复杂度为 。
代码
void Solve() {
int n; std::cin >> n;
std::vector<int> a(n), b(n);
for (auto &i : a) {
std::cin >> i;
}
for (auto &i : b) {
std::cin >> i;
}
int cur = 1 + (a[n - 1] == b[n - 1]);
for (int i = 0; i + 1 < n; i++) {
cur += 2 + (a[i] == b[i + 1]) + (b[i] == a[i + 1]);
}
int ans = cur;
for (int i = 0; i + 1 < n; i++) {
cur += (a[i] == b[i]) - (a[i] == b[i + 1]);
ans = std::max(ans, cur);
}
std::cout << ans << '\n';
}F. Tea Blend
题意
有 包茶,每包的强度是正整数 。选择任意一对下标 ,先取前 包茶,再额外取第 包茶,混合后的强度为:
统计有多少对 ,使 的正约数个数为奇数。 和 独立选择,都在 内,没有 的限制。
思路
第一步:将约数问题转化为完全平方数问题。
正整数的约数通常可以配成 。只有当 是完全平方数时, 会与自己配对。因此,约数个数为奇数,当且仅当这个数是完全平方数。
我们需要统计使 为完全平方数的数对。
第二步:只保留质因子指数的奇偶性。
若一个正整数的质因数分解为:
定义它的平方自由核为:
也就是去掉所有平方因子,只保留指数为奇数的质因子。例如:
两个数相乘成为完全平方数,当且仅当它们每个质因子的指数奇偶性都相同,即平方自由核相同。所以:
先把所有输入数字替换为各自的平方自由核,统计每个核的出现次数 cnt。由于 可以选择任意位置,这个计数必须包含整个数组,而不仅是当前前缀。
接下来,对于每个前缀,只要求出它的平方自由核 val,就能增加 cnt[val] 个合法数对。
第三步:维护前缀中指数为奇数的质因子。
前缀乘积可能非常大,但无需真的计算这个乘积,只需维护哪些质因子的指数为奇数。
代码中的 v 保存这些质数,pos[p] 记录质数 在 v 中的位置加一,0 表示它不在集合中。
处理一个平方自由核时,它的每个质因子只出现一次,因此对每个质因子 切换状态:
- 原本不在
v中,就加入,表示指数从偶数变为奇数。 - 原本在
v中,就删除,表示指数从奇数变为偶数。
删除时,把 v 的最后一个元素放到待删除位置,更新该元素的 pos,再弹出最后一个元素并清空 pos[p]。这样加入和删除都只需要 时间,v 也不需要保持有序。
第四步:利用 ,限制每次查询的计算量。
每个输入数字的平方自由核都不超过原数,所以一定不超过 。
前缀的平方自由核是 v 中所有质数的乘积。计算时,一旦部分乘积超过 ,完整乘积就更不可能匹配任何输入数字的核,可以停止本次乘法。
这个循环最多检查 个质数,因为最小的八个不同质数之积已经超过 :
在每次乘法前,val 不超过 ,质数也不超过 ,因此乘法的中间结果至多为 。
如果 v 为空,前缀的平方自由核就是 。如果计算结果不超过 ,就累加 cnt[val];否则该前缀没有贡献。超过 时只停止本次查询,质因子状态仍然完整保留,因为后续前缀可能再次消去一些质因子,使核重新变小。
例如数组为 时,各元素的核为 ,这四种核的频次都是 :
| 前缀位置 | 前缀乘积的平方自由核 | 贡献 cnt[val] |
|---|---|---|
| 1 | 2 | 1 |
| 2 | 6 | 1 |
| 3 | 1 | 1 |
| 4 | 1 | 1 |
总答案为 。
存 的最小质因子,筛表只建立一次,之后用它进行分解。每组结束时,清空最终仍在 v 中的质数位置,以及本组出现过的核的计数;已从 v 删除的质数位置在删除时已经清零。
筛表的时间复杂度为 ,每组处理的时间复杂度为 。所有测试合计的时间复杂度为 ,空间复杂度为 。
代码
void Solve() {
const int M = 1000000;
static int spf[M + 1], cnt[M + 1], pos[M + 1];
if (!spf[2]) {
for (int i = 2; i <= M; ++i) if (!spf[i]) {
spf[i] = i;
if (i * i <= M) {
for (int j = i * i; j <= M; j += i) {
if (!spf[j]) {
spf[j] = i;
}
}
}
}
}
int n; std::cin >> n;
std::vector<int> a(n), v;
for (auto &i : a) {
std::cin >> i;
int y = i; i = 1;
while (y > 1) {
int p = spf[y], odd = 0;
while (y % p == 0) {
y /= p;
odd ^= 1;
}
if (odd) {
i *= p;
}
}
cnt[i]++;
}
int ans = 0;
for (auto i : a) {
while (i > 1) {
int p = spf[i];
i /= p;
if (pos[p]) {
int j = pos[p] - 1, q = v.back();
v[j] = q;
pos[q] = j + 1;
v.pop_back();
pos[p] = 0;
} else {
pos[p] = v.size() + 1;
v.push_back(p);
}
}
int val = 1;
for (auto i : v) {
val *= i;
if (val > M) {
break;
}
}
if (val <= M) {
ans += cnt[val];
}
}
for (auto i : v) {
pos[i] = 0;
}
for (auto i : a) {
cnt[i] = 0;
}
std::cout << ans << '\n';
}
评论
在 GitHub Discussions 中交流。分享你的想法,或补充一个细节。