CSP-S-2023 题解
应该是一年一次的活儿了(
今年的题比去年简单,所以补起来相对容易一点。
T1 lock
暴力,枚举每种密码锁的状态,再判断是否合法,注意出现相同的状态要判掉。
最大计算次数为 $10^{5} \times 8 \times 5 = 4 \times 10^{6}$ 不会炸。
丑陋的考场代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, ans, pos, sub, now[10], a[10][10];
bool flag;
int main() {
// freopen("lock/lock1.in", "r", stdin);
// freopen("lock.in", "r", stdin);
// freopen("lock.out", "w", stdout);
scanf("%d", &n);
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= 5; ++j) {
scanf("%d", &a[i][j]);
}
}
for(now[1] = 0; now[1] <= 9; ++now[1]) {
for(now[2] = 0; now[2] <= 9; ++now[2]) {
for(now[3] = 0; now[3] <= 9; ++now[3]) {
for(now[4] = 0; now[4] <= 9; ++now[4]) {
for(now[5] = 0; now[5] <= 9; ++now[5]) {
flag = true;
for(int i = 1; i <= n; ++i) {
pos = 0, sub = 0;
for(int j = 1; j <= 5; ++j) {
if(a[i][j] != now[j]) {
if(!pos) pos = j, sub = (now[j] - a[i][j] + 10) % 10;
else {
if(pos != j - 1 || sub != (now[j] - a[i][j] + 10) % 10) {
flag = false;
break;
}
}
}
}
if(!pos) flag = false;
if(!flag) break;
}
if(flag) ++ans;
}
}
}
}
}
printf("%d", ans);
return 0;
}
/*
g++ ./lock.cpp -o lock -Wall ‐O2 ‐std=c++14 ‐static
./lock
*/
/*
感觉是道暴力
10^5*8*5=4e6
能过,开敲
15min过大样例
应该没问题,想T2
*/
T2 game
讲个笑话,类似这道题目的一道字符串计数类题目,我在考前做过,还给一个学弟讲过,但是考场上就是没想出来。
$\mathcal{O}(n^{3})$ 的区间dp不讲。
$\mathcal{O}(n^{2})$ 的思路是,把每种字符看成一种括号,枚举左端点来进行括号匹配,如果某一刻栈为空则说明当前子串满足条件,于是统计答案。
$\mathcal{O}(n)/\mathcal{O}(n \log n)$ 的正解其实和 $\mathcal{O}(n^{2})$ 的思路非常接近,我们发现我们枚举每一种情况的左端点非常费时,考虑如何优化掉枚举左端点这一维。
看统计答案的「栈为空」这个条件,如果我们把左端点前面那一段字符串加进去,那么当前栈也会加上同样一段的字符串(可以理解为两个相同的空串被相同地更改后得到的答案是一样的)!
于是可以哈希当前栈的状态,每次答案加上和当前哈希状态相同的位置数量,这样就省掉了枚举左端点那一维!
顺带一提,哈希这个技巧去年T3也考了!
解题思路和解题方法我都知道,为什么没做出来?!
丑陋的补题代码(模板略去):
const int mul1 = 299987, mul2 = 97, mod1 = 998244353, mod2 = 100007;
ll n, ans, top, hsh1, hsh2, power1[2000005], power2[2000005];
char s[2000005], st[2000005];
unordered_map<ll, ll> mp[mod2];
int main() {
read(n);
scanf("%s", s + 1);
power1[0] = power2[0] = 1;
++mp[0][0];
fu(i, 1, n) {
power1[i] = power1[i - 1] * mul1 % mod1;
power2[i] = power2[i - 1] * mul2 % mod2;
if(top && st[top] == s[i]) {
hsh1 = ((hsh1 - st[top] * power1[top] % mod1) % mod1 + mod1) % mod1;
hsh2 = ((hsh2 - st[top] * power2[top] % mod2) % mod2 + mod2) % mod2;
--top;
}
else {
st[++top] = s[i];
hsh1 = (hsh1 + s[i] * power1[top] % mod1) % mod1;
hsh2 = (hsh2 + s[i] * power2[top] % mod2) % mod2;
}
if(mp[hsh2].count(hsh1)) ans += mp[hsh2][hsh1];
++mp[hsh2][hsh1];
}
write(ans);
flush();
return 0;
}
T3 struct
大模拟,先把代码放上来,最后写思路。
还上台讲了这道题,太社死了qwq
我主要讲几个易错点:
- 地址是否属于当前内存区间的时候左端点或右端点忘判。
- 分配内存的时候没有对齐,直接连续分配。
- 地址偏移量算错。
- 少输出了空地址的情况。
还有!我要吐槽的是!CCF你的题面能安排得不要这么阴间吗?最重要的定义你放提示里去???
代码里有考场注释,应该不难理解吧qwq
丑陋的考场代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n, k, op, hs, addr, o, b, now_add = -1;
string t, name;
ll str_idx, str_mx[110], str_size[110];//str_idx表示当前有多少已定义的结构体,str_mx表示当前结构体最大的元素大小,str_size表示结构体大小
string str_name[110];//str_name表示结构体名称
struct meb {
ll typ, name, add;
meb(ll Typ = 0, ll Name = 0, ll Add = 0): typ(Typ), name(Name), add(Add) {}
};//成员,typ表示类型,name表示名称,add表示起始位置
vector<meb> str[110];//结构体
map<string, ll> str_id;//结构体对应编号
string temp_name[110];//temp_name表示元素名称
ll temp_idx, temp_type[110], temp_add[110];//temp_idx表示当前有多少元素,temp_tpye表示元素类型,temp_add表示起始地址
map<string, ll> temp_id;//元素对应编号
bool have_found;
ll get_hsh(string s) {//得到名称对应的哈希值
ll ret = 0;
for(const auto& i : s) {
ret = ret * 27 + (i - 'a' + 1);
}
return ret;
}
string rehsh(ll hs) {//解哈希
string ret = "";
while(hs) {
ret = (char)(hs % 27 - 1 + 'a') + ret;
hs /= 27;
}
return ret;
}
void find_temp(ll now, ll typ, string pth) {
// cout << now << " " << typ << " " << pth << '\n';
if(pth.find('.') != pth.npos) name = pth.substr(0, pth.find('.'));
else name = pth;
hs = get_hsh(name);
for(const auto& i : str[typ]) {
if(i.name == hs) {
if(pth.find('.') != pth.npos) {//继续访问子成员
find_temp(now + i.add, i.typ, pth.substr(pth.find('.') + 1));
}
else {//已访问到目标成员
cout << now + i.add << '\n';
}
return;
}
}
}
void find_addr(ll now, ll typ, string pth) {
// cout << now << " " << typ << " " << pth << '\n';
if(typ <= 4) {
if(addr < now + str_size[typ]) cout << pth << '\n';
else cout << "ERR\n";
return;
}
pth += '.';
for(const auto& i : str[typ]) {
if(now + i.add <= addr && addr < now + i.add + str_size[i.typ]) return find_addr(now + i.add, i.typ, pth + rehsh(i.name));
}
cout << "ERR\n";
}
int main() {
// freopen("struct/struct3.in", "r", stdin);
// freopen("struct.in", "r", stdin);
// freopen("struct.out", "w", stdout);
// ios::sync_with_stdio(0);
// cin.tie(0), cout.tie(0);
// 为了保险还是不关同步流了罢
cin >> n;
str_id["byte"] = ++str_idx;
str_mx[str_idx] = str_size[str_idx] = 1;
str_id["short"] = ++str_idx;
str_mx[str_idx] = str_size[str_idx] = 2;
str_id["int"] = ++str_idx;
str_mx[str_idx] = str_size[str_idx] = 4;
str_id["long"] = ++str_idx;
str_mx[str_idx] = str_size[str_idx] = 8;
while(n--) {
cin >> op;
// cout << n << ":\n";
// cout << op << '\n';
switch(op) {
case 1: {//定义一个结构体类型
cin >> t >> k;
str_id[t] = ++str_idx;
str_name[str_idx] = t;
o = 0, b = 0;
for(int i = 1; i <= k; ++i) {
cin >> t >> name;
str_mx[str_idx] = max(str_mx[str_idx], str_mx[str_id[t]]);
if(i > 1) {
o = ((o + str_size[str[str_idx].back().typ] + str_mx[str_id[t]] - 1) / str_mx[str_id[t]]) * (str_mx[str_id[t]]);
b = ((b + str_size[str[str_idx].back().typ] + str_mx[str_id[t]] - 1) / str_mx[str_id[t]]) * (str_mx[str_id[t]]);
}
// cout << o << " " << b << '\n';
str[str_idx].push_back(meb(str_id[t], get_hsh(name), b));
}
str_size[str_idx] = ((o + str_size[str[str_idx].back().typ] + str_mx[str_idx] - 1) / str_mx[str_idx]) * (str_mx[str_idx]);
cout << str_size[str_idx] << ' ' << str_mx[str_idx] << '\n';
break;
}
case 2: {//定义一个元素
cin >> t >> name;
temp_id[name] = ++temp_idx;
temp_name[temp_idx] = name;
temp_type[temp_idx] = str_id[t];
temp_add[temp_idx] = ((now_add + str_mx[str_id[t]]) / str_mx[str_id[t]]) * str_mx[str_id[t]];
now_add = temp_add[temp_idx] + str_size[str_id[t]] - 1;
cout << temp_add[temp_idx] << '\n';
break;
}
case 3: {//访问某个元素
cin >> t;
if(t.find('.') == t.npos) {//没有成员
cout << temp_add[temp_id[t]] << '\n';
}
else {//有成员
name = t.substr(0, t.find('.'));
t = t.substr(t.find('.') + 1);
find_temp(temp_add[temp_id[name]], temp_type[temp_id[name]], t);
}
break;
}
case 4: {//访问某个内存地址
cin >> addr;
have_found = false;
for(int i = 1; i <= temp_idx; ++i) {
if(temp_add[i] <= addr && addr < temp_add[i] + str_size[temp_type[i]]) {
find_addr(temp_add[i], temp_type[i], temp_name[i]);
have_found = true;
break;
}
}
if(!have_found) cout << "ERR\n";
break;
}
}
}
return 0;
}
/*
g++ ./struct.cpp -o struct -Wall ‐O2 ‐std=c++14 ‐static
./struct
*/
/*
int->short
0~3->0~1
short->int
4~5->4~7
*/
/*
感觉像大模拟,不是很难的感觉
应该比T2更可做
15:25来打的
【】!过大样例了!
17:35
还有不到1h,先把T4暴力打了
*/
T4 tree
二分套二分再加贪心。
要下课了,先把代码贴上来。
首先我们可以发现满足要求答案区间是一个 $\left[ans, +\infty\right)$,这个区间具有一个可以二分的性质(具体是什么性质我忘了,有无好心人指出),考虑二分答案。
现在来想 check
怎么写(假设当前检查是否合法的答案是 ck
),令树种下的时间为 $d_{i}$,那么这棵树长高的时间就是 $[d_{i}, ck]$,如果要这段时间树长高的高度正好大于 $a_{i}$,我们同样可以二分 $d_{i}$。
二分出最大的 $d_{i}$ 后,我先是想了一个简单树形dp,求出每个节点最晚在多久种树,然后挂了。。。
改成普通贪心就过了,每次找到一个 $d_{i}$ 最小的节点,一直往父亲节点跳,用栈记录经过了哪些节点,从上往下依次设置种植时间,每次时间变化量为 $+1$。
直到跳到一个被设置过时间的节点就退出(设置的时候不包含这个节点)。
如果发现一个节点设置的时间大于了最晚种植时间,那么肯定无解。
于是 check
就打完了。
至于二分套二分的内层 check
怎么打,有点抽象,我也不好说,可以配合代码yy一下。
卡常!luogu上会T一个点,但是把 long double
(或是 __int128
) 换成 long long
可以过,但这样可以卡!
丑陋的补题代码(模板略去):
typedef long double lxd;
int n, u, v, l, r, mid, ans, cnt, top, L, R, Mid, st[100001], fa[100001];
ll a[100001], b[100001], c[100001], d[100001];
vector<int> g[100001];
bool vis[100001];
pair<ll, int> p[100001];
bool calc(ll s, ll t, int i) {
lxd hight = 0;
if(c[i] >= 0) hight = b[i] * (t - s + 1) + (lxd)(s + t) * (t - s + 1) / 2 * c[i];
else {
ll m = min(max((b[i] + -c[i] - 1) / -c[i] - 1, s - 1), t);
if(m >= s) hight = (m - s + 1) * b[i] + (lxd)(s + m) * (m - s + 1) / 2 * c[i];
hight += t - m;
}
return hight >= a[i];
}
void dfs(int now) {
for(const auto& i : g[now]) {
if(i != fa[now]) {
fa[i] = now;
dfs(i);
}
}
}
bool check(int ck) {
for(int i = 1; i <= n; ++i) {
if(!calc(1, ck, i)) return false;
vis[i] = false;
L = 1, R = ck;
while(L <= R) Mid = (L + R) >> 1, (calc(Mid, ck, i)) ? (d[i] = Mid, L = Mid + 1) : (R = Mid - 1);
p[i].first = d[i], p[i].second = i;
}
stable_sort(p + 1, p + 1 + n);
cnt = 0;
for(int i = 1; i <= n; ++i) {
u = p[i].second;
if(!vis[u]) {
top = 0;
while(!vis[u]) {
st[++top] = u;
u = fa[u];
}
while(top) {
vis[st[top]] = true;
d[st[top]] = ++cnt;
--top;
}
}
if(d[p[i].second] > p[i].first) return false;
}
return true;
}
int main() {
read(n);
for(int i = 1; i <= n; ++i) {
read(a[i], b[i], c[i]);
}
for(int i = 2; i <= n; ++i) {
read(u, v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1);
l = 1, r = 1000000000;
vis[0] = true;
while(l <= r) mid = (l + r) >> 1, check(mid) ? (ans = mid, r = mid - 1) : (l = mid + 1);
printf("%d", ans);
return 0;
}
本文来自博客园,作者:A_box_of_yogurt,转载请注明原文链接:https://www.cnblogs.com/A-box-of-yogurt/p/18016421
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· Windows编程----内核对象竟然如此简单?