CSP2023 游寄
前言
很难想象,去年一个超越S组一等线50多分的人,这次估分出来比S组一等线低不知道多少分。
还好只是初三。
虽然但是,CCF确实适合去当心理学家。
游记
考前晚上也利用起来搞,一到信息课就直接冲向机房,个人认为当我发现自己的问题之后,确实也努力了,只能说时机未到。用一年的职业生涯买了个教训。
-
\(\text{0-40 min}\),因为各种原因调 T1,各种题读错了,然后直接过了大样例就没管了。事实上忘记判重了,直接挂掉。(虽然也不知道最终多少分)
-
\(\text{40-60 min}\),读了一下T2,第一时间想到了括号匹配,之后又想到了栈,已经一度非常接近正解了(事实上最后就差一点没调出来,然后就怀疑自己是不是这个做法有问题),最后看到 T3 是个大模拟,直接慌了,打了暴力直接跑了。
-
\(\text{60-238 min}\),最难崩的一段时间,一直在看第三题,西西弗非要把最重要的东西放在最后面,我浪费了90分钟,就是被那个对齐给卡死了,一种卡在第三个大样例。中途去上个厕所,然后吃了点面包调整一下,继续硬怼T3,最后两分钟的时候过了大样例。(结果没有A,原因是我三操作有一行范智了。。。。)
-
考试最后两分钟,仔细检查了一下文件,然后连静态差错都没有,第四题输出个N直接跑了。
考完之后直接裂开,本来自己感觉还没什么,然后就是被我妈,竞赛教练,班主任一个一个说,害怕我心态炸了,让我保持空杯心态等等。(于是我决定好还研究赤壁赋。)班主任甚至把他在班会课要讲的东西直接给我在谈话的时候全讲了一遍。
只能说,机会只会留给准备足够充足的人。明年继续加油!!!
题解
T1
没啥好说的,暴力枚举5位数字,然后暴力Check。
TMD,考场上忘记判重了,CCF我真的无语。
T2
很容易想到一个暴力,维护一个栈,先枚举左端点,然后一位一位的向后看,如果栈头的字母与当前字母相同,那就弹掉栈顶,否则就往栈里面加。显然,当有时间栈的大小是空的,那就会贡献一个答案。
然后有一个简单的优化,就是当枚举左端点,当往后枚举到一个位置 \(j\) 的时候,如果此时栈顶是空的,那么显然,对于当前左端点对于剩余答案的贡献,就是 \(1+\) 当前与右端点 \(+1\) 对于答案的贡献。
大致就可以写出下面这一份代码:
for (int i = 1; i <= n; ++i)
{
if(vis[i]) continue;
int now = 1;
while(!p.empty()) p.pop();
p.push(i);
for (int j = i + 1; j <= n; ++j)
{
if(p.empty() || (a[p.top()] != a[j]))
{
p.push(j);
if(p.size() == 1)
{
vis[j] = true;
now++;
}
}
else
{
p.pop();
if(p.empty()) ans += 1ll * now;
}
}
}
于是 60 分就到手了,可以过掉一个特殊性质。
于是,我们考虑继续优化。
可以发现,我们只需要求出一个点作为左端点第一次栈空的位置在哪里。
显然,当你在算一个左端点的时候,你要让当前栈为空,显然会经过若干个 \(\text{push}\) 和 \(\text{pop}\),然后遇到最后一个就空掉了。所以实际上,我们所有 \(\text{push}\) 的点最终都会 \(\text{pop}\) 掉,实际上就是相当于他作为左端点时空栈的情况,所以每个点只要 \(\text{push}\) 一次就可以算出最终答案。
说起来稍微有一点抽象,代码如下:
for (int i = 1; i <= n; ++i)
{
ans += num[i];
if(vis[i])
{
if(nxt[i]) num[nxt[i]] += num[i] + 1;
continue;
}
while(!p.empty()) p.pop();
p.push(i);
for (int j = i + 1; j <= n; ++j)
{
if(p.empty() || (a[p.top()] != a[j]))
{
if(vis[j])//如果已经被push过了,一定要防止下次不被push
{
if(!nxt[j]) j = n;
else j = nxt[j] - 1;
continue;
}
p.push(j), vis[j] = true;
}
else
{
nxt[p.top()] = j + 1;
p.pop();
if(p.empty()) break;
}
}
if(nxt[i]) num[nxt[i]] += num[i] + 1;
}
ans += num[n + 1];
T3
SB题,3个小时的努力,被一行给毁了。
说一下大致流程吧:
-
操作1,我们算出这个结构体的对齐要求,空间大小,以及里面所有元素占的是哪几位(维护一个 \([l,r]\) 即可)。
-
操作2,算出这个元素在对齐之后所占用的空间是连续的哪一段。(对齐的话,就是假设能用的空地址的最开始是 \(x\),对齐要求是 \(y\),那么这个元素开头就是 \(\lceil \dfrac{x}{y}\rceil \times y\),对应的 \(r\) 直接加上大小就可以了。)
-
操作3,先找出最外层结构体在所有元素中的位置,对于结构体内部的元素,我们直接递归找到即可。
-
操作4,仍然是在最外层找到它位于哪一个结构体内部,然后递归处理即可。一定要注意中间对齐搞出来的空位特判和要开 \(\text{long long}\)!
大多为 \(\text{string}\) 类型的操作,所以可以活用 \(\text{STL}\) 减少码量。考场,三操作犯智写错了一行。。。
放一下代码:
#include <bits/stdc++.h>
using namespace std;
bool Stflg;
int t;
unordered_map<string, long long> siz;
unordered_map<string, long long> ned;
unordered_map<string, vector<pair<string, string> > > mem;
unordered_map<string, vector<long long> > st;
unordered_map<string, vector<long long> > en;
vector<int> p;
vector<string> q, r, need;
vector<pair<long long, long long> > qwq;
long long sum, by;
void dfs(int x, string type)
{
if(x >= need.size())
{
// sum += siz[type];
return;
}
for (int i = 0; i < mem[type].size(); ++i)
{
if(mem[type][i].second == need[x])
{
sum += st[type][i];
dfs(x + 1, mem[type][i].first);
return;
}
}
}
string ans;
void dfs2(string type, string name)
{
if(!mem[type].size())
{
if(siz[type] <= by) ans = "ERR";
else ans += name;
// cout << name << endl;
// sum += siz[type];
return;
}
for (int i = 0; i < mem[type].size(); ++i)
{
int En;
if(i == mem[type].size() - 1) En = siz[type];
else En = st[type][i + 1];
if(st[type][i] <= by && en[type][i] >= by)
{
ans += name;
ans += '.';
by -= st[type][i];
dfs2(mem[type][i].first, mem[type][i].second);
return;
}
}
ans = "ERR";
}
int main()
{
// freopen("struct.in", "r", stdin);?
siz["byte"] = 1, siz["short"] = 2, siz["int"] = 4, siz["long"] = 8;
ned["byte"] = 1, ned["short"] = 2, ned["int"] = 4, ned["long"] = 8;
cin >> t;
while(t--)
{
sum = 0;
int op;
cin >> op;
if(op == 1)
{
string name;
int k;
cin >> name >> k;
int K = k;
long long now = 0;
while(k--)
{
string type, Name;
cin >> type >> Name;
mem[name].push_back(make_pair(type, Name));
if(K - k > 1)
en[name].push_back(now - 1);
now = ceil(now / (ned[type] * 1.0)) * ned[type];
st[name].push_back(now);
ned[name] = max(ned[type], ned[name]);
now += siz[type];
}
en[name].push_back(now - 1);
siz[name] = ceil(now / (ned[name] * 1.0)) * ned[name];
cout << siz[name] << " " << ned[name] << endl;
}
else if(op == 2)
{
string type, name;
cin >> type >> name;
long long L = 0;
if(qwq.size()) L = qwq[qwq.size() - 1].second + 1;
q.push_back(type), r.push_back(name);
p.push_back(siz[type]);
L = ceil(L / (ned[type] * 1.0)) * ned[type];
qwq.push_back(make_pair(L, L + siz[type] - 1));
cout << L << endl;
}
else if(op == 3)
{
string s;
cin >> s;
string now = "";
need.clear();
for (int i = 0; i < s.length(); ++i)
{
if(s[i] == '.') need.push_back(now), now.clear();
else now += s[i];
}
need.push_back(now);
for (int i = 0; i < p.size(); ++i)
{
if(r[i] == need[0])
{
if(i) sum += qwq[i].first;
dfs(1, q[i]);
break;
}
}
cout << sum << endl;
}
else
{
ans.clear();
cin >> by;
bool flg = 0;
for (int i = 0; i < p.size(); ++i)
{
int En = 0;
if(i == p.size() - 1) En = qwq[i].second;
else En = qwq[i + 1].first - 1;
if(qwq[i].first <= by && qwq[i].second >= by)
{
flg = true;
by -= qwq[i].first;
dfs2(q[i], r[i]);
break;
}
}
if(!flg) puts("ERR");
else cout << ans << endl;
}
}
// bool EndFlg = 0;
// cout << (&EndFlg - &Stflg) / 1024.0 / 1024.0 << endl;
return 0;
}
T4
近年来最简单的第四题,没有之一,考场上想到做法不敢打。。。
显然有二分答案,然后二分完之后显然可以继续二分算出每个地块,算出他至少要在第几天种树才能符合要求,至于第二个二分的检查,就是个等差数列和一堆 \(1\) 的和看是否大于 \(a_i\)。
预处理完之后,很容易想到将每个地块按照天数从小到大排序,就说明越靠前的地块就必须越先被种上树,一个简单的贪心策略。
至于具体的维护,就是我们可以以 \(1\) 为根建树,预处理出每个地块的父亲,给 \(1\) 打个标记,表示他已经被种上树了,\(now\) 表示当前天数,预处理为 \(1\),排序后一个一个枚举地块,进行如下操作:
-
如果这个地块被标记,即已经种上树了,那不管他。
-
如果没有被标记,一直向他的父亲跳,跳的第一颗被种了树的地块为止,并把这一路上所有的节点放进一个动态数组里面。如果你发现这一路的距离加上 \(now\) 已经大于这个地块的天数要求了,那么就说明这个答案不合法,否则就将 \(now+\) 这个数组的大小,并把数组里面所有的点打上标记。
很容易证明这样操作的正确性。
好后悔考场上没写这道题啊。。
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n;
int a[100005], b[100005], c[100005];
struct node
{
int tar, nxt;
}arr[200005];
int fst[100005], cnt;
void adds(int x, int y)
{
arr[++cnt].tar = y, arr[cnt].nxt = fst[x], fst[x] = cnt;
}
int fa[100005];
void dfs(int x, int last)
{
fa[x] = last;
for (int i = fst[x]; i; i = arr[i].nxt)
{
int j = arr[i].tar;
if(j == last) continue;
dfs(j, x);
}
}
bool vis[100005];
pair<int, int> d[100005];
bool check(int mid)
{
for (int i = 1; i <= n; ++i)
{
int l = 1, r = mid, Mid, ans = -1;
int limit;
if(c[i] >= 0) limit = mid;
else limit = (b[i] - 1) / abs(c[i]), limit = min(limit, mid);
// cout << i << " " << limit << endl;
while(l <= r)
{
__int128 now = 0;
Mid = (l + r) >> 1ll;
if(Mid <= limit)
{
if(b[i] + c[i] * Mid + b[i] + c[i] * limit >= a[i])
{
ans = Mid;
l = Mid + 1;
continue;
}
now = __int128(1) * (b[i] + c[i] * Mid + b[i] + c[i] * limit) * (limit - Mid + 1) / 2 + (mid - limit);
}
else now = mid - Mid + 1;
if(now >= a[i]) ans = Mid, l = Mid + 1;
else r = Mid - 1;
}
if(ans == -1) return false;
d[i] = make_pair(ans, i);
}
stable_sort(d + 1, d + n + 1);
memset(vis, 0, sizeof(vis));
vis[1] = 1;
int now = 1;
for (int qwq = 1; qwq <= n; ++qwq)
{
int i = d[qwq].second;
if(vis[i]) continue;
vector<int> p;
while(!vis[i])
{
p.push_back(i);
i = fa[i];
}
if(p.size() + now > d[qwq].first) return false;
now += p.size();
for (int j = 0; j < p.size(); ++j) vis[p[j]] = true;
}
return true;
}
signed main()
{
scanf("%lld", &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 x, y;
scanf("%lld %lld", &x, &y);
adds(x, y);
adds(y, x);
}
dfs(1, 0);
int l = n, r = 1000000000, mid, ans = 0;
while(l <= r)
{
mid = (l + r) >> 1;
if(check(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
}
cout << ans << endl;
}