JSCPC补题
JSCPC补题
赛时只切了签到,太丢脸,I本可以但是被误导了(太可惜),A被卡PY了,1:1转换的C++可过,太可惜,萌新第一次参加省赛,本就带着被搏杀的心态,不过也是感受到了XCPC比赛现场的氛围,不后悔(补题以H这道反悔贪心做结束)。
下面是补题(×),补个der,拼尽全力也想不出除ACI外的其他题,太多思维题了,被吓哭了,于是召唤gemini3PRO,协助补题
Easy:I、C
Easy-Mid:A、B、F、L
Mid:H、J、K
其余三题不属于我能看的,,,,
除ICA外其余题的题解均来自gemini3PRO,提供的代码均AC
I-模拟题
非常遗憾的一道签到题,赛时队友说是经典题,要动用神秘小算法,实际上,只要简简单单的模拟即可,太可惜!
word = []import stringwords = list(string.ascii_lowercase)for a in words: for b in words: word.append(a+b)sorted(word)for _ in range(int(input())): ok = False ans = '' s1 = input().strip() s2 = input().strip() for c in word: c1 = c[0] c2 = c[1] if (c1 in s1 and c2 in s1 and s1.find(c1) < s1.find(c2,s1.find(c1)+1)) and (c1 in s2 and c2 in s2 and s2.find(c1) < s2.find(c2,s2.find(c1)+1)): ok = True ans = c break if ok: print(ans) else: print('HENG!')C-位运算思维题
熟悉按位与、异或、或的运算先后顺序以及字符串为01串的性质
只要字符串有’0’存在,在每个字符都进行按位与运算即可
对于全是1的情况,由于按位与的运算优先级最高,故只需要在第一个和第二个字符之间插入按位异或,其余位置插入按位与即可
n = int(input().strip())s = input().strip()ans = ''if '0' in s: ans = '&'*(n-1)else: ans = '^'+'&'*(n-2)print(ans)A-二分答案+区间覆盖
非常可惜的一道题,赛时思路和正解一模一样,可惜被卡了PY,哭死
区间覆盖转化:
假设当前二分枚举到的时间是 。对于第 个舞者:
- 如果他还没有出生(即 ),那么他对当前时间没有贡献。
- 如果他已经出生(即 ),他已经存活了 秒。由于他每秒向左右各扩展 1 个单位,所以该舞者及其召唤出的分身能覆盖的连续坐标区间为:
将所有满足条件的舞者的覆盖区间收集起来,问题就转换为了:判断这些线段能否完全覆盖住区间 的所有整数点。
合并区间判定:
- 将收集到的所有区间按照左端点从小到大排序。
- 设定当前已经连续覆盖到的最右侧端点
cur_R = -1。 - 遍历排序后的区间:
- 如果下一个区间的左端点 (注意:因为坐标是整数,所以两个区间哪怕没有交点,只要相差 1 也能算作整数连续覆盖,例如 和 合起来能覆盖 )。
- 更新
cur_R。 - 如果 ,说明出现了无法弥补的空隙,直接跳出判断。
- 最后只需检查
cur_R是否大于等于 即可。
import sysdef solve(): input = lambda:sys.stdin.readline().strip() n,k = map(int,input().strip().split()) zombies = [] for _ in range(n): a,b = map(int,input().split()) zombies.append((a, b)) def check(T): t = [] for a, b in zombies: if T >= a: spread = T - a t.append((b - spread, b + spread)) if not t: return False t.sort() cur_R = -1 for L, R in t: if L <= cur_R + 1: if R > cur_R: cur_R = R else: break return cur_R >= k l,r = -1,10**9 while l + 1 < r: mid = (l + r)//2 if check(mid): r = mid else: l = mid print(r)if __name__ == '__main__': solve()#include <iostream>#include <vector>#include <algorithm>using namespace std;void solve() { long long n, k; if (!(cin >> n >> k)) return; vector<pair<long long, long long>> zombies(n); for (int i = 0; i < n; ++i) { cin >> zombies[i].first >> zombies[i].second; } auto check = [&](long long T) { vector<pair<long long, long long>> t; for (const auto& zombie : zombies) { long long a = zombie.first; long long b = zombie.second; if (T >= a) { long long spread = T - a; t.push_back({b - spread, b + spread}); } } if (t.empty()) { return false; } sort(t.begin(), t.end()); long long cur_R = -1; for (const auto& interval : t) { long long L = interval.first; long long R = interval.second;
if (L <= cur_R + 1) { if (R > cur_R) { cur_R = R; } } else { break; } } return cur_R >= k; }; long long l = -1; long long r = 1000000000000000000LL;
while (l + 1 < r) { long long mid = (l + r) / 2; if (check(mid)) { r = mid; } else { l = mid; } }
cout << r << "\n";}int main() { ios_base::sync_with_stdio(false); cin.tie(NULL); solve(); return 0;}F-数学思维题
取值范围的连续性:
假设能通过某组 和 得到 ,那么一定也能得到 。只需要找到原数组 中等于 的那个数,给它加上一个足够大的 的倍数(例如 ),让它变成一个极大的数即可。这样 就从数组中消失了,而比 小的数不受影响,新的 就变成了 。
结论:可能得到的 值一定是从 开始的**连续整数区间 **,因此答案就是 。
模数 的下界限制:
由于 ,根据取模的定义,余数一定小于模数,所以必须满足 。
令 ,则**模数 最少为 **。
**如何得到最大的 **
如果要凑出 ,意味着 都要在数组 中出现。
因为 ,对于任何 ,如果要让 ,由于 ,必定要求 。
这里分为两种情况:
- 情况一:
这意味着在 到 之间,有一个最小的数(即 )在 中完全没有出现过。因为无论选多大的 ,都无法凭空变出这个余数,所以永远也凑不出这个缺失的数。
在这种情况下,最大的 。
- 情况二:
这意味着 每一个数都在 中出现了至少一次。
此时为了让 尽可能大,只需取 的下界,即 。
对于 中的某个值 (),它在 中出现了 次。它能贡献出的 的值依次为:。
这 个坑位,最多只能撑到 。一旦试图突破,就会因为 对应的余数不够用而断档。断档的那个值就是 。
因为 取决于“木桶效应”里最短的那块板,所以最大能达到的 就是所有余数断档值的最小值:
n = int(input().strip())a = list(map(int,input().strip().split()))m = max(a)p = [False]*(n+2)for x in a: if x <= n + 1: p[x] = Truemex = 0while p[mex]: mex += 1if mex <= m: print(mex + 1)else: cnt = [0]*(m+1) for x in a: if x <= m: cnt[x] += 1 max_k = min(x + cnt[x]*(m+1) for x in range(m + 1)) print(max_k + 1)B-数学思维题
向平方根靠拢
题目要求寻找非负整数 满足 ,且极差 尽可能小。
为了让三者的差距最小,理想情况下它们应该尽可能接近。由于 ,那么 和 必然都在 的附近。
枚举策略
设定 。为了以最快速度找到最优解,我们**从 开始,逆向向下枚举 **。
对于每一个固定的 ,为了让 靠近 且 最小,基础的 应该是商:。
此时基础的 就是余数:。
内层枚举
有时候基础的 可能会比 和 小很多。此时可以“牺牲”一点 ,来“喂饱” 。
具体来说,如果将 减小 ,那么为了保持总和等于 , 就必须增加 。
因为 在 附近,这种补偿非常剧烈,我们只需要让 向下减小 到 次,就绝对能覆盖所有可能让极差缩小的组合了。超出这个微调范围, 就会变得太大,极差反而会飙升。
剪枝
这是最关键的一步,也是解决 TLE 的根本!
随着枚举的推进, 越来越小,导致 越来越大。
这意味着 和 的差距是严格单调递增的。
如果发现当前的 已经大于或等于我们手中目前的最优极差(加上内层循环微调的容错值 5),那就说明:再往下枚举,即便怎么微调,a 和 b 的撕裂程度都已经超过了历史最优解,不可能再产生更小的极差了!
此时直接 break 结束当前用例的寻找。
#include <iostream>#include <cmath>#include <algorithm>using namespace std;void solve() { long long x; cin >> x; if (x == 0) { cout << 0 << "\n"; return; } long long k = sqrt(x); long long ans = x;
for (long long a = k; a >= 1; --a) { long long b_base = x / a; if (b_base - a >= ans + 5) { break; }
long long rem = x % a; for (long long nx = 0; nx < 5; ++nx) { long long b = b_base - nx; if (b < 0) { break; } long long c = rem + nx * a;
long long mn = min({a, b, c}); long long mx = max({a, b, c}); long long sp = mx - mn;
if (sp < ans) { ans = sp; } } } cout << ans << "\n";}
int main() { ios_base::sync_with_stdio(false); cin.tie(NULL); int t; if (cin >> t) { while (t--) { solve(); } } return 0;}L-博弈思维题
状态转换:从原数组到差分数组
将数组 降序排序,并在末尾补上一个 (即 )。
观察相邻元素的差值,构造差分数组 :
比如数组 ,补 后为 。
对应的差分数组 。
操作的本质
如果在降序数组中,玩家选择了一个值 (必须满足 ,也就是差分 ),根据游戏规则:
- 所有 的元素全部变成 。
- 选中的 变成 。
- 其余 的元素保持不变。
对应到差分数组 上,这个操作等价于:
- (如果 )
- 全部被清零 (即 )
- 及以后的元素完全不变。
必胜态(W)与必败态(L)的判定
注意差分数组的一个致命特性:对 的操作,会瞬间将 之前的所有历史状态全部抹除(归零)!
通过严格的博弈论推导,我们发现只有奇数索引位置()的奇偶性才决定游戏胜负:
- 必胜态(Insight 赢):存在至少一个奇数索引 (如 1, 3, 5…),使得 为奇数。
- 必败态(Maya 赢):所有奇数索引 上的 全都是偶数。
为什么这个结论是对的?
- 如果当前是必败态(全是偶数):不管玩家选哪个合法的 (),
- 如果选奇数 , 减 变成奇数,把一个奇数留给了对手。
- 如果选偶数 ,规则会强制令 。因为 是偶数, 就是奇数,同样把一个奇数留给了对手。
- 无论怎么走,都会把必胜态拱手让给对方。
- 如果当前是必胜态(有奇数):玩家只需找到**最靠右的、值为奇数的奇数索引 **,然后选择它。
- 减 变成了偶数。
- 虽然变成 ,但 是偶数索引,不影响胜负判定。
- 最恐怖的是,所有 的差分值全部被清零(变成了偶数)。
- 这完美地将场上所有奇数索引的值变成了偶数,直接把必败态踢给对手!
结论:降序排序后,将相邻两项两两分组。如果存在一组的差值为奇数,则先手(Insight)必胜;否则后手(Maya)必胜。
def main(): for _ in range(int(input().strip())): n = int(input().strip()) a = list(map(int,input().strip().split())) a.sort(reverse=True) a.append(0) ok = False for i in range(0, n, 2): diff = a[i] - a[i + 1] if diff % 2 != 0: ok = True break if ok: print("Insight") else: print("Maya")if __name__ == '__main__': main()K-离线处理+线段树+贪心
固定的买入行为
买入阈值 是固定的。无论询问的卖出阈值 是多少,触发买入的时间点和总买入次数永远不变(即满足 的点)。
因此,我们能够在一开始就算出:总买入股票数量 Total_B 和 总买入成本 Total_Cost。
左到右贪心匹配机制
对于任何给定的 ,合法的卖出点就是那些 的点。
机器人的交易逻辑是“遇到能卖的,只要手里有股票就立刻卖”。这可以完全等效为一个括号匹配模型:
- 买入操作()视作左括号
( - 卖出操作()视作右括号
) - 机器人从左到右匹配,只要有未匹配的
(,遇到)就立刻匹配。 - 任何到最后都没有被匹配的
((即最后手中剩下的股票),都会在最后以平仓价 卖出。
收益公式的统一化
根据以上结论,总收益的计算公式可以简化为:
在这其中,买入数量、平仓价 、买入成本都是常数,我们唯一需要动态维护的,就是“成功匹配的卖出点所带来的金额总和”以及“成功匹配的数量”。
如果针对每个询问单独从头模拟,复杂度将是 ,必将超时。
由于多个询问之间只有参数 不同,我们采用离线处理策略:
- 将所有查询按照 从大到小降序排序。
- 随着 的减小,会有越来越多的历史价格 跨过 的门槛,成为“有效卖出点”。
- 我们每次只需单点激活这些新的有效卖出点即可。
线段树的设计(核心难点):
为了在 时间内更新一次,我们需要一颗区间线段树。线段树的每一个节点管理一个时间区间 ,并维护该区间内部:
cnt_B:内部未被匹配的买入次数(左括号)cnt_S:内部未被匹配的激活卖出次数(右括号)sum_S:内部未被匹配的激活卖出的金额之和matched_sum:内部已经成功匹配的卖出金额之和
合并两个子区间(Left 和 Right)时的物理意义:
- 左区间的剩余
(会遇到右区间的剩余)并发生匹配。 - 匹配数量 。
- 贪心策略下,左边过来的股票,一定会匹配到右区间最靠前的 个可用卖出点!
- 为了实现这一点,我们需要在右区间内进行递归搜索函数
get_sum_slice,询问:“右区间内前 个未匹配的卖出点,金额之和是多少?” - 借助这个函数,父节点可以精准快速地()合并左右区间。
#include <iostream>#include <vector>#include <algorithm>
using namespace std;
struct Node { int cnt_B; int cnt_S; long long sum_S; long long matched_sum;};
const int MAXN = 200005;Node tree[4 * MAXN];long long v[MAXN];long long a;
// 核心函数:查询节点 node 内部,前 k 个未被匹配的卖出点的金额总和long long get_sum_slice(int node, int L, int R, int st, int k) { if (k == 0) return 0; // 如果需要的数量正好等于该节点总的未匹配卖出数,直接 $O(1)$ 返回,极大加速 if (k == tree[node].cnt_S) return tree[node].sum_S;
int mid = L + (R - L) / 2; int left_child = 2 * node; int right_child = 2 * node + 1;
int C_L = tree[left_child].cnt_S; if (st + k <= C_L) { return get_sum_slice(left_child, L, mid, st, k); }
int M = min(tree[left_child].cnt_B, tree[right_child].cnt_S); if (st >= C_L) { int st_R = M + (st - C_L); return get_sum_slice(right_child, mid + 1, R, st_R, k); }
// 如果横跨左右子树的未匹配队列 int k1 = C_L - st; int k2 = k - k1; return get_sum_slice(left_child, L, mid, st, k1) + get_sum_slice(right_child, mid + 1, R, M, k2);}
void pull(int node, int L, int R) { int left_child = 2 * node; int right_child = 2 * node + 1; int mid = L + (R - L) / 2;
int M = min(tree[left_child].cnt_B, tree[right_child].cnt_S);
tree[node].cnt_B = tree[left_child].cnt_B - M + tree[right_child].cnt_B; tree[node].cnt_S = tree[left_child].cnt_S + tree[right_child].cnt_S - M;
// 获取右子区间被匹配掉的前 M 个卖出点金额 long long matched_R = get_sum_slice(right_child, mid + 1, R, 0, M);
tree[node].sum_S = tree[left_child].sum_S + tree[right_child].sum_S - matched_R; tree[node].matched_sum = tree[left_child].matched_sum + tree[right_child].matched_sum + matched_R;}
void build(int node, int L, int R) { if (L == R) { if (v[L] <= a) { tree[node].cnt_B = 1; tree[node].cnt_S = 0; tree[node].sum_S = 0; tree[node].matched_sum = 0; } else { tree[node].cnt_B = 0; tree[node].cnt_S = 0; tree[node].sum_S = 0; tree[node].matched_sum = 0; } return; } int mid = L + (R - L) / 2; build(2 * node, L, mid); build(2 * node + 1, mid + 1, R); pull(node, L, R);}
void update(int node, int L, int R, int idx) { if (L == R) { tree[node].cnt_S = 1; tree[node].sum_S = v[idx]; return; } int mid = L + (R - L) / 2; if (idx <= mid) update(2 * node, L, mid, idx); else update(2 * node + 1, mid + 1, R, idx); pull(node, L, R);}
void solve() { int n; cin >> n >> a;
long long Total_B = 0; long long Total_Cost = 0; vector<pair<long long, int>> sells;
for (int i = 1; i <= n; i++) { cin >> v[i]; if (v[i] <= a) { Total_B++; Total_Cost += v[i]; } else { sells.push_back({v[i], i}); } }
build(1, 1, n);
int m; cin >> m; vector<pair<long long, int>> queries(m); for (int i = 0; i < m; i++) { cin >> queries[i].first; queries[i].second = i; }
// 降序排序 sort(sells.rbegin(), sells.rend()); sort(queries.rbegin(), queries.rend());
vector<long long> ans(m); int p = 0;
for (int i = 0; i < m; i++) { long long b = queries[i].first; int q_idx = queries[i].second;
// 激活大于等于当前 b 阈值的卖出点 while (p < sells.size() && sells[p].first >= b) { update(1, 1, n, sells[p].second); p++; }
long long matched_count = Total_B - tree[1].cnt_B; long long profit = tree[1].matched_sum + (Total_B - matched_count) * v[n] - Total_Cost; ans[q_idx] = profit; }
for (int i = 0; i < m; i++) { cout << ans[i] << (i == m - 1 ? "" : " "); } cout << "\n";}
int main() { // 接触 I/O 封印,极大提升运行速度 ios_base::sync_with_stdio(false); cin.tie(NULL); int T; if (cin >> T) { while (T--) solve(); } return 0;}H-反悔贪心
统一价值度量:
一枚金币等于 块钱,也可以造成 点伤害。一枚银币等于 块钱,造成 点伤害。
因此,不论是金币还是银币,** 点伤害的成本永远是 块钱 **。我们要让最终剩余的钱最多,就等价于让打怪时浪费的伤害总和最小。
只有在使用金币对抗不足 点血量的目标时,才会产生“伤害溢出”的浪费。
贪心策略与反悔机制:
面对血量为 的怪物,令 ,。
- 对于 的部分:用 枚金币是完美无缺的(浪费为 )。如果没有足够的金币,只能强制用银币凑,这也是无法避免的。
- 对于余数 的部分:
我们总是优先假设使用 枚银币(因为这也不会造成任何浪费)。
但是,万一我们后来遇到银币不足的情况怎么办?
这时候,我们可以进行反悔(Regret):我们可以假装“之前某个怪物我不花 枚银币了,我改用 枚金币砸死它”,这样我们虽然浪费了 的伤害,但是成功拿回了 枚银币用于救急!
数据结构维护:
我们需要维护当前拥有的金币 、银币 ,以及一个记录所有可反悔操作的数据结构 H(存放着所有我们决定用银币垫付的 )。
H的容量不能超过当前 的数量。因为如果要反悔,你必须得交得出 枚金币。- 如果 不够用了(比如要支付 的时候金币不够),我们就从
H中删去最小的 (取消反悔权),以腾出金币去支付 。 - 如果 欠债了,我们就从
H中弹出最大的 执行反悔(消耗 枚金币,换取最多的银币补齐欠款)。
在 C++ 中,std::multiset 天然支持完美获取/删除最值,代码可以写得极为优雅紧凑。注意开 long long 防止 G 累加越界。
#include <iostream>#include <vector>#include <set>#include <algorithm>
using namespace std;
void solve() { long long n, k; cin >> n >> k;
long long G = 0, S = 0; multiset<long long> H; bool possible = true;
for (int i = 0; i < n; ++i) { long long o, x; cin >> o >> x;
if (!possible) continue; // 如果已经无法存活,只需读取剩下的输入,不再进行逻辑处理
if (o == 1) { G += x; } else if (o == 2) { S += x; } else if (o == 3) { long long q = x / k; long long r = x % k;
// 处理大块 q:必须用到实打实的金币 long long free_G = G - H.size(); long long use_free = min(q, free_G); G -= use_free; q -= use_free;
// 如果自由金币不够,只能从历史记录中强行取消一些反悔权(取消最小的,损失最少) while (q > 0 && !H.empty()) { H.erase(H.begin()); G -= 1; q -= 1; }
if (q > 0) { S -= q * k; }
// 处理余数 r:优先用银币支付 S -= r; if (r > 0) { if (G > (long long)H.size()) { H.insert(r); } else if (!H.empty() && r > *H.begin()) { H.erase(H.begin()); H.insert(r); } }
// 债务结算:如果银币超支了,行使反悔权(消耗1金币换回当初交出去的银币) while (S < 0 && !H.empty()) { auto it = prev(H.end()); long long r_max = *it; H.erase(it); G -= 1; S += r_max; }
if (S < 0) { possible = false; } } }
if (possible) { cout << G * k + S << "\n"; } else { cout << "-1\n"; }}
int main() { ios_base::sync_with_stdio(false); cin.tie(NULL);
int t; if (cin >> t) { while (t--) solve(); }
return 0;}