【题解】第36次CCF-CSP认证
UPDATE
update(2024/12/10) : 修正了E题代码的小错误,十分感谢
@Andyqian7
提供的hack数据!
update(2024/12/15) : 修正了B题题解中存在问题的表述,十分感谢@iy88
指出这个问题!
概述
本次重现赛已经上传到SYNU OJ,本校的同学可以去对应的页面补题。
A. 移动 | B. 梦境巡查 | C. 缓存模拟 | D. 跳房子 | E. 梦魇 | |
---|---|---|---|---|---|
题目参考难度 | 800 | 1400 | 2000 | 2100 | 3500 |
涉及知识点 | 模拟 | 前缀和,枚举 | 数据结构 | 记忆化搜索,bfs | 深度优先搜索,笛卡尔树,单调栈 |
题解报告
A. 移动
直接模拟即可。
int main()
{
cin.tie(0)->sync_with_stdio(0);
cout.tie(0);
int n, k;
cin >> n >> k;
while (k--)
{
int x, y;
cin >> x >> y;
string op;
cin >> op;
for (int i = 0; i < op.size(); i++)
{
int tx = x, ty = y;
if (op[i] == 'f')
ty++;
else if (op[i] == 'b')
ty--;
else if (op[i] == 'l')
tx--;
else
tx++;
if (tx >= 1 and tx <= n and ty >= 1 and ty <= n)
{
x = tx, y = ty;
}
}
cout << x << ' ' << y << '\n';
}
}
B. 梦境巡查
update(2024/12/15) :实际上,我这里表示的
实际上应该表示的是题意中的 ,这是因为我在题目中处理输入的时候自动将 向后偏移了一位,也就是代码 for(int i = 1; i <= n + 1; i ++) cin >> a[i];
的部分,原始的输入应该是,而我将他处理成了 。但是,最后的代码的正确性并不会受到影响。请将下文中的 当成题意中的 即可!
首先,不考虑题目中对于
虽然我们求出的
这一步表示,我给之后的
上述是不带修改的
通过上述公式,我们发现,
int main()
{
cin.tie(0)->sync_with_stdio(0);
cout.tie(0);
int n;
cin >> n;
vector<int> a(n + 2), b(n + 1), f(n + 2);
for(int i = 1; i <= n + 1; i ++) cin >> a[i];
for(int i = 1; i <= n; i ++) cin >> b[i];
int w = 0;
for(int i = 1; i <= n + 1; i ++)
{
f[i] = f[i-1] - a[i] + b[i-1];
w = min(w, f[i]);
}
vector<int> sufmin(n + 3, 1e9);
for(int i = n + 1; i >= 1; i --)
{
sufmin[i] = min(sufmin[i + 1], f[i]);
}
for(int i = 1; i <= n; i ++)
{
cout << -min(w, sufmin[i + 1] - b[i]) << ' ';
}
}
C. 模拟缓存
大模拟,实际上先把流程图画出来会好很多,这样就知道什么时候该记录答案了。
图片中标红的部分就是应该记录答案的地方。
实际上我们只需要考虑实现上述过程中的部分瓶颈操作即可。
- 如何记录操作的先后顺序:可以对每次操作都记录一个时间戳,再用一个
set
维护pair
即可,pair第一维存时间戳,第二维存 。由于set
存储是有序的,因此时间戳应该倒序赋值,这样在set中,时间戳小的排在前面,表示发生时间距离当前最近。 - 查询是否在缓存中:可以采用set的
lower_bound
,每次查询 是否在缓存中,可以查询 最近的时间戳,这样即可快速判断,并且直接获取到 的迭代器。 - 删除距离当前时间最久的缓存内容:可以采用
set
的rbegin()
,这样每次删除最久远的元素即可。 - 验证缓存中的
是否被修改过,可以使用一个map
来作为标记数组。
注意事项:
- 每次别忘记操作完
之后,要及时更新 的时间戳(尤其是set中的时间戳)。 - 每次修改时记得打上修改标记,同时如果被修改过的数要被删除了,记得去掉它的标记。
- 如果缓存满了要删除某个元素,同时要写入内存,记住记录答案时写入操作的顺序在读入前面。
using i64 = long long;
using pii = pair<int, int>;
signed main()
{
cin.tie(0)->sync_with_stdio(0);
cout.tie(0);
int n, N, q;
cin >> n >> N >> q;
// n 组大小 N 组数
auto getid = [&](int x)
{
return x / n % N;
};
vector<set<pii>> group(N); // 缓存组
vector<pii> ans;
map<int, int> ismodify; // 是否被修改
int idx = 0;
map<int, int> timestamp; // 时间戳
while(q--)
{
int op, a;
cin >> op >> a;
if(op == 0)
{
int id = getid(a);
auto it = group[id].lower_bound({timestamp[a], 0});
if(it != group[id].end() and it -> second == a) // 如果a已经在缓存中
{
// 更新时间戳
group[id].erase(it);
group[id].insert({q, a});
}
else // 不在缓存中
{
// 缓存组未满
if(group[id].size() < n)
{
// 直接读入内存
ans.push_back({0, a});
group[id].insert({q, a});
}
// 缓存组满了
else
{
// 找到最后一个没有被修改的,也即是被替换的
auto tmp = group[id].rbegin() -> second;
// 如果他在缓存中被修改过
if(ismodify[tmp])
{
// 先同步修改到内存
ans.push_back({1, tmp});
// 去掉修改标记
ismodify[tmp] = 0;
}
group[id].erase(group[id].lower_bound({timestamp[tmp], tmp}));
// 再读入新的数据进来
group[id].insert({q, a});
ans.push_back({0, a});
}
}
}
else
{
int id = getid(a);
auto it = group[id].lower_bound({timestamp[a], 0});
if(it != group[id].end() and it -> second == a) // 如果a已经在缓存中
{
// 更新时间戳
// 直接修改,并将它标记成已修改
group[id].erase(it);
group[id].insert({q, a});
ismodify[a] = 1;
}
else
{
// 缓存组未满, 先读入,再修改
if(group[id].size() < n)
{
// 直接读入内存
ans.push_back({0, a});
group[id].insert({q, a});
ismodify[a] = 1;
}
else
{
// 找到最后一个没有被修改的,也即是被替换的
auto tmp = group[id].rbegin() -> second;
// 如果他在缓存中被修改过
if(ismodify[tmp])
{
// 先同步修改到内存
ans.push_back({1, tmp});
// 去掉修改标记
ismodify[tmp] = 0;
}
group[id].erase(group[id].lower_bound({timestamp[tmp], tmp}));
// 再读入新的数据进来
group[id].insert({q, a});
ans.push_back({0, a});
ismodify[a] = 1;
}
}
}
timestamp[a] = q;
}
// cout << "------------" << '\n';
for(auto [x, y] : ans)
{
cout << x << ' ' << y << '\n';
}
}
D. 跳房子
看上去是一个 set
容器,每次搜索的时候,如果已经搜到了的点,就直接暴力erase掉。这样每个点只会被搜到一次,复杂度就变成了
实际上本题还有更优秀的做法,只需要在搜索过程中维护一个
int main()
{
cin.tie(0)->sync_with_stdio(0);
cout.tie(0);
int n;
cin >> n;
vector<int> a(n + 1), k(n + 1), vis(n + 1);
for(int i = 1; i <= n; i ++)
cin >> a[i];
for(int i = 1; i <= n; i ++)
cin >> k[i];
queue<int> q;
q.push(1);
vis[1] = 1;
set<int> unvis_pos;
for(int i = 2; i <= n; i ++) unvis_pos.insert(i);
int dist = 0;
while(q.size())
{
int T = q.size();
dist ++;
while(T--)
{
auto u = q.front();
q.pop();
if(u + k[u] >= n)
{
cout << dist << '\n';
return 0;
}
auto it = unvis_pos.lower_bound(u + 1);
vector<int> del;
for(;*it <= u + k[u] and it != unvis_pos.end(); it++)
{
del.push_back(*it);
// cout << *it << '\n';
if(!vis[*it - a[*it]])
{
// cout << *it - a[*it] << '\n';
vis[*it - a[*it]] = 1;
q.push(*it - a[*it]);
}
}
for(int i : del) unvis_pos.erase(i);
}
}
cout << -1 << '\n';
}
E. 梦魇
本题解法是群友
Bezime
提供的,特别鸣谢!Orz
单调栈建立笛卡尔树,维护maxl
和maxr
。答案就是
update(2024/12/10) : 将代码单调栈从后往前扫的部分的
while (qt && q[qt] <= a[i])
改为while (qt && q[qt] < a[i])
using i64 = long long;
const int N = 5e6 + 5;
inline void qread(i64 &x)
{
x = 0;
short f = 1;
char c = getchar();
while ((c < '0' || c > '9') && c != '-')
c = getchar();
if (c == '-')
f = -1, c = getchar();
while (c >= '0' && c <= '9')
x = x * 10 + c - '0', c = getchar();
x *= f;
}
inline void qwrite(i64 x)
{
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
qwrite(x / 10);
putchar(x % 10 + '0');
}
i64 T = 1, n, t, k, ans;
i64 a[N], b[N], c[N], d[N];
i64 sum[N], mxl[N], mxr[N];
i64 q[N], o[N], qt;
i64 mx;
i64 f[N][3];
void dfs(i64 x)
{
i64 xl = mxl[x], xr = mxr[x];
// 如果左边的那个f的3个值没有定下来,求值
if (!f[xl][0])
dfs(xl);
// 如果右边的那个f的3个值没有定下来,求值
if (!f[xr][0])
dfs(xr);
// 这些是关于xl,x,xr的整段(xl+1~xr-1)、左段(xl+1~x-1)、右段(x+1~xr-1)区间和
i64 w = sum[xr - 1] - sum[xl], wl = sum[x] - sum[xl], wr = sum[xr - 1] - sum[x - 1];
// 后面操作不拿这一整段的最小起始攻击力
//(如果xl<=xr,那么f[xl][1]<=f[xr][2],而mxr[xl]应恰好等于xr,故f[xl][1]恰好跳过了xl+1~xr-1)。
// 这一整段都没拿过,因此求f的3个值时可以随意加上区间内b的值
i64 mn = min(f[xl][1], f[xr][2]);
// 第一步自身起手,至少带a[x]的攻击力,拿走整段的b值
f[x][0] = max(a[x], mn - w);
// 拿走左段的b值,留下右段的b值不拿,用来求右段的f的3个值
f[x][1] = max(a[x], mn - wl);
// 拿走右段的b值,留下左段的b值不拿,用来求左段的f的3个值
f[x][2] = max(a[x], mn - wr);
}
void solve()
{
qread(n);
for (i64 i = 1; i <= n; i++)
qread(c[i]);
for (i64 i = 1; i <= n; i++)
qread(d[i]);
qread(t);
while (t--)
{
for (i64 i = 1; i <= n; i++)
a[i] = c[i], b[i] = d[i], f[i][0] = 0;
qread(k);
while (k--)
{
i64 x, l, r;
qread(x), qread(l), qread(r);
a[x] = l, b[x] = r;
}
qt = ans = mx = 0;
for (i64 i = 1; i <= n; i++)
{
// b的前缀和
sum[i] = sum[i - 1] + b[i];
// 记录最大a值,f的值不可能大于它
mx = max(mx, a[i]);
while (qt && q[qt] <= a[i])
qt--;
// 左边第一个大于
mxl[i] = o[qt];
q[++qt] = a[i], o[qt] = i;
}
// 给边界一个初值
f[0][0] = f[0][1] = f[0][2] = f[n + 1][0] = f[n + 1][1] = f[n + 1][2] = mx;
qt = 0, o[0] = n + 1; // n+1是右边界
for (i64 i = n; i; i--)
{
while (qt && q[qt] < a[i]) // update: 这里原来的 <= 改为 <
qt--;
mxr[i] = o[qt]; // 右边第一个大于等于
q[++qt] = a[i], o[qt] = i;
}
for (i64 i = 1; i <= n; i++)
if (!f[i][0])
dfs(i); // 如果f值还没有定下来,求值
for (i64 i = 1; i < n; i++)
ans ^= min(f[i][0], f[i + 1][0]);
qwrite(ans), puts("");
}
}
int main()
{
while (T--)
solve();
}
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 《HelloGitHub》第 108 期
· Windows桌面应用自动更新解决方案SharpUpdater5发布
· 我的家庭实验室服务器集群硬件清单
· C# 13 中的新增功能实操
· Supergateway:MCP服务器的远程调试与集成工具