具体题面在洛谷里找就好了 : www.luogu.com.cn/contest/140…
T2是 CF1223F 的值域缩小版(这场比赛是2020年,当时我还打了呢),T4是 ABC304ex 阉割版再加了一点东西。剩下 T1 是大水题,T3 是恶心的模拟。以后原题改编可能成为常态了,所以要多打比赛。
再补充一个冷知识:今年提高组各省省一的分数线一定是 的倍数 ! 这样会导致重分人数大大增加,尤其接近省一线的,为了避免这个问题,建议组题人去做一下《小凯的疑惑》。
T1 密码锁
纯送分题,全国有 的人拿了满分。
只要认真读题就能得出:第 个密码状态能扩展出 个相邻状态,记为一个集合 ,答案就是 的大小。
一个密码状态显然就是一个五位十进制数,开个桶记录下这些相邻状态被标记的次数,最后标记次数是 的状态就是一种可能的密码。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int a[5];
int cal() {
return a[0] + a[1] * 10 + a[2] * 100 + a[3] * 1000 + a[4] * 10000;
}
int c[N];
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 5; j++) {
scanf("%d", &a[j]);
}
for (int j = 0; j < 5; j++) {
for (int k = 0; k < 10; k++) {
a[j] = (a[j] + 1) % 10;
if (k != 9) c[cal()]++;
}
}
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 10; k++) {
a[j] = (a[j] + 1) % 10;
a[j + 1] = (a[j + 1] + 1) % 10;
if (k != 9) c[cal()]++;
}
}
}
int ans = 0;
for (int i = 0; i < 100000; i++) {
if (c[i] == n) ans++;
}
printf("%d\n", ans);
return 0;
}
这题还有别的做法,比如直接枚举 个密码,一个密码如果和 个给出的状态都相邻,就是一种可能得密码。
T2 消消乐
容易发现,消除操作的次序不影响能否消除。按照能消就消的原则,一个字符串是否可消除,可以用一个栈来判定。
方法就是维护一个栈,然后从左到右考虑每个字符:
- 若当前字符等于栈顶字符,则把栈顶元素移出,相当于消除操作
- 若栈为空或者当前字符不等于栈顶字符,则把当前字符压入栈中
最后这个栈为空,说明整个字符串是可消除的,这样枚举起点就有 分的 算法了。
真的需要枚举起点吗?如果只从 开始扫一遍,然后想知道以 为起点的子串情况,只要把起点之前的状态 记录下来就好了,当遇到一个 ,就说明 这个区间用栈扫一遍结果为空,是可消除的。这里有一个疑问,就是中间过程可能让 这个状态弹栈让元素变少,然后最后再补回来形成 。这其实是没有任何影响的,随便举个例子都能搞明白,就是中间正常的先消除,前面弹出的和补回来的明显也能俩俩配对消除。
那么这个状态怎么记录呢?一个简单的方法是采用字符串 hash,把每个状态 变成一个数,然后开一个 map 作为桶记录每个 hash 值出现的个数。
关于字符串 hash,不推荐使用自然溢出,因为我已经被卡了无数次了。这东西很好构造,只要是 无论基数取什么都能卡掉,具体网上教程很多。所以字符串 hash一定要 一个质数,再怕被卡可以双 hash,用两个基数或者两个模数。
时间复杂度 ,代码如下:
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const LL mod = 1e9 + 7;
const int N = 2e6 + 5;
char a[N];
char s[N];
const int base1 = 131, base2 = 151;
LL ha1[N], ha2[N], rp1[N], rp2[N];
int main() {
rp1[0] = rp2[0] = 1;
for (int i = 1; i <= 2000000; i++) {
rp1[i] = rp1[i - 1] * base1 % mod;
rp2[i] = rp2[i - 1] * base2 % mod;
}
int n;
scanf("%d%s", &n, a + 1);
int tot = 0;
map<pair<LL, LL>, int> mp;
mp[{ha1[0], ha2[0]}]++;
LL ans = 0;
for (int i = 1; i <= n; i++) {
if (s[tot] == a[i]) {
ha1[i] = (ha1[i - 1] - a[i] * rp1[tot] % mod + mod) % mod;
ha2[i] = (ha2[i - 1] - a[i] * rp2[tot] % mod + mod) % mod;
tot--;
} else {
s[++tot] = a[i];
ha1[i] = (ha1[i - 1] + a[i] * rp1[tot]) % mod;
ha2[i] = (ha2[i - 1] + a[i] * rp2[tot]) % mod;
}
ans += mp[{ha1[i], ha2[i]}]++;
}
printf("%lld\n", ans);
return 0;
}
觉得 hash 会冲突,可以换成字典树做,时间复杂度 :
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const LL mod = 1e9 + 7;
const int N = 2e6 + 5;
char a[N];
char s[N];
int ch[N][26], fa[N], val[N], ck = 1;
int main() {
int n;
scanf("%d%s", &n, a + 1);
LL ans = 0;
int tot = 0, u = ck;
val[u]++;
for (int i = 1; i <= n; i++) {
if (s[tot] == a[i]) {
tot--;
u = fa[u];
} else {
s[++tot] = a[i];
int &v = ch[u][a[i] - '0'];
if (v == 0) v = ++ck, fa[v] = u;
u = v;
}
ans += val[u]++;
}
printf("%lld\n", ans);
return 0;
}
本题还可以 DP 做,记 表示以 为结尾的可消串个数,那么答案就是 。
记 表示以 为结尾消掉一段后,上一段结尾字符为 的最大位置,也就是满足 为 的最大位置,使得 可消除。
初始情况 表示以 为结尾没消除。
那么包含 最近的可消除的一段就是 ,从 这个位置的状态,就能转移到 位置。
时间复杂度 ,代码如下:
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 2e6 + 5;
char a[N];
int f[N], g[N][26];
int main() {
int n;
scanf("%d%s", &n, a + 1);
LL ans = 0;
for (int i = 1; i <= n; i++) {
int x = a[i] - 'a';
int k = g[i - 1][x] - 1; // 消掉最近的一段后,上一段的结尾是k
if (k >= 0) {
ans += f[i] = f[k] + 1;
for (int j = 0; j < 26; j++) g[i][j] = g[k][j];
}
g[i][x] = i;
}
printf("%lld\n", ans);
return 0;
}
还有一个神奇 DP 做法,时间复杂度 ,水平有限不会证明:
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 2e6 + 5;
char a[N];
// f[i] 表示以i为结尾的可消串个数
// g[i] 表示i左边最接近i的位置 满足 [g[i],i] 是可消的
int f[N], g[N];
int main() {
int n;
scanf("%d%s", &n, a + 1);
LL ans = 0;
for (int i = 1; i <= n; i++) {
int x = i - 1;
while (x >= 1 && a[x] != a[i]) x = g[x] - 1;
if (x >= 1) {
g[i] = x;
ans += f[i] = f[g[i] - 1] + 1;
}
}
printf("%lld\n", ans);
return 0;
}
T3 结构体
超级无聊的模拟题,考验读题能力,编码能力,考场一不小心就陷进去好几个小时。
特殊性质A比较简单,不需要考虑结构体,以及 操作会直接给出变量名,不需要解析非常好写。注意给的样例文件没有特殊性质 的,比赛时需要手动构造几个验证正确性。这样就拿到 分的高分了,代码如下:
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
map<string, LL> ts; // 每种类型占据的大小
// 变量信息
struct OBJ {
string type, name;
} a[105];
LL b[105]; // 起始位置数组
map<string, int> id; // 通过名字找到变量下标
int main() {
ts["byte"] = 1;
ts["short"] = 2;
ts["int"] = 4;
ts["long"] = 8;
int n = 0; // 变量个数
LL ck = 0; // 当前分配到的内存地址
int m;
cin >> m;
while (m--) {
int op;
cin >> op;
if (op == 1) {
return 0;
} else if (op == 2) {
string type, name;
cin >> type >> name;
a[++n] = {type, name};
id[name] = n;
ck = (ck + ts[type] - 1) / ts[type] * ts[type];
cout << ck << "\n";
b[n] = ck;
ck += ts[type];
} else if (op == 3) {
string s;
cin >> s;
cout << b[id[s]] << "\n";
} else {
LL x;
cin >> x;
if (x >= ck) {
cout << "ERR\n";
continue;
}
int k = upper_bound(b + 1, b + n + 1, x) - b - 1;
if (x <= b[k] + ts[a[k].type] - 1)
cout << a[k].name << "\n";
else
cout << "ERR\n";
}
}
return 0;
}
接下里可以尝试下特殊性质 的写法,结构体是没有嵌套的,也比较好写,全写出来就有 分了。特殊性质 是不需要考虑内存对齐的,也可以试试。下面考虑正解。
因为存在结构体嵌套,是一个树形结构,一个变量所占的空间是十分庞大的,本题最大内存可达 。把所有叶结点的变量都在内存上标记是不行的 ,只能通过类型树和每个成员的相对地址的偏移量来计算实际内存地址( 操作就这么做)。
具体而言,一个变量的信息可以设计如下:
struct OBJ {
string name; // 变量名
int type; // 类型编号
LL l, r; // 所占相对内存地址的范围
};
一个类型的信息设计如下:
struct TYPE {
LL sz; // 类型大小
LL w; // 对齐要求
vector<OBJ> a; // 成员变量
map<string, int> id; // 变量名字 ->下标
void add(int, string); // 增加一个成员变量
} T[105];
对齐要求 是其子类型对齐要求的最大值。
注意 既是类型大小,又是分配内存的起点,因为 都被分配了。而在新增成员变量时,类型大小作为分配内存的起点要先对齐,等加完所有成员变量后,还要再根据 对齐一次,保证最终 是 的倍数。
具体对齐的时候,比如本来要从 地址开始分配,现在要求是 的倍数开始,那么真正的分配地址起点就是 。
关于 操作定义变量,可以定义一个宇宙类型#,认为全宇宙只有一个变量,而后面定义的变量都是#动态增加的成员变量,这样的好处就是有唯一根节点了。
注意 操作不需要二分,只要暴力即可,反正暴力一次也是 。
其他细节见下面的完整代码:
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
// 当前内存从x开始分配,但必须是y的倍数
LL cal(LL x, LL y) {
return (x + y - 1) / y * y;
}
// 变量信息
struct OBJ {
string name; // 变量名
int type; // 类型编号
LL l, r; // 所占相对内存地址的范围
};
map<string, int> tid; // 类型名->T数组下标
// 类型信息
struct TYPE {
LL sz; // 类型大小
LL w; // 对齐要求
vector<OBJ> a; // 成员变量
map<string, int> id; // 变量名字->下标
void add(int, string); // 增加一个成员变量
} T[105];
// 定义写外面是因为用了 T[tp]
void TYPE::add(int tp, string name) {
// 计算成员x的信息
OBJ x = {name, tp, 0, 0};
x.l = cal(sz, T[tp].w);
x.r = x.l + T[tp].sz - 1;
// 更新当前类型
sz = x.r + 1;
w = max(w, T[tp].w);
id[name] = a.size();
a.push_back(x);
}
int main() {
int n = 0; // 类型个数
tid["#"] = ++n; // 宇宙类型,存所有变量 ,此类型动态增加成员
tid["byte"] = ++n, T[n].sz = T[n].w = 1;
tid["short"] = ++n, T[n].sz = T[n].w = 2;
tid["int"] = ++n, T[n].sz = T[n].w = 4;
tid["long"] = ++n, T[n].sz = T[n].w = 8;
int _;
cin >> _;
while (_--) {
int op;
cin >> op;
if (op == 1) {
string s;
int k;
cin >> s >> k;
tid[s] = ++n; // 新建一个类型
while (k--) {
string type_name, name;
cin >> type_name >> name;
T[n].add(tid[type_name], name);
}
T[n].sz = cal(T[n].sz, T[n].w); // 最后的类型大小,别忘记对齐啊
cout << T[n].sz << " " << T[n].w << "\n";
} else if (op == 2) {
string type_name, name;
cin >> type_name >> name;
T[1].add(tid[type_name], name);
cout << T[1].a.back().l << "\n";
} else if (op == 3) {
string s, name;
cin >> s;
s.push_back('.');
LL ans = 0;
int u = 1;
for (auto x : s) {
if (x == '.') {
OBJ z = T[u].a[T[u].id[name]];
ans += z.l;
u = z.type;
name.clear();
} else {
name.push_back(x);
}
}
cout << ans << "\n";
} else {
LL x;
cin >> x;
int u = 1;
string ans;
while (1) {
if (u >= 2 && u <= 5) {
cout << ans << "\n";
break;
}
int flag = 0;
for (auto z : T[u].a) {
if (x <= z.r) {
if (x >= z.l) {
flag = 1;
if (ans.size()) ans += ".";
ans += z.name;
x -= z.l;
u = z.type;
}
break;
}
}
if (flag == 0) {
cout << "ERR\n";
break;
}
}
}
}
return 0;
}
T4 种树
先考虑骗分点,对于特殊性质 ,是一条链的情况且 号点在端点上,所以走法是完全唯一的。
那么问题转换为已知一个等差数列的首项和公差(在公差为负数的情况下,降到 之后全部为 需要特判),需要到多少项使得总和 。当然可以用解一个二次方程求出这个项数,为避免误差也可以采用二分法。
这样就能拿到高贵的 分了:
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int INF = 0x3f3f3f3f;
const LL mod = 1e9 + 7;
const int N = 100005;
int n;
LL a[N], b[N], c[N];
vector<int> G[N];
// 首项 v 公差 d,总和需要至少s,求至少需要的天数
LL cal(LL v, LL d, LL s) {
if (d == 0) return (s + v - 1) / v;
if (d > 0) {
LL l = 1, r = s / v + 1;
while (l < r) {
LL mid = l + r >> 1;
if (mid * v + (__int128)(mid - 1) * mid / 2 * d >= s)
r = mid;
else
l = mid + 1;
}
return l;
}
LL x = (v - 1 + -d - 1) / -d; // 第x+1天开始 都=1
LL s0 = x * v + (x - 1) * x / 2 * d; // 前x天总和,这里不会爆LL
if (s > s0) return x + s - s0;
LL l = 1, r = x;
while (l < r) {
LL mid = l + r >> 1;
if (mid * v + (mid - 1) * mid / 2 * d >= s)
r = mid;
else
l = mid + 1;
}
return l;
}
int main() {
bool case_B = true;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%lld%lld%lld", &a[i], &b[i], &c[i]);
}
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
if (u != i || v != i + 1) case_B = false;
}
if (case_B) {
LL ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, i + cal(max(b[i] + i * c[i], 1LL), c[i], a[i]) - 1);
}
printf("%lld\n", ans);
} else {
puts("114514"); // 随便猜一个了
}
return 0;
}
-
的范围骗分挺麻烦,需要状压 DP,感觉比正解还难写,不考虑了。当然可以 爆搜 + 剪枝试试运气,最后交给奇迹,洛谷民间数据爆搜是过了 个点。
-
特殊性质 ,是两条链的选择,也比较麻烦,不考虑了。
-
特殊性质 就是直接知道每个点需要种几天,然后贪心做,其实就是 ABC304ex 阉割版。关键就是贪心的同时,要消除拓扑序的影响!代码点我
-
特殊性质 就是不考虑树形结构下的问题。
通过这 两个部分分的启发,如果都分别做出来的话,就能得出以下正解了:
首先每个点树高的增长值是一个数列,根据 的正负性,有两种形式:
- 时,
- 时,
时前半部分等差数列,后面全是 ,可以算出最后一个不是 的下标 。
我们目标是找到一段区间,使得区间和 。现在两头都不确定很难计算,所以选择二分答案,这样能固定好一个右端点,然后计算每个点的最晚种树时间 ,即只有种树时刻在 范围内时,才能保证在最终时刻高度满足要求。
求 也是采用二分的方式,在 时要分三种情况(就是看 在哪个位置),详情见代码。注意中间有些步骤会爆long long,需要使用int128。注意 最多考虑到 ,所以二分的右边界可以缩小。
越小的受限越严重,只能放前面不能放后面,一个极其显然的贪心是根据 排序,然后一个个种植,但这样不满足树结构的要求——先种了父亲,才能种自己。
达到这个要求其实很简单:对于一个点 ,找到其 值最小的儿子 ,容易发现若 , 在 这些时刻内种树已经没有意义了,后代 必然失败。所以可以用 来对 更新最小值。
遍历整个树,让所有结点都满足后,父节点自然都排在儿子前面了,即按 从小到大排序后和树上拓扑序一致,就可以愉快贪心了。
排序后若 ,说明在最初的二分条件下是不满足的。其实这个排序也可以开了个桶优化掉,但要注意判 直接返回 false,不然会 RE。
记 为值域范围,时间复杂度 ,代码如下:
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 100005;
int n;
LL a[N], b[N], c[N];
int w[N]; // w[i] 表示i这个点最晚种树的天数
vector<int> G[N];
void dfs(int u, int fa) {
for (auto v : G[u]) {
if (v == fa) continue;
dfs(v, u);
w[u] = min(w[u], w[v] - 1);
}
}
bool check(LL t) {
for (int i = 1; i <= n; i++) {
LL x = 4e18;
if (c[i] < 0) x = (b[i] - 1 - c[i] - 1) / -c[i] - 1; // 第 x+1 天开始 都 =1
LL l = 1, r = min(2LL * n, t) + 1;
while (l < r) {
LL mid = l + r >> 1;
__int128 s = 0;
if (x > t)
s = (__int128)(b[i] + c[i] * mid + b[i] + c[i] * t) * (t - mid + 1) / 2;
else if (x >= mid)
s = (__int128)(b[i] + c[i] * mid + b[i] + c[i] * x) * (x - mid + 1) / 2 + t - x;
else
s = t - mid + 1;
if (s < a[i])
r = mid;
else
l = mid + 1;
}
if (l == 1) return false;
w[i] = l - 1;
}
dfs(1, 0);
sort(w + 1, w + n + 1);
for (int i = 1; i <= n; i++) {
if (w[i] < i) return false;
}
return true;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%lld%lld%lld", &a[i], &b[i], &c[i]);
}
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
int l = n, r = 1e9;
while (l < r) {
int mid = l + r >> 1;
if (check(mid))
r = mid;
else
l = mid + 1;
}
printf("%d\n", l);
return 0;
}
补充:
严格上 ABC304ex 不算原题,只能说 T4 这个题是在 ABC304ex 基础上改编的。
ABC304ex 题意就是 T4 去掉等差数列后,把树变成有向图,找出一种拓扑序,记 是 这个点在拓扑排序后的下标,限制变为 ,最后求出一组合法 。
这个题有环直接无解,然后也是一样先把范围变紧, 比如一条边是 ,直接变成 。注意修改的顺序,右端点的更新是拓扑序从大到小,左端点反过来。改完后就能保证,贪心结果和拓扑序一致。
然后就是贪心,按每个区间右端点排序 ,一个个来分配集合 里的数。那么就需要一个 set 来维护剩余集合,每次从集合中找到大于等于左端点的最小的数作为 ,然后从集合里删除。
时间复杂度 ,代码如下:
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int INF = 0x3f3f3f3f;
const LL mod = 1e9 + 7;
const int N = 200005;
vector<int> G[N];
int l[N], r[N];
int in[N], a[N], tot;
vector<int> b[N];
int ans[N];
int main() {
int n, m;
scanf("%d%d", &n, &m);
while (m--) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
in[v]++;
}
queue<int> q;
for (int i = 1; i <= n; i++) {
scanf("%d%d", &l[i], &r[i]);
if (in[i] == 0) q.push(i);
}
while (q.size()) {
int u = q.front();
q.pop();
a[++tot] = u;
for (auto v : G[u]) {
in[v]--;
if (in[v] == 0) q.push(v);
l[v] = max(l[v], l[u] + 1);
}
}
if (tot < n) return puts("No"), 0;
for (int i = n; i >= 1; i--) {
int u = a[i];
for (auto v : G[u]) {
r[u] = min(r[u], r[v] - 1);
}
if (l[u] > r[u]) return puts("No"), 0;
b[r[u]].push_back(u);
}
set<int> se;
for (int i = 1; i <= n; i++) {
se.insert(i);
for (auto u : b[i]) {
auto it = se.lower_bound(l[u]);
if (it == se.end()) return puts("No"), 0;
ans[u] = *it;
se.erase(it);
}
}
puts("Yes");
for (int i = 1; i <= n; i++) printf("%d%c", ans[i], " \n"[i == n]);
return 0;
}