2026.10 - 做题记录与方法总结
噫吁嚱,危乎高哉!大学之难,难于听讲座!
2026.10.21
CCPC 2026 长春站
K.纸带染色
剃头 + 扫描线
显然需要注意到的性质为,每次染色操作的时候,所染的色一定会置顶。
那么染色只会在开始染色后的操作中产生贡献,然后到完全覆盖后消除贡献。
很自然的考虑,预处理颜色的贡献在操作时间序列的存活区间。
当一个颜色的贡献被消除,说明这个颜色被完全覆盖,存在两种情况:
- 被同一侧的颜色gank,可以用单调栈处理
- 被两边的颜色一起gank,

显然两边的颜色会一起把下面的颜色覆盖,
考虑到进入顺序,我们还要实现栈的底端弹出,也就是说我们需要实现双端队列。
当我们处理好存活区间,那么问题就变成求某个区间内 \(r\) 所属,且左端点在 \(l\) 右侧的全部线段数。
显然考虑扫描线,套上一个简单 单点修改+区间查询 的线段树维护。
#include<bits/stdc++.h>
using namespace std;
int rd() {
int x = 0, w = 1;
char ch = 0;
while (ch < '0' || ch > '9') {
if (ch == '-') w = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + (ch - '0');
ch = getchar();
}
return x * w;
}
void wt(int x) {
static int sta[35];
int f = 1;
if(x < 0) f = -1,x *= f;
int top = 0;
do {
sta[top++] = x % 10, x /= 10;
} while (x);
if(f == -1) putchar('-');
while (top) putchar(sta[--top] + 48);
}
const int N = 5e5+5;
int n,m,q;
array<int,2> o[N];
vector<array<int,2> > sc[N];
int ans[N];
struct que{
int que[N],hd,tl,st[N];
int& operator[](const int pos) {return que[pos];}
void push(int x,int sr) {que[++hd] = x;st[hd] = sr;}
bool empty(){return hd <= tl;}
void pop_front(){
if(empty())return;
else hd--;
}
void pop_back(){
if(empty())return;
else tl++;
}
int top(){
if(empty())return 0;
return que[hd];
}
int back(){
if(empty()) return 0;
return que[tl + 1];
}
}q1,q2;
namespace sgt{
#define ls (p << 1)
#define rs (ls | 1)
#define mid ((pl + pr) >> 1)
int t[N<<2];
void push_up(int p) {
t[p] = t[ls] + t[rs];
}
void update(int p,int pl,int pr,int k,int d) {
if(pl == pr) {
t[p] += d;
return;
}
if(k <= mid) update(ls,pl,mid,k,d);
else update(rs,mid+1,pr,k,d);
push_up(p);
}
int query(int p,int pl,int pr,int l,int r) {
if(l <= pl && pr <= r) return t[p];
int res = 0;
if(l <= mid) res += query(ls,pl,mid,l,r);
if(r > mid) res += query(rs,mid+1,pr,l,r);
return res;
}
}
signed main() {
n = rd(),m = rd(),q = rd();
for(int i = 1;i<=m;i++) {
int op = rd(),x = rd();
o[i] = {op,x};
}
for(int i = 1;i<=q;i++) {
int l = rd(),r = rd();
sc[r].push_back({i,l});
}
for(int i = 1;i<=m;i++) {
auto [op,x] = o[i];
sc[i].push_back({-1,i});
if(!op) {
while(!q1.empty() && q1.top() <= x) {
int dead = q1.st[q1.hd];
sc[i].push_back({0,dead});
q1.pop_front();
}
q1.push(x,i);
while(q2.hd > q2.tl + 1 && min(n - x,q2[q2.tl + 1]) <= min(n - x,q2[q2.tl + 2])) {
int dead = q2.st[q2.tl + 1];
sc[i].push_back({0,dead});
q2.pop_back();
}
if(!q2.empty()) q2[q2.tl + 1] = min(n - x,q2[q2.tl + 1]);
while(!q2.empty() && !min(n - x,q2.back())) {
int dead = q2.st[q2.tl + 1];
sc[i].push_back({0,dead});
q2.pop_back();
}
}else {
x = n - x + 1;
while(!q2.empty() && q2.top() <= x) {
int dead = q2.st[q2.hd];
sc[i].push_back({0,dead});
q2.pop_front();
}
q2.push(x,i);
while(q1.hd > q1.tl + 1 && min(n - x,q1[q1.tl + 1]) <= min(n - x,q1[q1.tl + 2])) {
int dead = q1.st[q1.tl + 1];
sc[i].push_back({0,dead});
q1.pop_back();
}
if(!q1.empty()) q1[q1.tl + 1] = min(n - x,q1[q1.tl + 1]);
while(!q1.empty() && !min(n - x,q1.back())) {
int dead = q1.st[q1.tl + 1];
sc[i].push_back({0,dead});
q1.pop_back();
}
}
}
for(int i = 1;i<=m;i++) {
sort(sc[i].begin(),sc[i].end());
for(auto [op,x] : sc[i]) {
if(op == 0) {
sgt::update(1,1,m,x,-1);
}else if(op == -1) {
sgt::update(1,1,m,x,1);
}else {
ans[op] = sgt::query(1,1,m,x,i);
}
}
}
for(int i = 1;i<=q;i++) wt(ans[i]),putchar('\n');
return 0;
}
2026.10.29
明天就要面试了,有点紧张啊。
给出一个数据结构题,以抚慰我幼小的心灵(笑。
CF2272G Ranges
这道题是两种典型模型的整合,区间连通性并查集(如P5994)和操作时间轴-线段树分治。
当一个区间能被其他的小区间完美覆盖,自然是不会产生代价的,如何判断一个区间是否会被完美覆盖呢?
对于一个区间 \([l,r]\),我们在 DSU 上连接 \(l \Leftrightarrow r + 1\),如果未成功连接(\(l\) 和 \(r + 1\) 拥有相同的祖先)则不产生代价,这个时候说明区间 \([l,r]\) 被完美覆盖了,否则要花费 \(1\) 的代价,连接 \(l \Leftrightarrow r + 1\)。
为什么可以判断出完美覆盖
假设我们有两个区间 \([l,k]\) 和 \([k + 1,r]\),那么我们可以通过拼接得到 \([l,r]\),对应到我们的 DSU 做法,我们连上了 \(l \Leftrightarrow k + 1 \Leftrightarrow r + 1\),进而得到了代表 \([l,r]\) 的 \(l \rightarrow r + 1\)。
同时我们发现,添加顺序对判断完美覆盖代价没有影响。
如果先有 \([l,r]\) 和 \([k+1,r]\),不能拆出 \([l,k]\);但如果先有 \([l,k]\) 和 \([k+1,r]\),就能免费得到 \([l,r]\)。所以代价可以“转移”到更小的区间上。用并查集看,就是每条边 \((l,r+1)\) 第一次成功合并时代价贡献 \(1\),与添加顺序无关。
以上
每次操作会替代一个区间,即我们在 DSU 上连的一条边,我们自然可以离线算出每个区间的存活时间,然后根据存活时间,把 DSU 上的连边操作放到时间轴线段树上,配合上可撤销并查集,在叶子节点得到每次操作后的答案。
这样题目就化归为最基本的线段树分治题目了。
时间复杂度:\(O((m + q)\log q\log n)\)。
AC code
#include<bits/stdc++.h>
using namespace std;
int rd() {
int x = 0, w = 1;
char ch = 0;
while (ch < '0' || ch > '9') {
if (ch == '-') w = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + (ch - '0');
ch = getchar();
}
return x * w;
}
void wt(int x) {
static int sta[35];
int f = 1;
if(x < 0) f = -1,x *= f;
int top = 0;
do {
sta[top++] = x % 10, x /= 10;
} while (x);
if(f == -1) putchar('-');
while (top) putchar(sta[--top] + 48);
}
const int N = 2e5+5;
int n,m,q;
array<int,2> p[N];
int s[N],h[N],top,lst[N];
array<int,3> st[N];
void init() {for(int i = 1;i<=n + 1;i++) s[i] = i,h[i] = 1;}
int find(int x) {
if(s[x] ^ x) x = find(s[x]);
return s[x];
}
bool merge(int x,int y) {
int fx = find(x),fy = find(y);
if(fx == fy) return false;
if(h[fx] > h[fy]) swap(fx,fy);
st[++top] = {fx,fy,h[fy]};
s[fx] = fy;
h[fy] = h[fy] + (h[fx] == h[fy]);
return true;
}
int ans[N];
namespace sgt{
#define ls (p << 1)
#define rs (ls | 1)
#define mid ((pl + pr) >> 1)
vector<array<int,2> > t[N << 2];
int res = 0;
void build(int p,int pl,int pr) {
t[p].clear();
if(pl == pr) return;
build(ls,pl,mid);
build(rs,mid+1,pr);
}
void insert(int p,int pl,int pr,int l,int r,array<int,2> w) {
if(l <= pl && pr <= r) {
t[p].push_back(w);
return;
}
if(l <= mid) insert(ls,pl,mid,l,r,w);
if(r > mid) insert(rs,mid+1,pr,l,r,w);
}
void solve(int p,int pl,int pr) {
int now = top;
int cnt = 0;
for(auto [x,y] : t[p])
cnt += merge(x,y);
if(pl == pr)
ans[pl] = res + cnt;
res += cnt;
if(pl ^ pr) {
solve(ls,pl,mid);
solve(rs,mid+1,pr);
}
res -= cnt;
while(top > now) {
auto [x,y,H] = st[top--];
s[x] = x;
h[y] = H;
}
}
}
void solve() {
top = 0;
n = rd(),m = rd(),q = rd();
init();sgt::build(1,0,q);
for(int i = 1;i<=m;i++) lst[i] = 0;
for(int i = 1;i<=m;i++) {
int l = rd(),r = rd();
p[i] = {l,r};
}
for(int i = 1;i<=q;i++){
int P = rd(),l = rd(),r = rd();
auto &[L,R] = p[P];
sgt::insert(1,0,q,lst[P],i - 1,{L,R + 1});
lst[P] = i;L = l,R = r;
}
for(int i = 1;i<=m;i++) {
auto [l,r] = p[i];
sgt::insert(1,0,q,lst[i],q,{l,r + 1});
}
sgt::solve(1,0,q);
for(int i = 0;i<=q;i++) wt(ans[i]),putchar('\n');
}
signed main() {
int T = rd();
while(T--) solve();
return 0;
}
P5994 [PA 2014] Kuglarz
看到我们的 ckain 大佬%% 以前写过这道题,感觉题目类型很经典,积累一下:区间连通性并查集
我们查看一个区间是否能被完美覆盖(或是否能通过区间相减得到),可以通过 DSU 维护区间,具体来说,对于 \([l,r]\),考虑用 DSU 将 \(l \leftrightarrow r + 1\) 连接到一起,可以见上面题目对 区间连通性并查集 的理解。
然后要让代价最小,显然建图完成后考虑最小生成树。
AC-code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int rd() {
int x = 0, w = 1;
char ch = 0;
while (ch < '0' || ch > '9') {
if (ch == '-') w = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + (ch - '0');
ch = getchar();
}
return x * w;
}
void wt(int x) {
static int sta[35];
int f = 1;
if(x < 0) f = -1,x *= f;
int top = 0;
do {
sta[top++] = x % 10, x /= 10;
} while (x);
if(f == -1) putchar('-');
while (top) putchar(sta[--top] + 48);
}
const int N = 2e3+5;
int n,s[N];
vector<array<int,3> > e;
int find(int x) {
if(s[x] ^ x) s[x] = find(s[x]);
return s[x];
}
void kru() {
int fx,fy,ans = 0;
for(int i = 1;i<=n + 1;i++) s[i] = i;
for(auto it : e) {
fx = find(it[1]);
fy = find(it[2]);
if(fx ^ fy) {
ans += it[0];
s[fx] = fy;
}
}
wt(ans);
}
signed main() {
n = rd();
for(int i = 1;i<=n;i++)
for(int j = i;j<=n;j++) {
int x = rd();
e.push_back({x,i,j + 1});
}
sort(e.begin(),e.end());
kru();
return 0;
}
The 2025 ICPC Shanghai RC /The 4th UCup. Stage 12: Shanghai
D. 非子集和
三进制状压 \(dp\),考虑将位上的 \(2\) 认定为 \(?\)。
不难发现,对于 \(\text{xx}\ ?\ \text{xx}\) 可以由 \(\text{xx}\ 1\ \text{xx}\) 和 \(\text{xx}\ 0\ \text{xx}\) 两个状态的答案相加得来。
也就是说,从没有 \(?\) 的已知答案入手,一步一步推理 \(k\) 个 \(?\) 对应状态的答案。
然后,因为这道题 \(n \leq 16\) 要通过只能 \(O(3^n)\) 做法,不接受 \(O(n3^n)\)
自然考虑固定最高位 \(2\),从这里将状态分成 \(\text{xx}+2+\text{xx}\) 分别枚举前后的数
(这里不用担心枚举出比我们钦定的 \(2\) 更高位的 \(2\) 的问题,因为这个状态在我们没有意愿枚举到的时候是 \(0 = 0 + 0\) 的 \(dp\),没有任何效力)
做一步二进制数转三进制数,最后状压 \(dp\) 即可。
#include<array>
#include<vector>
#include<bitset>
#include<iostream>
using namespace std;
const int N = 43046721;
int rd() {
int x = 0, w = 1;
char ch = 0;
while (ch < '0' || ch > '9') {
if (ch == '-') w = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + (ch - '0');
ch = getchar();
}
return x * w;
}
void wt(int x) {
static int sta[35];
int f = 1;
if(x < 0) f = -1,x *= f;
int top = 0;
do {
sta[top++] = x % 10, x /= 10;
} while (x);
if(f == -1) putchar('-');
while (top) putchar(sta[--top] + 48);
}
int turn(int x) {
static int a[20],top;
top = 0;
int res = 0;
while(x) {
a[top++] = x & 1;
x >>= 1;
}
int k = 1;
for(int i = 0;i<top;i++) {
res += a[i] * k;
k *= 3;
}
return res;
}
int a[N],pw[20];
signed main() {
int n = rd();pw[0] = 1;
for(int i = 1;i<=n;i++) pw[i] = pw[i - 1] * 3;
for(int i = 0;i<(1ll << n);i++) a[i] = rd();
int c = pw[n];
vector<int> dp(c + 5);
for(int i = 0;i<(1ll<<n);i++){
int o = turn(i);
dp[o] = a[i];
}
int ans = 0;
for(int k = 0;k<n;k++) {
int sp = pw[k] * 3;
int pk = pw[k];
for(int b = 0;b < c;b += sp) {
for(int o = 0;o<pk;o++) {
int k0 = o + b;
int k1 = k0 + pk;
int k2 = k1 + pk;
dp[k2] += dp[k0] + dp[k1];
}
}
}
for(int i = 0;i<c;i++) ans ^= dp[i];
wt(ans);
return 0;
}
H. AGI
博弈题
如果每个数都是成双出现的,那么 Alice 拿什么,显然Bob 就会拿走对应的,最后看 Alice 拿到的异或值是不是 \(0\)。(模仿策略的优秀在于结局固定性,若模仿有效,可以强制对方走入必败)
如果出现落单的,那么至少落单俩。
考虑 Alice 选到只剩 \(2\) 个落单的值了,手上的值为 \(X\)。
看这两个落单的值里面有没有 \(X\) ,有的话就异或消掉了,否则就输了。
如果落单了大于 \(2\) 个值,当 Alice 手上有 \(X\) 且场上剩余 \(3\) 个值时,假设 Alice 只要拿 \(Y\) 就能赢,那么作为 Bob,肯定会先发制人拿走让 Alice 必胜的 \(Y\),致使 Bob 获得胜利。
#include<bits/stdc++.h>
using namespace std;
int rd() {
int x = 0, w = 1;
char ch = 0;
while (ch < '0' || ch > '9') {
if (ch == '-') w = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + (ch - '0');
ch = getchar();
}
return x * w;
}
void wt(int x) {
static int sta[35];
int f = 1;
if(x < 0) f = -1,x *= f;
int top = 0;
do {
sta[top++] = x % 10, x /= 10;
} while (x);
if(f == -1) putchar('-');
while (top) putchar(sta[--top] + 48);
}
set<int> s;
void solve() {
s.clear();
int n = rd(),res = 0;
for(int i = 1;i<=(n << 1);i++) {
int c = rd();
if(s.find(c) != s.end()) {
res ^= c;
s.erase(c);
}else s.insert(c);
}
if(s.size() == 0) {
if(!res) {
printf("Menji\n");
return;
}
}else if(s.size() == 2) {
bool flag = false;
for(auto it : s)
if(it == res)
flag = true;
if(flag) {
printf("Menji\n");
return;
}
}
printf("Bot\n");
}
signed main() {
int T = rd();
while(T--) solve();
return 0;
}
G. 宝石箱
首先,分成 \(3\) 组以上肯定不优。
\(3\) 组以上表示要求更严格(因为结算答案方式为 \(\&\)),然后我们假设某一位在最终答案为 \(1\) ,对应 \(3\) 组中该位,每一组里面都有奇数个 \(1\),那我为什么不把这些数(合起来共有奇数个,对于低位,分开意味着更难让最后结果得到 \(1\))放在一组里面呢?
所以最多 \(2\) 组。
什么时候可以一组呢?先保证最高位,
如果最高位 \(1\) 出现次数为奇数次,所有数肯定要放到一个组里,分开不就是一个 偶数 + 奇数 导致 \(\&\) 后爆了。
如果最高位 \(1\) 出现偶数次,那么这个时候考虑分 \(2\) 组。即 奇数 + 奇数 型。
然后,我们假定两组异或值为 \(S_a,S_b\)
我们要最大化 \(S_a\ \&\ S_b\),不妨先整理我们已知的信息: \(S_a \oplus S_b = X\) 全局异或值。
根据这个信息,我们怎么转化 \(S_a\ \&\ S_b\) 的值呢?
我们看看异或干了什么事:
如果 \(S_a(k) \oplus S_b(k) = 1(k)\) 那么说明 \(S_a(k)\) 和 \(S_b(k)\) 不同,\(\&\) 的结果肯定是 \(0\)
如果 \(S_a(k) \oplus S_b(k) = 0(k)\) ,说明相同,\(\&\) 的结果不会和 \(S_a(k)\) 和 \(S_b(k)\) 有差别。
所以,可以发现,通过转化,\(S_a\ \&\ S_b = S_a\ \&\ \overline X\) ,其中 \(\overline X\) 把异或等于 \(1\) 的位置全部干掉(战斗爽)
\(\overline X\) 显然是个定值,在给定掩码下求最大异或值,显然上异或线性基,得到最大异或值。
Over!
#include<bits/stdc++.h>
using namespace std;
#define int long long
int rd() {
int x = 0, w = 1;
char ch = 0;
while (ch < '0' || ch > '9') {
if (ch == '-') w = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + (ch - '0');
ch = getchar();
}
return x * w;
}
void wt(int x) {
static int sta[35];
int f = 1;
if(x < 0) f = -1,x *= f;
int top = 0;
do {
sta[top++] = x % 10, x /= 10;
} while (x);
if(f == -1) putchar('-');
while (top) putchar(sta[--top] + 48);
}
int hibit(int x) {
for(int i = 60;i >= 0;i--)
if(x & (1ll << i))
return i;
return -1;
}
const int N = 61;
int p[N];
void greedy(int x) {
for(int i = 60;i >= 0;i--) {
if((x >> i) & 1) {
if(!p[i]) {p[i] = x;return;}
x ^= p[i];
}
}
}
void solve() {
int n = rd();
for(int i = 0;i<N;i++) p[i] = 0;
vector<int> a(n + 1);
for(int i = 1;i<=n;i++) a[i] = rd();
int maxn = 0,cnt = 0;
for(int i = 1;i<=n;i++) {
int c = hibit(a[i]);
if(c == maxn) cnt++;
else if(c > maxn)
cnt = 1,maxn = c;
}
if(cnt & 1) {
int res = 0;
for(int i = 1;i<=n;i++) res ^= a[i];
wt(res),putchar('\n');
}else {
int res = 0;
for(int i = 1;i<=n;i++) res ^= a[i];
res = ~res;
for(int i = 1;i<=n;i++) a[i] &= res;
for(int i = 1;i<=n;i++) greedy(a[i]);
int ans = 0;
for(int i = 60;i >= 0;i--)
ans = max(ans,ans ^ p[i]);
wt(ans);
putchar('\n');
}
}
signed main() {
int T = rd();
while(T--) solve();
return 0;
}

浙公网安备 33010602011771号