若第一个取 L),则最后一个必须取「另一个
于是
L);R)。中间
设左臂区间 L:
每种配对里,头是「先取」(记入前半),尾是「后取」(记入后半)。任何一步四种都不成立就无解。
两个注意点:单臂只剩一个元素时不能自己配自己(需
1#include <bits/stdc++.h>
2using namespace std;
3
4const int maxn = 5e5 + 5;
5int a[2 * maxn]; char ans[2 * maxn]; int n;
6int check(int l, int m, int r) {
7 int i = 1, l1 = l, r1 = m - 1, l2 = m + 1, r2 = r;
8 while (l1 <= r1 && l2 <= r2) {
9 if (a[l1] == a[r1] && l1 < r1) {
10 ans[i] = 'L'; ans[2 * n - 1 - i] = 'L'; l1++; r1--;
11 } else if (a[l1] == a[l2]) {
12 ans[i] = 'L'; ans[2 * n - 1 - i] = 'R'; l1++; l2++;
13 } else if (a[r2] == a[l2] && l2 < r2) {
14 ans[i] = 'R'; ans[2 * n - 1 - i] = 'R'; r2--; l2++;
15 } else if (a[r2] == a[r1]) {
16 ans[i] = 'R'; ans[2 * n - 1 - i] = 'L'; r2--; r1--;
17 } else return 0;
18 i++;
19 }
20 while (l1 < r1 && a[l1] == a[r1]) {
21 ans[i] = 'L'; ans[2 * n - 1 - i] = 'L';
22 i++; l1++; r1--;
23 }
24 while (l2 < r2 && a[l2] == a[r2]) {
25 ans[i] = 'R'; ans[2 * n - 1 - i] = 'R';
26 i++; l2++; r2--;
27 }
28 return l1 > r1 && l2 > r2;
29}
30
31int solve() {
32 cin >> n;
33 for (int i = 0; i < 2 * n; i++) cin >> a[i];
34
35 for (int i = 1; i < 2 * n; i++)
36 if (a[i] == a[0] && check(1, i, 2 * n - 1)) {
37 ans[0] = 'L', ans[2 * n - 1] = 'L';
38 return 1;
39 }
40 for (int i = 0; i < 2 * n - 1; i++)
41 if (a[i] == a[2 * n - 1] && check(0, i, 2 * n - 2)) {
42 ans[0] = 'R', ans[2 * n - 1] = 'L';
43 return 1;
44 }
45 return 0;
46}
47
48int main() {
49 int T;
50 cin >> T;
51 while (T--)
52 if (solve()) {
53 ans[2 * n] = 0;
54 cout << ans << "\n";
55 } else cout << -1 << "\n";
56}
check(l, m, r):ans[i] 与 ans[2n-1-i] 成对填(一前一后),避免中间反复移位。a[0] 的配对者(L 起手,字典序更小),失败再试 a[2n-1](R 起手),都失败输出 -1。l1 < r1、l2 < r2 的守卫)。| 测试点 | 特殊性质 | 拿法 | ||
|---|---|---|---|---|
| 1—7 | 10 | 50 | 无 | 爆搜所有 L/R 序列 |
| 8—10 | 20 | 1000 | 无 | 同上 |
| 11—12 | 100 | 1000 | 无 | 同上(剪枝) |
| 13—15 | 1000 | 25000 | 无 | 同上或朴素贪心 |
| 16—17 | 无 | 两臂合并贪心 | ||
| 18—20 | 有 | 贪心 | ||
| 21—25 | 无 | 贪心 |
特殊性质:每次删除
这道题的分差全在「把取数过程看成两臂合并」这一步,代码本身很短。
知识点:DP、两条贪心性质
难度:提高
给定数组
相邻两个相等的数(
记
合并后序列无相邻相等。位置
sum。答案 sum 是观察 1 拿走的相邻相等贡献。
1#include <bits/stdc++.h>
2using namespace std;
3typedef long long ll;
4const int MAXA = 1e6 + 5;
5
6int main() {
7 ios::sync_with_stdio(false); cin.tie(0);
8 int T; cin >> T;
9 while (T--) {
10 int n; cin >> n;
11 vector<ll> a(n + 1), f(n + 1);
12 for (int i = 1; i <= n; i++) cin >> a[i];
13 vector<int> last(MAXA, 0);
14 ll sum = 0; int m = 0;
15 for (int i = 1; i <= n; i++) {
16 if (i > 1 && a[i] == a[i-1]) { sum += a[i]; continue; } // 观察 1
17 m++;
18 int pr = last[a[i]];
19 last[a[i]] = m;
20 f[m] = f[m-1];
21 if (pr >= 1) f[m] = max(f[m], f[pr+1] + a[i]); // 观察 2
22 }
23 cout << f[m] + sum << "\n";
24 }
25 return 0;
26}
这是「笨做法」:不显式记 pre,用 g[x] 存「上一个值为 add 抵消相邻相等的固定收益来滚动。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4
5using namespace std;
6const int MAXN = 2e5 + 5;
7int n;
8int a[MAXN];
9int T;
10int main() {
11 ios :: sync_with_stdio(false);
12 cin.tie(0);
13 cin >> T;
14 while (T--) {
15 cin >> n;
16 for (int i = 1; i <= n; i++) {
17 cin >> a[i];
18 }
19 vector<long long> g(1e6 + 5, -1e15);
20 long long add = 0;
21 long long f = 0;
22 for (int i = 2; i <= n; i ++) {
23
24 long long t = max(f, g[a[i]] + add + a[i]);
25 f = max(a[i] == a[i - 1] ? f + a[i] : f, t);
26 if (a[i] == a[i - 1])
27 add += a[i];
28 g[a[i - 1]] = max(g[a[i - 1]], t - add);
29 }
30 cout << f << '\n';
31 }
32 return 0;
33}
last[x] 记值 f[m] = max(f[m-1], f[pre+1] + a[i]),无偏移、语义直白。g[x] 直接存「配对起点最优值」,add 抵消相邻相等,t - add 是倒扣回起点,要小心 add 更新时机。last/f;滚动 DP 每组重建 g。| 测试点 | 拿法 | ||
|---|---|---|---|
| 1—4 | 15 | 15 | 爆搜 |
| 5—7 | 100 | 100 | 爆搜或朴素 DP |
| 8—10 | 2000 | 2000 | |
| 11—12 | 相邻预处理 + DP | ||
| 13—15 | 10 | 值域小,DP 常数小 | |
| 16—20 | 满分做法 |
这道题先想清「相邻相等直接吃」和「只配上一个同值点」两条性质,再写 DP 就顺了;滚动 DP 是等价写法,单数组 DP 更直观。
知识点:模拟、内存对齐
难度:提高+/省选−
基本类型四种:byte、short、int、long,大小分别为
处理
a.b.c 的路径,输出最内层元素的起始地址;ERR。定义元素时,所有元素从地址
1//
2// Created by zjs on 10/26/23.
3//
4#include <bits/stdc++.h>
5using namespace std;
6struct member {
7 struct Type *type;
8 string name;
9 long long offset;
10};
11
12long long ceil(long long x, int align) {
13 return (x + align - 1) / align * align;
14}
15
16struct Type {
17 vector<member> mem;
18 long long size = 0;
19 int align = 0;
20 void add_member(Type *type, string name) {
21 size = ceil(size, type->align);
22// cout << type << ' ' << name << ' ' << size << '\n';
23 mem.push_back({type, name, size});
24 align = max(align, type->align);
25 size += type->size;
26 }
27};
28
29long long access(Type *t, long long offset, string path) {
30// cout << "access " << offset << ' ' << path << '\n';
31 if (path.empty())
32 return offset;
33 int len = 0;
34 while (len < path.size() && path[len] != '.') len++;
35 string name = path.substr(0, len);
36 if (len < path.size()) len++;
37 for (auto &i: t->mem)
38 if (i.name == name) {
39 return access(i.type, offset + i.offset, path.substr(len));
40 }
41 return -1;
42}
43
44string locate(Type *t, string path, long long offset) {
45// cout << "path: " << path << ' ' << offset << '\n';
46 if (t->mem.empty()) {
47 if (offset < t->size)
48 return path;
49 return "ERR";
50 }
51 for (int i = (int) t->mem.size() - 1; i >= 0; i--)
52 if (t->mem[i].offset <= offset) {
53// cout << t.mem[i].name << '\n';
54 string new_path = path.empty() ? path + t->mem[i].name : path + "." + t->mem[i].name;
55 return locate(t->mem[i].type, new_path, offset - t->mem[i].offset);
56 }
57}
58
59
60int main() {
61// freopen("struct2.in", "r", stdin);
62
63 map<string, Type> typeInfo;
64 typeInfo["byte"] = {{}, 1, 1};
65 typeInfo["short"] = {{}, 2, 2};
66 typeInfo["int"] = {{}, 4, 4};
67 typeInfo["long"] = {{}, 8, 8};
68
69 int n;
70 cin >> n;
71 Type global;
72 while (n--) {
73 int op;
74 cin >> op;
75 if (op == 1) {
76 string name;
77 int k;
78 cin >> name >> k;
79 Type cur;
80 while (k--) {
81 string N, T;
82 cin >> T >> N;
83 cur.add_member(&typeInfo[T], N);
84 }
85 cur.size = ceil(cur.size, cur.align);
86 typeInfo[name] = cur;
87 cout << cur.size << ' ' << cur.align << '\n';
88 } else if (op == 2) {
89 string type, name;
90 cin >> type >> name;
91 global.add_member(&typeInfo[type], name);
92 cout << global.mem.back().offset << '\n';
93 } else if (op == 3) {
94 string s;
95 cin >> s;
96 cout << access(&global, 0, s) << '\n';
97 } else {
98 long long addr;
99 cin >> addr;
100 cout << locate(&global, "", addr) << '\n';
101 }
102 }
103}
Type::add_member 做「对齐到成员对齐 → 记偏移 → 累加大小」,对齐取成员最大值;类型定义完把大小再对齐到自身对齐。map<string, Type> 存类型,byte/short/int/long 先手写进去;global 也当成一个 Type,元素就是它的成员,天然按序排布。access 沿 . 逐段找成员名累加偏移;locate 从大到小找「偏移 ≤ addr」的成员向下钻,钻到基本类型判断是否越界。map + 线性遍历即可。| 测试点 | 特殊性质 | 拿法 |
|---|---|---|
| 1 | A、D | 只有元素,long 一种基本类型 |
| 2—3 | A | 没有操作 1,只处理元素与查询 |
| 4—5 | B、D | 只有一个结构体类型 |
| 6—8 | B | 只有一个结构体类型(含嵌套) |
| 9—10 | C、D | 成员全基本类型 |
| 11—13 | C | 成员全基本类型 |
| 14—16 | D | 基本类型只有 long |
| 17—20 | 无 | 满分做法 |
特殊性质:A 没有操作 1;B 只有一个操作 1;C 操作 1 的成员类型全为基本类型;D 基本类型只有 long。
这道题是纯模拟,分差在「对齐、偏移、地址查询」三处细节,想清楚再写就能一遍过。
知识点:拓扑排序、贡献计算
难度:提高+/省选−
有
给定一个执行序列
mul[i]:函数 mul 之积)。c[i]:函数 mul 在逆拓扑序(被调用者先)里算。c 从执行序列倒着来:维护后缀乘数 cheng,遇到 cheng 累加进 c[f_k],再 cheng *= mul[f_k];最后 cheng 就是总乘数。
c 顺着拓扑序(调用者先)传给被调用者,同样从后往前、维护后缀乘数。
cheng;mul 和 cheng,不必单独数执行次数。1#include <iostream>
2#include <algorithm>
3#include <vector>
4
5using namespace std;
6
7const int maxn = 1e5 + 5;
8vector<int> g[maxn];
9vector<int> fn; //按拓扑序排列的m个函数。
10int vis[maxn];
11
12void dfs(int u) {
13 vis[u] = 1;
14 for (int v : g[u])
15 if (!vis[v])
16 dfs(v);
17 fn.push_back(u);
18}
19
20
21const int mod = 998244353;
22
23int main() {
24 int n;
25 cin >> n;
26 vector<int> a(n + 1);
27 for (int i = 1; i <= n; i++)
28 cin >> a[i];
29 int m;
30 cin >> m;
31 vector<long long> mul(m + 1, 1);
32 vector<pair<int, long long>> add(m + 1);
33 vector<int> type(m + 1);
34
35 for (int i = 1; i <= m; i++) {
36 cin >> type[i];
37 if (type[i] == 1) {
38 cin >> add[i].first >> add[i].second;
39 } else if (type[i] == 2) {
40 cin >> mul[i];
41 } else {
42 int c;
43 cin >> c;
44 while (c--) {
45 int x;
46 cin >> x;
47 g[i].push_back(x);
48 }
49 }
50 }
51
52 //拓扑排序
53 for (int i = 1; i <= m; i++)
54 if (!vis[i])
55 dfs(i);
56
57 for (int i : fn)
58 for (int j : g[i])
59 mul[i] = mul[i] * mul[j] % mod;
60
61 int Q;
62 cin >> Q;
63 vector<int> f(Q);
64 for (int i = 0; i < Q; i++)
65 cin >> f[i];
66
67 vector<long long> c(m + 1); // c[i]:函数i里的加操作被乘的倍数。
68 long long cheng = 1;
69 for (int j = Q - 1; j >= 0; j--) {
70 int i = f[j];
71 if (type[i] != 2)
72 c[i] = (c[i] + cheng) % mod;
73 cheng = cheng * mul[i] % mod;
74 }
75
76 for (int i = 1; i <= n; i++)
77 a[i] = a[i] * cheng % mod;
78
79//顺着拓扑序从前往后”传播“。
80 for (int i = m - 1; i >= 0; i--) {
81 int j = fn[i];
82 cheng = 1;
83 for (int k = (int) g[j].size() - 1; k >= 0; k--) {
84 int l = g[j][k];
85 if (type[l] != 2) {
86 c[l] = (c[l] + c[j] * cheng % mod) % mod;
87 }
88 cheng = cheng * mul[l] % mod;
89 }
90 if (type[j] == 1) {
91 a[add[j].first] = (a[add[j].first] + add[j].second * c[j] % mod) % mod;
92 }
93 }
94 for (int i = 1; i <= n; i++)
95 cout << a[i] << ' ';
96 cout << "\n";
97 return 0;
98}
fn 数组);mul 在拓扑序上乘子函数的 mul。c[i]:主序列倒着维护 cheng,非乘函数 c[i] += cheng,再 cheng *= mul[i];cheng 收尾即总乘数。c 传给子函数(从后往前、维护后缀乘数);type 1 直接 a[P] += V * c[i]。a 先整体乘 cheng;全程取模,cheng 用 long long 防中间溢出。| 测试点 | 特殊限制 | 拿法 | |
|---|---|---|---|
| 1—2 | 1000 | 调用关系是树 | 直接递归模拟 |
| 3—4 | 1000 | 递归模拟 | |
| 5—6 | 只有加或只有乘 | 单向贡献 | |
| 7 | 无 type 3,直接算 | ||
| 8—9 | 调用关系是树 | 树上递归 + 后缀乘数 | |
| 10—11 | 拓扑排序 | ||
| 12—13 | 只有加或只有乘 | 单向贡献 | |
| 14 | 直接算 | ||
| 15—16 | 调用关系是树 | 树上后缀乘数 | |
| 17—18 | 拓扑排序 | ||
| 19—20 | 满分做法 |
这道题的难点全在「加值后面跟了多少乘数」这个后缀贡献,想清楚 mul 与 coef 两个量就通了。
知识点:哈希、字符串、离线统计
难度:提高+/省选−
给定
替换不改变长度,且只改一个连续区间。所以
记
第一处不同
{ +({ + 公共后缀({ 取 z 后一位当分隔符)。1#include <bits/stdc++.h>
2using namespace std;
3
4#ifdef LOCAL
5#include "debug.h"
6#else
7#define debug(...) 42
8#endif
9
10template <int sigma_size, char alpha>
11struct Trie {
12 vector<array<int, sigma_size>> go;
13 int new_node() {
14 go.push_back({});
15 return (int) go.size() - 1;
16 }
17 Trie() {
18 new_node();
19 }
20 int add(string s) {
21 int p = 0;
22 for (char c : s) {
23 int i = c - alpha;
24 if (go[p][i] == 0) {
25 go[p][i] = new_node();
26 }
27 p = go[p][i];
28 }
29 return p;
30 }
31 vector<pair<int,int>> get_suffix_link() {
32 vector<pair<int,int>> q;
33 for (int i = 0; i < sigma_size; i++)
34 if (go[0][i])
35 q.push_back({go[0][i], 0});
36
37 for (int j = 0; j < (int) q.size(); j++) {
38 int u = q[j].first, v = q[j].second;
39 for (int i = 0; i < sigma_size; i++)
40 if (go[u][i])
41 q.push_back({go[u][i], go[v][i]});
42 else
43 go[u][i] = go[v][i];
44 }
45 return q;
46 }
47};
48
49string work(string a, string b) {
50 int n = (int) a.size();
51 for (int i = 0; i < n; i++)
52 if (a[i] != b[i])
53 for (int j = n - 1; j >= 0; j--)
54 if (a[j] != b[j]) {
55 string k = a.substr(i, j - i + 1) + b.substr(i, j - i + 1);
56 return a.substr(0, i) + '{' + k + '{' + b.substr(j + 1); // '{' 是 'z' 的下一个字符
57 }
58 return "";
59}
60
61int main() {
62 ios::sync_with_stdio(0);
63 cin.tie(0);
64 int n, q;
65 cin >> n >> q;
66
67 Trie<27, 'a'> trie;
68 vector<int> id;
69 for (int i = 0; i < n; i++) {
70 string s1, s2;
71 cin >> s1 >> s2;
72 string s = work(s1, s2);
73 if (!s.empty())
74 id.push_back(trie.add(s));
75 }
76 vector<int> weight(trie.go.size()); // 节点的权值
77 for (int i : id)
78 weight[i]++;
79 // 正向传播
80 for (auto [u, v] : trie.get_suffix_link())
81 weight[u] += weight[v];
82 // 回答询问
83 for (int i = 0; i < q; i++) {
84 string t1, t2;
85 cin >> t1 >> t2;
86 int ans = 0;
87 if (t1.size() == t2.size()) {
88 string t = work(t1, t2);
89 int p = 0;
90 for (char c : t) {
91 p = trie.go[p][c - 'a'];
92 ans += weight[p];
93 }
94 }
95 cout << ans << '\n';
96 }
97}
work(a,b) 返回签名:公共前缀 + { + a、b 差异段拼接 + { + 公共后缀;两串相同返回空串(题面保证询问不会发生)。array<int,27> 存子节点,get_suffix_link 是标准 AC 自动机 fail 链 BFS。t1.size()!=t2.size() 直接 0。| 测试点 | 拿法 | ||
|---|---|---|---|
| 1—2 | 枚举位置 × 二元组 | ||
| 3—5 | 同上(哈希) | ||
| 6—8 | 哈希 + 按差异段分组 | ||
| 9—10 | 分组 + Trie | ||
| 11—14 | 满分做法 |
这道题要同时处理「公共前后缀」两个维度,分差全在能不能把它们统一成「差异段 + 两棵 Trie」这一步。
知识点:哈希、度数统计
难度:省选/NOI−
每次操作后输出:是否所有据点都能反击且能连续穿梭(即「反攻时刻」)。
每个据点出度都为
直接维护每个点的出度,批量操作(毁/修一个点的所有入边)会波及很多点的出度,难以 O(1) 更新。改用随机哈希判「出度全 1」。
给每个据点
批量更新只需维护「每个据点当前存活入边的起点权值和」s[u] 与它的全量 sum[u]:毁点 tot -= s[u], s[u]=0;修点 tot += sum[u]-s[u], s[u]=sum[u];单边同理。
1#include <bits/stdc++.h>
2using namespace std;
3
4int main() {
5 ios::sync_with_stdio(0);
6 int n, m; cin >> n >> m;
7 random_device r;
8 default_random_engine e(r());
9 uniform_int_distribution<int> dist(1, INT_MAX);
10 long long S = 0; vector<int> w(n + 1);
11 for (int i = 1; i <= n; i++) {
12 w[i] = dist(e); S += w[i];
13 }
14 vector<long long> sum(n + 1), s(n + 1);
15 for (int i = 0; i < m; i++) {
16 int u, v; cin >> u >> v; sum[v] += w[u];
17 }
18 long long tot = 0;
19 for (int i = 1; i <= n; i++) {
20 s[i] = sum[i]; tot += s[i];
21 }
22 int q; cin >> q;
23 for (int i = 0; i < q; i++) {
24 int t, u, v; cin >> t >> u;
25 if (t == 1) {
26 cin >> v; s[v] -= w[u]; tot -= w[u];
27 } else if (t == 3) {
28 cin >> v; s[v] += w[u]; tot += w[u];
29 } else if (t == 2) {
30 tot -= s[u]; s[u] = 0;
31 } else {
32 tot += sum[u] - s[u]; s[u] = sum[u];
33 }
34 cout << (tot == S ? "YES" : "NO") << '\n';
35 }
36}
w[u] 随机权值,S = Σw;sum[u] 是 s[u] 是当前存活入边的权值和。tot -= s[u],s[u] = 0;修点 4:tot += sum[u] - s[u],s[u] = sum[u]。s 与全局 tot。tot == S 判「出度全 1」;random_device + default_random_engine 随机权值,碰撞概率可忽略(Monte Carlo)。| 测试点 | 特殊限制 | 拿法 | |
|---|---|---|---|
| 1—3 | 无 | 每次暴力算出度 | |
| 4—8 | 无 | 暴力 + 邻接表 | |
| 9—10 | 没有 t=2、t=4 | 只维护单边,直接算出度 | |
| 11—12 | 没有 t=4 | 拆点/懒标记 | |
| 13—16 | 无 | 随机哈希判出度 | |
| 17—20 | 无 | 满分做法 |
这道题的难点在「批量毁/修一个点的入边」会波及很多出度,随机哈希把一个全局判定压成
五类套路,六道题各归一类
贪心与双指针:把取数过程看成两臂合并成回文(回文)
DP 与贡献:相邻相等直接吃、配对转移(染色);乘数与后缀贡献分离(函数调用)
模拟:内存对齐逐条模拟(结构体)
哈希与字符串:签名 + Trie 后缀链接(谐音替换);随机哈希把出度判定压成 O(1)(星战)
一套查错习惯:哈希冲突与取模、对齐与偏移、多组数据的清空